import re
import subprocess
from pathlib import Path
from typing import Dict, Optional

from .core.codex_runner import build_codex_command
from .core.engine import Engine
from .core.prompts import SPEC_INGEST_PROMPT
from .core.utils import atomic_write


class SpecIngestError(Exception):
    """Raised when ingesting a SPEC fails."""


def _extract_section(text: str, tag: str) -> Optional[str]:
    pattern = re.compile(rf"<{tag}>\s*(.*?)\s*</{tag}>", re.DOTALL | re.IGNORECASE)
    match = pattern.search(text)
    if not match:
        return None
    return match.group(1).strip()


def build_spec_ingest_prompt(spec: str, todo: str, progress: str, opinions: str) -> str:
    return SPEC_INGEST_PROMPT.format(
        spec=spec.strip(),
        todo=todo.strip(),
        progress=progress.strip(),
        opinions=opinions.strip(),
    )


def parse_spec_ingest_output(text: str) -> Dict[str, str]:
    todo = _extract_section(text, "TODO")
    progress = _extract_section(text, "PROGRESS")
    opinions = _extract_section(text, "OPINIONS")
    if not todo or not progress or not opinions:
        raise SpecIngestError(
            "Failed to parse ingest output; missing TODO/PROGRESS/OPINIONS sections"
        )
    return {"todo": todo, "progress": progress, "opinions": opinions}


def generate_docs_from_spec(
    engine: Engine, spec_path: Optional[Path] = None
) -> Dict[str, str]:
    path = spec_path or engine.config.doc_path("spec")
    if not path.exists():
        raise SpecIngestError(f"SPEC not found at {path}")
    spec_text = path.read_text(encoding="utf-8")
    if not spec_text.strip():
        raise SpecIngestError(f"SPEC at {path} is empty")

    prompt = build_spec_ingest_prompt(
        spec_text,
        engine.docs.read_doc("todo"),
        engine.docs.read_doc("progress"),
        engine.docs.read_doc("opinions"),
    )
    cmd = build_codex_command(engine.config, prompt)
    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            cwd=str(engine.repo_root),
        )
    except FileNotFoundError as exc:
        raise SpecIngestError(
            f"Codex binary not found: {engine.config.codex_binary}"
        ) from exc

    if result.returncode != 0:
        stderr = result.stderr.strip() if result.stderr else ""
        stdout_tail = (result.stdout or "").strip()[-400:]
        raise SpecIngestError(
            f"Codex ingest failed (code {result.returncode}). {stderr or stdout_tail}"
        )

    return parse_spec_ingest_output(result.stdout or "")


def ensure_can_overwrite(engine: Engine, force: bool) -> None:
    if force:
        return
    for key in ("todo", "progress", "opinions"):
        existing = engine.docs.read_doc(key).strip()
        if existing:
            raise SpecIngestError(
                "TODO/PROGRESS/OPINIONS already contain content; rerun with --force to overwrite"
            )


def write_ingested_docs(
    engine: Engine, docs: Dict[str, str], force: bool = False
) -> None:
    ensure_can_overwrite(engine, force)
    for key, content in docs.items():
        target = engine.config.doc_path(key)
        text = content if content.endswith("\n") else content + "\n"
        atomic_write(target, text)


def clear_work_docs(engine: Engine) -> Dict[str, str]:
    defaults = {
        "todo": "# TODO\n\n",
        "progress": "# Progress\n\n",
        "opinions": "# Opinions\n\n",
    }
    for key, content in defaults.items():
        atomic_write(engine.config.doc_path(key), content)
    # Read back to reflect actual on-disk content.
    return {k: engine.docs.read_doc(k) for k in defaults.keys()}
