"""Executor for ScriptEditProposal.

Write target: episode script file in Fountain format. The script file
is located by resolving the proposal target's episode identifier.
"""
from __future__ import annotations

import hashlib
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from fastapi import HTTPException

from recoil.api.eventbus import BUS
from recoil.api import notes_ledger
from recoil.api.notes_snapshot import snapshot_artifact
from recoil.core.atomic_write import atomic_write_text
from recoil.core.paths import projects_root

logger = logging.getLogger(__name__)

_SCOPE = "api/executors/script_edit"


def _find_script_path(episode_id: str, project_id: str) -> Optional[Path]:
    ep_dir = projects_root() / project_id / "episodes" / episode_id
    for name in ("script.fountain", f"{episode_id}.fountain"):
        candidate = ep_dir / name
        if candidate.exists():
            return candidate
    return None


def execute(
    episode_id: str,
    new_script_text: str,
    project_id: Optional[str] = None,
    proposal_id: Optional[str] = None,
) -> dict:
    """Replace the script file contents for episode_id.

    Args:
        episode_id: Episode identifier (e.g. "ep_001").
        new_script_text: Full replacement Fountain script text.
        project_id: Required project slug (raises 422 if absent or empty).
        proposal_id: Optional proposal id to link from the minted note.

    Returns:
        Script path, note id, snapshot ref, and applied flag.

    Raises:
        HTTPException(404): If script file not found.
        HTTPException(422): If project_id is absent or empty.
    """
    if not project_id:
        raise HTTPException(
            status_code=422,
            detail={"error": "project_id_required", "message": "project_id is required"},
        )
    script_path = _find_script_path(episode_id, project_id)
    if script_path is None:
        BUS.emit_sync(
            severity="failure",
            scope=_SCOPE,
            summary=f"script_edit_target_not_found: {episode_id}",
            payload={"episode_id": episode_id, "project_id": project_id},
        )
        raise HTTPException(
            status_code=404,
            detail={
                "error": "script_not_found",
                "episode_id": episode_id,
                "project_id": project_id,
                "message": f"No script file found for episode {episode_id!r} in project {project_id!r}",
            },
        )
    episode_root = script_path.parent
    pre_edit_bytes = script_path.read_bytes()
    pre_edit_sha = hashlib.sha256(pre_edit_bytes).hexdigest()
    target_sha = hashlib.sha256(new_script_text.encode("utf-8")).hexdigest()
    note_id = notes_ledger.new_note_id()
    rec = notes_ledger.NoteRecord(
        note_id=note_id,
        schema_version=notes_ledger.NOTE_SCHEMA_VERSION,
        created_at=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
        author="director",
        project_id=project_id,
        episode_id=episode_id,
        raw_text="",
        action_type="fix",
        domain="script",
        mechanism="patch_script",
        blast_tier="T1_ONE_ARTIFACT",
        scope="episode",
        target={
            "script_sha256": target_sha,
            "pre_edit_sha256": pre_edit_sha,
        },
        reference_target=None,
        classification={},
        artifacts_edited=[],
        derive_triggered=None,
        approvals={},
        spend={},
        links={"proposal_id": proposal_id} if proposal_id else {},
        status="proposed",
        rolled_back_by=None,
        error=None,
    )
    notes_ledger.write_note(rec, episode_root)

    pre = snapshot_artifact(
        script_path,
        kind="script_edits",
        note_id=note_id,
        episode_root=episode_root,
    )

    try:
        atomic_write_text(script_path, new_script_text)
    except OSError as exc:
        rec.status = "failed"
        rec.error = repr(exc)
        try:
            notes_ledger.write_note(rec, episode_root)
        except OSError as exc2:
            logger.error(
                "script_edit: mutation failed AND the failed-note write failed "
                "(state is proposed-note + unchanged script): note_id=%s "
                "mut_err=%r note_err=%r",
                note_id,
                exc,
                exc2,
            )
        raise

    rec.status = "applied"
    rec.artifacts_edited = [
        {
            "kind": "script_edits",
            "path": str(script_path),
            "pre_snapshot_ref": str(pre),
        }
    ]
    try:
        notes_ledger.write_note(rec, episode_root)
    except OSError as exc:
        logger.error(
            "script_edit flip failed AFTER mutation (note stays proposed; future "
            "reconcile self-heals via the preserved snapshot + pre_edit_sha256): "
            "note_id=%s err=%r",
            note_id,
            exc,
        )
        raise

    BUS.emit_sync(
        severity="success",
        scope=_SCOPE,
        summary=f"script_edit_applied: {episode_id}",
        payload={
            "episode_id": episode_id,
            "project_id": project_id,
            "script_path": str(script_path),
            "note_id": note_id,
            "pre_snapshot_ref": str(pre),
            "new_text_length": len(new_script_text),
        },
    )
    return {
        "episode_id": episode_id,
        "script_path": str(script_path),
        "note_id": note_id,
        "pre_snapshot_ref": str(pre),
        "script_edit_applied": True,
    }
