"""Publishing, retirement, and rollback flows for Beaver skills."""

from __future__ import annotations

import shutil
from pathlib import Path

from beaver.skills.catalog.utils import strip_frontmatter
from beaver.skills.specs import SkillDraft, SkillReviewState, SkillSpec, SkillSpecStore, SkillStatus, SkillVersion
from beaver.skills.specs.serialization import canonical_hash, normalize_frontmatter, summarize_skill_content


class SkillPublisher:
    def __init__(self, store: SkillSpecStore) -> None:
        self.store = store

    def publish(self, skill_name: str, draft_id: str, publisher: str, notes: str = "") -> SkillVersion:
        draft = self._require_draft(skill_name, draft_id)
        if draft.status not in {SkillReviewState.IN_REVIEW.value, SkillReviewState.APPROVED.value}:
            raise ValueError("Draft must be submitted for review before publish")
        if draft.proposal_kind == "retire_skill":
            raise ValueError("Retire proposals must be applied through apply_retire_proposal")

        next_version = self._next_version(skill_name)
        content = self._render_skill_content(draft.proposed_frontmatter, draft.proposed_content)
        body = strip_frontmatter(content).strip()
        if not body:
            raise ValueError("Published skill content cannot be empty")
        version = SkillVersion(
            skill_name=skill_name,
            version=next_version,
            content_hash=canonical_hash(content),
            summary_hash=canonical_hash(body),
            created_at=_utc_now(),
            created_by=publisher,
            change_reason=notes or draft.reason,
            parent_version=draft.base_version,
            review_state=SkillReviewState.PUBLISHED.value,
            frontmatter=normalize_frontmatter(draft.proposed_frontmatter),
            summary=summarize_skill_content(body),
            tool_hints=self.store._extract_tool_hints(normalize_frontmatter(draft.proposed_frontmatter)),
            provenance={
                "draft_id": draft_id,
                "proposal_kind": draft.proposal_kind,
                "trigger_run_id": draft.trigger_run_id,
                "trigger_session_id": draft.trigger_session_id,
            },
        )
        self.store.write_skill_version(version, content)
        self._copy_uploaded_supporting_files(draft, next_version)
        self.store.set_current_version(skill_name, next_version)

        spec = self.store.get_skill_spec(skill_name)
        if spec is None:
            description = str(version.frontmatter.get("description") or skill_name)
            spec = SkillSpec(
                name=skill_name,
                display_name=skill_name,
                description=description,
                created_at=_utc_now(),
                updated_at=_utc_now(),
                current_version=next_version,
                status=SkillStatus.ACTIVE.value,
                tags=[],
                owners=[publisher],
                source_kind="managed",
                lineage=[],
            )
        else:
            spec.current_version = next_version
            spec.updated_at = _utc_now()
            spec.status = SkillStatus.ACTIVE.value
            if not spec.description:
                spec.description = str(version.frontmatter.get("description") or skill_name)
        self.store.write_skill_spec(spec)

        draft.status = SkillReviewState.PUBLISHED.value
        self.store.write_draft(draft)
        self._refresh_indexes(skill_name, spec.status)
        return version

    def apply_retire_proposal(self, skill_name: str, draft_id: str, actor: str, notes: str = "") -> SkillSpec:
        draft = self._require_draft(skill_name, draft_id)
        if draft.status not in {SkillReviewState.IN_REVIEW.value, SkillReviewState.APPROVED.value}:
            raise ValueError("Retire proposal must be submitted for review before apply")
        if draft.proposal_kind != "retire_skill":
            raise ValueError("Only retire_skill proposals can be applied as retire proposals")

        spec = self._require_spec(skill_name)
        if draft.base_version and spec.current_version and draft.base_version != spec.current_version:
            raise ValueError(
                f"Retire proposal targets {draft.base_version}, but current version is {spec.current_version}"
            )

        reason = notes or draft.reason
        spec.status = SkillStatus.DISABLED.value
        spec.updated_at = _utc_now()
        if actor and actor not in spec.owners:
            spec.owners.append(actor)
        spec.lineage.append(f"retire_proposal:{draft_id}:{reason}")
        self.store.write_skill_spec(spec)

        draft.status = SkillReviewState.DISABLED.value
        self.store.write_draft(draft)
        self._refresh_indexes(skill_name, spec.status)
        return spec

    def disable(self, skill_name: str, actor: str, reason: str) -> SkillSpec:
        spec = self._require_spec(skill_name)
        spec.status = SkillStatus.DISABLED.value
        spec.updated_at = _utc_now()
        if actor and actor not in spec.owners:
            spec.owners.append(actor)
        if reason:
            spec.lineage.append(f"disabled:{reason}")
        self.store.write_skill_spec(spec)
        self._refresh_indexes(skill_name, spec.status)
        return spec

    def rollback(self, skill_name: str, target_version: str, actor: str, reason: str) -> SkillSpec:
        if self.store.read_published_skill(skill_name, target_version) is None:
            raise ValueError(f"Unknown skill version for rollback: {skill_name}/{target_version}")
        spec = self._require_spec(skill_name)
        spec.current_version = target_version
        spec.updated_at = _utc_now()
        spec.status = SkillStatus.ACTIVE.value
        if reason:
            spec.lineage.append(f"rollback:{target_version}:{reason}")
        if actor and actor not in spec.owners:
            spec.owners.append(actor)
        self.store.write_skill_spec(spec)
        self.store.set_current_version(skill_name, target_version)
        self._refresh_indexes(skill_name, spec.status)
        return spec

    def _next_version(self, skill_name: str) -> str:
        versions = [item for item in self.store.list_versions(skill_name) if item.startswith("v")]
        if not versions:
            return "v0001"
        numbers = [int(item[1:]) for item in versions if item[1:].isdigit()]
        return f"v{(max(numbers) if numbers else 0) + 1:04d}"

    @staticmethod
    def _render_skill_content(frontmatter: dict, body: str) -> str:
        normalized = normalize_frontmatter(frontmatter)
        if not normalized:
            return body.strip() + ("\n" if body.strip() else "")
        lines = ["---"]
        for key, value in normalized.items():
            if isinstance(value, list):
                lines.append(f"{key}:")
                for item in value:
                    lines.append(f"  - {item}")
            else:
                lines.append(f"{key}: {value}")
        lines.append("---")
        lines.append("")
        lines.append(body.strip())
        return "\n".join(lines).rstrip() + "\n"

    def _refresh_indexes(self, skill_name: str, status: str) -> None:
        published = self.store.read_index("published")
        disabled = self.store.read_index("disabled")
        if status == SkillStatus.DISABLED.value:
            if skill_name in published:
                published = [item for item in published if item != skill_name]
            if skill_name not in disabled:
                disabled.append(skill_name)
        else:
            if skill_name not in published:
                published.append(skill_name)
            disabled = [item for item in disabled if item != skill_name]
        self.store.update_index("published", published)
        self.store.update_index("disabled", disabled)

    def _copy_uploaded_supporting_files(self, draft: SkillDraft, version: str) -> None:
        for evidence in draft.evidence_refs:
            if not isinstance(evidence, dict) or evidence.get("kind") != "upload":
                continue
            raw_dir = evidence.get("supporting_upload_dir")
            if not raw_dir:
                continue
            source_root = Path(str(raw_dir))
            if not source_root.exists() or not source_root.is_dir():
                continue
            target_root = self.store.root / draft.skill_name / "versions" / version
            for source in sorted(source_root.rglob("*")):
                if not source.is_file() or source.is_symlink():
                    continue
                relative = source.relative_to(source_root)
                if any(part in {"", ".", ".."} for part in relative.parts):
                    continue
                target = target_root / relative
                target.parent.mkdir(parents=True, exist_ok=True)
                shutil.copyfile(source, target)

    def _require_draft(self, skill_name: str, draft_id: str) -> SkillDraft:
        draft = self.store.read_draft(skill_name, draft_id)
        if draft is None:
            raise ValueError(f"Draft not found: {skill_name}/{draft_id}")
        return draft

    def _require_spec(self, skill_name: str) -> SkillSpec:
        spec = self.store.get_skill_spec(skill_name)
        if spec is None:
            raise ValueError(f"Skill spec not found: {skill_name}")
        return spec


def _utc_now() -> str:
    from datetime import datetime, timezone

    return datetime.now(timezone.utc).isoformat()
