"""``retire <id>`` — the completion-gated state-flip verb (SPEC §3).

Flips a declared credential ``active → retiring`` (or ``held → retiring`` only via a
⚖ package, never solo) and, on apply, removes its store entry + manifest row. The
source-file prune stays a separate deliberate ``materialize`` concern (this never
deletes a ``my_stuff/`` file).

**Completion-gated.** ``retire <id> --apply`` requires ``--allow-retire`` and refuses
outright when the credential is still load-bearing:

* **referenced by a live pipeline** — any committed ``.woodpecker.yml`` under the
  federation root still names it in a ``from_secret`` (the mgf-hrb lesson: a token
  retired while a pipeline still consumes it breaks that pipeline). Remove the
  ``from_secret`` usage first.
* **founder-held** — a ``state = "held"`` entry is the founder's hold; retiring it is
  a ⚖ decision package fired with the founder, never a solo CLI act.

The state flip is a surgical text edit of the owning ``SECRETS.toml`` (only the
entry's ``state = "…"`` line changes), so the registry stays the single source of
truth and the change is reviewable in git.
"""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any

from .errors import UsageError
from .pipeline import parse_pipeline

if TYPE_CHECKING:
    from collections.abc import Sequence

    from .registry import Registry

RETIRING = "retiring"

#: A TOML table header line: ``[name]`` / ``[name.sub]`` / ``[[name.array]]``.
_HEADER_RE = re.compile(r"^\s*\[\[?\s*([^\]\[]+?)\s*\]\]?\s*(?:#.*)?$")
#: The entry's top-level ``state = "…"`` assignment.
_STATE_RE = re.compile(r'^(\s*state\s*=\s*)(["\']).*?(["\'])(.*)$')


@dataclass
class RetirePlan:
    """What ``retire`` would do — value-free. ``refused`` gates ``--apply``."""

    id: str
    from_state: str
    to_state: str
    references: list[str] = field(default_factory=list)
    store_present: bool = False
    refused: bool = False
    refusal: str = ""

    def to_json(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "from_state": self.from_state,
            "to_state": self.to_state,
            "references": self.references,
            "store_present": self.store_present,
            "refused": self.refused,
            "refusal": self.refusal,
        }


@dataclass
class RetireResult:
    id: str
    from_state: str
    to_state: str
    state_flipped: bool = False
    toml_file: str | None = None
    store_removed: bool = False

    def to_json(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "from_state": self.from_state,
            "to_state": self.to_state,
            "state_flipped": self.state_flipped,
            "toml_file": self.toml_file,
            "store_removed": self.store_removed,
        }


# --------------------------------------------------------------------------- #
# Pipeline-reference scan (the mgf-hrb gate) — committed pipelines, no network.
# --------------------------------------------------------------------------- #


def references_in_pipelines(entry_id: str, *, fed_root: Path) -> list[str]:
    """Every committed ``<repo>/.woodpecker.yml`` under ``fed_root`` that still names
    ``entry_id`` in a ``from_secret`` (``"repo (step)"``). Deterministic, read-only."""
    out: list[str] = []
    root = Path(fed_root)
    if not root.is_dir():
        return out
    for wp in sorted(root.glob("*/.woodpecker.yml")):
        repo = wp.parent.name
        try:
            _, refs = parse_pipeline(wp.read_text(encoding="utf-8"))
        except OSError:
            continue
        for ref in refs:
            if ref.secret == entry_id:
                out.append(f"{repo} ({ref.step})")
    return sorted(set(out))


# --------------------------------------------------------------------------- #
# Plan + gates.
# --------------------------------------------------------------------------- #


