"""Разговорный режим CLI Чекана.

`chekan` или `ц` без аргументов открывают режим: логотип КодоЦех, список коротких
команд с пояснением, дальше команды подаются строками. Полная форма и короткая
работают одинаково: `с` — то же самое, что `status`.

Единственное жёсткое условие: **вход не терминал — режима нет вовсе**. Панель
(`/api/cli` зовёт `python -m chekan`), хуки и чужие агенты разговаривают с CLI
трубой; лишняя строка в выводе или ожидание ввода ломают их разбор насмерть.
Поэтому решение принимается по `isatty` до всякой печати, а не после.

Список команд и пояснения берутся из самого разбора аргументов — второй
руками составленный список разошёлся бы с первым в первую же неделю.
"""

from __future__ import annotations

import argparse
import difflib
import shlex
import sys
from typing import Callable, Iterable, NamedTuple

WIDTH = 80


def _(text: str) -> str:
    """Шов перевода: строку инструмента пропускаем через каталог, если он есть.

    Каталог (`chekan/i18n.py`) делается отдельно. Пока его нет — возвращаем русский
    как есть, и режим от этого не зависит.
    """
    try:
        from .i18n import translate  # type: ignore[attr-defined]
    except Exception:
        return text
    try:
        return translate(text)
    except Exception:  # каталог не смеет ронять режим
        return text


# Марка КодоЦех: наковальня с искрой — та же, что бьёт молотом в шапке панели
# (assets/forge.js) и лежит иконкой assets/kodoceh-96.png. Ширина держится с
# запасом: в узком терминале разъехавшийся логотип хуже отсутствующего.
LOGO: tuple[str, ...] = (
    "",
    "         *  .  *",
    "      +----------+",
    "    ,-+----------+-.",
    "    `--+        +--'",
    "       |        |",
    "    ,--+--------+--.",
    "    `---------------'",
    "",
    "    К О Д О Ц Е Х   ·   Чекан",
    "",
)

# Короткие формы. Кириллица для русской раскладки, латиница — для английской.
# Обе раскладки исполняются всегда: путаница раскладок не должна стоить повтора.
CYRILLIC_ALIASES: dict[str, str] = {
    "н": "init",
    "д": "next",
    "па": "pause",
    "пр": "resume",
    "сп": "ask",
    "от": "answer",
    "вп": "advance",
    "с": "status",
    "т": "req",
    "з": "task",
    "п": "run",
    "г": "gate",
    "пм": "accept",
    "поч": "why",
    "о": "report",
    "вр": "doctor",
    "б": "board",
    "аг": "agents",
    "пе": "handoff",
    "м": "manifest",
    "пан": "dash",
    "син": "sync",
    "бр": "brief",
    "ск": "skills",
    "пт": "prompt",
}

LATIN_ALIASES: dict[str, str] = {
    "i": "init",
    "n": "next",
    "pa": "pause",
    "re": "resume",
    "a": "ask",
    "ans": "answer",
    "adv": "advance",
    "s": "status",
    "rq": "req",
    "t": "task",
    "r": "run",
    "g": "gate",
    "acc": "accept",
    "w": "why",
    "rep": "report",
    "doc": "doctor",
    "b": "board",
    "ag": "agents",
    "ho": "handoff",
    "m": "manifest",
    "d": "dash",
    "syn": "sync",
    "br": "brief",
    "sk": "skills",
    "pr": "prompt",
}

ALIASES: dict[str, str] = {**CYRILLIC_ALIASES, **LATIN_ALIASES}

HELP_WORDS = ("?", "помощь", "команды", "help", "h")
EXIT_WORDS = ("выход", "выйти", "quit", "exit", "q")

PROMPT = "ц › "


class Command(NamedTuple):
    short: str
    full: str
    help: str


def is_interactive(stdin=None, stdout=None) -> bool:
    """Режим открывается только человеку за терминалом.

    Труба, файл, отсутствующий stdin у демона — всё это «не человек»: там нужен
    прежний однокомандный CLI без единой лишней строки.
    """
    stdin = sys.stdin if stdin is None else stdin
    stdout = sys.stdout if stdout is None else stdout
    try:
        return bool(stdin.isatty() and stdout.isatty())
    except Exception:
        return False


def _subparsers(parser: argparse.ArgumentParser):
    for action in parser._actions:
        if isinstance(action, argparse._SubParsersAction):
            return action
    return None


