"""Canonical review-merge workflow load proof for reviewer mutations (#389, #403, #559)."""

from __future__ import annotations

import hashlib
import os
import re
from pathlib import Path

import mcp_session_state
import review_workflow_boundary as boundary

WORKFLOW_REL_PATH = (
    "skills/llm-project-workflow/workflows/review-merge-pr.md"
)
SCHEMA_REL_PATH = (
    "skills/llm-project-workflow/schemas/review-merge-final-report.md"
)
TASK_MODE = "review-merge-pr"
LOAD_TOOL_NAME = "gitea_load_review_workflow"

_REVIEW_WORKFLOW_LOAD: dict | None = None


def compute_content_hash(text: str) -> str:
    """Short deterministic hash for workflow/schema version proof."""
    return hashlib.sha256((text or "").encode("utf-8")).hexdigest()[:12]


def _read_text(path: Path) -> str:
    return path.read_text(encoding="utf-8")


def _canonical_paths(project_root: str) -> tuple[Path, Path]:
    root = Path(project_root)
    workflow = root / WORKFLOW_REL_PATH
    schema = root / SCHEMA_REL_PATH
    if not workflow.is_file():
        raise FileNotFoundError(f"canonical workflow missing: {workflow}")
    if not schema.is_file():
        raise FileNotFoundError(f"final report schema missing: {schema}")
    return workflow, schema


def build_canonical_workflow_metadata(
    project_root: str,
    *,
    prompt_text: str | None = None,
) -> dict:
    """Load workflow + schema from disk and compute proof metadata."""
    workflow_path, schema_path = _canonical_paths(project_root)
    workflow_text = _read_text(workflow_path)
    schema_text = _read_text(schema_path)
    workflow_hash = compute_content_hash(workflow_text)
    schema_hash = compute_content_hash(schema_text)
    conflict, conflict_reasons = assess_prompt_conflict(prompt_text)
    return {
        "workflow_source": WORKFLOW_REL_PATH,
        "workflow_path": str(workflow_path),
        "task_mode": TASK_MODE,
        "workflow_hash": workflow_hash,
        "workflow_version": workflow_hash,
        "final_report_schema_path": SCHEMA_REL_PATH,
        "final_report_schema_hash": schema_hash,
        "prompt_conflicts_with_workflow": conflict,
        "prompt_conflict_reasons": conflict_reasons,
        "load_tool": LOAD_TOOL_NAME,
    }


def assess_prompt_conflict(prompt_text: str | None) -> tuple[bool, list[str]]:
    """Detect obvious task-mode conflicts between prompt and review workflow."""
    if not (prompt_text or "").strip():
        return False, []
    text = prompt_text.lower()
    reasons: list[str] = []
    conflicting = (
        (r"\bwork[- ]issue\b", "work-issue author mode"),
        (r"\bcreate[- ]issue\b", "create-issue mode"),
        (r"\bauthor/coder\b", "author/coder mode"),
        (r"\breconcile[- ]landed\b", "reconcile-landed mode"),
    )
    for pattern, label in conflicting:
        if re.search(pattern, text):
            reasons.append(
                f"active prompt appears to request {label} while loading "
                f"{TASK_MODE} workflow"
            )
    return bool(reasons), reasons


def _session_binding_fields() -> dict:
    """Capture profile identity used to share state across daemon processes."""
    env_lock = (os.environ.get(mcp_session_state.SESSION_PROFILE_LOCK_ENV) or "").strip()
    profile_name = (os.environ.get("GITEA_MCP_PROFILE") or "").strip()
    remote = (os.environ.get("GITEA_MCP_REMOTE") or "").strip() or None
    identity = mcp_session_state.current_profile_identity(
        profile_name=profile_name,
        session_profile_lock=env_lock,
    )
    return {
        "session_profile": profile_name or identity,
        "session_profile_lock": env_lock or identity,
        "profile_identity": identity,
        "remote": remote,
    }


