"""统一容错原语 — 供 corpus + cognition + MCP 共用

5 个共享原语：
- ServiceCooldown: 外部服务指数退避冷却
- CircuitBreaker: 跨进程文件熔断器
- safe_db_write(): 单条 DB 写保护
- safe_db_transaction(): 原子批量写入
- mcp_error_boundary(): MCP tool 错误边界装饰器
"""

from __future__ import annotations

import fcntl
import json
import logging
import os
import sqlite3
import tempfile
import time
from functools import wraps
from pathlib import Path
from typing import Any, Callable

logger = logging.getLogger(__name__)


# ────────────────────────────────────────────────────
# ServiceCooldown — 外部服务指数退避
# ────────────────────────────────────────────────────


class ServiceCooldown:
    """外部服务冷却器 — 指数退避，单进程内存状态

    使用者：
    - embed.py: 替代 _last_check + _RETRY_INTERVAL
    - cognitive/hooks.py: orient backfill / maintenance chat
    """

    def __init__(
        self,
        name: str,
        base_cooldown: float = 60.0,
        max_cooldown: float = 300.0,
        backoff_factor: float = 2.0,
    ):
        self.name = name
        self._base = base_cooldown
        self._max = max_cooldown
        self._factor = backoff_factor
        self._last_failure: float = 0.0
        self._consecutive_failures: int = 0
        self._last_success: float = 0.0

    @property
    def is_cooling(self) -> bool:
        """冷却中返回 True — O(1)，可频繁调用"""
        if self._last_failure == 0:
            return False
        elapsed = time.monotonic() - self._last_failure
        return elapsed < self.current_backoff

    @property
    def current_backoff(self) -> float:
        """当前冷却时间（秒）"""
        if self._consecutive_failures == 0:
            return 0.0
        return min(
            self._base * (self._factor ** (self._consecutive_failures - 1)),
            self._max,
        )

    def record_failure(self):
        self._last_failure = time.monotonic()
        self._consecutive_failures += 1
        logger.warning(
            "%s 失败 (连续第 %d 次)，冷却 %.0fs",
            self.name,
            self._consecutive_failures,
            self.current_backoff,
        )

    def record_success(self):
        self._consecutive_failures = 0
        self._last_failure = 0.0
        self._last_success = time.monotonic()

    def reset(self):
        """手动重置 — 用户主动重连时调用"""
        self._consecutive_failures = 0
        self._last_failure = 0.0

    @property
    def stats(self) -> dict:
        """健康状态 — 供 doctor/status 查询"""
        return {
            "name": self.name,
            "is_cooling": self.is_cooling,
            "consecutive_failures": self._consecutive_failures,
            "current_backoff_s": self.current_backoff,
            "last_success_ago_s": (
                round(time.monotonic() - self._last_success, 1)
                if self._last_success
                else None
            ),
        }


# ────────────────────────────────────────────────────
# CircuitBreaker — 跨进程文件熔断器
# ────────────────────────────────────────────────────


class CircuitBreaker:
    """跨进程熔断器 — JSON 文件状态，供独立 hook 进程共享

    与 ServiceCooldown 的区别：
    - ServiceCooldown: 单进程内存，适合长驻 MCP Server
    - CircuitBreaker: 文件状态，适合短命 hook 进程

    使用者：
    - cognitive/hooks.py observe: 写 cognitive.db 失败时熔断
    - watcher.py: observer 连续崩溃时熔断
    """

    def __init__(
        self,
        marker_path: Path,
        max_failures: int = 5,
        reset_after: float = 300.0,
    ):
        self._marker = Path(marker_path)
        self._max_failures = max_failures
        self._reset_after = reset_after

    def is_open(self) -> bool:
        """熔断器打开（应跳过操作）返回 True"""
        if not self._marker.exists():
            return False
        try:
            data = json.loads(self._marker.read_text())
            failures = data.get("failures", 0)
            last_failure = data.get("last_failure", 0)
            # 超过重置时间 → 自动关闭
            if time.time() - last_failure > self._reset_after:
                self._marker.unlink(missing_ok=True)
                return False
            return failures >= self._max_failures
        except (json.JSONDecodeError, OSError):
            return False

    def record_failure(self):
        try:
            data: dict[str, Any] = {"failures": 0}
            if self._marker.exists():
                try:
                    data = json.loads(self._marker.read_text())
                except (json.JSONDecodeError, OSError):
                    pass
            data["failures"] = data.get("failures", 0) + 1
            data["last_failure"] = time.time()
            self._marker.parent.mkdir(parents=True, exist_ok=True)
            self._marker.write_text(json.dumps(data))
        except OSError:
            pass  # 文件系统也写不了就放弃

    def record_success(self):
        try:
            self._marker.unlink(missing_ok=True)
        except OSError:
            pass


