<?php
declare(strict_types=1);

$config = require __DIR__ . '/../config.php';

function config(string $path = '', $default = null)
{
    global $config;
    if ($path === '') {
        return $config;
    }

    $value = $config;
    foreach (explode('.', $path) as $segment) {
        if (!is_array($value) || !array_key_exists($segment, $value)) {
            return $default;
        }
        $value = $value[$segment];
    }
    return $value;
}

function is_https(): bool
{
    return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
        (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
}

function apply_security_headers(): void
{
    header('X-Content-Type-Options: nosniff');
    header('X-Frame-Options: DENY');
    header('Referrer-Policy: no-referrer');
    header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
    header("Content-Security-Policy: default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self' https://checkout.stripe.com");
    if (is_https()) {
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
    }
}

apply_security_headers();

set_exception_handler(static function (Throwable $error): void {
    error_log('[nexo] ' . $error->getMessage() . "\n" . $error->getTraceAsString());
    if (!headers_sent()) {
        http_response_code(500);
        header('Content-Type: application/json; charset=utf-8');
    }
    echo json_encode(['ok' => false, 'error' => 'Internal server error'], JSON_UNESCAPED_UNICODE);
});

function require_app_key(): string
{
    $key = (string) config('app_key', '');
    if (strlen($key) < 32) {
        throw new RuntimeException('APP_KEY must contain at least 32 characters');
    }
    return $key;
}

function db(): PDO
{
    static $pdo = null;
    if ($pdo instanceof PDO) {
        return $pdo;
    }

    $db = config('db');
    if (empty($db['name']) || empty($db['user'])) {
        throw new RuntimeException('Database configuration is incomplete');
    }

    $dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=%s', $db['host'], $db['port'], $db['name'], $db['charset']);
    $pdo = new PDO($dsn, $db['user'], $db['pass'], [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]);
    return $pdo;
}

function request_origin(): string
{
    return trim((string) ($_SERVER['HTTP_ORIGIN'] ?? ''));
}

function apply_cors_headers(): void
{
    $origin = request_origin();
    $allowed = config('app.cors_origins', ['*']);
    if (in_array('*', $allowed, true)) {
        header('Access-Control-Allow-Origin: *');
    } elseif ($origin !== '' && in_array($origin, $allowed, true)) {
        header('Access-Control-Allow-Origin: ' . $origin);
        header('Vary: Origin');
    }
    header('Access-Control-Allow-Headers: Content-Type, X-Device-Token');
    header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
}

function json_response(array $data, int $status = 200): void
{
    apply_cors_headers();
    header('Content-Type: application/json; charset=utf-8');
    header('Cache-Control: no-store');
    http_response_code($status);
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function handle_cors_preflight(): void
{
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') {
        apply_cors_headers();
        http_response_code(204);
        exit;
    }
}

function require_method(array $methods): void
{
    $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
    if (!in_array($method, $methods, true)) {
        header('Allow: ' . implode(', ', $methods));
        json_response(['ok' => false, 'error' => 'Method not allowed'], 405);
    }
}

function input_json(): array
{
    static $input = null;
    if (is_array($input)) {
        return $input;
    }
    $raw = file_get_contents('php://input');
    $decoded = json_decode($raw ?: '', true);
    $input = is_array($decoded) ? $decoded : $_POST;
    return $input;
}

function truncate_text(string $value, int $maxLength): string
{
    return function_exists('mb_substr') ? mb_substr($value, 0, $maxLength) : substr($value, 0, $maxLength);
}

function normalize_mac(string $mac): string
{
    $mac = strtoupper(trim($mac));
    if (preg_match('/^[0-9A-F]{12}$/', $mac)) {
        $mac = implode(':', str_split($mac, 2));
    }
    return $mac;
}

function valid_device_id(string $mac): bool
{
    return (bool) preg_match('/^(?:[0-9A-F]{2}:){5}[0-9A-F]{2}$|^WEB-[A-Z0-9-]{8,64}$/', $mac);
}

function client_ip(): string
{
    return substr((string) ($_SERVER['REMOTE_ADDR'] ?? 'unknown'), 0, 64);
}

function rate_limit(string $bucket, int $limit, int $windowSeconds): void
{
    $key = hash('sha256', $bucket . '|' . client_ip());
    $path = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'nexo-rate-' . $key . '.json';
    $now = time();
    $state = ['start' => $now, 'count' => 0];
    $handle = @fopen($path, 'c+');
    if (!$handle) {
        return;
    }
    try {
        if (!flock($handle, LOCK_EX)) {
            return;
        }
        $raw = stream_get_contents($handle);
        $stored = json_decode($raw ?: '', true);
        if (is_array($stored) && ($now - (int) ($stored['start'] ?? 0)) < $windowSeconds) {
            $state = $stored;
        }
        $state['count'] = (int) ($state['count'] ?? 0) + 1;
        if ($state['count'] > $limit) {
            json_response(['ok' => false, 'error' => 'Too many requests'], 429);
        }
        ftruncate($handle, 0);
        rewind($handle);
        fwrite($handle, json_encode($state));
        fflush($handle);
    } finally {
        flock($handle, LOCK_UN);
        fclose($handle);
    }
}

function device_token_from_request(array $input = []): string
{
    return trim((string) ($_SERVER['HTTP_X_DEVICE_TOKEN'] ?? $input['device_token'] ?? $_GET['device_token'] ?? ''));
}

function token_hash(string $token): string
{
    return hash_hmac('sha256', $token, require_app_key());
}

function find_device(string $mac): ?array
{
    $stmt = db()->prepare('SELECT * FROM devices WHERE mac = ? LIMIT 1');
    $stmt->execute([$mac]);
    $device = $stmt->fetch();
    return $device ?: null;
}

function create_device(string $mac, string $token): array
{
    $trialDays = max(0, (int) config('app.trial_days', 7));
    db()->prepare(
        'INSERT INTO devices (mac, device_token_hash, trial_started_at, trial_ends_at, last_seen_at)
         VALUES (?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? DAY), NOW())'
    )->execute([$mac, token_hash($token), $trialDays]);
    return find_device($mac) ?? throw new RuntimeException('Could not create device');
}

