<?php
/**
 * AI crawler log — feed 5 of the visibility tracker.
 *
 * public/.htaccess sends requests from known AI / search crawlers here (pages and
 * agent files only). This script appends one JSON line per request to
 * ~/domains/weewilds.com/ai-crawler-log/YYYY-MM-DD.jsonl, outside the web root,
 * then serves the same static file with the same headers LiteSpeed would send.
 * Logging can never stop the page being served. To switch it off, delete the
 * "AI crawler log" block in .htaccess.
 */

const BOTS = [
    'GPTBot', 'ChatGPT-User', 'OAI-SearchBot',
    'ClaudeBot', 'Claude-User', 'Claude-SearchBot', 'anthropic-ai',
    'PerplexityBot', 'Perplexity-User',
    'Google-Extended', 'Google-CloudVertexBot', 'GoogleOther', 'Googlebot',
    'bingbot', 'Amazonbot', 'Applebot', 'Bytespider', 'CCBot',
    'meta-externalagent', 'meta-externalfetcher', 'DuckAssistBot', 'MistralAI-User',
    'cohere-ai', 'YouBot', 'Diffbot',
];

// Mirrors LiteSpeed's responses for these files as of 2026-10-06
const TYPES = [
    'html' => ['text/html', 'max-age=0, must-revalidate'],
    'txt' => ['text/plain', 'max-age=600, must-revalidate'],
    'xml' => ['application/xml', 'max-age=600, must-revalidate'],
    'json' => ['application/json', 'max-age=600, must-revalidate'],
    'webmanifest' => ['text/plain', 'max-age=600, must-revalidate'],
];

$root = __DIR__;
$path = rawurldecode(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/');
$file = $root . $path . (substr($path, -1) === '/' ? 'index.html' : '');
$real = realpath($file);
$ext = strtolower(pathinfo($real ?: $file, PATHINFO_EXTENSION));

$ok = $real !== false
    && is_file($real)
    && strpos($real, $root . DIRECTORY_SEPARATOR) === 0
    && isset(TYPES[$ext]);
$status = $ok ? 200 : 404;
if (!$ok) {
    $real = $root . '/404.html';
    $ext = 'html';
}

// ---- log (never fatal) ------------------------------------------------------
try {
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    $bot = 'other';
    foreach (BOTS as $b) {
        if (stripos($ua, $b) !== false) { $bot = $b; break; }
    }
    $dir = dirname($root) . '/ai-crawler-log';
    if (!is_dir($dir)) { @mkdir($dir, 0750, true); }
    $line = json_encode([
        'ts' => gmdate('Y-m-d\TH:i:s\Z'),
        'bot' => $bot,
        'path' => $path,
        'status' => $status,
        'method' => $_SERVER['REQUEST_METHOD'] ?? 'GET',
        'ip' => $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? ''),
        'ua' => substr($ua, 0, 300),
    ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    @file_put_contents($dir . '/' . gmdate('Y-m-d') . '.jsonl', $line . "\n", FILE_APPEND | LOCK_EX);
    if (mt_rand(1, 200) === 1) {   // prune files older than 60 days, now and then
        foreach (glob($dir . '/*.jsonl') ?: [] as $f) {
            if (filemtime($f) < time() - 60 * 86400) { @unlink($f); }
        }
    }
} catch (\Throwable $e) {
    // Serving the page matters more than the log line
}

// ---- serve ------------------------------------------------------------------
// Match LiteSpeed exactly: no charset suffix, no X-Powered-By
ini_set('default_charset', '');
header_remove('X-Powered-By');
[$type, $cache] = TYPES[$ext];
$mtime = filemtime($real);
http_response_code($status);
header('Content-Type: ' . $type);
header('Cache-Control: ' . $cache);
if ($ext === 'html') {
    header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
}
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
header('Vary: Accept-Encoding');

$since = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';
if ($status === 200 && $since !== '' && strtotime($since) !== false && strtotime($since) >= $mtime) {
    http_response_code(304);
    exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'HEAD') {
    header('Content-Length: ' . filesize($real));
    exit;
}
readfile($real);