# ────────────────────────────────────────────────────
# FileStateCircuitBreaker — 文件态统一熔断器 (#237)
# ────────────────────────────────────────────────────


class FileStateCircuitBreaker:
    """文件态熔断器 — 统一 CircuitBreaker + PersistentCooldown (#237)

    状态机：closed → open → half-open → closed/open
    存储：JSON 文件 + fcntl.flock + atomic rename
    退避：指数退避 base * 2^(failures-threshold)，上限 max_cooldown

    与 CircuitBreaker 的区别：
    - CircuitBreaker: 简单标记文件，无计数/退避/half-open
    - FileStateCircuitBreaker: 完整状态(计数+退避+half-open)，并发安全

    与 ServiceCooldown 的区别：
    - ServiceCooldown: 内存态，每次 hook 进程重置（RC3 根因）
    - FileStateCircuitBreaker: 文件态，跨进程持久化
    """

    def __init__(
        self,
        name: str,
        state_dir: Path,
        threshold: int = 3,
        cooldown_seconds: float = 300.0,
        max_cooldown_seconds: float = 3600.0,
        time_func: Callable[[], float] | None = None,
    ):
        self._state_file = Path(state_dir) / f"{name}.cb.json"
        self._threshold = threshold
        self._base_cooldown = cooldown_seconds
        self._max_cooldown = max_cooldown_seconds
        self._time = time_func or time.time
        # 确保目录存在
        try:
            self._state_file.parent.mkdir(parents=True, exist_ok=True)
        except OSError:
            pass

    def _load(self) -> dict:
        """读取状态（共享锁），文件不存在或损坏返回默认值。"""
        try:
            with open(self._state_file) as f:
                fcntl.flock(f, fcntl.LOCK_SH)
                try:
                    data = json.load(f)
                finally:
                    fcntl.flock(f, fcntl.LOCK_UN)
                return data
        except (FileNotFoundError, json.JSONDecodeError, OSError):
            return {"failures": 0, "last_failure": 0.0, "half_open": False}

    def _save(self, state: dict) -> None:
        """原子写入（排他锁 + 临时文件 + rename）。"""
        # 附带 threshold 方便外部读取（observer_health.py 无需硬编码同步）
        state["threshold"] = self._threshold
        try:
            fd, tmp = tempfile.mkstemp(
                dir=str(self._state_file.parent),
                suffix=".tmp",
            )
            try:
                with os.fdopen(fd, "w") as f:
                    fcntl.flock(f, fcntl.LOCK_EX)
                    try:
                        json.dump(state, f)
                    finally:
                        fcntl.flock(f, fcntl.LOCK_UN)
                os.rename(tmp, self._state_file)
            except Exception:
                try:
                    os.unlink(tmp)
                except OSError:
                    pass
                raise
        except OSError as e:
            logger.debug("CB state save failed: %s", e)

    def _current_cooldown(self, failures: int) -> float:
        """指数退避冷却期：base * 2^(failures-threshold), 上限 max_cooldown。

        failures=3 → 300s, failures=4 → 600s, failures=5 → 1200s, cap 3600s
        """
        exponent = max(0, failures - self._threshold)
        return min(self._base_cooldown * (2 ** exponent), self._max_cooldown)

    def is_open(self) -> bool:
        """熔断器打开（应跳过操作）返回 True。

        兼容 CircuitBreaker.is_open() 接口。
        """
        state = self._load()
        failures = state.get("failures", 0)
        if failures < self._threshold:
            return False  # closed
        elapsed = self._time() - state.get("last_failure", 0)
        cooldown = self._current_cooldown(failures)
        if elapsed < cooldown:
            return True  # open（冷却中）
        # half-open：冷却期过，允许单次探测
        if not state.get("half_open", False):
            state["half_open"] = True
            self._save(state)
        return False

    def is_available(self) -> bool:
        """is_open 的反义 — 可用时返回 True。"""
        return not self.is_open()

    def record_failure(self) -> None:
        state = self._load()
        state["failures"] = state.get("failures", 0) + 1
        state["last_failure"] = self._time()
        state["half_open"] = False
        self._save(state)

    def record_success(self) -> None:
        state = self._load()
        if state.get("failures", 0) > 0 or state.get("half_open", False):
            state["failures"] = 0
            state["half_open"] = False
            self._save(state)

    @property
    def stats(self) -> dict:
        """健康状态 — 供 doctor/status 查询"""
        state = self._load()
        failures = state.get("failures", 0)
        return {
            "failures": failures,
            "threshold": self._threshold,
            "is_open": failures >= self._threshold,
            "half_open": state.get("half_open", False),
            "cooldown_seconds": self._current_cooldown(failures) if failures >= self._threshold else 0,
        }


