<?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 voting booth (SPEC §10.6): open questions (unvoted flagged), one vote
 * per caller (voting_cast PK), bar-graph results once you've cast.
 */
final class VotingScreen implements Screen
{
    private const ROW_IN = 21;
    private const ROW_MSG = 24;

    public function describe(): string
    {
        return 'In the voting booth';
    }

    public function enter(Ctx $c): array
    {
        $c->set('step', 'pick');
        $questions = $this->questions($c);
        $o = $c->out()->cls();
        $o->at(2, 1)->txt($c->center($c->tstr('vote.title')));
        $o->at(3, 1)->fg(8)->txt($c->center(str_repeat('─', 44)))->reset();
        $sysop = $this->isSysop($c);
        if ($questions === []) {
            $o->at(10, 1)->txt($c->center($c->tstr('vote.empty')));
            $mode = $c->promptLine($o, self::ROW_IN, $sysop ? 16 : 26, $sysop ? 'vote.pick_sysop' : 'vote.pick_empty', 4);
            return [$c->ansi($o->str()), $mode];
        }
        $o->at(5, 6)->txt($c->tstr('vote.columns'));
        $row = 7;
        foreach ($questions as $i => $q) {
            $mark = $q['voted'] ? '|10[*]|07' : '|14[ ]|07';
            $o->at($row++, 6)->txt($c->pipe(sprintf(
                '%2d) %s %s',
                $i + 1,
                $mark,
                Pipe::escape(mb_strimwidth((string)$q['question'], 0, 60))
            )));
            if ($row > 19) {
                break;
            }
        }
        $mode = $c->promptLine($o, self::ROW_IN, $sysop ? 10 : 20, $sysop ? 'vote.pick_sysop' : 'vote.pick', 4);
        return [$c->ansi($o->str()), $mode];
    }

    public function input(Ctx $c, In $in): array
    {
        $step = (string)$c->get('step', 'pick');
        if ($in->kind === 'key') {
            return $step === 'results' ? $this->enter($c) : [$c->bell()];
        }
        if ($in->kind !== 'line') {
            return [$c->bell()];
        }
        if ($step === 'new_q' || $step === 'new_opt') {
            return $this->buildQuestion($c, $in->line);
        }
        if ($in->line === null || trim($in->line) === '') {
            return $step === 'ballot' ? $this->enter($c) : $c->pop();
        }
        $pick = strtoupper(trim($in->line));
        // Sysop booth-keeping (owner round 3): N opens a question, D# removes.
        if ($step === 'pick' && $this->isSysop($c)) {
            if ($pick === 'N') {
                $c->set('step', 'new_q');
                $c->set('new_opts', []);
                $o = Ansi::make()->at(self::ROW_IN, 1)->txt("\x1b[K");
                $mode = $c->promptLine($o, self::ROW_IN, 6, 'vote.ask_q', 100);
                return [$c->ansi($o->str()), $mode];
            }
            if (preg_match('/^D(\d+)$/', $pick, $m)) {
                $questions = $this->questions($c);
                $q = $questions[(int)$m[1] - 1] ?? null;
                if ($q === null) {
                    return $this->notice($c, 'vote.bad_pick');
                }
                $c->db()->q('DELETE FROM voting_cast WHERE q_id = ?', [(int)$q['id']]);
                $c->db()->q('DELETE FROM voting_opt WHERE q_id = ?', [(int)$q['id']]);
                $c->db()->q('DELETE FROM voting_q WHERE id = ?', [(int)$q['id']]);
                $c->db()->insert('events', [
                    'at' => now_utc(), 'level' => 'admin', 'actor' => 'node' . $c->node(),
                    'msg' => 'Deleted voting question "' . $q['question'] . '"',
                ]);
                return $this->notice($c, 'vote.deleted');
            }
        }
        if (!ctype_digit($pick)) {
            return $this->enter($c);
        }
        if ($step === 'ballot') {
            return $this->castVote($c, (int)$pick);
        }
        $questions = $this->questions($c);
        $q = $questions[(int)$pick - 1] ?? null;
        if ($q === null) {
            $frames = $this->enter($c);
            $frames[] = $c->ansi(Ansi::make()->at(self::ROW_MSG, 1)->txt("\x1b[K")
                ->txt($c->center($c->tstr('vote.bad_pick')))->str());
            return $frames;
        }
        return $q['voted'] ? $this->results($c, (int)$q['id']) : $this->ballot($c, (int)$q['id']);
    }