def plan_retire(
    registry: Registry,
    entry_id: str,
    *,
    fed_root: Path,
    store_root: Path | None = None,
) -> RetirePlan:
    entry = registry.get(entry_id)
    if entry is None:
        raise UsageError(f"unknown id {entry_id!r} — run `mgf-secrets list` to see ids")
    references = references_in_pipelines(entry_id, fed_root=fed_root)
    store_present = False
    if store_root is not None:
        from .store import cipher_path

        store_present = cipher_path(store_root, entry_id).is_file()
    plan = RetirePlan(
        id=entry_id,
        from_state=entry.state,
        to_state=RETIRING,
        references=references,
        store_present=store_present,
    )
    if entry.state == "held":
        plan.refused = True
        plan.refusal = (
            f"{entry_id!r} is state=held — the founder's hold. Retiring a held credential is a "
            "⚖ decision package fired with the founder, never a solo `retire`."
        )
    elif references:
        plan.refused = True
        plan.refusal = (
            f"{entry_id!r} is still referenced by {len(references)} live pipeline(s): "
            f"{', '.join(references)}. Remove the from_secret usage first (the mgf-hrb lesson), "
            "then retire — or stage a ⚖ package."
        )
    elif entry.state == "tombstone":
        plan.refused = True
        plan.refusal = f"{entry_id!r} is already a tombstone — nothing to retire."
    return plan


# --------------------------------------------------------------------------- #
# Apply — surgical state flip + store removal.
# --------------------------------------------------------------------------- #


def _entry_span(lines: list[str], entry_id: str) -> tuple[int, int] | None:
    """[start, end) line range of ``[entry_id]`` incl. its sub-tables / surface arrays."""
    start: int | None = None
    for i, ln in enumerate(lines):
        m = _HEADER_RE.match(ln)
        if m and m.group(1).strip() == entry_id and not ln.lstrip().startswith("[["):
            start = i
            break
    if start is None:
        return None
    end = len(lines)
    for j in range(start + 1, len(lines)):
        m = _HEADER_RE.match(lines[j])
        if m and m.group(1).split(".")[0].strip() != entry_id:
            end = j
            break
    return start, end


def flip_state_in_file(path: Path, entry_id: str, new_state: str) -> bool:
    """Rewrite ``entry_id``'s ``state = "…"`` to ``new_state`` in ``path``. Returns True
    iff the entry was found and its state line edited. Only that one line changes."""
    text = path.read_text(encoding="utf-8")
    lines = text.splitlines(keepends=True)
    span = _entry_span(lines, entry_id)
    if span is None:
        return False
    start, end = span
    for k in range(start, end):
        m = _STATE_RE.match(lines[k].rstrip("\n"))
        if m:
            eol = "\n" if lines[k].endswith("\n") else ""
            lines[k] = f"{m.group(1)}{m.group(2)}{new_state}{m.group(3)}{m.group(4)}{eol}"
            path.write_text("".join(lines), encoding="utf-8")
            return True
    return False


def _remove_store_entry(entry_id: str, store_root: Path) -> bool:
    from .store import cipher_path, manifest_load, manifest_save

    removed = False
    cpath = cipher_path(store_root, entry_id)
    if cpath.is_file():
        cpath.unlink()
        removed = True
    manifest = manifest_load(store_root)
    entries = manifest.get("entries", {})
    if isinstance(entries, dict) and entry_id in entries:
        del entries[entry_id]
        manifest_save(store_root, manifest)
        removed = True
    return removed


def apply_retire(
    registry: Registry,
    entry_id: str,
    *,
    fed_root: Path,
    toml_paths: Sequence[Path],
    store_root: Path | None = None,
    allow_retire: bool = False,
) -> RetireResult:
    plan = plan_retire(registry, entry_id, fed_root=fed_root, store_root=store_root)
    if not allow_retire:
        raise UsageError(
            f"retire {entry_id!r} --apply requires --allow-retire (completion-gated; SPEC §3)"
        )
    if plan.refused:
        raise UsageError(plan.refusal)
    result = RetireResult(id=entry_id, from_state=plan.from_state, to_state=RETIRING)
    for path in toml_paths:
        p = Path(path)
        if p.is_file() and flip_state_in_file(p, entry_id, RETIRING):
            result.state_flipped = True
            result.toml_file = str(p)
            break
    if store_root is not None:
        result.store_removed = _remove_store_entry(entry_id, store_root)
    return result
