<?php

declare(strict_types=1);

/**
 * ATDT bootstrap (SPEC §9.1): constants, autoload, config access,
 * error handling. Safe to require before the board is installed — nothing
 * here touches the database until atdt_db() is called.
 *
 * Entry points that speak JSON define ATDT_JSON before requiring this
 * file so the error handler answers in kind.
 */

if (!defined('ATDT_ROOT')) {
    define('ATDT_ROOT', dirname(__DIR__));
}
if (!defined('ATDT_VERSION')) {
    define('ATDT_VERSION', '1.7.4');
}

error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
if (is_dir(ATDT_ROOT . '/data/logs')) {
    ini_set('error_log', ATDT_ROOT . '/data/logs/php-' . gmdate('Y-m-d') . '.log');
}

spl_autoload_register(static function (string $class): void {
    $prefix = 'Atdt\\';
    if (str_starts_with($class, $prefix)) {
        $rel = substr($class, strlen($prefix));
        if (str_starts_with($rel, 'Screens\\')) {
            $rel = 'screens/' . substr($rel, strlen('Screens\\')); // Atdt\Screens\X → lib/screens/X.php (§5)
        }
        $file = ATDT_ROOT . '/lib/' . str_replace('\\', '/', $rel) . '.php';
        if (is_file($file)) {
            require $file;
        }
    }
    // doors/<slug>/ namespaces are wired up by DoorRunner in M8.
});

/** Load data/config.php once; null when the board is not yet installed. */
function atdt_config(): ?array
{
    static $cfg = null;
    static $loaded = false;
    if (!$loaded) {
        $loaded = true;
        $file = ATDT_ROOT . '/data/config.php';
        if (is_file($file)) {
            $raw = require $file;
            $cfg = is_array($raw) ? $raw : null;
        }
    }
    return $cfg;
}

function atdt_installed(): bool
{
    $cfg = atdt_config();
    return $cfg !== null && !empty($cfg['install_lock']);
}

function atdt_db(): \Atdt\Db
{
    static $db = null;
    if ($db === null) {
        $cfg = atdt_config();
        if ($cfg === null) {
            throw new \RuntimeException('ATDT is not installed (data/config.php missing)');
        }
        $db = \Atdt\Db::fromConfig($cfg['db'] ?? [], ATDT_ROOT . '/data');
    }
    return $db;
}

function atdt_settings(): \Atdt\Settings
{
    static $settings = null;
    if ($settings === null) {
        $settings = new \Atdt\Settings(atdt_db());
    }
    return $settings;
}

/**
 * Asset URL with a cache-busting version (owner round 6): shared-host
 * browsers cache JS/CSS for days on heuristics alone, so upgrades used
 * to leave callers running weeks-old client code. filemtime changes with
 * every shipped build; a miss falls back to the version string.
 */
function atdt_asset(string $rel): string
{
    $t = @filemtime(ATDT_ROOT . '/' . $rel);
    return $rel . '?v=' . rawurlencode(ATDT_VERSION . ($t !== false ? '.' . $t : ''));
}

/**
 * Bring an existing board up to the current schema (SPEC §8.1). Called on
 * every entry point; one settings read when current, the ladder when not —
 * so uploading a new build over an old install upgrades itself on first hit.
 */
function atdt_migrate(): void
{
    static $done = false;
    if ($done || !atdt_installed()) {
        return;
    }
    $done = true;
    if (atdt_settings()->int('schema_version', 1) < \Atdt\Schema::VERSION) {
        // Serialize the ladder (1.1.0): two callers hitting a freshly
        // uploaded build at the same instant must not both run it.
        $lockFile = ATDT_ROOT . '/data/locks/migrate.lock';
        $fh = @fopen($lockFile, 'c');
        if ($fh !== false) {
            @flock($fh, LOCK_EX);
        }
        try {
            if (atdt_settings()->fresh('schema_version', 1) < \Atdt\Schema::VERSION) {
                \Atdt\Schema::migrate(atdt_db());
                atdt_log_event('admin', 'system', 'Schema migrated to v' . \Atdt\Schema::VERSION);
            }
        } finally {
            if ($fh !== false) {
                @flock($fh, LOCK_UN);
                @fclose($fh);
            }
        }
    }
}

