<?php

declare(strict_types=1);

namespace Atdt\Screens;

use Atdt\Ansi;
use Atdt\Ctx;
use Atdt\In;
use Atdt\Messages;
use Atdt\Screen;

/**
 * The message reader (SPEC §10.5): header block, paged body with the
 * "More? [Y/n/=]" pause, footer commands N/P/R/A/T/E/Q. Marks each message
 * read as it displays (1.6.1: individually — only what is actually read).
 * In scan mode (§10.5 new scan), running off the end of the area pops back
 * to the NewScan controller.
 */
final class ReaderScreen implements Screen
{
    private const BODY_TOP = 7;
    private const BODY_ROWS = 16;
    private const ROW_PROMPT = 24;

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

    public function enter(Ctx $c): array
    {
        $area = $this->area($c);
        if ($area === null) {
            return $this->bailWithNotice($c, 'msg.no_area');
        }
        $c->set('area_id', (int)$area['id']);
        if ($c->get('num') === null) {
            $num = Messages::firstUnreadNum($c->db(), $c->user(), (int)$area['id'])
                ?? (Messages::lastNum($c->db(), (int)$area['id']) ?: null);
            if ($num === null) {
                return $this->bailWithNotice($c, 'msg.area_empty');
            }
            $c->set('num', $num);
        }
        return $this->show($c, 0);
    }

    public function input(Ctx $c, In $in): array
    {
        if ($in->kind !== 'key') {
            return [$c->bell()];
        }
        $key = strtoupper((string)$in->key);
        if ($c->get('step') === 'pause') {
            return match ($key) {
                'N' => $this->footer($c),
                '=' => $this->show($c, -1),
                default => $this->show($c, (int)$c->get('page', 0) + 1),
            };
        }
        return match ($key) {
            'N', 'ENTER' => $this->jump($c, +1),
            'P' => $this->jump($c, -1),
            'A' => $this->show($c, 0),
            'T' => $this->thread($c),
            'R' => $c->push('Composer', [
                'mode' => 'reply',
                'area_id' => (int)$c->get('area_id'),
                'reply_to' => $this->currentId($c),
            ]),
            'E' => $c->push('Composer', [
                'mode' => 'post',
                'area_id' => (int)$c->get('area_id'),
            ]),
            'Q', 'ESC' => $c->pop(),
            '?' => $this->show($c, 0),
            default => [$c->bell()],
        };
    }

    // ------------------------------------------------------------------

    private function area(Ctx $c): ?array
    {
        $id = $c->get('area_id');
        if ($id !== null) {
            $area = Messages::area($c->db(), (int)$id);
            if ($area !== null) {
                return $area;
            }
        }
        return Messages::currentArea($c->db(), $c->user());
    }

    private function currentId(Ctx $c): ?int
    {
        $msg = Messages::byNum($c->db(), (int)$c->get('area_id'), (int)$c->get('num'));
        return $msg === null ? null : (int)$msg['id'];
    }

    /** Render a page of the current message. $page -1 = nonstop to the end. */
    private function show(Ctx $c, int $page): array
    {
        $areaId = (int)$c->get('area_id');
        $msg = Messages::byNum($c->db(), $areaId, (int)$c->get('num'));
        if ($msg === null) {
            // Deleted underneath us — slide to a neighbor or bail out.
            $near = Messages::neighborNum($c->db(), $areaId, (int)$c->get('num'), +1)
                ?? Messages::neighborNum($c->db(), $areaId, (int)$c->get('num'), -1);
            if ($near === null) {
                return $this->bailWithNotice($c, 'msg.area_empty');
            }
            $c->set('num', $near);
            $msg = Messages::byNum($c->db(), $areaId, $near);
        }
        // The message on screen is the one that is read (1.6.1): the
        // pointer no longer leaps over unread messages below a reply.
        Messages::markReadOne($c->db(), $c->user(), $areaId, (int)$msg['num']);

        $lines = $this->bodyLines($c, (string)$msg['body']);
        $pages = max(1, (int)ceil(count($lines) / self::BODY_ROWS));
        if ($page === -1) {
            $page = $pages - 1; // '=': no more pauses, land on the tail
        }
        $page = max(0, min($page, $pages - 1));
        $c->set('page', $page);

        $o = $c->out()->cls();
        $this->header($c, $o, $msg);
        $chunk = array_slice($lines, $page * self::BODY_ROWS, self::BODY_ROWS);
        $row = self::BODY_TOP;
        foreach ($chunk as $line) {
            $o->at($row++, 2)->fg(7)->txt(\Atdt\Pipe::render($line, $c->mciVars()));
        }
        $o->reset();

        if ($page < $pages - 1) {
            $c->set('step', 'pause');
            $o->at(self::ROW_PROMPT, 1)->txt("\x1b[K")
                ->txt($c->center($c->tstr('msg.more', ['PG' => ($page + 2) . '/' . $pages])));
            return [$c->ansi($o->str()), $c->hotkey()];
        }
        $c->set('step', 'cmd');
        $o->at(self::ROW_PROMPT, 1)->txt("\x1b[K")->txt($c->center($c->tstr('msg.reader_cmds')));
        return [$c->ansi($o->str()), $c->hotkey()];
    }

