"""Observer Worker — 持久 HTTP 服务（#247）

解决 PostToolUse hook 每次 spawn claude -p 进程的 13s 固定开销。
Worker 常驻后台，维持 provider 长连接，hook 通过 HTTP fire-and-forget。

数据流（SDK 持久会话优先）:
  PostToolUse hook → POST /observe (payload) → SQLite pending_observations → consumer
    → SessionManager.get_or_create(session_id) → ClaudeSession.generate() → parse → DB
    → 失败 fallback: observer.observe_tool_call() (原有 provider chain)
  持久化队列 — crash 重启后自动恢复未处理消息（#257 P0 fix）

并发模型:
  N 个 consumer 线程竞争 claim SQLite pending_observations，自然负载均衡。
  N = cpu_count（Mac M1 Pro = 10），每个线程独立 asyncio event loop。
  每个 session_id 对应一个持久 claude 进程（per-session 架构，照搬 claude-mem）。

空闲关闭:
  5 分钟无新事件 → 自动 SIGTERM 退出（config: observer_worker_idle_timeout）。
  下次 hook 触发 → ensure_observer_running() 自动重启。

幂等启动:
  ensure_observer_running() — PID 文件 + 端口检测 + 健康检查
"""

from __future__ import annotations

import atexit
import asyncio
import json
import logging
import os
import shutil
import signal
import socket
import subprocess
import threading
import time
from pathlib import Path
from typing import TYPE_CHECKING

from flask import Flask, Response, jsonify, request

if TYPE_CHECKING:
    from ..config import Config

logger = logging.getLogger(__name__)

# ── 常量 ──────────────────────────────────────────────────────────

_INDEX1_HOME = Path(os.environ.get("INDEX1_HOME", "")) or Path.home() / ".claude-index1"
_PID_FILE = _INDEX1_HOME / ".observer_worker.pid"
_LOG_FILE = _INDEX1_HOME / "observer_worker.log"
_FAILURE_LOG = _INDEX1_HOME / "observer_failures.log"


def _record_failure(message: str) -> None:
    """L3 事后日志：consumer 崩溃写 observer_failures.log（#271）"""
    try:
        from datetime import datetime, timezone
        ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S+00:00")
        _FAILURE_LOG.parent.mkdir(parents=True, exist_ok=True)
        with open(_FAILURE_LOG, "a") as f:
            f.write(f"{ts} {message}\n")
    except Exception:
        pass  # 日志写入本身不能阻塞 consumer


# ── Flask HTTP Server ──────────────────────────────────────────────

