<?php

/**
 * POST /api/ixg_create.php
 *
 * Secure proxy for IXG shortlink creation.
 *
 * Upstream base app:
 *   https://v3.ixg.llc/api.php?op=create
 *
 * Public output:
 *   https://<random>.ixg.llc/<slug>
 *
 * Required .env:
 *   IXG_UPSTREAM_API_KEY=<upstream bearer token>
 *
 * Optional .env:
 *   IXG_UPSTREAM_BASE=https://v3.ixg.llc
 *   IXG_PUBLIC_ROOT_DOMAIN=ixg.llc
 *   IXG_PROXY_CLIENT_KEY=<optional inbound bearer token>
 */

declare(strict_types=1);

ini_set('display_errors', '0');
ini_set('log_errors', '1');

require_once __DIR__ . '/../session_guards.php';

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('Referrer-Policy: no-referrer');
header('Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');

if (is_https_request()) {
    header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}

try {
    load_local_env();

    if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
        json_response(
            405,
            [
                'ok' => false,
                'err' => 'method-not-allowed',
                'allow' => 'POST',
            ],
        );
    }

    require_session_or_bearer_auth();

    if (!extension_loaded('curl')) {
        json_response(
            500,
            [
                'ok' => false,
                'err' => 'curl-extension-missing',
            ],
        );
    }

    $target = request_string('longurl');
    if ($target === '') {
        $target = request_string('url');
    }

    $target = normalize_url_input($target);

    if (!is_public_http_url($target)) {
        json_response(
            422,
            [
                'ok' => false,
                'err' => 'invalid-url',
            ],
        );
    }

    $title = normalize_text_field(request_string('title'), 160);
    $desc = normalize_text_field(request_string('desc'), 300);
    $img = normalize_url_input(request_string('img'));
    $code = normalize_slug_input(request_string('code'));

    if ($img !== '' && !is_public_http_url($img)) {
        json_response(
            422,
            [
                'ok' => false,
                'err' => 'invalid-image-url',
            ],
        );
    }

    $upstreamBase = rtrim(ixg_env('IXG_UPSTREAM_BASE', 'https://v3.ixg.llc'), '/');
    if (!is_allowed_upstream_base($upstreamBase)) {
        json_response(
            500,
            [
                'ok' => false,
                'err' => 'invalid-upstream-base',
            ],
        );
    }

    $upstreamKey = ixg_env('IXG_UPSTREAM_API_KEY', '');
    if ($upstreamKey === '') {
        json_response(
            500,
            [
                'ok' => false,
                'err' => 'missing-upstream-api-key',
            ],
        );
    }

    $rootDomain = strtolower(ixg_env('IXG_PUBLIC_ROOT_DOMAIN', 'ixg.llc'));
    if (!is_valid_root_domain($rootDomain)) {
        json_response(
            500,
            [
                'ok' => false,
                'err' => 'invalid-public-root-domain',
            ],
        );
    }

    $payload = [
        'url' => $target,
    ];

    if ($title !== '') {
        $payload['title'] = $title;
    }

    if ($desc !== '') {
        $payload['desc'] = $desc;
    }

    if ($img !== '') {
        $payload['img'] = $img;
    }

    if ($code !== '') {
        $payload['code'] = $code;
    }

    $upstreamUrl = $upstreamBase . '/api.php?op=create';
    $upstream = call_ixg_upstream($upstreamUrl, $upstreamKey, $payload);

    $http = $upstream['http'];
    $body = $upstream['body'];
    $curlErr = $upstream['curl_error'];

    if ($curlErr !== '') {
        error_log('ixg: curl error, err=' . safe_log_sample($curlErr, 180));

        json_response(
            502,
            [
                'ok' => false,
                'err' => 'upstream-network-error',
            ],
        );
    }

    $json = json_decode($body, true);
    if (!is_array($json)) {
        error_log(
            'ixg: invalid upstream json, http=' . $http
            . ', body=' . safe_log_sample($body, 300),
        );

        json_response(
            502,
            [
                'ok' => false,
                'err' => 'upstream-invalid-json',
            ],
        );
    }

    if ($http < 200 || $http >= 300 || (($json['success'] ?? false) !== true)) {
        $upErr = isset($json['error']) && is_string($json['error']) ? $json['error'] : '';

        error_log(
            'ixg: upstream API error, http=' . $http
            . ', err=' . ($upErr !== '' ? safe_log_sample($upErr, 180) : '(none)')
            . ', body=' . safe_log_sample($body, 300),
        );

        json_response(
            502,
            [
                'ok' => false,
                'err' => 'upstream-api-error',
                'upstream' => $upErr,
            ],
        );
    }

    $data = is_array($json['data'] ?? null) ? $json['data'] : [];

    $upstreamShortRaw = $data['shortUrl'] ?? '';
    $upstreamShort = is_string($upstreamShortRaw) ? trim($upstreamShortRaw) : '';

    $upstreamCodeRaw = $data['code'] ?? '';
    $upstreamCode = is_string($upstreamCodeRaw) ? trim($upstreamCodeRaw) : '';

    try {
        $slug = extract_slug_from_upstream($upstreamShort, $upstreamCode);
        $publicHost = random_dns_label(8) . '.' . $rootDomain;
        $publicShortUrl = 'https://' . $publicHost . '/' . rawurlencode($slug);
    } catch (Throwable $e) {
        error_log('ixg: invalid upstream short output');

        json_response(
            502,
            [
                'ok' => false,
                'err' => 'invalid-upstream-short-url',
            ],
        );
    }

    json_response(
        200,
        [
            'ok' => true,
            'short_url' => $publicShortUrl,
            'slug' => $slug,
            'host' => $publicHost,
        ],
    );
} catch (Throwable $e) {
    error_log('ixg: fatal handler error');

    json_response(
        500,
        [
            'ok' => false,
            'err' => 'server-error',
        ],
    );
}

