<?php

declare(strict_types=1);

namespace Atdt\Screens;

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

/**
 * Threaded message list for the current area (owner request, post-M4):
 * replies nest under parents with tree glyphs, a lightbar cursor rides
 * arrows/PgUp/PgDn/Home/End, Enter (or R) reads, G jumps to a number,
 * E posts, A changes area, S flips the thread order (newest-first by
 * default since 1.6.0; the choice sticks to the account), Q backs out.
 * Opens with the cursor on the oldest unread message; the reader pops
 * back here with the cursor kept.
 */
final class MsgListScreen implements Screen
{
    private const VIEW = 17;      // list rows 5..21
    private const ROW_TOP = 5;
    private const ROW_CMD = 23;
    private const ROW_MSG = 24;

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

    /**
     * The area this list shows. A menu item pinned one (1.6.5 targeting
     * or a scoped menu's resolution) rides in as area_id and is honored
     * for the whole visit -- rechecked every input, because an area can
     * be deleted or re-ACS'd underneath an open screen. Unpinned, the
     * legacy current area through the legacy path, as ever (HC-2).
     */
    private function areaFor(Ctx $c): ?array
    {
        $id = $c->get('area_id');
        if ($id !== null) {
            $area = Messages::area($c->db(), (int)$id);
            return $area !== null && \Atdt\Acs::check((string)$area['acs_read'], $c->user())
                ? $area
                : null; // pinned and gone: bail, never silently fall back
        }
        return Messages::currentArea($c->db(), $c->user());
    }

    public function enter(Ctx $c): array
    {
        $area = $this->areaFor($c);
        if ($area === null) {
            return $this->bail($c, 'msg.no_area');
        }
        $sort = $this->sort($c);
        $threads = Messages::threadList($c->db(), (int)$area['id'], $sort);
        if ($threads === []) {
            return $this->bail($c, 'msg.area_empty');
        }
        $c->set('step', 'list');
        if ((int)$c->get('last_area', 0) !== (int)$area['id']) {
            $c->set('last_area', (int)$area['id']);
            $c->set('cur', $this->openingCursor($c, $area, $threads, $sort));
        }
        return $this->render($c, $area, $threads);
    }

    /**
     * Where the cursor lands on arrival: the OLDEST unread message
     * (wherever the sort put it), else the newest message in the area.
     * @param array<string, mixed> $area @param list<array<string, mixed>> $threads
     */
    private function openingCursor(Ctx $c, array $area, array $threads, string $sort): int
    {
        $lr = Messages::lastRead($c->db(), $c->user(), (int)$area['id']);
        $marks = Messages::readSet($c->db(), $c->user(), (int)$area['id']);
        $best = null;
        $bestNum = PHP_INT_MAX;
        $newest = 0;
        $newestNum = -1;
        foreach ($threads as $i => $row) {
            $num = (int)$row['num'];
            if ($this->isNew($c, $row, $lr, $marks) && $num < $bestNum) {
                $best = $i;
                $bestNum = $num;
            }
            if ($num > $newestNum) {
                $newest = $i;
                $newestNum = $num;
            }
        }
        return $best ?? $newest;
    }

    /** The caller's thread order: 'new' (default) or 'old'. */
    private function sort(Ctx $c): string
    {
        return Users::getPref($c->db(), $c->user(), 'msg_sort', 'new') === 'old' ? 'old' : 'new';
    }

