"""Persistent circuit breaker for micro-scanner.

Architecture Design §3.3 — FR-02 fix.

State file: ``microcap_cb.json`` (schema_version: 1, D-5 fix).
Trigger: 8% session loss.
Reset:    session P&L returns to 0 AND manual reset (scripts/reset-circuit-breaker.sh).

Clearance is logged to ``rollback_clearance_log.jsonl`` for audit.
"""

from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from pathlib import Path

import structlog

log = structlog.get_logger(__name__)

_SCHEMA_VERSION = 1
_CB_FILENAME = "microcap_cb.json"
_CLEARANCE_LOG_FILENAME = "rollback_clearance_log.jsonl"


class CircuitBreakerError(Exception):
    """Raised when a reset is requested without manual approval."""


class MicroCircuitBreaker:
    """Persistent circuit breaker backed by a JSON state file.

    The file is written atomically (write-then-rename) to prevent partial writes
    corrupting the state.

    safe-fail rule: if the state file exists but is unreadable, treat as OPEN.
    """

    def __init__(self, data_dir: str) -> None:
        self._data_dir = Path(data_dir)
        self._cb_path = self._data_dir / _CB_FILENAME
        self._clearance_path = self._data_dir / _CLEARANCE_LOG_FILENAME

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def is_open(self) -> bool:
        """Return True when the circuit breaker is OPEN (trading blocked).

        safe-fail: unreadable or corrupt state file → OPEN.
        """
        state = self._read_state()
        if state is None:
            return False  # No state file → breaker never triggered → closed
        return state.get("is_open", True)  # corrupt → safe-fail open

    def trigger(self, reason: str) -> None:
        """Open the circuit breaker and persist state.

        Emits a structured log event and updates the Prometheus metric.
        """
        state = {
            "schema_version": _SCHEMA_VERSION,
            "is_open": True,
            "triggered_at": datetime.now(timezone.utc).isoformat(),
            "trigger_reason": reason,
            "reset_at": None,
            "reset_reason": None,
        }
        self._write_state(state)
        log.critical(
            "circuit_breaker_triggered",
            reason=reason,
            cb_path=str(self._cb_path),
        )
        # Update Prometheus metric (import here to avoid circular at module load)
        try:
            from src.micro_scanner.metrics import sigil_micro_circuit_breaker_state

            sigil_micro_circuit_breaker_state.set(1)
        except ImportError:
            pass

    def reset(self, reason: str, manual_approved: bool = False) -> bool:
        """Close the circuit breaker.

        Args:
            reason: Human-readable explanation of why the breaker is being reset.
            manual_approved: Must be True; if False, raises CircuitBreakerError.

        Returns:
            True on success.

        Raises:
            CircuitBreakerError: if manual_approved is False.
        """
        if not manual_approved:
            raise CircuitBreakerError(
                "Circuit breaker reset requires manual approval "
                "(set manual_approved=True, triggered by scripts/reset-circuit-breaker.sh)"
            )

        state = self._read_state() or {}
        state.update(
            {
                "schema_version": _SCHEMA_VERSION,
                "is_open": False,
                "reset_at": datetime.now(timezone.utc).isoformat(),
                "reset_reason": reason,
            }
        )
        self._write_state(state)
        self._append_clearance_log(reason)

        log.info(
            "circuit_breaker_reset",
            reason=reason,
            cb_path=str(self._cb_path),
        )
        try:
            from src.micro_scanner.metrics import sigil_micro_circuit_breaker_state

            sigil_micro_circuit_breaker_state.set(0)
        except ImportError:
            pass
        return True

    def load_state(self) -> dict:
        """Return the raw state dict, or an empty dict if no file exists."""
        return self._read_state() or {}

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _read_state(self) -> dict | None:
        """Read and parse the CB state file.

        Returns None if the file does not exist.
        Returns the dict on success.
        On any error (corrupt JSON, permission denied), logs a warning and
        returns a safe-fail open state dict.
        """
        if not self._cb_path.exists():
            return None
        try:
            raw = self._cb_path.read_text(encoding="utf-8")
            return json.loads(raw)
        except Exception as exc:
            log.warning(
                "circuit_breaker_read_error",
                path=str(self._cb_path),
                error=str(exc),
            )
            return {"schema_version": _SCHEMA_VERSION, "is_open": True}

    def _write_state(self, state: dict) -> None:
        """Atomically write state to disk via a temp file + rename."""
        self._data_dir.mkdir(parents=True, exist_ok=True)
        tmp = self._cb_path.with_suffix(".tmp")
        try:
            tmp.write_text(json.dumps(state, indent=2), encoding="utf-8")
            tmp.replace(self._cb_path)
        except Exception as exc:
            log.error(
                "circuit_breaker_write_error",
                path=str(self._cb_path),
                error=str(exc),
            )
            raise

    def _append_clearance_log(self, reason: str) -> None:
        """Append a JSONL record to the clearance audit log."""
        record = {
            "cleared_at": datetime.now(timezone.utc).isoformat(),
            "reason": reason,
            "operator": os.getenv("USER", "unknown"),
        }
        try:
            with open(self._clearance_path, "a", encoding="utf-8") as f:
                f.write(json.dumps(record) + "\n")
        except Exception as exc:
            log.warning(
                "clearance_log_write_error",
                path=str(self._clearance_path),
                error=str(exc),
            )
