<?php

declare(strict_types=1);

namespace Atdt\Screens;

use Atdt\Acs;
use Atdt\Ansi;
use Atdt\Ctx;
use Atdt\In;
use Atdt\Pipe;
use Atdt\Screen;

/**
 * Bulletins (SPEC §10.6): ACS-filtered list, pick a number, read with the
 * classic pause. The post-login sequence shows freshly-updated ones.
 */
final class BulletinsScreen implements Screen
{
    private const BODY_ROWS = 16;
    private const ROW_IN = 21;
    private const ROW_MSG = 24;

    public function describe(): string
    {
        return 'Reading the bulletins';
    }

    public function enter(Ctx $c): array
    {
        $c->set('step', 'pick');
        $rows = self::visible($c);
        $o = $c->out()->cls();
        $o->at(2, 1)->txt($c->center($c->tstr('bull.title')));
        $o->at(3, 1)->fg(8)->txt($c->center(str_repeat('─', 44)))->reset();
        if ($rows === []) {
            $o->at(10, 1)->txt($c->center($c->tstr('bull.empty')));
            $mode = $c->promptLine($o, self::ROW_IN, 26, 'vote.pick_empty', 3);
            return [$c->ansi($o->str()), $mode];
        }
        $row = 6;
        foreach ($rows as $i => $b) {
            $o->at($row++, 10)->txt($c->pipe(sprintf(
                '|14%2d|08) |15%-46s |08%s',
                $i + 1,
                Pipe::escape(mb_strimwidth((string)$b['title'], 0, 46)),
                $c->localStamp((string)$b['updated_at'], 'Y-m-d')
            )));
            if ($row > 19) {
                break;
            }
        }
        $mode = $c->promptLine($o, self::ROW_IN, 22, 'bull.pick', 3);
        return [$c->ansi($o->str()), $mode];
    }

    public function input(Ctx $c, In $in): array
    {
        $step = (string)$c->get('step', 'pick');
        if ($in->kind === 'key') {
            if ($step === 'read') {
                $pages = (int)$c->get('pages', 1);
                $page = (int)$c->get('page', 0);
                if ($page < $pages - 1 && strtoupper((string)$in->key) !== 'N') {
                    return $this->read($c, (int)$c->get('bull', 0), $page + 1);
                }
                return $this->enter($c);
            }
            return [$c->bell()];
        }
        if ($in->kind !== 'line') {
            return [$c->bell()];
        }
        if ($in->line === null || trim($in->line) === '') {
            return $c->pop();
        }
        $rows = self::visible($c);
        $pick = trim($in->line);
        $b = ctype_digit($pick) ? ($rows[(int)$pick - 1] ?? null) : null;
        if ($b === null) {
            $frames = $this->enter($c);
            $frames[] = $c->ansi(Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K")
                ->txt($c->center($c->tstr('bull.bad_pick')))->str());
            return $frames;
        }
        return $this->read($c, (int)$b['id'], 0);
    }

    private function read(Ctx $c, int $id, int $page): array
    {
        $b = $c->db()->one('SELECT * FROM bulletins WHERE id = ?', [$id]);
        if ($b === null) {
            return $this->enter($c);
        }
        // Art-backed bulletins (M7): the .ans full-screen, any key returns.
        // Names a gallery piece since 1.1.0 (theme slots as legacy);
        // a missing piece falls through to the text body below.
        if (trim((string)($b['art'] ?? '')) !== '') {
            $art = \Atdt\Art::load(trim((string)$b['art'])) ?? $c->theme()->art(trim((string)$b['art']));
            if ($art !== null) {
                $c->set('step', 'read');
                $c->set('bull', $id);
                $c->set('page', 0);
                $c->set('pages', 1);
                // The pause follows the art (1.5.5) instead of row 25.
                $o = Ansi::make()->cls()->txt($c->mci($art))
                    ->at(min(25, \Atdt\Art::textRows($art) + 2), 1)
                    ->txt($c->center($c->tstr('nua.press_any_key')));
                return [$c->ansi($o->reset()->str()), $c->hotkey()];
            }
        }
        $c->set('step', 'read');
        $c->set('bull', $id);
        // Sysop ink: pipes always render, and the body gets the full 80
        // columns so classic-width text pasted into the console survives.
        $lines = [];
        foreach (explode("\n", str_replace("\r", '', (string)$b['body'])) as $line) {
            foreach (Pipe::wrapVisible(rtrim($line), 80) as $piece) {
                $lines[] = $piece;
            }
        }
        $pages = max(1, (int)ceil(count($lines) / self::BODY_ROWS));
        $page = max(0, min($page, $pages - 1));
        $c->set('page', $page);
        $c->set('pages', $pages);

        $o = $c->out()->cls();
        $o->at(2, 1)->txt($c->center($c->pipe('|15' . Pipe::escape((string)$b['title']))));
        $o->at(3, 1)->fg(8)->txt($c->center(str_repeat('─', 60)))->reset();
        $row = 5;
        foreach (array_slice($lines, $page * self::BODY_ROWS, self::BODY_ROWS) as $line) {
            $o->at($row++, 1)->fg(7)->txt(Pipe::render($line, $c->mciVars()));
        }
        $o->reset()->at(22, 1)->txt($c->center(
            $page < $pages - 1 ? $c->tstr('pause.more') : $c->tstr('nua.press_any_key')
        ));
        return [$c->ansi($o->str()), $c->hotkey()];
    }

    /** @return list<array<string, mixed>> */
    public static function visible(Ctx $c): array
    {
        // Newest ink first (1.5.5). A sysop's manual sort still rules;
        // within a sort band the freshest bulletin tops the list.
        return array_values(array_filter(
            $c->db()->all('SELECT * FROM bulletins ORDER BY sort, id DESC'),
            fn(array $b) => Acs::check((string)($b['acs'] ?? ''), $c->user())
        ));
    }
}