    private function footer(Ctx $c): array
    {
        $c->set('step', 'cmd');
        $o = Ansi::make()->at(self::ROW_PROMPT, 1)->txt("\x1b[K")
            ->txt($c->center($c->tstr('msg.reader_cmds')));
        return [$c->ansi($o->str()), $c->hotkey()];
    }

    private function jump(Ctx $c, int $dir): array
    {
        $areaId = (int)$c->get('area_id');
        $next = Messages::neighborNum($c->db(), $areaId, (int)$c->get('num'), $dir);
        if ($next === null) {
            if ($dir > 0 && (bool)$c->get('scan', false)) {
                return $c->pop(); // area consumed — NewScan advances (§10.5)
            }
            $o = Ansi::make()->at(self::ROW_PROMPT, 1)->txt("\x1b[K")
                ->txt($c->center($c->tstr($dir > 0 ? 'msg.at_end' : 'msg.at_start')));
            return [$c->bell(), $c->ansi($o->str()), $c->hotkey()];
        }
        $c->set('num', $next);
        return $this->show($c, 0);
    }

    private function thread(Ctx $c): array
    {
        $msg = Messages::byNum($c->db(), (int)$c->get('area_id'), (int)$c->get('num'));
        $parent = $msg === null || $msg['reply_to_id'] === null
            ? null
            : Messages::byId($c->db(), (int)$msg['reply_to_id']);
        if ($parent === null || (int)$parent['area_id'] !== (int)$c->get('area_id')) {
            $o = Ansi::make()->at(self::ROW_PROMPT, 1)->txt("\x1b[K")
                ->txt($c->center($c->tstr('msg.no_thread')));
            return [$c->bell(), $c->ansi($o->str()), $c->hotkey()];
        }
        $c->set('num', (int)$parent['num']);
        return $this->show($c, 0);
    }

    private function header(Ctx $c, Ansi $o, array $msg): void
    {
        $area = Messages::area($c->db(), (int)$msg['area_id']) ?? ['name' => '?', 'id' => 0];
        $total = Messages::lastNum($c->db(), (int)$msg['area_id']);
        $vars = [
            'AR' => Messages::areaLabel($area),
            'MN' => (string)(int)$msg['num'],
            'MT' => (string)$total,
        ];
        $o->at(1, 2)->fg(8)->txt(str_repeat('─', 78));
        $o->at(2, 4)->txt($c->tstr('msg.hdr_area', $vars));
        $o->at(3, 4)->txt($c->tstr('msg.hdr_from', [
            'FR' => (string)$msg['from_name'],
            'TO' => (string)$msg['to_name'],
        ]));
        // Subject rides in as a var: values are never re-scanned, so its
        // pipes display as typed with no escaping (1.1.1 fixes the doubled
        // pipe), while mci() lets %XX expand for the reader.
        $o->at(4, 4)->txt($c->tstr('msg.hdr_subject', [
            'SU' => $c->mci((string)$msg['subject']),
            'DA' => $c->localStamp((string)$msg['posted_at'], 'Y-m-d H:i'),
        ]));
        $o->at(5, 2)->fg(8)->txt(str_repeat('─', 78))->reset();
    }

    /**
     * Body rows, still pipe-coded — rendering happens per row at display.
     * The wrap width matches the composer's 79 columns exactly, or near-full
     * lines shed orphan words on re-wrap. allow_user_color decides whether
     * a caller's pipes color the text or show as typed, same rule as the
     * chat room and the toys. MCI variables expand at display time for
     * every author (1.1.1), each reader seeing their own values.
     * @return list<string>
     */
    private function bodyLines(Ctx $c, string $body): array
    {
        $out = [];
        $colors = $c->settings()->bool('allow_user_color', false);
        foreach (explode("\n", str_replace("\r", '', $body)) as $line) {
            $line = rtrim($line);
            foreach (\Atdt\Pipe::wrapVisible($colors ? $line : \Atdt\Pipe::escape($line), 79) as $piece) {
                $out[] = $piece;
            }
        }
        return $out;
    }

    private function bailWithNotice(Ctx $c, string $stringKey): array
    {
        $frames = $c->pop();
        $frames[] = $c->ansi(
            Ansi::make()->at(self::ROW_PROMPT, 1)->txt("\x1b[K")
                ->txt($c->center($c->tstr($stringKey)))->str()
        );
        return $frames;
    }
}