def create_app(config: Config | None = None, *, start_background_threads: bool = True) -> Flask:
    """创建 Observer Worker Flask app

    初始化: CogDatabase + Config + SessionObserver (providers warm)
    N 个 consumer 线程并行处理，空闲自动关闭。

    Args:
        config: 覆盖默认配置（None 时自动 load_config）
        start_background_threads: False 跳过 consumer/idle_watcher 线程启动，
            用于测试环境防止 daemon 线程发 SIGTERM 杀 pytest (#259)
    """
    from ..config import load_config
    from .db_cog import CogDatabase

    if config is None:
        config = load_config()

    # 独立 DB 连接（worker 是独立进程）
    # P0 fix: 必须传 effective_embedding_dim，否则用默认 768 触发破坏性维度迁移
    db_cog = CogDatabase(config.cognitive_db_path, config.effective_embedding_dim)
    db_cog.connect()
    # L1 事前校验：连接失败 → 拒绝启动（不让 consumer 带着 None _conn 跑）
    if db_cog._conn is None:
        raise RuntimeError(
            f"cognitive.db 连接失败: {config.cognitive_db_path}"
        )

    # 初始化 observer providers（warm connections）— skip_cli 避免与 SessionManager 重复 spawn
    from .observer import create_observer
    observer = create_observer(db_cog, config, skip_cli=True)

    # SDK 持久会话管理器 — per-session 架构
    from .observer.claude_session import SessionManager
    session_mgr = SessionManager(config)

    # P1: 意外退出时清理孤儿 claude 持久进程（start_new_session=True 进程不随父进程死亡）
    atexit.register(session_mgr.stop_all)

    # 持久化队列 — 使用 cognitive.db pending_observations 表（#257 crash-safe）
    _shutdown_event = threading.Event()

    # 启动恢复：crash 前 processing 的消息重置为 pending（#257 — processPendingQueues）
    try:
        recovered = db_cog.retry_stuck_observations(stuck_timeout_seconds=0.0)
        if recovered:
            logger.info("启动恢复: %d 条 crash 前未完成消息重置为 pending", recovered)
    except Exception as e:
        logger.warning("启动恢复失败: %s", e)

    # 并发配置 — cpu_count 个 consumer，10 窗口也不卡
    num_consumers = os.cpu_count() or 4
    try:
        idle_timeout = float(getattr(config, "observer_worker_idle_timeout", 300.0))
    except (TypeError, ValueError):
        idle_timeout = 300.0

    # L3 启动诊断日志（#271）
    provider_names = [p.name for p in observer.providers] if observer else []
    pending_count = db_cog.count_pending_observations()
    logger.info(
        "Observer worker 启动: consumers=%d, db_connected=%s, providers=%s, pending=%d",
        num_consumers, db_cog._conn is not None, provider_names, pending_count,
    )

    # 统计 + 活动追踪（_lock 保护非原子操作）
    _lock = threading.Lock()
    stats = {
        "processed": 0,
        "errors": 0,
        "started_at": time.time(),
        "last_activity": time.time(),
    }
    active_sessions: dict[str, float] = {}  # session_id → last_seen

    # ── 后台消费线程（N 个共享 1 个 queue，自然负载均衡）──

    def _consumer():
        """消费 SQLite 持久队列 — 持久会话优先，provider chain fallback

        从 pending_observations 表 claim → 处理 → mark_done/mark_failed。
        crash 重启后自动恢复未处理消息（#257 P0 fix）。

        L2 事中监控（#271）：外层 try-except 防止线程静默死亡。
        连续异常超过 _MAX_CONSECUTIVE_ERRORS 次才退出（防死循环刷日志）。
        """
        _MAX_CONSECUTIVE_ERRORS = 10
        thread_name = threading.current_thread().name
        consecutive_errors = 0

        try:
            from .observer import build_tool_prompt, process_raw_observation

            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
        except Exception as e:
            # L3: import 或 event loop 创建失败 — 写日志后退出
            logger.error("consumer %s 初始化失败: %s", thread_name, e)
            _record_failure(f"consumer-init: {e}")
            return

        while not _shutdown_event.is_set():
            try:
                # 从 SQLite 持久队列获取一条消息（CAS 原子 claim）
                row = db_cog.claim_observation()
                if row is None:
                    # 无消息 — 等待，但 shutdown 可随时唤醒
                    consecutive_errors = 0  # idle 不算连续错误
                    _shutdown_event.wait(timeout=2.0)
                    continue

                obs_id = row["id"]
                session_id = row["session_id"]

                # 防御：tool_input 是 JSON 字符串，解析为 dict
                ti_raw = row.get("tool_input", "{}")
                try:
                    ti = json.loads(ti_raw) if isinstance(ti_raw, str) else ti_raw
                except (json.JSONDecodeError, TypeError):
                    ti = {}

                try:
                    # ── 优先：SDK 持久会话（~3-5s 复用，首次 ~15s spawn）──
                    session = session_mgr.get_or_create(session_id)
                    if session:
                        prompt = build_tool_prompt(
                            row["tool_name"],
                            ti,
                            row["tool_output"],
                        )
                        raw = session.generate(prompt, timeout=60.0)
                        if raw:
                            result = process_raw_observation(
                                db_cog=db_cog,
                                raw=raw,
                                collection=row["collection"],
                                session_id=session_id,
                                provider_name="claude_persistent",
                                tool_input=ti,
                            )
                            with _lock:
                                stats["processed"] += 1
                            if result:
                                logger.info(
                                    "Observer worker (persistent) 写入 %d facts", len(result),
                                )
                            db_cog.mark_observation_done(obs_id)
                            consecutive_errors = 0
                            continue
                        else:
                            logger.debug(
                                "持久会话 generate() 返回 None (session=%s)，走 fallback",
                                session_id,
                            )

                    # ── Fallback：原有 provider chain（Gemini/OpenRouter/OpenClaw/Ollama）──
                    # L2 事中监控：独立 timeout 防止 provider chain 卡死 consumer 线程
                    try:
                        result = loop.run_until_complete(
                            asyncio.wait_for(
                                observer.observe_tool_call(
                                    tool_name=row["tool_name"],
                                    tool_input=ti,
                                    tool_output=row["tool_output"],
                                    collection=row["collection"],
                                    session_id=session_id,
                                ),
                                timeout=30.0,
                            )
                        )
                    except asyncio.TimeoutError:
                        logger.warning(
                            "Observer fallback 超时 (30s), session=%s", session_id,
                        )
                        result = None
                    if result is None:
                        with _lock:
                            stats["errors"] += 1
                        logger.warning("Observer 全部 provider 失败（含持久会话）")
                        db_cog.mark_observation_failed(obs_id, "all providers failed")
                    else:
                        with _lock:
                            stats["processed"] += 1
                        if result:
                            logger.info("Observer worker (fallback) 写入 %d facts", len(result))
                        db_cog.mark_observation_done(obs_id)
                    consecutive_errors = 0
                except Exception as e:
                    with _lock:
                        stats["errors"] += 1
                    logger.error("Observer worker 处理失败: %s", e)
                    try:
                        db_cog.mark_observation_failed(obs_id, str(e)[:500])
                    except Exception:
                        pass  # mark_failed 本身也可能失败（DB 锁），不能阻塞 consumer

            except Exception as e:
                # L2: 外层兜底 — claim_observation 或其他未预期异常
                consecutive_errors += 1
                with _lock:
                    stats["errors"] += 1
                logger.error(
                    "consumer %s 异常 (%d/%d): %s",
                    thread_name, consecutive_errors, _MAX_CONSECUTIVE_ERRORS, e,
                )
                _record_failure(f"{thread_name}: {type(e).__name__}: {e}")
                if consecutive_errors >= _MAX_CONSECUTIVE_ERRORS:
                    logger.error(
                        "consumer %s 连续 %d 次异常，退出线程",
                        thread_name, _MAX_CONSECUTIVE_ERRORS,
                    )
                    break
                # 短暂退避，避免死循环刷日志
                _shutdown_event.wait(timeout=min(2.0 * consecutive_errors, 30.0))

        try:
            loop.close()
        except Exception:
            pass

    # ── 空闲监视线程 ──

    def _idle_watcher():
        """空闲超时自动关闭 + 定期清理空闲 session + stuck 消息恢复

        每 30s 检查：
        1. 清理空闲超过 idle_timeout 的 claude 持久会话
        2. 恢复 stuck 消息（processing 超 5min → 重置为 pending，max_retries=3）
        3. GC 已完成/失败的旧消息（>7 天）
        4. Worker 整体空闲超时 → 自动关闭（含 stop_all）
        """
        gc_counter = 0  # GC 每 10 轮执行一次（~5 分钟）
        while not _shutdown_event.is_set():
            _shutdown_event.wait(timeout=30)
            if _shutdown_event.is_set():
                break

            # 1. 清理空闲 claude 持久会话
            try:
                cleaned = session_mgr.cleanup_idle(max_idle=idle_timeout)
                if cleaned:
                    logger.info("清理 %d 个空闲 Claude 持久会话", cleaned)
            except Exception as e:
                logger.debug("session cleanup 失败: %s", e)

            # 2. 恢复 stuck 消息（#257 — processing 超 5min → 重置为 pending）
            try:
                retried = db_cog.retry_stuck_observations(stuck_timeout_seconds=300.0)
                if retried:
                    logger.info("恢复 %d 条 stuck 消息（processing > 5min）", retried)
            except Exception as e:
                logger.debug("stuck 消息恢复失败: %s", e)

            # 3. GC 旧消息（每 ~5 分钟一次）
            gc_counter += 1
            if gc_counter >= 10:
                gc_counter = 0
                try:
                    gc_count = db_cog.gc_pending_observations(max_age_days=7)
                    if gc_count:
                        logger.info("GC 清理 %d 条旧 pending_observations", gc_count)
                except Exception as e:
                    logger.debug("pending_observations GC 失败: %s", e)

            # 4. 空闲超时 → 自动关闭
            with _lock:
                idle_secs = time.time() - stats["last_activity"]
            if idle_secs > idle_timeout:
                logger.info(
                    "Observer worker 空闲 %ds（超时 %ds），自动关闭",
                    int(idle_secs), int(idle_timeout),
                )
                session_mgr.stop_all()
                _shutdown_event.set()
                time.sleep(0.5)  # 让 DB 连接 + stderr 文件关闭
                os.kill(os.getpid(), signal.SIGTERM)
                break

    # 启动 N 个消费线程 + 空闲监视（测试时可禁用）
    consumers: list[threading.Thread] = []
    if observer and start_background_threads:
        for i in range(num_consumers):
            t = threading.Thread(
                target=_consumer, daemon=True, name=f"observer-consumer-{i}",
            )
            t.start()
            consumers.append(t)

        watcher = threading.Thread(
            target=_idle_watcher, daemon=True, name="observer-idle-watcher",
        )
        watcher.start()

    app = Flask(__name__)

    @app.route("/observe", methods=["POST"])
    def observe():
        """接收 observation payload，写入 SQLite 持久队列"""
        if observer is None:
            return jsonify({"error": "observer not initialized"}), 503

        try:
            payload = request.get_json(silent=True)
            if not payload:
                return jsonify({"error": "invalid JSON"}), 400

            # 必需字段
            for key in ("tool_name", "tool_input", "tool_output", "collection"):
                if key not in payload:
                    return jsonify({"error": f"missing field: {key}"}), 400

            # tool_input 序列化为 JSON 字符串（DB 存 TEXT）
            ti = payload.get("tool_input", {})
            ti_str = json.dumps(ti) if isinstance(ti, dict) else str(ti)

            obs_id = db_cog.enqueue_observation(
                session_id=payload.get("session_id", "default"),
                collection=payload["collection"],
                tool_name=payload["tool_name"],
                tool_input=ti_str,
                tool_output=payload.get("tool_output", ""),
            )
            if obs_id is None:
                return jsonify({"error": "enqueue failed (db write)"}), 503

            # 更新活动追踪
            now = time.time()
            with _lock:
                stats["last_activity"] = now
                sid = payload.get("session_id")
                if sid:
                    active_sessions[sid] = now
                    # 清理过期 session
                    cutoff = now - idle_timeout
                    stale = [k for k, v in active_sessions.items() if v < cutoff]
                    for k in stale:
                        del active_sessions[k]

            return jsonify({"status": "queued", "obs_id": obs_id}), 202

        except Exception as e:
            logger.error("POST /observe 处理失败: %s", e)
            return jsonify({"error": str(e)}), 500

    @app.route("/health", methods=["GET"])
    def health():
        """健康检查 — 返回 worker 状态（含并发 + 空闲 + 线程存活信息）"""
        now = time.time()
        with _lock:
            uptime = now - stats["started_at"]
            idle_secs = now - stats["last_activity"]
            n_sessions = len(active_sessions)
            processed = stats["processed"]
            errors = stats["errors"]
        provider_names = [p.name for p in observer.providers] if observer else []

        # L2 事中监控：检查 consumer 线程存活状态
        consumers_alive = sum(1 for t in consumers if t.is_alive())
        consumers_dead = num_consumers - consumers_alive

        status = "ok"
        if not observer:
            status = "no_observer"
        elif consumers and consumers_dead > 0:
            status = "degraded"

        pending_depth = db_cog.count_pending_observations()
        return jsonify({
            "status": status,
            "queue_depth": pending_depth,
            "consumers": num_consumers,
            "consumers_alive": consumers_alive,
            "consumers_dead": consumers_dead,
            "active_sessions": n_sessions,
            "processed": processed,
            "errors": errors,
            "idle_seconds": round(idle_secs, 1),
            "idle_timeout": idle_timeout,
            "uptime_seconds": round(uptime, 1),
            "providers": provider_names,
            "claude_sessions": session_mgr.stats,
        })

    @app.route("/cleanup-session", methods=["POST"])
    def cleanup_session():
        """主动清理指定 session（reflect hook SessionEnd 调用）"""
        data = request.get_json(silent=True) or {}
        sid = data.get("session_id", "")
        if not sid:
            return jsonify({"error": "missing session_id"}), 400
        cleaned = session_mgr.cleanup_session(sid)
        return jsonify({"cleaned": cleaned})

    @app.route("/shutdown", methods=["POST"])
    def shutdown():
        """优雅关闭 — 停止所有 claude 持久会话 + consumer 线程"""
        session_mgr.stop_all()
        # 通知所有 consumer 线程退出（Event 立即唤醒 wait）
        _shutdown_event.set()
        # L2: join 有 timeout 防止永久阻塞
        _shutdown_deadline = time.monotonic() + 10.0
        for t in consumers:
            remaining = _shutdown_deadline - time.monotonic()
            if remaining > 0:
                t.join(timeout=remaining)
        still_alive = sum(1 for t in consumers if t.is_alive())
        if still_alive:
            logger.warning("Shutdown: %d consumer 线程仍在运行，强制关闭", still_alive)
        os.kill(os.getpid(), signal.SIGTERM)
        return jsonify({"status": "shutting_down"})

    return app