function authenticate_device(array $input = [], bool $allowCreate = false): array
{
    $mac = normalize_mac((string) ($input['mac'] ?? $_GET['mac'] ?? ''));
    $token = device_token_from_request($input);
    if (!valid_device_id($mac) || strlen($token) < 24 || strlen($token) > 200) {
        json_response(['ok' => false, 'error' => 'Valid device credentials are required'], 401);
    }

    $device = find_device($mac);
    if (!$device) {
        if (!$allowCreate) {
            json_response(['ok' => false, 'error' => 'Device not registered'], 404);
        }
        rate_limit('device-register', 20, 3600);
        $device = create_device($mac, $token);
        audit_log('device.registered', (int) $device['id'], ['mac' => $mac]);
    } elseif (empty($device['device_token_hash'])) {
        if (!config('app.allow_legacy_device_claim', false)) {
            rate_limit('legacy-device-claim-blocked', 10, 900);
            json_response(['ok' => false, 'error' => 'Device credential migration is required'], 409);
        }
        db()->prepare('UPDATE devices SET device_token_hash = ? WHERE id = ? AND device_token_hash IS NULL')
            ->execute([token_hash($token), (int) $device['id']]);
        $device = find_device($mac) ?? $device;
        audit_log('device.claimed', (int) $device['id'], ['mac' => $mac]);
    } elseif (!hash_equals((string) $device['device_token_hash'], token_hash($token))) {
        rate_limit('device-auth-failed', 25, 900);
        json_response(['ok' => false, 'error' => 'Invalid device credentials'], 401);
    }

    db()->prepare('UPDATE devices SET last_seen_at = NOW() WHERE id = ?')->execute([(int) $device['id']]);
    return $device;
}

function device_access(array $device): array
{
    $now = new DateTimeImmutable('now');
    $trialEnds = new DateTimeImmutable((string) $device['trial_ends_at']);
    $activeUntil = !empty($device['active_until']) ? new DateTimeImmutable((string) $device['active_until']) : null;
    if (($device['status'] ?? '') === 'disabled') {
        return ['allowed' => false, 'reason' => 'disabled'];
    }
    if ($activeUntil && $activeUntil >= $now) {
        return ['allowed' => true, 'reason' => 'paid'];
    }
    if ($trialEnds >= $now) {
        return ['allowed' => true, 'reason' => 'trial'];
    }
    return ['allowed' => false, 'reason' => 'expired'];
}

function require_active_device(array $device): void
{
    if (!device_access($device)['allowed']) {
        json_response(['ok' => false, 'error' => 'Subscription required'], 403);
    }
}

function base64url_encode(string $value): string
{
    return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
}

function base64url_decode(string $value): string
{
    return (string) base64_decode(strtr($value, '-_', '+/'), true);
}

