<?php

declare(strict_types=1);

$root = dirname(__DIR__);
$public = $root . DIRECTORY_SEPARATOR . 'public';
$baseUrl = null;
foreach (array_slice($argv ?? [], 1) as $arg) {
    if (str_starts_with((string) $arg, '--base-url=')) {
        $baseUrl = rtrim(substr((string) $arg, 11), '/');
    }
}

if (!is_dir($public)) {
    fwrite(STDERR, "public directory not found\n");
    exit(1);
}

$htmlFiles = glob($public . DIRECTORY_SEPARATOR . '*.html') ?: [];
$issues = [];
$warnings = [];
$checked = 0;

foreach ($htmlFiles as $file) {
    $html = (string) file_get_contents($file);
    $document = new DOMDocument();
    libxml_use_internal_errors(true);
    $document->loadHTML($html);
    libxml_clear_errors();

    foreach (['a' => 'href', 'link' => 'href', 'script' => 'src', 'img' => 'src', 'form' => 'action'] as $tag => $attr) {
        foreach ($document->getElementsByTagName($tag) as $node) {
            if (!$node instanceof DOMElement || !$node->hasAttribute($attr)) {
                continue;
            }

            $raw = trim($node->getAttribute($attr));
            if ($raw === '' || shouldSkip($raw)) {
                continue;
            }

            if ($raw === '#') {
                $warnings[] = warning($public, $file, $raw, 'Hash-only link used as an action control.');
                continue;
            }

            if (str_starts_with($raw, './api/') || str_starts_with($raw, 'api/') || str_starts_with($raw, '/api/')) {
                $checked++;
                continue;
            }

            $checked++;
            [$pathPart, $fragment] = splitLink($raw);
            $target = $pathPart === '.' && str_starts_with($raw, '#')
                ? $file
                : resolveTarget($root, dirname($file), $pathPart);
            if ($target === null || !is_file($target)) {
                if ($baseUrl !== null) {
                    $status = httpStatus(resolvePublicUrl($baseUrl, $public, $file, $raw));
                    if ($status >= 200 && $status < 400) {
                        continue;
                    }
                }
                $issues[] = issue($public, $file, $raw, 'Missing local target file.');
                continue;
            }

            if ($fragment !== '' && isHtmlTarget($target) && !targetHasAnchor($target, $fragment)) {
                $issues[] = issue($public, $file, $raw, 'Missing fragment target.');
            }
        }
    }
}

if ($baseUrl !== null) {
    foreach (['home.html', 'events.html', 'event.html?slug=demo-gala', 'account.html', 'admin.html', 'organiser.html', 'organiser/apply', 'scanner.html', 'mvp-profiles.html'] as $url) {
        $checked++;
        $status = httpStatus($baseUrl . '/' . $url);
        if ($status < 200 || $status >= 400) {
            $issues[] = [
                'page' => '[http]',
                'target' => $baseUrl . '/' . $url,
                'message' => 'HTTP smoke check failed.',
                'status' => $status,
            ];
        }
    }
}

$result = [
    'status' => $issues === [] ? ($warnings === [] ? 'ok' : 'warning') : 'fail',
    'checked' => $checked,
    'issue_count' => count($issues),
    'warning_count' => count($warnings),
    'issues' => $issues,
    'warnings' => $warnings,
    'ran_at' => date(DATE_ATOM),
];

echo json_encode($result, JSON_PRETTY_PRINT) . PHP_EOL;
exit($issues === [] ? 0 : 1);

function shouldSkip(string $value): bool
{
    $lower = strtolower($value);
    return str_starts_with($lower, 'http://')
        || str_starts_with($lower, 'https://')
        || str_starts_with($lower, 'mailto:')
        || str_starts_with($lower, 'tel:')
        || str_starts_with($lower, 'sms:')
        || str_starts_with($lower, 'data:')
        || str_starts_with($lower, 'blob:')
        || str_starts_with($lower, 'javascript:');
}

/** @return array{0:string,1:string} */
function splitLink(string $raw): array
{
    $path = $raw;
    $fragment = '';
    $hashPosition = strpos($path, '#');
    if ($hashPosition !== false) {
        $fragment = substr($path, $hashPosition + 1);
        $path = substr($path, 0, $hashPosition);
    }
    $queryPosition = strpos($path, '?');
    if ($queryPosition !== false) {
        $path = substr($path, 0, $queryPosition);
    }
    if ($path === '') {
        $path = '.';
    }
    return [$path, rawurldecode($fragment)];
}

