<?php
/*
 * Searchable AI crawler tracker for evolutiondigitalmarketing.ca (static site on Hostinger).
 *
 * .htaccess sends page requests from AI crawlers and AI-assistant referrals here.
 * This file serves exactly the same static file a normal visitor gets, closes the
 * response, and only then reports the hit to Searchable (REST ingest API), so the
 * crawler never waits on Searchable. Ordinary visitors never touch this file.
 *
 * Added 2026-10-26 (Evolution Digital Marketing). Searchable project: Evolution Digital Marketing
 * Property Services. Docs: https://docs.searchable.com/setup/rest-api
 */

// Keys live in searchable-config.php next to this file (a PHP file, so the web
// never shows its contents). Until both keys are filled in, pages are still
// served normally and nothing is reported.
$pps_cfg = is_file(__DIR__ . '/searchable-config.php') ? (include __DIR__ . '/searchable-config.php') : [];
define('SEARCHABLE_SITE_TOKEN', is_array($pps_cfg) ? (string) ($pps_cfg['site_token'] ?? '') : '');
define('SEARCHABLE_API_KEY', is_array($pps_cfg) ? (string) ($pps_cfg['api_key'] ?? '') : '');
const SEARCHABLE_ENDPOINT = 'https://tracker.searchableanalytics.com/v1/events';

// Only these file types are ever served from here (never .php, never dotfiles).
const SERVABLE = [
    'html' => 'text/html; charset=UTF-8',
    'htm'  => 'text/html; charset=UTF-8',
    'xml'  => 'application/xml; charset=UTF-8',
    'txt'  => 'text/plain; charset=UTF-8',
];

$started = microtime(true);
$method  = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
$root    = realpath($_SERVER['DOCUMENT_ROOT'] ?? __DIR__) ?: __DIR__;
$uri     = $_SERVER['REQUEST_URI'] ?? '/';
$path    = parse_url($uri, PHP_URL_PATH);
$path    = is_string($path) && $path !== '' ? rawurldecode($path) : '/';

// ---- 1. Resolve the request to a real static file inside the web root ----------
function pps_resolve(string $root, string $path): ?string
{
    if (strpos($path, "\0") !== false) {
        return null;
    }
    foreach (explode('/', $path) as $segment) {
        if ($segment !== '' && $segment[0] === '.') {
            return null; // no dotfiles / dot-folders, no "../"
        }
    }
    $candidate = $root . $path;
    if (substr($path, -1) === '/') {
        $candidate .= 'index.html';
    }
    $real = realpath($candidate);
    if ($real === false || !is_file($real) || strpos($real, $root . DIRECTORY_SEPARATOR) !== 0) {
        return null;
    }
    $ext = strtolower(pathinfo($real, PATHINFO_EXTENSION));
    return isset(SERVABLE[$ext]) ? $real : null;
}

$file   = pps_resolve($root, $path);
$status = 200;
if ($file === null) {
    $status = 404;
    $file   = is_file($root . '/404.html') ? $root . '/404.html' : null;
}

// ---- 2. Serve it (same bytes as a normal visit) -------------------------------
if ($method !== 'GET' && $method !== 'HEAD') {
    $status = 405;
    http_response_code(405);
    header('Allow: GET, HEAD');
} elseif ($file === null) {
    http_response_code(404);
    header('Content-Type: text/plain; charset=UTF-8');
    if ($method === 'GET') {
        echo "Not found\n";
    }
} else {
    $mtime = filemtime($file);
    $etag  = '"' . dechex($mtime) . '-' . dechex(filesize($file)) . '"';
    $ext   = strtolower(pathinfo($file, PATHINFO_EXTENSION));

    $notModified = false;
    if ($status === 200) {
        $inm = $_SERVER['HTTP_IF_NONE_MATCH'] ?? '';
        $ims = $_SERVER['HTTP_IF_MODIFIED_SINCE'] ?? '';
        if ($inm !== '') {
            $notModified = trim($inm) === $etag;
        } elseif ($ims !== '' && ($t = strtotime($ims)) !== false) {
            $notModified = $mtime <= $t;
        }
    }

    header('Content-Type: ' . SERVABLE[$ext]);
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
    header('ETag: ' . $etag);

    if ($notModified) {
        $status = 304;
        http_response_code(304);
    } else {
        http_response_code($status);
        header('Content-Length: ' . filesize($file));
        if ($method === 'GET') {
            readfile($file);
        }
    }
}

// ---- 3. Close the response, then report to Searchable -------------------------
$elapsedMs = (int) round((microtime(true) - $started) * 1000);

if (function_exists('litespeed_finish_request')) {
    litespeed_finish_request();
} elseif (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();
} else {
    @ob_end_flush();
    @flush();
}
ignore_user_abort(true);

if (strpos(SEARCHABLE_SITE_TOKEN, 'st_') !== 0 || strpos(SEARCHABLE_API_KEY, 'sk_') !== 0) {
    exit; // keys not set yet: page was served, nothing to report
}

function pps_anon_ip(string $ip): string
{
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        return preg_replace('/\.\d+$/', '.0', $ip);
    }
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
        $packed = inet_pton($ip);
        return inet_ntop(substr($packed, 0, 6) . str_repeat("\0", 10));
    }
    return '0.0.0.0';
}

$host     = strtolower($_SERVER['HTTP_HOST'] ?? 'evolutiondigitalmarketing.ca');
$referrer = $_SERVER['HTTP_REFERER'] ?? '';
$query    = [];
parse_str((string) parse_url($uri, PHP_URL_QUERY), $query);

$event = [
    'event_name'       => 'server_request',
    'timestamp'        => (int) round(microtime(true) * 1000),
    'domain'           => $host,
    'method'           => $method,
    'path'             => $path,
    'url'              => 'https://' . $host . $uri,
    'status_code'      => $status,
    'response_time_ms' => $elapsedMs,
    'user_agent'       => (string) ($_SERVER['HTTP_USER_AGENT'] ?? ''),
    'ip_address'       => pps_anon_ip((string) ($_SERVER['REMOTE_ADDR'] ?? '')),
    'headers'          => array_filter([
        'accept'          => $_SERVER['HTTP_ACCEPT'] ?? null,
        'accept-language' => $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? null,
        'from'            => $_SERVER['HTTP_FROM'] ?? null,
    ]),
];
if ($referrer !== '') {
    $event['referrer']        = $referrer;
    $event['referrer_domain'] = (string) parse_url($referrer, PHP_URL_HOST);
}
foreach (['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'] as $k) {
    if (isset($query[$k]) && is_string($query[$k])) {
        $event[$k] = $query[$k];
    }
}
if ($query) {
    $event['query_parameters'] = $query;
}

$body = json_encode(['site_token' => SEARCHABLE_SITE_TOKEN, 'events' => [$event]], JSON_UNESCAPED_SLASHES);

if (function_exists('curl_init')) {
    $ch = curl_init(SEARCHABLE_ENDPOINT);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . SEARCHABLE_API_KEY,
            'Content-Type: application/json',
        ],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CONNECTTIMEOUT => 2,
        CURLOPT_TIMEOUT        => 4,
    ]);
    curl_exec($ch);
    curl_close($ch);
} else {
    @file_get_contents(SEARCHABLE_ENDPOINT, false, stream_context_create(['http' => [
        'method'  => 'POST',
        'header'  => "Authorization: Bearer " . SEARCHABLE_API_KEY . "\r\nContent-Type: application/json\r\n",
        'content' => $body,
        'timeout' => 4,
    ]]));
}
