<?php

declare(strict_types=1);

namespace Atdt\Screens;

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

/**
 * The BBS list (SPEC §10.6): the traditional "other boards" directory.
 * Viewing is open to everyone; adding is gated by the bbslist_add_acs
 * setting (default v = any validated caller). Addresses are validated as
 * web URLs, telnet URLs, or host:port. The console's Community page edits
 * and prunes entries.
 */
final class BbsListScreen implements Screen
{
    private const PER_PAGE = 7; // two rows per entry (name line + notes line)
    private const ROW_IN = 22;
    private const ROW_MSG = 24;

    public function describe(): string
    {
        return 'Browsing the BBS list';
    }

    public function enter(Ctx $c): array
    {
        $c->set('step', 'list');
        $rows = $c->db()->all('SELECT * FROM bbslist ORDER BY id');
        $page = max(0, (int)$c->get('page', 0));
        $pages = max(1, (int)ceil(count($rows) / self::PER_PAGE));
        $page = min($page, $pages - 1);
        $c->set('page', $page);

        $o = $c->out()->cls();
        $o->at(2, 1)->txt($c->center($c->tstr('bbs.title')));
        $o->at(3, 1)->fg(8)->txt($c->center(str_repeat('─', 50)))->reset();
        if ($rows === []) {
            $o->at(10, 1)->txt($c->center($c->tstr('bbs.empty')));
        } else {
            $o->at(5, 4)->txt($c->tstr('bbs.columns', ['PG' => ($page + 1) . '/' . $pages]));
            if ($pages > 1) {
                $tag = 'page ' . ($page + 1) . '/' . $pages;
                $o->at(5, 78 - mb_strlen($tag))->txt($c->pipe('|08' . $tag));
            }
            $row = 7;
            foreach (array_slice($rows, $page * self::PER_PAGE, self::PER_PAGE) as $i => $b) {
                $o->at($row++, 4)->txt($c->pipe(sprintf(
                    '|08%2d) |15%-24s |11%s',
                    $page * self::PER_PAGE + $i + 1,
                    Pipe::escape(mb_strimwidth((string)$b['name'], 0, 24)),
                    Pipe::escape(mb_strimwidth((string)$b['host'], 0, 46))
                )));
                $notes = trim((string)$b['notes']);
                if ($notes !== '') {
                    $o->at($row, 8)->txt($c->pipe('|08' . Pipe::escape(mb_strimwidth($notes, 0, 70))));
                }
                $row++;
            }
        }
        $sysop = (int)(($c->user() ?? [])['level'] ?? 0) === 255;
        $o->at(21, 1)->txt($c->center($c->tstr($sysop ? 'bbs.prompt_sysop' : 'bbs.prompt')));
        return [$c->ansi($o->reset()->str()), $c->hotkey()];
    }

    public function input(Ctx $c, In $in): array
    {
        if ($in->kind === 'line') {
            return $this->lineEntered($c, $in->line);
        }
        if ($in->kind !== 'key' || $c->get('step') !== 'list') {
            return [$c->bell()];
        }
        switch (strtoupper((string)$in->key)) {
            case 'A':
                $acs = (string)($c->settings()->get('bbslist_add_acs', 'v') ?? 'v');
                if (!\Atdt\Acs::check($acs, $c->user())) {
                    return [
                        $c->bell(),
                        $c->ansi(Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K")
                            ->txt($c->center($c->tstr('bbs.no_add')))->str()),
                        $c->hotkey(),
                    ];
                }
                $c->set('step', 'name');
                $c->set('draft', []);
                return $this->ask($c, 'bbs.ask_name');
            case 'D':
                if ((int)(($c->user() ?? [])['level'] ?? 0) !== 255) {
                    return [$c->bell()];
                }
                $c->set('step', 'del');
                return $this->ask($c, 'bbs.ask_del');
            case 'N':
                $c->set('page', (int)$c->get('page', 0) + 1);
                return $this->enter($c);
            case 'P':
                $c->set('page', max(0, (int)$c->get('page', 0) - 1));
                return $this->enter($c);
            case 'Q':
            case 'ESC':
                return $c->pop();
            case '?':
                return $this->enter($c);
            default:
                return [$c->bell()];
        }
    }