def run_worker(config: Config | None = None) -> None:
    """前台运行 observer worker（CLI 入口点）"""
    from ..config import load_config

    if config is None:
        config = load_config()

    port = getattr(config, "observer_worker_port", 6889)

    # 写 PID 文件
    _write_pid_file(os.getpid(), port)

    try:
        app = create_app(config)
        logger.info(
            "Observer worker 启动: port=%d, pid=%d, consumers=%d",
            port, os.getpid(), os.cpu_count() or 4,
        )
        # 绑定 localhost only — 安全
        app.run(host="127.0.0.1", port=port, threaded=True)
    finally:
        _cleanup_pid_file()


# ── PID 文件管理（复用 web.py 模式）────────────────────────────────

def _is_port_open(port: int, host: str = "127.0.0.1") -> bool:
    """检测端口是否有进程在监听"""
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(0.5)
            return s.connect_ex((host, port)) == 0
    except OSError:
        return False


def _is_process_alive(pid: int) -> bool:
    """检测进程是否存活"""
    try:
        os.kill(pid, 0)
        return True
    except (OSError, ProcessLookupError):
        return False


def _read_pid_file() -> dict | None:
    """读取 PID 文件"""
    try:
        if _PID_FILE.exists():
            data = json.loads(_PID_FILE.read_text())
            if isinstance(data, dict) and "pid" in data:
                return data
    except (json.JSONDecodeError, OSError):
        pass
    return None


