<?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;

/**
 * The auto-message (SPEC §10.6): one board-wide message, shown at logon,
 * overwritable by ANY caller who can reach the command (1.5.0: the menu
 * item's ACS is the one and only gate; the old internal s20 check made
 * it SysOp-only in practice on small boards). Stored in settings
 * (automsg_body/by/at) — a single message needs no table.
 */
final class AutoMsgScreen implements Screen
{
    private const MAX_ROWS = 8;
    private const ROW_MSG = 24;

    public function describe(): string
    {
        return 'At the auto-message wall';
    }

    public function enter(Ctx $c): array
    {
        $c->set('step', 'view');
        $s = $c->settings();
        $body = trim((string)($s->get('automsg_body', '') ?? ''));
        $o = $c->out()->cls();
        $o->at(2, 1)->txt($c->center($c->tstr('auto.title')));
        $o->at(3, 1)->fg(8)->txt($c->center(str_repeat('─', 44)))->reset();
        if ($body === '') {
            $o->at(9, 1)->txt($c->center($c->tstr('auto.empty')));
        } else {
            $row = 6;
            $colors = $s->bool('allow_user_color', false);
            // Full width (1.6.0): the editor writes 79-column lines, so the
            // wall shows 79 columns - clipping narrower ate word tails.
            foreach (array_slice(explode("\n", $body), 0, self::MAX_ROWS) as $line) {
                $o->at($row++, 1)->txt($c->pipe(ChatScreen::clip(
                    '|07' . ($colors ? $line : Pipe::escape($line)),
                    79
                )));
            }
            $by = (string)($s->get('automsg_by', '') ?? '');
            if ($by !== '') {
                $o->at($row + 1, 1)->txt($c->center($c->tstr('auto.by', [
                    'AB' => Pipe::escape($by),
                    'AT' => $c->localStamp((string)($s->get('automsg_at', '') ?? ''), 'Y-m-d'),
                ])));
            }
        }
        $o->at(21, 1)->txt($c->center($c->tstr('auto.prompt_edit')));
        return [$c->ansi($o->reset()->str()), $c->hotkey()];
    }

    public function input(Ctx $c, In $in): array
    {
        if ($in->kind === 'editor') {
            $c->set('step', 'view');
            if ($in->lines === null) {
                return $this->enter($c);
            }
            // Keep every character the 79-column editor allowed (1.6.0):
            // the old clip to 70 silently ate the tail of full lines.
            // Rows are re-flowed into sentences first, then wrapped, so
            // a word stranded on its own row by the non-reflowing editor
            // (or a paste wrapped narrow elsewhere) reads clean again.
            $lines = [];
            foreach (self::reflow($in->lines) as $l) {
                foreach (self::wrap79($l) as $w) {
                    $lines[] = $w;
                }
            }
            $lines = array_slice($lines, 0, self::MAX_ROWS);
            if (trim(implode('', $lines)) === '') {
                // Saving a blank coat repaints nothing (1.6.0): the editor
                // opens empty now, so an accidental ^S must not wipe the
                // wall somebody else painted.
                return $this->enter($c);
            }
            $s = $c->settings();
            $s->set('automsg_body', implode("\n", $lines));
            $s->set('automsg_by', (string)(($c->user() ?? [])['handle'] ?? 'caller'));
            $s->set('automsg_at', now_utc());
            $frames = $this->enter($c);
            $frames[] = $c->ansi(Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K")
                ->txt($c->center($c->tstr('auto.saved')))->str());
            return $frames;
        }
        if ($in->kind !== 'key') {
            return [$c->bell()];
        }
        return $this->keyPressed($c, strtoupper((string)$in->key));
    }

    /**
     * Join the editor's soft-wrapped rows back into sentences (1.6.0
     * final polish, owner screenshot): the caller editor never re-flows,
     * so a word nudged onto its own row while editing stays stranded
     * there ("but" alone between two full rows), and pasted text keeps
     * whatever narrow wrap it arrived with. A row flows into the next
     * unless it plainly ends a thought - . ! ? : ; with a closing quote
     * or bracket allowed after - or the next row opens a list or quote.
     * Blank rows keep their place: they are the paragraph breaks.
     * clean() strips control bytes; its default 70-column chat clip must
     * NOT apply here - wrap79 handles the width after the join.
     * @param list<string> $raw
     * @return list<string>
     */
    private static function reflow(array $raw): array
    {
        $out = [];
        $open = false; // the previously kept row still wants more words
        foreach ($raw as $l) {
            $l = rtrim(\Atdt\Chat::clean((string)$l, 640));
            if (trim($l) === '') {
                $out[] = '';
                $open = false;
                continue;
            }
            $listy = preg_match('/^\s*([-*>·]\s|\d+[.)]\s)/u', $l) === 1;
            if ($open && !$listy && $out !== []) {
                $out[count($out) - 1] .= ' ' . ltrim($l);
            } else {
                $out[] = $l;
            }
            $tail = rtrim($l, '"\')]');
            $open = !in_array(mb_substr($tail, -1), ['.', '!', '?', ':', ';'], true);
        }
        return $out;
    }

    /**
     * Wrap one line the way the caller editor does (§7.5): 79 columns,
     * breaking at spaces, hard-splitting a word longer than a whole row.
     * @return list<string>
     */
    private static function wrap79(string $line): array
    {
        $out = [];
        while (mb_strlen($line) > 79) {
            $cut = mb_substr($line, 0, 79);
            $sp = mb_strrpos($cut, ' ');
            if ($sp === false || $sp < 1) {
                $sp = 79;
            }
            $out[] = rtrim(mb_substr($line, 0, $sp));
            $line = ltrim(mb_substr($line, $sp));
        }
        $out[] = $line;
        return $out;
    }

    private function keyPressed(Ctx $c, string $key): array
    {
        switch ($key) {
            case 'E':
                // A fresh coat (1.6.0): the editor opens EMPTY - painting
                // the wall replaces it, it never hands you someone else's
                // words to edit. The cap rides the mode frame so the
                // client refuses a ninth line while typing; what the
                // caller sees is exactly what the board keeps.
                $c->set('step', 'edit');
                return [[
                    't' => 'mode', 'm' => 'editor',
                    'rows' => self::MAX_ROWS, 'cols' => 79,
                    'cap' => self::MAX_ROWS,
                    'initial' => [],
                    'title' => Pipe::strip($c->tstr('auto.editor_title')),
                ]];
            case 'Q':
            case 'ESC':
                return $c->pop();
            case '?':
                return $this->enter($c);
            default:
                return [$c->bell()];
        }
    }
}