def commands() -> list[Command]:
    """Список команд с пояснением. Истина — разбор аргументов, не копия списка."""
    from .__main__ import build_parser
    from .i18n import ENGLISH, language

    sub = _subparsers(build_parser())
    if sub is None:
        return []
    helps = {a.dest: (a.help or "") for a in sub._choices_actions}
    active_aliases = LATIN_ALIASES if language() == ENGLISH else CYRILLIC_ALIASES
    by_full = {full: short for short, full in active_aliases.items()}
    return [
        Command(short=by_full.get(name, ""), full=name, help=helps.get(name, ""))
        for name in sub.choices
    ]


def _fit(line: str, width: int = WIDTH) -> str:
    """Ни одна строка режима не шире терминала: узкое окно не должно разъезжаться."""
    return line if len(line) <= width else line[: width - 1] + "…"


def screen() -> list[str]:
    """Всё, что печатается при старте: логотип и список команд."""
    lines = [_(line) if line else "" for line in LOGO]
    lines.append(_("Команды — полностью или короткой формой. Выход: «выход»."))
    lines.append("")
    cmds = commands()
    pad = max((len(c.short) for c in cmds), default=0)
    for c in cmds:
        lines.append(_fit(f"  {c.short:<{pad}}  {c.full:<9} {_(c.help)}".rstrip()))
    lines.append("")
    return [_fit(line) for line in lines]


def expand(words: list[str]) -> list[str] | None:
    """Короткая форма → полная. None — такой команды нет."""
    if not words:
        return None
    head = words[0]
    full = ALIASES.get(head, head)
    from .__main__ import build_parser

    sub = _subparsers(build_parser())
    if sub is None or full not in sub.choices:
        return None
    return [full, *words[1:]]


def suggest(word: str) -> str | None:
    """Ближайшая команда к опечатке — или ничего, если непохоже вовсе."""
    from .__main__ import build_parser

    sub = _subparsers(build_parser())
    names = list(sub.choices) if sub else []
    near = difflib.get_close_matches(word, [*names, *ALIASES], n=1, cutoff=0.5)
    if not near:
        return None
    hit = near[0]
    return ALIASES.get(hit, hit)


def dispatch(line: str, write: Callable[[str], None]) -> int | None:
    """Выполнить строку. None — просьба закончить режим, иначе код возврата."""
    try:
        words = shlex.split(line)
    except ValueError:
        write(_("Кавычки не закрыты — команда не разобрана."))
        return 1
    if not words:
        return 0
    head = words[0].lower()
    if head in EXIT_WORDS:
        return None
    if head in HELP_WORDS:
        for row in screen():
            write(row)
        return 0

    argv = expand(words)
    if argv is None:
        near = suggest(head)
        write(_("Нет такой команды: {cmd}").format(cmd=head))
        if near:
            write(_("Может быть, «{near}»? Весь список: «?»").format(near=near))
        else:
            write(_("Весь список команд: «?»"))
        return 1

    from .__main__ import main as cli_main

    try:
        return cli_main(argv)
    except SystemExit as exc:  # argparse ругается на аргументы — режим живёт дальше
        return int(exc.code or 0)
    except KeyboardInterrupt:
        write(_("прервано"))
        return 1
    except Exception as exc:  # ни одна команда не смеет уронить режим
        write(f"{type(exc).__name__}: {exc}")
        return 1


def start(lines: Iterable[str] | None = None, write: Callable[[str], None] | None = None) -> int:
    """Открыть режим. `lines` — готовые строки вместо чтения с терминала (проверка)."""
    write = write or (lambda text: print(text))
    # Язык спрашивается один раз и только у человека за терминалом: решение внутри
    # `i18n.choose_on_first_run`, оно же зовёт `is_interactive`. Сюда труба не
    # попадает по построению, но каталог не смеет ронять режим — отсюда защита.
    try:
        from .i18n import choose_on_first_run

        choose_on_first_run(write=write)
    except Exception:
        pass
    for row in screen():
        write(row)

    source = iter(lines) if lines is not None else None
    while True:
        if source is None:
            try:
                line = input(PROMPT)
            except (EOFError, KeyboardInterrupt):
                write("")
                return 0
        else:
            try:
                line = next(source)
            except StopIteration:
                return 0
        if dispatch(line, write) is None:
            return 0