function resolveTarget(string $root, string $sourceDir, string $path): ?string
{
    $path = str_replace('/', DIRECTORY_SEPARATOR, $path);
    if (str_starts_with($path, DIRECTORY_SEPARATOR)) {
        $candidate = $root . $path;
    } else {
        $candidate = $sourceDir . DIRECTORY_SEPARATOR . $path;
    }

    $normalized = normalizePath($candidate);
    $rootNormalized = normalizePath($root);
    if (!str_starts_with(strtolower($normalized), strtolower($rootNormalized))) {
        return null;
    }

    if (is_dir($normalized)) {
        $index = $normalized . DIRECTORY_SEPARATOR . 'index.html';
        return is_file($index) ? $index : $normalized;
    }

    return $normalized;
}

function normalizePath(string $path): string
{
    $parts = [];
    foreach (preg_split('#[\\\\/]#', $path) ?: [] as $part) {
        if ($part === '' || $part === '.') {
            continue;
        }
        if ($part === '..') {
            array_pop($parts);
            continue;
        }
        $parts[] = $part;
    }

    $prefix = preg_match('#^[A-Za-z]:#', $path) === 1 ? array_shift($parts) . DIRECTORY_SEPARATOR : DIRECTORY_SEPARATOR;
    return $prefix . implode(DIRECTORY_SEPARATOR, $parts);
}

function isHtmlTarget(string $file): bool
{
    return str_ends_with(strtolower($file), '.html');
}

function targetHasAnchor(string $file, string $fragment): bool
{
    if ($fragment === '') {
        return true;
    }

    $html = (string) file_get_contents($file);
    $quoted = preg_quote($fragment, '#');
    return preg_match('#\s(?:id|name|data-route|data-route-panel)=["\']' . $quoted . '["\']#i', $html) === 1;
}

/** @return array<string,mixed> */
function issue(string $public, string $source, string $target, string $message): array
{
    return [
        'page' => relativeToPublic($public, $source),
        'target' => $target,
        'message' => $message,
    ];
}

/** @return array<string,mixed> */
function warning(string $public, string $source, string $target, string $message): array
{
    return issue($public, $source, $target, $message);
}

function relativeToPublic(string $public, string $file): string
{
    return str_replace(DIRECTORY_SEPARATOR, '/', ltrim(substr($file, strlen($public)), DIRECTORY_SEPARATOR));
}

function resolvePublicUrl(string $baseUrl, string $public, string $source, string $target): string
{
    [$path] = splitLink($target);
    if (preg_match('#^/zavvion-events/public/?(.*)$#', $path, $matches) === 1) {
        $path = ltrim($matches[1], '/');
    } elseif (str_starts_with($path, '/')) {
        $path = ltrim($path, '/');
    } elseif (!str_starts_with($path, './') && !str_starts_with($path, '../')) {
        $path = ltrim($path, '/');
    } else {
        $sourceRelative = str_replace(DIRECTORY_SEPARATOR, '/', trim(substr(dirname($source), strlen($public)), DIRECTORY_SEPARATOR));
        $path = trim($sourceRelative . '/' . $path, '/');
    }

    $parts = [];
    foreach (explode('/', str_replace('\\', '/', $path)) as $part) {
        if ($part === '' || $part === '.') {
            continue;
        }
        if ($part === '..') {
            array_pop($parts);
            continue;
        }
        $parts[] = $part;
    }

    return $baseUrl . '/' . implode('/', $parts);
}

function httpStatus(string $url): int
{
    $context = stream_context_create([
        'http' => [
            'method' => 'GET',
            'timeout' => 10,
            'ignore_errors' => true,
        ],
    ]);
    $stream = @fopen($url, 'r', false, $context);
    if ($stream === false) {
        return 0;
    }
    fclose($stream);

    $headers = $http_response_header ?? [];
    foreach ($headers as $header) {
        if (preg_match('#^HTTP/\S+\s+(\d{3})#', $header, $match) === 1) {
            return (int) $match[1];
        }
    }

    return 0;
}
