<?php

declare(strict_types=1);

namespace Atdt\Screens;

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

/**
 * New user application (SPEC §10.2): handle, password ×2, location, one
 * sysop-defined question. Policy approve → pending; auto → active + straight
 * in; closed is refused back at the matrix. ESC anywhere aborts to the matrix.
 */
final class NewUserAppScreen implements Screen
{
    private const ROW_HANDLE = 8;
    private const ROW_PASS = 10;
    private const ROW_PASS2 = 11;
    private const ROW_LOC = 13;
    private const ROW_EMAIL = 14;
    private const ROW_Q = 16;
    private const ROW_QIN = 17;
    private const ROW_MSG = 24;
    private const COL_LABEL = 14;
    private const COL_INPUT = 32;

    public function describe(): string
    {
        return 'Applying for access';
    }

    public function enter(Ctx $c): array
    {
        $c->set('step', 'handle');
        $o = $c->out()->cls();
        $o->at(2, 1)->txt($c->center($c->tstr('nua.title')));
        $o->at(3, 1)->fg(8)->txt($c->center(str_repeat('─', 40)))->reset();
        $o->at(5, 8)->txt($c->tstr('nua.intro'));
        $o->at(6, 8)->txt($c->tstr('nua.intro2'));
        $o->at(self::ROW_HANDLE, self::COL_LABEL)->txt($c->tstr('nua.ask_handle'));
        return [$c->ansi($o->str()), $c->line(self::COL_INPUT, self::ROW_HANDLE, 20)];
    }

    public function input(Ctx $c, In $in): array
    {
        if ($in->kind === 'key') {
            // Only the post-outcome "press any key" state expects a hotkey.
            if ($c->get('step') === 'done') {
                return $c->pop();
            }
            return [$c->bell()];
        }
        if ($in->kind !== 'line') {
            return [$c->bell()];
        }
        if ($in->line === null) {
            return $c->pop(); // ESC aborts the application (§7.4)
        }
        $line = $in->line;

        return match ($c->get('step')) {
            'handle' => $this->stepHandle($c, trim($line)),
            'pass' => $this->stepPass($c, $line),
            'pass2' => $this->stepPass2($c, $line),
            'location' => $this->stepLocation($c, trim($line)),
            'email' => $this->stepEmail($c, trim($line)),
            'question' => $this->stepQuestion($c, trim($line)),
            default => $c->pop(),
        };
    }

    private function stepHandle(Ctx $c, string $handle): array
    {
        // The prohibited-words gate (1.6.1) is checked first, on the raw
        // attempt: a foul name inside a malformed handle still counts.
        // Three prohibited names IN A ROW and the line drops, with the
        // caller's address read back to them; any clean attempt -- taken
        // or malformed included -- resets the row.
        if (Users::bannedWord($c->settings(), $handle) !== null) {
            $strikes = (int)$c->get('pw_strikes', 0) + 1;
            $c->set('pw_strikes', $strikes);
            if ($strikes >= 3) {
                $ip = trim((string)($c->session()['ip'] ?? '')) ?: 'unknown';
                $c->db()->insert('events', [
                    'at' => now_utc(), 'level' => 'security', 'actor' => 'node' . $c->node(),
                    'msg' => 'Three prohibited handles in a row from ' . $ip
                        . ' (last: "' . mb_strimwidth($handle, 0, 24) . '"); hung up',
                ]);
                $body = $c->out()->cls()
                    ->at(10, 1)->txt($c->center($c->tstr('nua.banned_bye')))
                    ->at(12, 1)->txt($c->center($c->tstr('nua.banned_ip', ['IP' => $ip])))
                    ->nl(2)
                    ->str();
                return $c->hangup('prohibited', $body);
            }
            return $this->retry($c, 'nua.banned_handle', self::ROW_HANDLE, 20);
        }
        $c->set('pw_strikes', 0);
        if (preg_match(Users::HANDLE_RE, $handle) !== 1) {
            return $this->retry($c, 'nua.bad_handle', self::ROW_HANDLE, 20);
        }
        if (Users::handleExists($c->db(), $handle)) {
            return $this->retry($c, 'nua.taken_handle', self::ROW_HANDLE, 20);
        }
        $c->set('handle', $handle);
        $c->set('step', 'pass');
        $o = $this->clearMsg($c)
            ->at(self::ROW_PASS, self::COL_LABEL)->txt($c->tstr('nua.ask_pass'));
        return [$c->ansi($o->str()), $c->line(self::COL_INPUT, self::ROW_PASS, 40, mask: true)];
    }

    private function stepPass(Ctx $c, string $pass): array
    {
        if (strlen($pass) < 8) {
            return $this->retry($c, 'nua.bad_pass', self::ROW_PASS, 40, mask: true);
        }
        $c->set('pass', $pass);
        $c->set('step', 'pass2');
        $o = $this->clearMsg($c)
            ->at(self::ROW_PASS2, self::COL_LABEL)->txt($c->tstr('nua.ask_pass2'));
        return [$c->ansi($o->str()), $c->line(self::COL_INPUT, self::ROW_PASS2, 40, mask: true)];
    }