    public function input(Ctx $c, In $in): array
    {
        if ($in->kind === 'line') {
            return $this->gotoEntered($c, $in->line);
        }
        if ($in->kind !== 'key') {
            return [$c->bell()];
        }
        $area = $this->areaFor($c);
        if ($area === null) {
            return $this->bail($c, 'msg.no_area');
        }
        $threads = Messages::threadList($c->db(), (int)$area['id'], $this->sort($c));
        if ($threads === []) {
            return $this->bail($c, 'msg.area_empty');
        }
        $cur = min((int)$c->get('cur', 0), count($threads) - 1);
        $prev = max(0, $cur);
        $key = strtoupper((string)$in->key);
        switch ($key) {
            case 'UP': $cur--; break;
            case 'DOWN': $cur++; break;
            case 'PGUP': $cur -= self::VIEW; break;
            case 'PGDN': $cur += self::VIEW; break;
            case 'HOME': $cur = 0; break;
            case 'END': $cur = count($threads) - 1; break;
            case 'ENTER':
            case 'R':
                return $c->push('Reader', [
                    'area_id' => (int)$area['id'],
                    'num' => (int)$threads[$cur]['num'],
                ]);
            case 'G':
                $c->set('step', 'goto');
                $o = Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K");
                $mode = $c->promptLine($o, self::ROW_MSG, 30, 'list.goto', 5);
                return [$c->ansi($o->str()), $mode];
            case 'E':
                return $c->push('Composer', ['mode' => 'post', 'area_id' => (int)$area['id']]);
            case 'S':
                // Flip the thread order and keep the cursor on the same
                // message; the choice rides the account (1.6.0).
                $flip = $this->sort($c) === 'old' ? 'new' : 'old';
                Users::setPref($c->db(), $c->user(), 'msg_sort', $flip);
                $c->refreshUser();
                $keepId = (int)$threads[$cur]['id'];
                $threads = Messages::threadList($c->db(), (int)$area['id'], $flip);
                foreach ($threads as $i => $row) {
                    if ((int)$row['id'] === $keepId) {
                        $cur = $i;
                        break;
                    }
                }
                break;
            case 'A':
                // The picker writes where this list lives (1.6.5): a
                // scoped visit hands its scope along; scopeless is the
                // board's pref, exactly as ever.
                return $c->push('MsgAreas', array_filter(['scope' => trim((string)$c->get('scope', ''))]));
            case 'Q':
            case 'ESC':
                return $c->pop();
            case '?':
                return $this->render($c, $area, $threads);
            default:
                return [$c->bell()];
        }
        $cur = max(0, min($cur, count($threads) - 1));
        $c->set('cur', $cur);
        // A cursor move inside the page repaints two rows, not the
        // screen (1.6.0, owner report: sub-9600 lines crawled). Leaving
        // the page - or any change to the list itself - redraws it.
        if (in_array($key, ['UP', 'DOWN', 'PGUP', 'PGDN', 'HOME', 'END'], true)
            && self::pageTop($cur, count($threads)) === self::pageTop($prev, count($threads))
            && count($threads) === (int)$c->get('lcount', -1)
        ) {
            return $this->moveCursor($c, $area, $threads, $prev, $cur);
        }
        return $this->render($c, $area, $threads);
    }

    /** The page holding this cursor: windows flip a pageful at a time
     *  so walking a long list is a lightbar, not a repaint per step. */
    private static function pageTop(int $cur, int $count): int
    {
        return intdiv(max(0, min($cur, max(0, $count - 1))), self::VIEW) * self::VIEW;
    }

    /** Two-row repaint: the old cursor row unlit, the new one lit.
     *  @param array<string, mixed> $area @param list<array<string, mixed>> $threads */
    private function moveCursor(Ctx $c, array $area, array $threads, int $prev, int $cur): array
    {
        $top = self::pageTop($cur, count($threads));
        $lr = Messages::lastRead($c->db(), $c->user(), (int)$area['id']);
        $marks = Messages::readSet($c->db(), $c->user(), (int)$area['id']);
        $o = Ansi::make();
        foreach (array_unique([$prev, $cur]) as $i) {
            $this->drawRow($c, $o, self::ROW_TOP + ($i - $top), $threads[$i], $this->isNew($c, $threads[$i], $lr, $marks), $i === $cur);
        }
        return [$c->ansi($o->reset()->str()), $c->hotkey()];
    }

    /** @param ?string $line */
    private function gotoEntered(Ctx $c, ?string $line): array
    {
        $c->set('step', 'list');
        $area = $this->areaFor($c);
        $pick = trim((string)$line);
        if ($area === null || $line === null || $pick === '' || !ctype_digit($pick)) {
            return $this->enter($c);
        }
        // Nearest message at or above the asked number, wherever the
        // current sort placed it (the list is no longer in num order).
        $threads = Messages::threadList($c->db(), (int)$area['id'], $this->sort($c));
        $best = null;
        $bestNum = PHP_INT_MAX;
        foreach ($threads as $i => $row) {
            $num = (int)$row['num'];
            if ($num >= (int)$pick && $num < $bestNum) {
                $best = $i;
                $bestNum = $num;
            }
        }
        if ($best !== null) {
            $c->set('cur', $best);
            return $c->push('Reader', [
                'area_id' => (int)$area['id'],
                'num' => $bestNum,
            ]);
        }
        $frames = $this->enter($c);
        $frames[] = $c->ansi(
            Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K")
                ->txt($c->center($c->tstr('list.bad_goto')))->str()
        );
        return $frames;
    }