    private function ballot(Ctx $c, int $qId): array
    {
        $q = $c->db()->one('SELECT * FROM voting_q WHERE id = ?', [$qId]);
        if ($q === null) {
            return $this->enter($c);
        }
        $c->set('step', 'ballot');
        $c->set('q', $qId);
        $opts = $c->db()->all('SELECT * FROM voting_opt WHERE q_id = ? ORDER BY id', [$qId]);
        $o = $c->out()->cls();
        $o->at(3, 1)->txt($c->center($c->pipe('|15' . Pipe::escape((string)$q['question']))));
        $o->at(4, 1)->fg(8)->txt($c->center(str_repeat('─', 44)))->reset();
        $row = 7;
        foreach ($opts as $i => $opt) {
            $o->at($row++, 20)->txt($c->pipe(sprintf(
                '|14%2d|08) |07%s',
                $i + 1,
                Pipe::escape(mb_strimwidth((string)$opt['label'], 0, 50))
            )));
        }
        $mode = $c->promptLine($o, self::ROW_IN, 24, 'vote.choice', 3);
        return [$c->ansi($o->str()), $mode];
    }

    private function castVote(Ctx $c, int $pick): array
    {
        $qId = (int)$c->get('q', 0);
        $opts = $c->db()->all('SELECT * FROM voting_opt WHERE q_id = ? ORDER BY id', [$qId]);
        $opt = $opts[$pick - 1] ?? null;
        $u = $c->user();
        if ($opt === null || $u === null) {
            return $this->ballot($c, $qId);
        }
        try {
            $c->db()->insert('voting_cast', [
                'q_id' => $qId, 'user_id' => (int)$u['id'], 'opt_id' => (int)$opt['id'],
            ]);
            $c->db()->q('UPDATE voting_opt SET votes = votes + 1 WHERE id = ?', [(int)$opt['id']]);
        } catch (\PDOException $e) {
            // Already voted (PK q_id,user_id) — straight to the results.
        }
        return $this->results($c, $qId);
    }

    /** Bar-graph results (§10.6), shown only after the caller has voted. */
    private function results(Ctx $c, int $qId): array
    {
        $q = $c->db()->one('SELECT * FROM voting_q WHERE id = ?', [$qId]);
        $opts = $c->db()->all('SELECT * FROM voting_opt WHERE q_id = ? ORDER BY id', [$qId]);
        if ($q === null) {
            return $this->enter($c);
        }
        $c->set('step', 'results');
        $total = max(1, array_sum(array_map(fn($o) => (int)$o['votes'], $opts)));
        $max = max(1, ...array_map(fn($o) => (int)$o['votes'], $opts));
        $o = $c->out()->cls();
        $o->at(2, 1)->txt($c->center($c->pipe('|15' . Pipe::escape((string)$q['question']))));
        $o->at(3, 1)->fg(8)->txt($c->center(str_repeat('─', 44)))->reset();
        $row = 6;
        foreach ($opts as $opt) {
            $votes = (int)$opt['votes'];
            $bar = str_repeat('▓', (int)round($votes / $max * 26));
            $o->at($row, 4)->fg(7)->txt(mb_strimwidth((string)$opt['label'], 0, 30));
            $o->at($row, 36)->fg(3)->txt($bar)
                ->fg(15)->txt(' ' . $votes)
                ->fg(8)->txt(sprintf(' (%d%%)', (int)round($votes / $total * 100)));
            $row += 2;
        }
        $o->reset()->at(22, 1)->txt($c->center($c->tstr('nua.press_any_key')));
        return [$c->ansi($o->str()), $c->hotkey()];
    }

