"""Вопрос наружу и ответ обратно.

Исполнитель упирается в развилку, которую сам решить не вправе, и спрашивает
владельца: `chekan ask "<текст>"`. Три вещи определяют устройство модуля.

**Вопрос живёт на диске, а не в памяти команды.** Он кладётся в состояние
прогона (`state.json` в каталоге прогона) той же транзакцией, что и пауза, —
поэтому убитый на середине `chekan ask` не теряет вопрос, а перезапущенная панель
видит его без всякой передачи из процесса в процесс.

**Пока вопрос без ответа, весь прогон стоит.** И эта остановка отличима от
ручной паузы: в состоянии есть `waiting_for` с идентификатором вопроса, а
отрезок простоя помечен причиной. Иначе владелец видит «на паузе» и думает, что
это он сам её поставил, — а на самом деле работа ждёт его слова.

**Таймаута нет.** Ожидание не решается само и не проваливается: вопрос задан
потому, что без ответа продолжать нельзя, и «истёк срок» тут значило бы
«продолжили без ответа». Поэтому в цикле ожидания нет ни срока, ни числа
попыток, а время берётся через `store.now()` — чтобы проверка могла сдвинуть
часы на сутки и убедиться, что ничего не изменилось.
"""

from __future__ import annotations

import time

from . import store
from .store import Run, StateError

# Как часто перечитывается состояние с диска, пока ждём ответа. Ответ приходит
# из другого процесса (`chekan answer`) или из опроса панели — общей памяти нет.
POLL_SECONDS = 0.4

AUTO_REFUSAL = (
    "режим auto: вопросов владельцу здесь не задают — спрашивать некого, "
    "прогон идёт без него.",
    "  Запишите предположение, на котором продолжаете, и пометьте его как "
    "предположение в спеке или в отчёте.",
    "  Нужен разговор — заведите прогон в режиме normal (`--mode normal`).",
)

PANEL_SILENT = (
    "Панель не запущена — канал в Телеграм молчит, вопрос никуда не ушёл.",
    "Ответ жду здесь: `chekan answer {qid} \"<текст>\"` — или запустите панель "
    "(`chekan dash`), и вопрос уедет владельцу.",
)


# --- чтение ---


def questions(run: Run) -> list[dict]:
    return list(run.state.get("questions") or [])


def question(run: Run, qid: str) -> dict | None:
    needle = (qid or "").strip().lower()
    return next((q for q in questions(run) if q["id"].lower() == needle), None)


def waiting(run: Run) -> dict | None:
    """Вопрос, из-за которого прогон стоит. Нет такого — None."""
    qid = run.state.get("waiting_for")
    return question(run, qid) if qid else None


def is_answered(run: Run, qid: str) -> bool:
    q = question(run, qid)
    return bool(q and q.get("status") == "answered")


def undelivered(run: Run) -> list[dict]:
    """Ждущие вопросы, которые ещё не доставлены владельцу.

    Шов для доставки: опрос панели спрашивает «есть новый вопрос?», отправляет и
    отмечает `mark_delivered`. Отметка нужна, чтобы тик опроса не слал один и тот
    же вопрос каждые несколько секунд.
    """
    return [q for q in questions(run) if q.get("status") == "waiting" and not q.get("delivered_at")]


def panel_running(run: Run) -> bool:
    """Работает ли панель этого прогона: только она опрашивает Телеграм."""
    try:
        from . import dash

        found = dash._read_pid(run)
        return bool(found and dash._is_ours(found[1], run))
    except Exception:
        return False


# --- запись ---


def _next_id(state: dict) -> str:
    used = {str(q.get("id", "")).upper() for q in state.get("questions") or []}
    n = 1
    while f"Q{n}" in used:
        n += 1
    return f"Q{n}"


def _guess_task(state: dict) -> str | None:
    live = [t["id"] for t in state.get("tasks") or [] if t.get("status") == "in-progress"]
    return live[0] if len(live) == 1 else None


def create(run: Run, text: str, task: str | None = None) -> dict:
    """Завести вопрос и остановить прогон. Вопрос и пауза — одна транзакция.

    Разделить их нельзя: между двумя записями прогон стоял бы с вопросом, но без
    паузы (и кто-то взял бы новую задачу) или наоборот.
    """
    text = (text or "").strip()
    if not text:
        raise StateError("вопрос пуст — спрашивать нечего")
    if run.state.get("mode") == "auto":
        raise StateError("\n".join(AUTO_REFUSAL))
    open_q = waiting(run)
    if open_q is not None:
        raise StateError(
            f"прогон уже ждёт ответа на {open_q['id']}: «{open_q['text']}».\n"
            f"  Пока он без ответа, новых вопросов не задают: "
            f"`chekan answer {open_q['id']} \"<текст>\"`"
        )
    with run.mutate() as st:
        qid = _next_id(st)
        question_row = {
            "id": qid,
            "text": text,
            "task": task or _guess_task(st),
            "asked_at": store.now(),
            "status": "waiting",
            "answer": None,
        }
        st.setdefault("questions", []).append(question_row)
        st["waiting_for"] = qid
        pauses = st.setdefault("pauses", [])
        if not pauses or pauses[-1].get("to") is not None:
            pauses.append({"from": store.now(), "to": None, "reason": "question", "question": qid})
    return question_row


def answer(run: Run, qid: str, text: str, by: str = "cli") -> dict:
    """Записать ответ рядом с вопросом и снять паузу ожидания.

    Отдельной команды «продолжить» нет: ответ и есть продолжение. Ручная пауза
    при этом остаётся стоять — её ставил владелец, и ответ на вопрос не отменяет
    его решения.
    """
    text = (text or "").strip()
    if not text:
        raise StateError("ответ пуст — записывать нечего")
    q = question(run, qid)
    if q is None:
        known = ", ".join(x["id"] for x in questions(run)) or "ни одного"
        raise StateError(f"нет вопроса {qid!r}. Заведённые вопросы: {known}")
    if q.get("status") == "answered":
        given = (q.get("answer") or {}).get("at", "")
        raise StateError(f"на {q['id']} уже ответили {given[:19].replace('T', ' ')}")
    with run.mutate() as st:
        target = next(x for x in st["questions"] if x["id"] == q["id"])
        target["status"] = "answered"
        target["answer"] = {"text": text, "at": store.now(), "by": by}
        st["waiting_for"] = None
        pauses = st.get("pauses") or []
        if pauses and pauses[-1].get("to") is None and pauses[-1].get("question") == q["id"]:
            pauses[-1]["to"] = store.now()
    return target


def mark_delivered(run: Run, qid: str, to: str | int | None = None) -> dict | None:
    """Шов доставки: вопрос ушёл владельцу. Второй раз его слать не надо."""
    q = question(run, qid)
    if q is None:
        return None
    with run.mutate() as st:
        target = next(x for x in st["questions"] if x["id"] == q["id"])
        target["delivered_at"] = store.now()
        if to is not None:
            target["delivered_to"] = str(to)
    return target


# --- ожидание ---


def wait_for_answer(run: Run, qid: str, sleeper=time.sleep, poll: float = POLL_SECONDS) -> dict:
    """Ждать ответа столько, сколько потребуется.

    Срока здесь нет и быть не может: вопрос задан там, где без ответа продолжать
    нельзя. Состояние перечитывается с диска каждый круг — ответ приходит из
    другого процесса.
    """
    while True:
        fresh = Run(run.root, run.slug)
        q = question(fresh, qid)
        if q is None:
            raise StateError(f"вопрос {qid} пропал из состояния прогона")
        if q.get("status") == "answered":
            return q
        sleeper(poll)