function make_signed_token(string $purpose, int $deviceId, int $minutes): string
{
    $payload = $deviceId . '.' . (time() + max(1, $minutes) * 60);
    $signature = hash_hmac('sha256', $purpose . '|' . $payload, require_app_key());
    return base64url_encode($payload . '.' . $signature);
}

function verify_signed_token(string $token, string $purpose): ?array
{
    $decoded = base64url_decode($token);
    $parts = explode('.', $decoded);
    if (count($parts) !== 3) {
        return null;
    }
    [$deviceId, $expires, $signature] = $parts;
    if (!ctype_digit($deviceId) || !ctype_digit($expires) || (int) $expires < time()) {
        return null;
    }
    $payload = $deviceId . '.' . $expires;
    $expected = hash_hmac('sha256', $purpose . '|' . $payload, require_app_key());
    return hash_equals($expected, $signature) ? ['device_id' => (int) $deviceId, 'expires' => (int) $expires] : null;
}

function device_from_signed_token(string $purpose): array
{
    $verified = verify_signed_token((string) ($_GET['token'] ?? ''), $purpose);
    if (!$verified) {
        json_response(['ok' => false, 'error' => 'Invalid or expired token'], 401);
    }
    $stmt = db()->prepare('SELECT * FROM devices WHERE id = ? LIMIT 1');
    $stmt->execute([$verified['device_id']]);
    $device = $stmt->fetch();
    if (!$device) {
        json_response(['ok' => false, 'error' => 'Device not found'], 404);
    }
    return $device;
}

function encrypt_secret(?string $plain): ?string
{
    if ($plain === null || $plain === '') {
        return null;
    }
    $key = hash('sha256', require_app_key(), true);
    $iv = random_bytes(12);
    $tag = '';
    $cipher = openssl_encrypt($plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
    if ($cipher === false) {
        throw new RuntimeException('Could not encrypt secret');
    }
    return 'enc:v1:' . base64url_encode($iv . $tag . $cipher);
}

function decrypt_secret(?string $encrypted): ?string
{
    if ($encrypted === null || $encrypted === '') {
        return null;
    }
    if (strpos($encrypted, 'enc:v1:') !== 0) {
        return $encrypted;
    }
    $raw = base64url_decode(substr($encrypted, 7));
    if (strlen($raw) < 29) {
        return null;
    }
    $iv = substr($raw, 0, 12);
    $tag = substr($raw, 12, 16);
    $cipher = substr($raw, 28);
    $plain = openssl_decrypt($cipher, 'aes-256-gcm', hash('sha256', require_app_key(), true), OPENSSL_RAW_DATA, $iv, $tag);
    return $plain === false ? null : $plain;
}

function get_device_playlist(int $deviceId): ?array
{
    $stmt = db()->prepare('SELECT * FROM playlists WHERE device_id = ? AND enabled = 1 ORDER BY id ASC LIMIT 1');
    $stmt->execute([$deviceId]);
    return $stmt->fetch() ?: null;
}

function get_device_playlists(int $deviceId): array
{
    $stmt = db()->prepare('SELECT * FROM playlists WHERE device_id = ? ORDER BY enabled DESC, id ASC');
    $stmt->execute([$deviceId]);
    return $stmt->fetchAll();
}

function generated_playlist_url(?array $playlist): string
{
    if (!$playlist) {
        return '';
    }
    if ($playlist['playlist_type'] === 'm3u') {
        return (string) $playlist['playlist_url'];
    }
    $password = decrypt_secret($playlist['password'] ?? null) ?? '';
    return rtrim((string) $playlist['server_url'], '/') . '/get.php?username=' . rawurlencode((string) $playlist['username']) .
        '&password=' . rawurlencode($password) . '&type=m3u_plus&output=' . rawurlencode((string) $playlist['output_format']);
}

function public_playlist_payload(?array $playlist): ?array
{
    if (!$playlist) {
        return null;
    }
    return [
        'id' => (string) $playlist['id'],
        'name' => $playlist['playlist_name'] ?? 'Main IPTV',
        'type' => $playlist['playlist_type'],
        'playlist_url' => $playlist['playlist_url'],
        'server_url' => $playlist['server_url'],
        'username' => $playlist['username'],
        'password' => decrypt_secret($playlist['password'] ?? null),
        'output' => $playlist['output_format'],
        'epg_url' => $playlist['epg_url'] ?? null,
        'catchup_mode' => $playlist['catchup_mode'] ?? null,
        'enabled' => (bool) ($playlist['enabled'] ?? true),
        'generated_url' => generated_playlist_url($playlist),
        'created_at' => $playlist['created_at'] ?? null,
        'updated_at' => $playlist['updated_at'] ?? null,
    ];
}

function public_playlists_payload(array $playlists): array
{
    return array_values(array_filter(array_map('public_playlist_payload', $playlists)));
}

function absolute_url(string $path): string
{
    $base = (string) config('app.site_url', '');
    if ($base === '') {
        throw new RuntimeException('APP_URL is not configured');
    }
    return $base . '/' . ltrim($path, '/');
}

function portal_url(array $device): string
{
    $token = make_signed_token('portal', (int) $device['id'], (int) config('app.portal_token_minutes', 30));
    return absolute_url('dashboard.php?mac=' . rawurlencode((string) $device['mac']) . '&token=' . rawurlencode($token));
}

function resolve_public_ips(string $host): array
{
    $normalized = trim($host, '[]');
    if ($normalized === '' || strtolower($normalized) === 'localhost') {
        return [];
    }
    if (filter_var($normalized, FILTER_VALIDATE_IP)) {
        return filter_var($normalized, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) ? [$normalized] : [];
    }
    $ips = gethostbynamel($normalized);
    if (!$ips) {
        return [];
    }
    foreach ($ips as $ip) {
        if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return [];
        }
    }
    return array_values(array_unique($ips));
}