    private function ask(Ctx $c, string $stringKey, int $max = 40): array
    {
        $o = Ansi::make()->at(self::ROW_IN, 1)->txt("\x1b[K");
        if ($stringKey === 'bbs.ask_host') {
            // The accepted formats ride their own row so the prompt stays
            // short and the address gets the full input width (round 4).
            $o->at(self::ROW_IN - 1, 1)->txt("\x1b[K")->txt($c->center($c->tstr('bbs.host_hint')));
        }
        $mode = $c->promptLine($o, self::ROW_IN, 10, $stringKey, $max);
        return [$c->ansi($o->str()), $mode];
    }

    /** @param ?string $line */
    private function lineEntered(Ctx $c, ?string $line): array
    {
        if ($line === null) {
            $c->set('step', 'list');
            return $this->enter($c);
        }
        $value = Chat::clean($line);
        $draft = (array)$c->get('draft', []);
        switch ((string)$c->get('step')) {
            case 'name':
                if (mb_strlen($value) < 2) {
                    return $this->ask($c, 'bbs.ask_name');
                }
                $draft['name'] = mb_strimwidth($value, 0, 30);
                $c->set('draft', $draft);
                $c->set('step', 'host');
                return $this->ask($c, 'bbs.ask_host', 60);
            case 'host':
                // Name + address is the whole application (owner round 3);
                // the console's Community page can add sysop/notes later.
                if (!self::validHost($value)) {
                    $frames = $this->ask($c, 'bbs.ask_host', 60);
                    array_unshift($frames, $c->ansi(
                        Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K")
                            ->txt($c->center($c->tstr('bbs.bad_host')))->str()
                    ));
                    return $frames;
                }
                $c->db()->insert('bbslist', [
                    'name' => (string)($draft['name'] ?? '?'),
                    'host' => mb_strimwidth($value, 0, 60),
                    'sysop' => '',
                    'notes' => '',
                    'added_by' => (string)(($c->user() ?? [])['handle'] ?? 'caller'),
                    'at' => now_utc(),
                ]);
                $c->set('step', 'list');
                $c->set('draft', null);
                $frames = $this->enter($c);
                $frames[] = $c->ansi(Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K")
                    ->txt($c->center($c->tstr('bbs.added')))->str());
                return $frames;
            case 'del':
                $all = $c->db()->all('SELECT * FROM bbslist ORDER BY id');
                $entry = ctype_digit($value) ? ($all[(int)$value - 1] ?? null) : null;
                if ($entry === null || (int)(($c->user() ?? [])['level'] ?? 0) !== 255) {
                    $c->set('step', 'list');
                    return $this->enter($c);
                }
                $c->db()->q('DELETE FROM bbslist WHERE id = ?', [(int)$entry['id']]);
                $c->db()->insert('events', [
                    'at' => now_utc(), 'level' => 'admin', 'actor' => 'node' . $c->node(),
                    'msg' => 'Removed BBS-list entry "' . $entry['name'] . '"',
                ]);
                $c->set('step', 'list');
                $frames = $this->enter($c);
                $frames[] = $c->ansi(Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K")
                    ->txt($c->center($c->tstr('bbs.deleted')))->str());
                return $frames;
            default:
                $c->set('step', 'list');
                return $this->enter($c);
        }
    }

    /**
     * A believable board address: a web URL, a telnet URL, or host[:port].
     * Shared with the console's Community editor.
     */
    public static function validHost(string $host): bool
    {
        if ($host === '' || mb_strlen($host) > 60) {
            return false;
        }
        if (preg_match('#^(https?|telnet)://#i', $host)) {
            return filter_var($host, FILTER_VALIDATE_URL) !== false;
        }
        return (bool)preg_match('/^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9-]+)*\.[a-z]{2,}(:\d{1,5})?$/i', $host);
    }
}