    /** @param array<string, mixed> $area @param list<array<string, mixed>> $threads */
    private function render(Ctx $c, array $area, array $threads): array
    {
        $cur = max(0, min((int)$c->get('cur', 0), count($threads) - 1));
        $c->set('cur', $cur);
        $c->set('lcount', count($threads));
        $top = self::pageTop($cur, count($threads));
        $lr = Messages::lastRead($c->db(), $c->user(), (int)$area['id']);
        $marks = Messages::readSet($c->db(), $c->user(), (int)$area['id']);
        $unread = Messages::unreadCount($c->db(), $c->user(), (int)$area['id']);

        $o = $c->out()->cls();
        $o->at(1, 2)->fg(8)->txt(str_repeat('─', 78));
        $o->at(2, 4)->txt($c->tstr('list.header', [
            'AR' => Messages::areaLabel($area),
            'AD' => (string)($area['descr'] ?? ''),
            'MT' => (string)count($threads),
            'UN' => (string)$unread,
        ]));
        $o->at(3, 2)->fg(8)->txt(str_repeat('─', 78));
        $o->at(4, 2)->txt($c->tstr('list.columns'));

        for ($v = 0; $v < self::VIEW; $v++) {
            $i = $top + $v;
            $row = self::ROW_TOP + $v;
            if ($i >= count($threads)) {
                break;
            }
            $this->drawRow($c, $o, $row, $threads[$i], $this->isNew($c, $threads[$i], $lr, $marks), $i === $cur);
        }

        $o->reset()->at(22, 2)->fg(8)->txt(str_repeat('─', 78));
        $o->at(self::ROW_CMD, 1)->txt("\x1b[K")->txt($c->center($c->tstr('list.commands')));
        $o->at(self::ROW_MSG, 1)->txt("\x1b[K");
        return [$c->ansi($o->reset()->str()), $c->hotkey()];
    }

    /**
     * New to THIS caller: past the high-water mark, not individually read
     * (1.6.1 - reading a reply early no longer swallows older messages),
     * and not their own words (1.6.0 - your reply is never "unread").
     * @param array<string, mixed> $m @param array<int, true> $marks
     */
    private function isNew(Ctx $c, array $m, int $lr, array $marks): bool
    {
        return (int)$m['num'] > $lr
            && !isset($marks[(int)$m['num']])
            && (int)($m['from_user_id'] ?? 0) !== (int)(($c->user() ?? [])['id'] ?? -1);
    }

    /** @param array<string, mixed> $m */
    private function drawRow(Ctx $c, Ansi $o, int $row, array $m, bool $unread, bool $selected): void
    {
        $depth = (int)($m['depth'] ?? 0);
        $tree = $depth === 0 ? '' : str_repeat('  ', $depth - 1) . (($m['last'] ?? false) ? '└─' : '├─');
        $subject = mb_strimwidth(
            $tree . \Atdt\Pipe::strip(\Atdt\Pipe::escape($c->mci((string)$m['subject']))),
            0,
            47
        );
        $from = mb_strimwidth((string)$m['from_name'], 0, 14);
        $date = $c->localStamp((string)$m['posted_at'], 'm-d-y');

        if ($selected) {
            $line = sprintf(
                ' %4d %s %-47s %-14s %s ',
                (int)$m['num'],
                $unread ? '*' : ' ',
                $subject,
                $from,
                $date
            );
            $o->at($row, 1)->bg(1)->fg(15)->txt(mb_strimwidth($line, 0, 79))->reset();
            return;
        }
        $o->at($row, 1)->txt("\x1b[K")
            ->at($row, 2)->fg($unread ? 15 : 8)->txt(sprintf('%4d', (int)$m['num']))
            ->at($row, 7)->fg(14)->txt($unread ? '*' : ' ')
            ->at($row, 9)->fg($unread ? 15 : 7)->txt($subject)
            ->at($row, 58)->fg(3)->txt($from)
            ->at($row, 73)->fg(8)->txt($date);
    }

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