    private function stepPass2(Ctx $c, string $pass2): array
    {
        if ($pass2 !== $c->get('pass')) {
            // Mismatch: wipe both password rows and start the pair over.
            $c->set('step', 'pass');
            $o = $this->clearMsg($c)
                ->at(self::ROW_MSG, 1)->txt($c->center($c->tstr('nua.mismatch')))
                ->at(self::ROW_PASS, self::COL_INPUT)->txt("\x1b[K")
                ->at(self::ROW_PASS2, 1)->txt("\x1b[K")
                ->at(self::ROW_PASS, self::COL_LABEL)->txt($c->tstr('nua.ask_pass'));
            return [$c->ansi($o->str()), $c->line(self::COL_INPUT, self::ROW_PASS, 40, mask: true)];
        }
        $c->set('step', 'location');
        $o = $this->clearMsg($c)
            ->at(self::ROW_LOC, self::COL_LABEL)->txt($c->tstr('nua.ask_location'));
        return [$c->ansi($o->str()), $c->line(self::COL_INPUT, self::ROW_LOC, 40)];
    }

    private function stepLocation(Ctx $c, string $location): array
    {
        $c->set('location', $this->clean($location, 40));
        $c->set('step', 'email');
        $o = $this->clearMsg($c)
            ->at(self::ROW_EMAIL + 1, self::COL_LABEL)->txt($c->tstr('nua.email_hint'))
            ->at(self::ROW_EMAIL, self::COL_LABEL)->txt($c->tstr('nua.ask_email'));
        // 48 keeps the field inside 80 columns (32 + 48); plenty for a mailbox.
        return [$c->ansi($o->str()), $c->line(self::COL_INPUT, self::ROW_EMAIL, 48)];
    }

    /** Optional; unverified on purpose. ENTER skips (1.5.0). */
    private function stepEmail(Ctx $c, string $email): array
    {
        $email = $this->clean($email, 120);
        if ($email !== '' && !\Atdt\Mailer::plausible($email)) {
            return $this->retry($c, 'nua.bad_email', self::ROW_EMAIL, 48);
        }
        $c->set('email', $email);
        $c->set('step', 'question');
        $question = (string)($c->settings()->get('nua_question', 'Where did you hear about this board?') ?? '');
        $o = $this->clearMsg($c)
            ->at(self::ROW_Q, self::COL_LABEL)->fg(3)->txt(mb_strimwidth($question, 0, 60))->reset()
            ->at(self::ROW_QIN, self::COL_LABEL)->fg(8)->txt('> ')->reset();
        return [$c->ansi($o->str()), $c->line(self::COL_LABEL + 2, self::ROW_QIN, 60)];
    }

    private function stepQuestion(Ctx $c, string $answer): array
    {
        $question = (string)($c->settings()->get('nua_question', '') ?? '');
        $note = "Q: $question\nA: " . $this->clean($answer, 120);
        $policy = $c->settings()->get('new_user_policy', 'approve') ?? 'approve';
        $status = $policy === 'auto' ? 'active' : 'pending';
        try {
            $id = Users::create(
                $c->db(),
                (string)$c->get('handle'),
                (string)$c->get('pass'),
                (string)$c->get('location', ''),
                $note,
                $status,
                (string)$c->get('email', '')
            );
        } catch (\PDOException $e) {
            // Two applicants raced the same handle; the second re-picks.
            $c->set('step', 'handle');
            return $this->retry($c, 'nua.taken_handle', self::ROW_HANDLE, 20);
        }
        // New callers start at the board's line speed (1.6.0), written
        // explicitly so a later change of the default never re-tunes them.
        $row = $c->db()->one('SELECT * FROM users WHERE id = ?', [$id]);
        if ($row !== null) {
            Users::setPref($c->db(), $row, 'baud', (int)($c->settings()->get('default_baud', '9600') ?? 9600));
        }
        $c->set('pass', null);

        if ($policy === 'auto') {
            $user = $c->db()->one('SELECT * FROM users WHERE id = ?', [$id]);
            $c->loginUser($user);
            return $c->swap('PostLogin');
        }
        $c->set('step', 'done');
        $o = $this->clearMsg($c)
            ->at(19, 1)->txt($c->center($c->tstr('nua.applied')))
            ->at(21, 1)->txt($c->center($c->tstr('nua.press_any_key')));
        return [$c->ansi($o->str()), $c->hotkey()];
    }

    /** Re-issue a line prompt after a validation miss. */
    private function retry(Ctx $c, string $msgKey, int $row, int $max, bool $mask = false): array
    {
        $o = $this->clearMsg($c)
            ->at(self::ROW_MSG, 1)->txt($c->center($c->tstr($msgKey)))
            ->at($row, self::COL_INPUT)->txt("\x1b[K");
        return [$c->ansi($o->str()), $c->line(self::COL_INPUT, $row, $max, mask: $mask)];
    }

    private function clearMsg(Ctx $c): Ansi
    {
        return $c->out()->at(self::ROW_MSG, 1)->txt("\x1b[K");
    }

    private function clean(string $s, int $max): string
    {
        $s = preg_replace('/[\x00-\x1f\x7f]/', '', $s) ?? '';
        return mb_strimwidth($s, 0, $max);
    }
}