def _persist_workflow_load(record: dict | None) -> dict | None:
    """Write durable workflow-load proof (or clear it)."""
    binding = _session_binding_fields()
    if record is None:
        mcp_session_state.clear_state(
            kind=mcp_session_state.KIND_WORKFLOW_LOAD,
            remote=binding.get("remote"),
            profile_identity=binding.get("profile_identity"),
        )
        return None
    payload = dict(record)
    payload.update({
        k: v for k, v in binding.items() if v is not None
    })
    return mcp_session_state.save_state(
        kind=mcp_session_state.KIND_WORKFLOW_LOAD,
        payload=payload,
        remote=payload.get("remote"),
        org=payload.get("org"),
        repo=payload.get("repo"),
        profile_identity=payload.get("profile_identity"),
    )


def _load_durable_workflow_load() -> dict | None:
    binding = _session_binding_fields()
    return mcp_session_state.load_state(
        kind=mcp_session_state.KIND_WORKFLOW_LOAD,
        remote=binding.get("remote"),
        profile_identity=binding.get("profile_identity"),
    )


def _active_workflow_load() -> dict | None:
    """Prefer in-process cache; fall back to durable shared state (#559)."""
    global _REVIEW_WORKFLOW_LOAD
    if _REVIEW_WORKFLOW_LOAD is not None:
        return _REVIEW_WORKFLOW_LOAD
    durable = _load_durable_workflow_load()
    if durable is not None:
        _REVIEW_WORKFLOW_LOAD = dict(durable)
    return _REVIEW_WORKFLOW_LOAD


def record_review_workflow_load(
    project_root: str,
    *,
    prompt_text: str | None = None,
) -> dict:
    """Record workflow load proof for the current MCP session (durable + memory)."""
    global _REVIEW_WORKFLOW_LOAD
    meta = build_canonical_workflow_metadata(
        project_root, prompt_text=prompt_text)
    boundary_state = boundary.assess_boundary_status(project_root)
    binding = _session_binding_fields()
    record = {
        **meta,
        "session_pid": os.getpid(),
        "loaded": True,
        "boundary_status": boundary_state.get("boundary_status"),
        "boundary_clean": boundary_state.get("boundary_clean"),
        "pre_review_command_count": boundary_state.get("pre_review_command_count"),
        "boundary_violation_count": boundary_state.get("boundary_violation_count"),
        "boundary_reasons": list(boundary_state.get("reasons") or []),
        **{k: v for k, v in binding.items() if v is not None},
    }
    persisted = _persist_workflow_load(record)
    _REVIEW_WORKFLOW_LOAD = dict(persisted or record)
    return dict(_REVIEW_WORKFLOW_LOAD)


def clear_review_workflow_load() -> None:
    """Test helper and review_pr session reset."""
    global _REVIEW_WORKFLOW_LOAD
    _REVIEW_WORKFLOW_LOAD = None
    _persist_workflow_load(None)
    boundary.clear_pre_review_commands()


def workflow_load_status(project_root: str | None = None) -> dict:
    """Non-throwing status for capability/runtime reports."""
    load = _active_workflow_load()
    if load is None:
        return {
            "workflow_load_proof_present": False,
            "workflow_load_valid": False,
            "workflow_source": None,
            "workflow_hash": None,
            "final_report_schema_path": SCHEMA_REL_PATH,
            "reasons": [
                f"{LOAD_TOOL_NAME} has not been called in this session "
                "(fail closed for reviewer mutations)"
            ],
        }
    reasons = _session_validation_reasons(load, project_root)
    boundary_reasons = boundary.boundary_blockers(project_root)
    if boundary_reasons:
        reasons = list(reasons) + boundary_reasons
    return {
        "workflow_load_proof_present": True,
        "workflow_load_valid": not reasons,
        "workflow_source": load.get("workflow_source"),
        "workflow_hash": load.get("workflow_hash"),
        "task_mode": load.get("task_mode"),
        "final_report_schema_path": load.get("final_report_schema_path"),
        "final_report_schema_hash": load.get("final_report_schema_hash"),
        "prompt_conflicts_with_workflow": load.get(
            "prompt_conflicts_with_workflow"),
        "session_pid": load.get("session_pid"),
        "writer_pid": load.get("writer_pid"),
        "profile_identity": load.get("profile_identity"),
        "boundary_status": load.get("boundary_status"),
        "boundary_clean": load.get("boundary_clean"),
        "workflow_load_helper_result": boundary.workflow_load_helper_result(
            load, project_root),
        "reasons": reasons,
    }