/**
 * @param array<string, mixed> $payload
 *
 * @return array{http:int, body:string, curl_error:string}
 */
function call_ixg_upstream(string $url, string $apiKey, array $payload): array
{
    $ch = curl_init($url);

    if ($ch === false) {
        return [
            'http' => 0,
            'body' => '',
            'curl_error' => 'curl-init-failed',
        ];
    }

    $postBody = http_build_query($payload, '', '&', PHP_QUERY_RFC3986);

    curl_setopt_array(
        $ch,
        [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $postBody,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT => 15,
            CURLOPT_FOLLOWLOCATION => false,
            CURLOPT_MAXREDIRS => 0,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_HTTPHEADER => [
                'Accept: application/json',
                'Content-Type: application/x-www-form-urlencoded',
                'Authorization: Bearer ' . $apiKey,
            ],
        ],
    );

    $resp = curl_exec($ch);
    $err = curl_error($ch);
    $http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

    curl_close($ch);

    return [
        'http' => $http,
        'body' => is_string($resp) ? $resp : '',
        'curl_error' => $err,
    ];
}

function extract_slug_from_upstream(string $shortUrl, string $fallbackCode): string
{
    if ($fallbackCode !== '' && is_valid_slug($fallbackCode)) {
        return $fallbackCode;
    }

    $shortUrl = trim($shortUrl);
    if ($shortUrl === '') {
        throw new RuntimeException('empty-short-url');
    }

    $parts = parse_url($shortUrl);
    $path = isset($parts['path']) && is_string($parts['path'])
        ? trim($parts['path'], '/')
        : '';

    if ($path === '') {
        throw new RuntimeException('missing-short-path');
    }

    $segments = explode('/', $path);
    $last = end($segments);
    $slug = rawurldecode(is_string($last) ? $last : '');

    if (!is_valid_slug($slug)) {
        throw new RuntimeException('invalid-short-slug');
    }

    return $slug;
}

function random_dns_label(int $length = 8): string
{
    if ($length < 4 || $length > 32) {
        $length = 8;
    }

    $alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
    $max = strlen($alphabet) - 1;
    $out = '';

    for ($i = 0; $i < $length; $i++) {
        $out .= $alphabet[random_int(0, $max)];
    }

    if (preg_match('/^[a-z]/', $out) !== 1) {
        $out = 'x' . substr($out, 1);
    }

    return $out;
}

function request_string(string $key): string
{
    $value = $_POST[$key] ?? '';

    if (!is_string($value)) {
        return '';
    }

    return trim($value);
}

function normalize_text_field(string $value, int $maxLength): string
{
    $value = trim(preg_replace('/[\x00-\x1F\x7F]+/u', ' ', $value) ?? '');

    if ($value === '') {
        return '';
    }

    if (mb_strlen($value, 'UTF-8') > $maxLength) {
        return mb_substr($value, 0, $maxLength, 'UTF-8');
    }

    return $value;
}

function normalize_url_input(string $url): string
{
    $url = trim($url);

    if ($url === '') {
        return '';
    }

    $url = preg_replace('/[\x00-\x20\x7F]+/', '', $url) ?? '';

    return trim($url);
}

function normalize_slug_input(string $code): string
{
    $code = trim($code);

    if ($code === '') {
        return '';
    }

    if (!is_valid_slug($code)) {
        json_response(
            422,
            [
                'ok' => false,
                'err' => 'invalid-code',
            ],
        );
    }

    return $code;
}

function is_valid_slug(string $slug): bool
{
    return preg_match('/^[A-Za-z0-9][A-Za-z0-9_-]{1,79}$/', $slug) === 1;
}

