"""Бриф задачи: один файл вместо пяти.

Исполнителю нужны пять вещей: требования из спеки, границы из `interfaces.md`,
зона, команда проверки и ловушки среды. Раньше он открывал пять файлов и читал
каждый целиком — платил контекстом за то, чтобы найти в них свою десятую часть.
`chekan brief NN` собирает эти пять частей в один файл, и отдавать исполнителю
нужно путь к нему.

Два правила, на которых всё держится.

1. **Дословно.** Разделы спеки берутся чтением с диска и кладутся знак в знак.
   Пересказ здесь — худший вид порчи: он выглядит как бриф, читается как бриф,
   а требование в нём уже другое, и разойдётся это только на приёмке.

2. **Чего нет — о том сказано словом.** Файл памяти проекта ищется на диске
   (`CLAUDE.md`, иначе `AGENTS.md`), а не берётся из поля состояния: поле там
   всегда пустое. Нет ни одного — раздел честно говорит, что ловушек среды
   никто не записывал. Молчаливо пропущенный раздел читается как «ловушек нет».
"""

from __future__ import annotations

import re
from pathlib import Path

from . import skills
from .store import Run, StateError

MEMORY_NAMES = ("CLAUDE.md", "AGENTS.md")

# `## R05 — Бриф задачи`, а также диапазон `## R10–R14 — Панель`.
HEADING = re.compile(r"^##\s+(?!#)(.*)$", re.M)
RANGE = re.compile(r"R(\d+)\s*[–—-]\s*R?(\d+)")
SINGLE = re.compile(r"R(\d+)")


def memory_file(root: Path) -> Path | None:
    """Файл памяти проекта — определяется, а не берётся из пустого поля."""
    for name in MEMORY_NAMES:
        path = root / name
        if path.is_file():
            return path
    return None


def interfaces_file(run: Run) -> Path | None:
    """`interfaces.md` пишется на фазе спеки — рядом с ней либо в корне."""
    for path in (run.dir / "interfaces.md", run.root / "interfaces.md"):
        if path.is_file():
            return path
    return None


def _heading_ids(heading: str) -> set[str]:
    ids = {f"R{int(n):02d}" for n in SINGLE.findall(heading)}
    for first, last in RANGE.findall(heading):
        ids |= {f"R{n:02d}" for n in range(int(first), int(last) + 1)}
    return ids


def spec_sections(spec_text: str, requirements: list[str]) -> list[str]:
    """Куски спеки по требованиям задачи — знак в знак, в порядке спеки."""
    wanted = {r.strip().upper() for r in requirements if r.strip()}
    if not wanted or not spec_text:
        return []
    marks = list(HEADING.finditer(spec_text))
    chunks = []
    for i, mark in enumerate(marks):
        if not (_heading_ids(mark.group(1)) & wanted):
            continue
        end = marks[i + 1].start() if i + 1 < len(marks) else len(spec_text)
        chunks.append(spec_text[mark.start(): end].rstrip())
    return chunks


def _read(path: Path | None) -> str:
    if path is None:
        return ""
    try:
        return path.read_text(encoding="utf-8").rstrip()
    except OSError:
        return ""


def build(run: Run, tid: str) -> str:
    """Текст брифа. Ничего не пишет на диск — этим занимается `write`."""
    task = run.task(tid)
    if task is None:
        raise StateError(f"нет таска {tid}")

    lines: list[str] = [
        f"# Задача {task['id']} — {task['title']}",
        "",
        f"Собрано `chekan brief {task['id']}` из состояния прогона «{run.state['title']}».",
        "Здесь всё, что нужно задаче: спеку, интерфейсы и память проекта отдельно "
        "открывать не нужно.",
        "",
        "## Зона",
        "",
    ]

    zone = (task.get("zone") or "").strip()
    if zone:
        lines += [f"- `{part.strip()}`" for part in zone.split(",") if part.strip()]
        lines += [
            "",
            "За её пределы не выходить: рядом работают другие исполнители, и общий "
            "файл двое правят только через конфликт.",
        ]
    else:
        lines.append("Зона у задачи не названа. Правьте минимум и назовите в отчёте, что тронули.")

    verify = (task.get("verify") or "").strip()
    lines += ["", "## Команда проверки", ""]
    if verify:
        lines += [
            "```",
            verify,
            "```",
            "",
            "Отметку `done` ставит код возврата этой команды, а не суждение — "
            "«должно работать» здесь не принимается.",
        ]
    else:
        lines.append(
            "Команда проверки у задачи не названа. Без неё закрыть задачу нечем: "
            "назовите её `chekan task set` и прогоните `chekan run`."
        )

    lines += ["", "## Требования из спеки (дословно)", ""]
    chunks = spec_sections(_read(run.dir / "spec.md"), task.get("requirements") or [])
    if chunks:
        for chunk in chunks:
            lines += [chunk, ""]
    else:
        rids = ", ".join(task.get("requirements") or []) or "их нет"
        lines += [
            f"Разделов спеки по требованиям задачи ({rids}) не нашлось. "
            "Читайте спеку целиком: `.chekan/<прогон>/spec.md`.",
            "",
        ]

    lines += ["## Границы (interfaces.md)", ""]
    interfaces = interfaces_file(run)
    body = _read(interfaces)
    if body:
        lines += [body, ""]
    else:
        lines += [
            "Файла `interfaces.md` нет — швы между кусками никто не записал. "
            "Придумаете шов сами — назовите его в отчёте, иначе соседний исполнитель "
            "придумает свой.",
            "",
        ]

    memory = memory_file(run.root)
    lines += [f"## Ловушки среды ({memory.name if memory else 'файла памяти нет'})", ""]
    body = _read(memory)
    if body:
        lines += [body, ""]
    else:
        lines += [
            "Ни `CLAUDE.md`, ни `AGENTS.md` в корне репозитория нет — ловушки среды "
            "никто не записывал. Это не «ловушек нет»: спрашивайте владельца, если "
            "упрётесь в среду.",
            "",
        ]

    lines += ["## Скиллы машины", ""]
    matched = skills.suggest(task)
    if matched:
        lines += [f"- `{s.name}` — {s.summary}" for s in matched]
    else:
        lines.append("Ничего не подошло. Случайный совет дороже тишины.")
    lines.append("")

    return "\n".join(lines)


def write(run: Run, tid: str) -> Path:
    """Собрать бриф и положить рядом с задачами прогона."""
    text = build(run, tid)
    path = run.dir / "tasks" / f"{tid}-brief.md"
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")
    return path