def _write_pid_file(pid: int, port: int) -> None:
    """写入 PID 文件"""
    try:
        _INDEX1_HOME.mkdir(parents=True, exist_ok=True)
        _PID_FILE.write_text(
            json.dumps({"pid": pid, "port": port, "started_at": time.time()})
        )
    except OSError:
        pass


def _cleanup_pid_file() -> None:
    """清理 PID 文件"""
    try:
        _PID_FILE.unlink(missing_ok=True)
    except OSError:
        pass


# ── 幂等启动（Task 2）────────────────────────────────────────────

def ensure_observer_running(config: Config | None = None) -> bool:
    """幂等启动 Observer Worker 后台服务。

    已运行 → 跳过，返回 True
    新启动 → 拉起后台进程，返回 True
    失败 → 返回 False（不崩溃）

    复用 ensure_web_running() 模式（web.py:962-1031）。
    """
    try:
        if config is None:
            from ..config import load_config
            config = load_config()

        if not getattr(config, "observer_worker_enabled", True):
            return False

        port = getattr(config, "observer_worker_port", 6889)

        # 1. 检查 PID 文件
        pid_info = _read_pid_file()
        if pid_info:
            pid = pid_info["pid"]
            saved_port = pid_info.get("port", port)
            # 进程存活 + 端口正常 → 已在运行
            if _is_process_alive(pid) and _is_port_open(saved_port):
                return True
            # stale PID → 清理
            _cleanup_pid_file()

        # 2. 端口已被占用（可能是手动启动的）
        if _is_port_open(port):
            return True

        # 3. 启动新进程
        index1_bin = shutil.which("index1")
        if not index1_bin:
            logger.warning("index1 不在 PATH 中，Observer worker 自动启动失败")
            return False

        _INDEX1_HOME.mkdir(parents=True, exist_ok=True)
        log_file = open(_LOG_FILE, "a")
        try:
            proc = subprocess.Popen(
                [index1_bin, "observer-worker"],
                start_new_session=True,
                stdout=subprocess.DEVNULL,
                stderr=log_file,
            )

            # 4. 启动后健康检查 — 等待最多 3s 确认进程存活且端口开放
            # 宪法#3 铁律#7：exit 0 不等于成功，必须验证
            for _ in range(6):
                time.sleep(0.5)
                if proc.poll() is not None:
                    logger.warning(
                        "Observer worker 启动后立即退出 (rc=%s)，详见 %s",
                        proc.returncode, _LOG_FILE,
                    )
                    _cleanup_pid_file()
                    return False
                if _is_port_open(port):
                    logger.info("Observer worker 后台启动: pid=%d, port=%d", proc.pid, port)
                    return True

            # 3s 后端口仍未开放，但进程活着 — 可能还在初始化
            logger.info("Observer worker 启动中(端口未确认): pid=%d, port=%d", proc.pid, port)
            return True
        finally:
            log_file.close()

    except Exception as e:
        logger.debug("Observer worker 自动启动失败: %s", e)
        return False
