<?php

declare(strict_types=1);

namespace Atdt;

/**
 * Loads and runs doors (SPEC §13.1-13.2). A door is doors/<slug>/ with a
 * door.json manifest and an entry file declaring a DoorInterface class.
 * The runner validates the manifest, includes the entry once, and keeps
 * the fatal wall: any Throwable inside a door is caught by DoorScreen.
 */

final class DoorRunner
{
    /** @var array<string, DoorInterface> per-request instance cache */
    private static array $instances = [];

    /**
     * The sanctioned announcement path (1.5.5): post a message into a
     * board area under the sysop's name on a door's behalf. Used by the
     * cron/admin door contexts; fail-soft on a missing area or account.
     * @param list<string> $lines pipe-coded body lines
     */
    public static function postAsSysop(Db $db, Settings $settings, string $slug, string $areaTag, string $subject, array $lines): bool
    {
        $area = $db->one('SELECT * FROM message_areas WHERE tag = ?', [strtoupper(trim($areaTag))]);
        if ($area === null) {
            return false;
        }
        $sysop = $db->one('SELECT * FROM users WHERE handle = ?', [(string)$settings->get('sysop_handle', '')])
            ?? $db->one('SELECT * FROM users ORDER BY id LIMIT 1');
        if ($sysop === null) {
            return false;
        }
        $body = array_slice(array_map('strval', array_values($lines)), 0, Messages::MAX_LINES);
        Messages::post(
            $db,
            $settings,
            (int)$area['id'],
            $sysop,
            'All',
            null,
            mb_strimwidth(trim($subject) === '' ? 'From the door ' . $slug : $subject, 0, 60),
            $body === [] ? ['(empty)'] : $body,
            null
        );
        atdt_log_event('info', 'door:' . $slug, 'posted "' . mb_strimwidth($subject, 0, 60) . '" to ' . strtoupper($areaTag));
        return true;
    }

    /** @return ?array<string, mixed> the manifest, or null when invalid */
    public static function manifest(string $dir): ?array
    {
        $file = $dir . '/door.json';
        if (!is_file($file)) {
            return null;
        }
        $m = json_decode((string)@file_get_contents($file), true);
        if (!is_array($m)) {
            return null;
        }
        $slug = (string)($m['slug'] ?? '');
        $entry = (string)($m['entry'] ?? '');
        $class = (string)($m['class'] ?? '');
        if (
            (int)($m['api'] ?? 0) !== 1
            || preg_match('/^[a-z0-9][a-z0-9_-]{1,31}$/', $slug) !== 1
            || preg_match('/^[a-z0-9_.-]+\.php$/i', $entry) !== 1
            || $class === ''
            || trim((string)($m['title'] ?? '')) === ''
        ) {
            return null;
        }
        $m['dir'] = $dir;
        return $m;
    }

    /** Where installed doors live; data/doors allows FTP-free installs too. */
    public static function dirFor(string $slugOrDir): ?string
    {
        foreach ([ATDT_ROOT . '/doors/', ATDT_ROOT . '/data/doors/'] as $root) {
            if (is_dir($root . $slugOrDir)) {
                return $root . $slugOrDir;
            }
        }
        return null;
    }

    /** @return list<array<string, mixed>> manifests found on disk */
    public static function scan(): array
    {
        $found = [];
        foreach ([ATDT_ROOT . '/doors/*', ATDT_ROOT . '/data/doors/*'] as $pattern) {
            foreach (glob($pattern, GLOB_ONLYDIR) ?: [] as $dir) {
                $m = self::manifest($dir);
                if ($m !== null && !isset($found[$m['slug']])) {
                    $found[$m['slug']] = $m;
                }
            }
        }
        return array_values($found);
    }

    /** Instantiate (once per request) the door registered under a slug. */
    public static function load(Db $db, string $slug): ?DoorInterface
    {
        if (isset(self::$instances[$slug])) {
            return self::$instances[$slug];
        }
        $row = $db->one('SELECT * FROM doors WHERE slug = ? AND enabled = 1', [$slug]);
        if ($row === null) {
            return null;
        }
        $dir = self::dirFor((string)$row['dir']) ?? self::dirFor($slug);
        if ($dir === null) {
            return null;
        }
        $m = self::manifest($dir);
        if ($m === null) {
            return null;
        }
        require_once $dir . '/' . $m['entry'];
        $class = (string)$m['class'];
        if (!class_exists($class) || !is_subclass_of($class, DoorInterface::class)) {
            return null;
        }
        return self::$instances[$slug] = new $class();
    }
}