/** UTC ISO-8601, the only timestamp format in storage (§20). */
function now_utc(): string
{
    return gmdate('Y-m-d\TH:i:s\Z');
}

/**
 * A stored UTC stamp rendered in the BOARD's timezone (owner round 4:
 * storage stays UTC, every human-facing display goes local).
 */
function atdt_board_stamp(string $utcStamp, string $format, \Atdt\Settings $settings): string
{
    try {
        $tz = new \DateTimeZone($settings->get('timezone', 'UTC') ?? 'UTC');
    } catch (\Throwable $e) {
        $tz = new \DateTimeZone('UTC');
    }
    $t = date_create_immutable($utcStamp, new \DateTimeZone('UTC'));
    return $t === false
        ? substr($utcStamp, 0, 16)
        : $t->setTimezone($tz)->format($format);
}

/**
 * The admin console rides its own PHP session (§11), separate from caller
 * tokens. Shared here because api.php's preview endpoint must peek at it.
 */
function atdt_admin_session_start(): void
{
    if (session_status() === PHP_SESSION_ACTIVE) {
        return;
    }
    session_name('ATDTADMIN');
    session_set_cookie_params([
        'httponly' => true,
        'samesite' => 'Lax',
        'path' => '/',
    ]);
    @session_start();
}

/** Logged-in admin user id, 0 when nobody is on the console. */
function atdt_admin_uid(): int
{
    atdt_admin_session_start();
    return (int)($_SESSION['admin_uid'] ?? 0);
}

/**
 * Timezone <option> markup: the common US & Canada zones first with plain
 * names, then UTC, then every IANA identifier. Values are always IANA ids.
 * Shared by the installer and the console settings page.
 */
function atdt_timezone_options(string $current): string
{
    $us = [
        'America/New_York' => 'Eastern',
        'America/Chicago' => 'Central',
        'America/Denver' => 'Mountain',
        'America/Phoenix' => 'Arizona (no DST)',
        'America/Los_Angeles' => 'Pacific',
        'America/Anchorage' => 'Alaska',
        'Pacific/Honolulu' => 'Hawaii',
    ];
    $esc = fn(string $s): string => htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
    $out = '<optgroup label="US &amp; Canada">';
    foreach ($us as $id => $label) {
        $out .= '<option value="' . $esc($id) . '"' . ($id === $current ? ' selected' : '') . '>'
            . $esc($label) . '</option>';
    }
    $out .= '</optgroup>';
    $out .= '<option value="UTC"' . ($current === 'UTC' ? ' selected' : '') . '>UTC</option>';
    $out .= '<optgroup label="Everywhere else (by region/city)">';
    foreach (\DateTimeZone::listIdentifiers() as $tz) {
        if ($tz === 'UTC' || isset($us[$tz])) {
            continue;
        }
        $out .= '<option' . ($tz === $current ? ' selected' : '') . '>' . $esc($tz) . '</option>';
    }
    $out .= '</optgroup>';
    return $out;
}

/** Best-effort event log; never throws (used from error paths). */
function atdt_log_event(string $level, string $actor, string $msg): void
{
    try {
        atdt_db()->insert('events', [
            'at' => now_utc(),
            'level' => $level,
            'actor' => mb_substr($actor, 0, 80),
            'msg' => mb_substr($msg, 0, 2000),
        ]);
    } catch (\Throwable $e) {
        error_log("[atdt:$level] $actor: $msg");
    }
}

/**
 * Uncaught exceptions: full details to the log + events table, a generic
 * message to the caller (§9.1.1, §19.10). Never leak traces.
 */
set_exception_handler(static function (\Throwable $e): void {
    $detail = sprintf(
        '%s: %s in %s:%d',
        get_class($e),
        $e->getMessage(),
        basename($e->getFile()),
        $e->getLine()
    );
    error_log('[atdt:uncaught] ' . $detail . "\n" . $e->getTraceAsString());
    atdt_log_event('error', 'core', $detail);
    http_response_code(500);
    if (defined('ATDT_JSON')) {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode(['error' => 'internal', 'frames' => [], 'events' => []]);
    } else {
        header('Content-Type: text/plain; charset=utf-8');
        echo "ATDT hit an internal error. Details were logged for the sysop.\n";
    }
    exit(1);
});