function is_public_http_url(string $url): bool
{
    if (!filter_var($url, FILTER_VALIDATE_URL)) {
        return false;
    }

    $parts = parse_url($url);
    if (!is_array($parts)) {
        return false;
    }

    $scheme = strtolower((string) ($parts['scheme'] ?? ''));
    if ($scheme !== 'http' && $scheme !== 'https') {
        return false;
    }

    $host = strtolower((string) ($parts['host'] ?? ''));
    if ($host === '') {
        return false;
    }

    if ($host === 'localhost' || str_ends_with($host, '.localhost') || str_ends_with($host, '.local')) {
        return false;
    }

    if (filter_var($host, FILTER_VALIDATE_IP)) {
        return filter_var(
            $host,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE,
        ) !== false;
    }

    if (preg_match('/^[a-z0-9.-]+$/i', $host) !== 1) {
        return false;
    }

    if (strpos($host, '..') !== false) {
        return false;
    }

    return true;
}

function is_allowed_upstream_base(string $base): bool
{
    if ($base !== 'https://v3.ixg.llc') {
        return false;
    }

    return true;
}

function is_valid_root_domain(string $domain): bool
{
    return preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $domain) === 1;
}

/**
 * Fail-closed auth: accept EITHER a valid authenticated portal session
 * (with CSRF for this state-changing POST) OR a valid machine-to-machine
 * bearer token (IXG_PROXY_CLIENT_KEY). Reject with 401 when neither is present.
 */
function require_session_or_bearer_auth(): void
{
    // Path A — machine-to-machine bearer (no cookies, so no CSRF needed).
    $clientKey = ixg_env('IXG_PROXY_CLIENT_KEY', '');
    if ($clientKey !== '') {
        $bearer = bearer_token();
        if ($bearer !== '' && hash_equals($clientKey, $bearer)) {
            return;
        }
    }

    // Path B — authenticated user-portal session + CSRF (same gate as ixgeu.php).
    if (function_exists('startUserGuardSession')) {
        startUserGuardSession();
        if (
            authenticatedUserSubId() !== ''
            && hasValidSessionCsrf('user_portal', requestCsrfToken())
        ) {
            return;
        }
    }

    header('WWW-Authenticate: Bearer');
    json_response(
        401,
        [
            'ok' => false,
            'err' => 'unauthorized',
        ],
    );
}

function bearer_token(): string
{
    $header = '';

    if (isset($_SERVER['HTTP_AUTHORIZATION']) && is_string($_SERVER['HTTP_AUTHORIZATION'])) {
        $header = $_SERVER['HTTP_AUTHORIZATION'];
    } elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) && is_string($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
        $header = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
    }

    $header = trim($header);

    if ($header === '') {
        return '';
    }

    if (preg_match('/^Bearer\s+(.+)$/i', $header, $m) !== 1) {
        return '';
    }

    return trim((string) $m[1]);
}

function load_local_env(): void
{
    $paths = [
        dirname(__DIR__) . '/.env',
        dirname(__DIR__, 2) . '/.env',
        __DIR__ . '/.env',
    ];

    foreach ($paths as $path) {
        if (is_file($path) && is_readable($path)) {
            ixg_load_env_file($path);

            return;
        }
    }
}

function ixg_load_env_file(string $path): void
{
    $lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);

    if ($lines === false) {
        return;
    }

    foreach ($lines as $line) {
        $line = trim($line);

        if ($line === '' || str_starts_with($line, '#')) {
            continue;
        }

        if (!str_contains($line, '=')) {
            continue;
        }

        [$key, $value] = explode('=', $line, 2);

        $key = trim($key);
        $value = trim($value);

        if ($key === '') {
            continue;
        }

        if (
            (str_starts_with($value, '"') && str_ends_with($value, '"'))
            || (str_starts_with($value, "'") && str_ends_with($value, "'"))
        ) {
            $value = substr($value, 1, -1);
        }

        if (getenv($key) === false) {
            putenv($key . '=' . $value);
        }

        $_ENV[$key] = $value;
    }
}

function ixg_env(string $key, string $default = ''): string
{
    $value = getenv($key);

    if ($value === false && isset($_ENV[$key])) {
        $value = $_ENV[$key];
    }

    if (!is_string($value)) {
        return $default;
    }

    $value = trim($value, " \t\n\r\0\x0B\"'");

    return $value !== '' ? $value : $default;
}

function safe_log_sample(string $value, int $max): string
{
    $value = preg_replace('/[\x00-\x1F\x7F]+/', ' ', $value) ?? '';
    $value = preg_replace('/Bearer\s+[A-Za-z0-9._~+\-\/]+=*/i', 'Bearer [redacted]', $value) ?? $value;
    $value = preg_replace('/(api[_-]?key|token|secret|password)=([^&\s]+)/i', '$1=[redacted]', $value) ?? $value;

    if (strlen($value) > $max) {
        return substr($value, 0, $max);
    }

    return $value;
}

function is_https_request(): bool
{
    if (isset($_SERVER['HTTPS']) && is_string($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on') {
        return true;
    }

    if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && is_string($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
        return true;
    }

    return false;
}

/**
 * @param array<string, mixed> $payload
 */
function json_response(int $status, array $payload): never
{
    http_response_code($status);

    echo json_encode(
        $payload,
        JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
    );

    exit;
}