# ────────────────────────────────────────────────────
# safe_db_write — 单条 DB 写保护
# ────────────────────────────────────────────────────


def safe_db_write(
    conn: sqlite3.Connection,
    sql: str,
    params: list | tuple | None = None,
) -> int | None:
    """安全 DB 写入 — 锁超时/磁盘满/DB损坏 返回 None 而非崩溃

    使用者：db.py, cognitive/db_cog.py 所有写操作
    """
    try:
        with conn:
            cursor = conn.execute(sql, params or [])
            return cursor.lastrowid
    except sqlite3.OperationalError as e:
        msg = str(e).lower()
        if "database is locked" in msg:
            logger.warning("DB 锁超时，写入跳过: %s", sql[:80])
            return None
        if "disk i/o error" in msg or "disk is full" in msg:
            logger.error("DB 磁盘错误: %s", e)
            return None
        raise
    except sqlite3.DatabaseError as e:
        msg = str(e).lower()
        if "malformed" in msg or "not a database" in msg:
            logger.error("DB 损坏: %s", e)
            return None
        raise


# ────────────────────────────────────────────────────
# safe_db_transaction — 原子批量写入
# ────────────────────────────────────────────────────


def safe_db_transaction(
    conn: sqlite3.Connection,
    fn: Callable[[sqlite3.Connection], Any],
) -> tuple[bool, Any]:
    """安全批量写入 — 在单个事务中执行 fn(conn)

    返回 (success, result):
    - (True, fn的返回值) — 成功
    - (False, None) — 锁超时/磁盘满/DB损坏

    使用者：indexer.py 原子化索引, cognitive backfill
    """
    try:
        with conn:
            result = fn(conn)
        return True, result
    except sqlite3.OperationalError as e:
        msg = str(e).lower()
        if "database is locked" in msg:
            logger.warning("DB 锁超时，事务回滚")
            return False, None
        if "disk i/o error" in msg or "disk is full" in msg:
            logger.error("DB 磁盘错误: %s", e)
            return False, None
        raise
    except sqlite3.DatabaseError as e:
        msg = str(e).lower()
        if "malformed" in msg or "not a database" in msg:
            logger.error("DB 损坏: %s", e)
            return False, None
        raise


# ────────────────────────────────────────────────────
# mcp_error_boundary — MCP tool 错误边界
# ────────────────────────────────────────────────────


def mcp_error_boundary(func: Callable) -> Callable:
    """MCP tool 错误边界装饰器 — 异常不传播到客户端

    使用者：mcp_server.py 所有 tool 函数
    """

    @wraps(func)
    async def wrapper(*args, **kwargs):
        try:
            return await func(*args, **kwargs)
        except sqlite3.DatabaseError as e:
            logger.error("MCP tool %s DB 错误: %s", func.__name__, e)
            return {"error": "数据库异常", "detail": str(e)}
        except Exception as e:
            logger.exception("MCP tool %s 未知错误", func.__name__)
            return {"error": "内部错误", "detail": str(e)}

    return wrapper