    /**
     * Sysop question builder (owner round 3): question line, then options
     * one per line; an empty line finishes (two options minimum).
     * @param ?string $line
     */
    private function buildQuestion(Ctx $c, ?string $line): array
    {
        $step = (string)$c->get('step');
        $o = Ansi::make()->at(self::ROW_IN, 1)->txt("\x1b[K");
        if ($line === null) { // ESC bails
            $c->set('step', 'pick');
            return $this->enter($c);
        }
        $value = mb_strimwidth(trim(preg_replace('/[\x00-\x1f\x7f]/', '', $line) ?? ''), 0, $step === 'new_q' ? 100 : 50);
        if ($step === 'new_q') {
            if ($value === '') {
                $c->set('step', 'pick');
                return $this->enter($c);
            }
            $c->set('new_q', $value);
            $c->set('step', 'new_opt');
            $mode = $c->promptLine($o, self::ROW_IN, 6, 'vote.ask_opt', 50, vars: ['NO' => '1']);
            return [$c->ansi($o->str()), $mode];
        }
        $opts = (array)$c->get('new_opts', []);
        if ($value !== '') {
            $opts[] = $value;
            $c->set('new_opts', $opts);
            if (count($opts) >= 10) {
                return $this->finishQuestion($c, $opts);
            }
            $mode = $c->promptLine($o, self::ROW_IN, 6, 'vote.ask_opt', 50, vars: ['NO' => (string)(count($opts) + 1)]);
            return [$c->ansi($o->str()), $mode];
        }
        if (count($opts) < 2) {
            $o2 = Ansi::make()
                ->at(self::ROW_MSG, 1)->txt("\x1b[K")->txt($c->center($c->tstr('vote.need_two')))
                ->at(self::ROW_IN, 1)->txt("\x1b[K");
            $mode = $c->promptLine($o2, self::ROW_IN, 6, 'vote.ask_opt', 50, vars: ['NO' => (string)(count($opts) + 1)]);
            return [$c->ansi($o2->str()), $mode];
        }
        return $this->finishQuestion($c, $opts);
    }

    /** @param list<string> $opts */
    private function finishQuestion(Ctx $c, array $opts): array
    {
        $qId = $c->db()->insert('voting_q', [
            'question' => (string)$c->get('new_q', '?'),
            'author' => (string)(($c->user() ?? [])['handle'] ?? 'Sysop'),
            'acs' => '',
            'created_at' => now_utc(),
            'open' => 1,
        ]);
        foreach ($opts as $label) {
            $c->db()->insert('voting_opt', ['q_id' => $qId, 'label' => $label, 'votes' => 0]);
        }
        $c->db()->insert('events', [
            'at' => now_utc(), 'level' => 'admin', 'actor' => 'node' . $c->node(),
            'msg' => 'Opened voting question "' . $c->get('new_q') . '"',
        ]);
        $c->set('new_q', null);
        $c->set('new_opts', null);
        $c->set('step', 'pick');
        return $this->notice($c, 'vote.added');
    }

    private function isSysop(Ctx $c): bool
    {
        return (int)(($c->user() ?? [])['level'] ?? 0) === 255;
    }

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

    /** Open, ACS-passing questions with this caller's voted flags. */
    private function questions(Ctx $c): array
    {
        $u = $c->user();
        $out = [];
        foreach ($c->db()->all('SELECT * FROM voting_q WHERE open = 1 ORDER BY id') as $q) {
            if (!Acs::check((string)($q['acs'] ?? ''), $u)) {
                continue;
            }
            $q['voted'] = $u !== null && $c->db()->one(
                'SELECT q_id FROM voting_cast WHERE q_id = ? AND user_id = ?',
                [(int)$q['id'], (int)$u['id']]
            ) !== null;
            $out[] = $q;
        }
        return $out;
    }
}