def _session_validation_reasons(
    load: dict,
    project_root: str | None,
) -> list[str]:
    """Validate durable/in-memory load proof for this session identity (#559)."""
    reasons: list[str] = []
    # Cross-process daemon pools are allowed when profile identity matches.
    # Reject only when the stored profile identity conflicts with this process.
    binding = _session_binding_fields()
    stored_identity = (
        load.get("session_profile_lock")
        or load.get("profile_identity")
        or ""
    ).strip()
    active_identity = (binding.get("profile_identity") or "").strip()
    if (
        stored_identity
        and active_identity
        and stored_identity != active_identity
        and active_identity != "unknown-profile"
        and stored_identity != "unknown-profile"
    ):
        reasons.append(
            "workflow load proof profile identity mismatch "
            f"(stored={stored_identity!r}, active={active_identity!r}; fail closed)"
        )
        return reasons

    # Expired durable records are treated as absent.
    identity_reasons = mcp_session_state.identity_match_reasons(
        load,
        remote=binding.get("remote") or load.get("remote"),
        org=load.get("org"),
        repo=load.get("repo"),
        profile_identity=active_identity or stored_identity,
    )
    # Filter out remote-mismatch noise when remote was not bound at load time.
    for reason in identity_reasons:
        if "missing recorded_at" in reason or "expired" in reason or "future" in reason:
            reasons.append(reason)
        elif "profile identity mismatch" in reason:
            reasons.append(reason)
    if reasons:
        return reasons

    if load.get("prompt_conflicts_with_workflow"):
        reasons.extend(load.get("prompt_conflict_reasons") or [
            "active prompt conflicts with loaded review-merge workflow"
        ])
    if project_root:
        try:
            current = build_canonical_workflow_metadata(project_root)
        except OSError as exc:
            reasons.append(f"cannot re-verify workflow hash: {exc}")
            return reasons
        if current["workflow_hash"] != load.get("workflow_hash"):
            reasons.append(
                "stored workflow hash is stale; reload via "
                f"{LOAD_TOOL_NAME} (fail closed)"
            )
        if current["final_report_schema_hash"] != load.get(
                "final_report_schema_hash"):
            reasons.append(
                "stored final-report schema hash is stale; reload via "
                f"{LOAD_TOOL_NAME} (fail closed)"
            )
    return reasons


def review_workflow_load_blockers(
    project_root: str | None = None,
) -> list[str]:
    """Reasons reviewer mutations must fail closed."""
    boundary_reasons = boundary.boundary_blockers(project_root)
    load = _active_workflow_load()
    if boundary_reasons and load is None:
        return boundary_reasons
    status = workflow_load_status(project_root)
    if not status.get("workflow_load_proof_present"):
        return list(status.get("reasons") or []) + boundary_reasons
    if not status.get("workflow_load_valid"):
        return list(status.get("reasons") or [])
    return []


def recovery_handoff_without_replay() -> list[str]:
    """Safe next-step lines that must not include approve/merge replay."""
    return [
        "Reload the canonical workflow via gitea_load_review_workflow, then "
        "rerun the full review-merge workflow from inventory.",
        "Do not call gitea_submit_pr_review, gitea_mark_final_review_decision, "
        "or gitea_merge_pr until workflow-load proof is present.",
        "Do not include approve/merge replay commands in the recovery handoff.",
    ]
