<?php

declare(strict_types=1);

namespace Atdt;

/**
 * What a door is allowed to touch (SPEC §13.4). Wraps the engine Ctx
 * without exposing it: output building, the two client input modes, the
 * slug-scoped store, the caller, and a handful of flavor services. The
 * state bag rides the session stack frame like any screen's.
 */
final class DoorContext
{
    /** @var array<string, mixed> arbitrary JSON-serializable door state */
    public array $state = [];

    public function __construct(
        private readonly Ctx $ctx,
        private readonly DoorStore $store,
        private readonly string $slug,
        private readonly string $doorTitle,
    ) {
        $this->state = (array)$ctx->get('door_state', []);
    }

    /** @internal engine-side: persist the bag back into the stack frame. */
    public function persistState(): void
    {
        $this->ctx->set('door_state', $this->state);
    }

    // ---- output ----------------------------------------------------------

    /** A fresh Ansi builder (->cls()->at()->fg()->bg()->txt()...). */
    public function out(): Ansi
    {
        return Ansi::make();
    }

    /** Render |NN pipe codes to ANSI. */
    public function pipe(string $s): string
    {
        return $this->ctx->pipe($s);
    }

    /** Center a rendered line on the 80-column screen. */
    public function center(string $s): string
    {
        return $this->ctx->center($s);
    }

    /** An ansi frame for the client. */
    public function ansi(string $payload): array
    {
        return $this->ctx->ansi($payload);
    }

    public function bell(): array
    {
        return $this->ctx->bell();
    }

    /** Hotkey mode: next keypress arrives as In::key. The hardware
     *  cursor and the key echo stay off inside doors (1.7.4): a door
     *  paints its own prompt marker wherever its layout wants one, its
     *  last output is rarely the prompt (Star Merchant parked the bar
     *  under the status line), and a door that repaints only part of
     *  the screen never erases an echoed key (Morrow's map collected
     *  every numpad press). The board's own screens keep both. */
    public function hotkey(): array
    {
        return $this->ctx->hotkey(false);
    }

    /** Line mode at x,y (1-based), max chars; arrives as In::line. */
    public function line(int $x, int $y, int $max, string $initial = ''): array
    {
        return $this->ctx->line($x, $y, $max, initial: $initial);
    }

    /** The full-screen editor (§7.5); arrives as In::editor lines. */
    public function editor(string $title, array $initial = []): array
    {
        return [
            't' => 'mode', 'm' => 'editor',
            'rows' => 23, 'cols' => 79,
            'initial' => array_values($initial),
            'title' => Pipe::strip($title),
        ];
    }

    /** Pop back to the menu (the door's leave() runs on the way out). */
    public function exitDoor(): array
    {
        return ['t' => '_door_exit'];
    }

    // ---- persistence -----------------------------------------------------

    public function kv(): DoorStore
    {
        return $this->store;
    }

    public function table(string $name): DoorTable
    {
        return $this->store->table($name);
    }

    public function files(): DoorStore
    {
        return $this->store;
    }

    // ---- the caller ------------------------------------------------------

    /** @return array<string, mixed> handle/level/location/... (never null in a door) */
    public function user(): array
    {
        return $this->ctx->user() ?? ['id' => 0, 'handle' => 'caller', 'level' => 0];
    }

    public function userId(): int
    {
        return (int)($this->user()['id'] ?? 0);
    }

    public function handle(): string
    {
        return (string)($this->user()['handle'] ?? 'caller');
    }

    public function node(): int
    {
        return $this->ctx->node();
    }

    /** Seconds of session left. Sessions are untimed (owner's call): a day. */
    public function timeLeft(): int
    {
        return 86400;
    }

    /** Optional flavor on an untimed board: recorded, never enforced. */
    public function chargeTime(int $secs): void
    {
        $this->log('charged ' . max(0, $secs) . 's of flavor time');
    }

    /** Add to the global door scoreboard (shown on the board's Stats). */
    public function award(int $points): void
    {
        $db = $this->ctx->db();
        $k = $this->slug . ':' . $this->userId();
        $have = (int)($db->val(
            "SELECT v FROM door_kv WHERE door_slug = '_scores' AND k = ?",
            [$k]
        ) ?? 0);
        // Upsert (1.7.4): an award of zero points used to read as "no
        // row" on MySQL and die on the primary key.
        $db->upsert('door_kv', ['door_slug' => '_scores', 'k' => $k, 'v' => (string)($have + $points)], ['door_slug', 'k']);
    }

    /** Events with the door's prefix, into the board's log. */
    public function log(string $msg): void
    {
        $this->ctx->db()->insert('events', [
            'at' => now_utc(), 'level' => 'info', 'actor' => 'door:' . $this->slug,
            'msg' => mb_strimwidth($msg, 0, 200),
        ]);
    }

    /** Board time for display. */
    public function localStamp(string $utc, string $format = 'm-d H:i'): string
    {
        return $this->ctx->localStamp($utc, $format);
    }

    /** Recent approved board rumors (the cantina cross-pollination, §15.2). */
    public function boardRumors(int $limit = 5): array
    {
        $rows = $this->ctx->db()->all(
            'SELECT body FROM rumors WHERE approved = 1 ORDER BY id DESC LIMIT ' . max(1, min(10, $limit))
        );
        return array_map(fn(array $r) => (string)$r['body'], $rows);
    }

    // ---- the drop file ---------------------------------------------------

    /** @return array<string, mixed> */
    public function dropfile(): array
    {
        $u = $this->user();
        return [
            'carrier' => [
                'version' => ATDT_VERSION,
                'api' => 1,
                'board' => (string)$this->ctx->settings()->get('board_name', 'ATDT'),
                'base_url' => (string)$this->ctx->settings()->get('base_url', ''),
            ],
            'node' => $this->node(),
            'user' => [
                'id' => (int)($u['id'] ?? 0),
                'handle' => (string)($u['handle'] ?? 'caller'),
                'level' => (int)($u['level'] ?? 0),
                'location' => (string)($u['location'] ?? ''),
                'calls' => (int)($u['calls'] ?? 0),
                'posts' => (int)($u['posts'] ?? 0),
            ],
            'session' => [
                'time_left_secs' => $this->timeLeft(),
                'ice_colors' => $this->ctx->settings()->bool('ice_colors', false),
            ],
            'door' => [
                'slug' => $this->slug,
                'title' => $this->doorTitle,
                'data_dir' => 'data/doorstore/' . $this->slug . '/',
            ],
        ];
    }
}