function host_is_public(string $host): bool
{
    return resolve_public_ips($host) !== [];
}

function playlist_hosts_for_device(int $deviceId): array
{
    $hosts = [];
    foreach (get_device_playlists($deviceId) as $playlist) {
        foreach (['playlist_url', 'server_url', 'epg_url'] as $field) {
            $host = strtolower((string) parse_url((string) ($playlist[$field] ?? ''), PHP_URL_HOST));
            if ($host !== '') {
                $hosts[] = $host;
            }
        }
    }
    return array_values(array_unique($hosts));
}

function validate_proxy_url(string $url, array $device, bool $allowExternalMetadata = false): array
{
    if (!filter_var($url, FILTER_VALIDATE_URL)) {
        json_response(['ok' => false, 'error' => 'Valid URL is required'], 422);
    }
    $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));
    $host = strtolower((string) parse_url($url, PHP_URL_HOST));
    $resolvedIps = resolve_public_ips($host);
    if (!in_array($scheme, ['http', 'https'], true) || $resolvedIps === []) {
        json_response(['ok' => false, 'error' => 'URL is not allowed'], 403);
    }
    $allowedHosts = playlist_hosts_for_device((int) $device['id']);
    if ($allowExternalMetadata) {
        $allowedHosts = array_merge($allowedHosts, config('proxy.external_hosts', []));
    }
    if (!in_array($host, array_unique($allowedHosts), true)) {
        json_response(['ok' => false, 'error' => 'Host is not allowed'], 403);
    }
    return [
        'url' => $url,
        'host' => $host,
        'scheme' => $scheme,
        'port' => (int) (parse_url($url, PHP_URL_PORT) ?: ($scheme === 'https' ? 443 : 80)),
        'resolved_ips' => $resolvedIps,
    ];
}

function curl_resolve_entries(array $target): array
{
    if (filter_var($target['host'] ?? '', FILTER_VALIDATE_IP)) {
        return [];
    }
    $host = (string) ($target['host'] ?? '');
    $port = (int) ($target['port'] ?? 0);
    return array_map(static fn(string $ip): string => $host . ':' . $port . ':' . $ip, $target['resolved_ips'] ?? []);
}

function audit_log(string $action, ?int $deviceId = null, array $details = []): void
{
    try {
        db()->prepare('INSERT INTO audit_logs (device_id, actor, action, ip_address, details) VALUES (?, ?, ?, ?, ?)')
            ->execute([$deviceId, $_SESSION['admin_username'] ?? 'system', $action, client_ip(), json_encode($details, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)]);
    } catch (Throwable $ignored) {
        error_log('[nexo] audit log unavailable: ' . $ignored->getMessage());
    }
}

function csrf_token(): string
{
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return (string) $_SESSION['csrf_token'];
}

function verify_csrf(): void
{
    $provided = (string) ($_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
    if ($provided === '' || !hash_equals(csrf_token(), $provided)) {
        http_response_code(419);
        exit('Invalid CSRF token');
    }
}
