<?php

declare(strict_types=1);

namespace Atdt\Screens;

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

/**
 * Your settings (SPEC §10.6, §7.6): BEL beep toggle, baud throttle choice
 * (persisted in users.prefs and pushed to the client), password change.
 */
final class UserSettingsScreen implements Screen
{
    private const BAUDS = [0, 300, 1200, 2400, 9600]; // 0 = no throttle
    private const ROW_IN = 19;
    private const ROW_MSG = 24;

    public function describe(): string
    {
        return 'Adjusting settings';
    }

    public function enter(Ctx $c): array
    {
        $c->set('step', 'menu');
        $u = $c->user() ?? [];
        $beep = (bool)Users::getPref($c->db(), $u, 'beep', false);
        $baud = (int)Users::getPref($c->db(), $u, 'baud', 9600);
        $email = trim((string)($u['email'] ?? ''));
        $vars = [
            'BP' => \Atdt\Pipe::strip($c->tstr($beep ? 'set.state_on' : 'set.state_off')),
            'BD' => $baud === 0 ? \Atdt\Pipe::strip($c->tstr('set.baud_off')) : (string)$baud,
            'EM' => $email === '' ? \Atdt\Pipe::strip($c->tstr('set.email_none')) : $email,
        ];
        $o = $c->out()->cls();
        $o->at(2, 1)->txt($c->center($c->tstr('set.title')));
        $o->at(3, 1)->fg(8)->txt($c->center(str_repeat('─', 44)))->reset();
        $row = 7;
        foreach (['row_beep', 'row_baud', 'row_email', 'row_pass'] as $key) {
            $o->at($row, 12)->txt($c->tstr('set.' . $key, $vars));
            $row += 2;
        }
        $o->at(16, 12)->txt($c->tstr('set.baud_note'));
        $o->at(17, 12)->txt($c->tstr('set.email_note'));
        $o->at(21, 1)->txt($c->center($c->tstr('set.prompt')));
        return [$c->ansi($o->str()), $c->hotkey()];
    }

    public function input(Ctx $c, In $in): array
    {
        if ($in->kind === 'line') {
            return $this->passLine($c, $in->line);
        }
        if ($in->kind !== 'key' || $c->get('step') !== 'menu') {
            return [$c->bell()];
        }
        $u = $c->user();
        if ($u === null) {
            return $c->pop();
        }
        switch (strtoupper((string)$in->key)) {
            case 'B':
                Users::setPref($c->db(), $u, 'beep', !(bool)Users::getPref($c->db(), $u, 'beep', false));
                $c->refreshUser();
                $c->prefsChanged = true;
                return $this->enter($c);
            case 'S':
                $baud = (int)Users::getPref($c->db(), $u, 'baud', 9600);
                $i = array_search($baud, self::BAUDS, true);
                $next = self::BAUDS[($i === false ? 0 : $i + 1) % count(self::BAUDS)];
                Users::setPref($c->db(), $u, 'baud', $next);
                $c->refreshUser();
                $c->prefsChanged = true;
                return $this->enter($c);
            case 'E':
                $c->set('step', 'email');
                $o = Ansi::make()->at(self::ROW_IN, 1)->txt("\x1b[K");
                $mode = $c->promptLine($o, self::ROW_IN, 12, 'set.ask_email', 60);
                return [$c->ansi($o->str()), $mode];
            case 'P':
                $c->set('step', 'pass_old');
                return $this->ask($c, 'set.ask_old');
            case 'Q':
            case 'ESC':
                return $c->pop();
            case '?':
                return $this->enter($c);
            default:
                return [$c->bell()];
        }
    }

    private function ask(Ctx $c, string $stringKey): array
    {
        $o = Ansi::make()->at(self::ROW_IN, 1)->txt("\x1b[K");
        $mode = $c->promptLine($o, self::ROW_IN, 18, $stringKey, 40, mask: true);
        return [$c->ansi($o->str()), $mode];
    }

    /** @param ?string $line */
    private function passLine(Ctx $c, ?string $line): array
    {
        if ($line === null) {
            $c->set('step', 'menu');
            return $this->enter($c);
        }
        $u = $c->user();
        if ($u === null) {
            return $c->pop();
        }
        switch ((string)$c->get('step')) {
            case 'email':
                // Blank clears; anything else must look like a mailbox.
                $c->set('step', 'menu');
                $email = trim($line);
                if ($email !== '' && !\Atdt\Mailer::plausible($email)) {
                    return $this->notice($c, 'set.email_bad');
                }
                $c->db()->q('UPDATE users SET email = ? WHERE id = ?', [
                    mb_strimwidth($email, 0, 120), (int)$u['id'],
                ]);
                $c->refreshUser();
                return $this->notice($c, $email === '' ? 'set.email_cleared' : 'set.email_saved');
            case 'pass_old':
                if (!password_verify($line, (string)$u['pass_hash'])) {
                    $c->set('step', 'menu');
                    return $this->notice($c, 'set.bad_old');
                }
                $c->set('step', 'pass_new');
                return $this->ask($c, 'set.ask_new');
            case 'pass_new':
                if (strlen($line) < 8) {
                    return $this->ask($c, 'set.ask_new');
                }
                $c->set('new_pass', $line);
                $c->set('step', 'pass_new2');
                return $this->ask($c, 'set.ask_new2');
            case 'pass_new2':
                $matches = $line === (string)$c->get('new_pass');
                $c->set('new_pass', null);
                $c->set('step', 'menu');
                if (!$matches) {
                    return $this->notice($c, 'set.mismatch');
                }
                $c->db()->q('UPDATE users SET pass_hash = ? WHERE id = ?', [
                    password_hash($line, PASSWORD_DEFAULT), (int)$u['id'],
                ]);
                $c->refreshUser();
                $c->db()->insert('events', [
                    'at' => now_utc(), 'level' => 'security', 'actor' => 'node' . $c->node(),
                    'msg' => $u['handle'] . ' changed their password',
                ]);
                return $this->notice($c, 'set.pass_changed');
            default:
                $c->set('step', 'menu');
                return $this->enter($c);
        }
    }

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