"""实时维护 — L0 步骤在 observe 中实时执行

RealtimeUpdater 封装所有 L0 纯规则维护步骤，在 observe hook 中调用。
每次 fact 写入后执行即时维护（因果推断、promote 检查），
并通过计数器周期性执行批量 L0 步骤（decay、extract_tactics、gc）。

与 maintenance.py 的协调通过 marker 文件实现：
- observe 写入 marker → maintenance 跳过已实时化的 L0 步骤
- reflect 清除 marker → maintenance 恢复全量执行
"""

from __future__ import annotations

import json
import logging
import os
import tempfile
import time

from ..config import INDEX1_HOME, Config
from .db_cog import CogDatabase
from .rules import looks_project_specific, match_edge_rule, match_triple_rule

logger = logging.getLogger(__name__)


# ── RealtimeUpdater 类 ──


_PERIODIC_INTERVAL = 300  # 5 分钟


class RealtimeUpdater:
    """L0 实时维护 — observe hook 中调用

    两类操作:
    1. on_fact_saved(): 每次 fact 写入后即时执行（因果推断 + promote 检查）
    2. periodic_l0(): 时间间隔控制（decay / extract_tactics / gc）

    注意: Hook 每次调用创建新进程新实例，内存计数器无效。
    改用 marker 文件记录上次执行时间，5 分钟内不重复。
    """

    def __init__(self, db_cog: CogDatabase, config: Config):
        self.db_cog = db_cog
        self.config = config
        self._embedder = None  # P3-6: lazy init for eager embed

    def on_fact_saved(
        self,
        fact: dict,
        collection: str,
        session_id: str | None,
        importance: float = 0.0,
    ) -> None:
        """fact 写入后立即执行 — 增量因果推断 + promote 检查 + eager embed"""
        try:
            self._infer_edges_incremental(fact, collection, session_id)
        except Exception as e:
            logger.debug("realtime infer_edges 失败: %s", e)

        # P3-6: Eager Embedding — 高 importance fact 同步 ONNX 嵌入
        if (self.config.eager_embed_enabled
                and importance >= self.config.eager_embed_min_importance
                and getattr(self.config, "embed_backend", "ollama") == "onnx"):
            try:
                fact_id = fact.get("id")
                if fact_id:
                    self._eager_embed_fact(
                        fact_id,
                        fact.get("assertion", ""),
                        fact.get("context_json", ""),
                    )
            except Exception as e:
                logger.debug("eager embed 失败（降级等 maintenance 回填）: %s", e)

    def periodic_l0(
        self,
        collection: str,
        session_id: str | None = None,
    ) -> None:
        """周期性 L0 步骤 — 时间间隔控制（替代失效的内存计数器）

        Hook 每次调用创建新进程新实例，内存计数器永远=0。
        改用 periodic marker 文件记录上次执行时间，5 分钟内不重复。
        """
        marker = INDEX1_HOME / f".periodic_last_run_{collection}"
        now = time.time()

        # 时间间隔检查
        try:
            last_run = float(marker.read_text().strip())
            if now - last_run < _PERIODIC_INTERVAL:
                return  # 5 分钟内已执行，跳过
        except (FileNotFoundError, ValueError, OSError):
            pass  # 首次运行或文件损坏 → 执行

        # 先 flush CB buffer（最先执行）
        self._flush_circuit_buffer()

        # 执行 L0 步骤
        try:
            count = self.db_cog.decay_facts(
                collection,
                stale_days=self.config.cognitive_decay_days,
            )
            if count > 0:
                logger.debug("realtime decay: %d facts", count)
        except Exception as e:
            logger.debug("realtime decay 失败: %s", e)

        try:
            count = self._extract_tactics_l0(collection)
            if count > 0:
                logger.debug("realtime extract_tactics: %d", count)
        except Exception as e:
            logger.debug("realtime extract_tactics 失败: %s", e)

        try:
            count = self.db_cog.garbage_collect(
                collection,
                min_confidence=self.config.cognitive_gc_min_confidence,
                stale_days=self.config.cognitive_gc_days,
            )
            if count > 0:
                logger.debug("realtime gc: %d facts", count)
        except Exception as e:
            logger.debug("realtime gc 失败: %s", e)

        try:
            self._check_promote(collection)
        except Exception as e:
            logger.debug("realtime check_promote 失败: %s", e)

        # raw_events GC — 7 天过期删除 (#124)
        try:
            count = self.db_cog.gc_raw_events(retention_days=7)
            if count > 0:
                logger.debug("realtime gc_raw_events: %d deleted", count)
        except Exception as e:
            logger.debug("realtime gc_raw_events 失败: %s", e)

        # 更新 periodic marker + realtime active marker
        try:
            INDEX1_HOME.mkdir(parents=True, exist_ok=True)
            marker.write_text(str(now))
        except OSError:
            pass
        self.write_marker(collection)

    def _flush_circuit_buffer(self) -> None:
        """replay CB buffer 中积累的 facts（periodic_l0 第一步）"""
        buffer_path = INDEX1_HOME / ".circuit_buffer.jsonl"
        if not buffer_path.exists():
            return
        try:
            lines = buffer_path.read_text().splitlines()
        except OSError:
            return
        if not lines:
            return

        replayed = 0
        skipped_dedup = 0
        remaining: list[str] = []
        for line in lines:
            line = line.strip()
            if not line:
                continue
            try:
                fact = json.loads(line)
                assertion = fact.get("assertion", "")
                # L1 事前: assertion 类型+长度校验
                if not isinstance(assertion, str) or len(assertion) < 10:
                    continue
                # L2 事中: dedup 检查（#264 — buffer facts 之前跳过了去重）
                try:
                    from .dedup import check_duplicate
                    dup = check_duplicate(
                        self.db_cog, assertion,
                        collection=fact.get("collection", ""),
                        session_id=fact.get("session_id"),
                    )
                    if dup.get("is_duplicate"):
                        skipped_dedup += 1
                        continue
                except Exception:
                    pass  # dedup 失败不阻塞回放
                result = self.db_cog.insert_fact(fact)
                if result is not None:
                    replayed += 1
                else:
                    remaining.append(line)
            except (json.JSONDecodeError, OSError, TypeError):
                remaining.append(line)

        # 更新 buffer 文件
        try:
            if remaining:
                buffer_path.write_text("\n".join(remaining) + "\n")
            else:
                buffer_path.unlink(missing_ok=True)
        except OSError:
            pass

        # L3 事后: 记录回放/跳过数量
        if replayed > 0 or skipped_dedup > 0:
            logger.debug(
                "CB buffer: replayed=%d, dedup_skipped=%d, remaining=%d",
                replayed, skipped_dedup, len(remaining),
            )

    # ── Marker 文件管理 ──

    def write_marker(self, collection: str) -> None:
        """写 realtime active marker — maintenance 据此跳过 L0 步骤"""
        marker = INDEX1_HOME / f".realtime_active_{collection}"
        try:
            INDEX1_HOME.mkdir(parents=True, exist_ok=True)
            fd, tmp_name = tempfile.mkstemp(
                dir=INDEX1_HOME, prefix=".tmp_rt_",
            )
            payload = json.dumps({
                "collection": collection,
                "timestamp": time.time(),
            })
            os.write(fd, payload.encode())
            os.close(fd)
            os.replace(tmp_name, marker)
        except OSError as e:
            logger.debug("write_marker 失败: %s", e)

    @staticmethod
    def clear_marker(collection: str) -> None:
        """清除 realtime active marker — reflect 时调用"""
        marker = INDEX1_HOME / f".realtime_active_{collection}"
        try:
            marker.unlink(missing_ok=True)
        except OSError as e:
            logger.debug("clear_marker 失败: %s", e)

    @staticmethod
    def clear_periodic_marker(collection: str) -> None:
        """清除 periodic marker — reflect 时调用，下次 observe 立即执行 periodic_l0"""
        marker = INDEX1_HOME / f".periodic_last_run_{collection}"
        try:
            marker.unlink(missing_ok=True)
        except OSError as e:
            logger.debug("clear_periodic_marker 失败: %s", e)

    @staticmethod
    def check_marker(collection: str) -> bool:
        """检查 marker 是否存在 — maintenance 用"""
        marker = INDEX1_HOME / f".realtime_active_{collection}"
        if not marker.exists():
            return False
        # 检查 marker 是否太旧（>15 分钟视为过期）
        try:
            data = json.loads(marker.read_text())
            age = time.time() - data.get("timestamp", 0)
            if age > 900:  # 15 分钟过期
                marker.unlink(missing_ok=True)
                return False
            return True
        except (OSError, json.JSONDecodeError):
            return False

    # ── 内部方法 ──

    def _eager_embed_fact(
        self,
        fact_id: int,
        assertion: str,
        context_json: str,
    ) -> None:
        """P3-6: 高 importance fact 同步 ONNX embedding

        observe hook 后台异步执行，~200ms 首次模型加载 + ~5ms embed。
        文本构造与 maintenance._backfill_embeddings 一致。
        失败时静默降级 → maintenance 回填。
        """
        import asyncio

        if not assertion:
            return

        # Lazy init embedder（同一 observe 调用内复用）
        if self._embedder is None:
            from ..embed_onnx import FastEmbedEmbedder
            self._embedder = FastEmbedEmbedder()

        # 构造 embedding 文本（与 maintenance._backfill_embeddings 一致）
        extra = ""
        if context_json:
            try:
                ctx = json.loads(context_json) if isinstance(context_json, str) else context_json
                parts = []
                for k in ("changes", "reasoning", "error_message", "fix"):
                    v = ctx.get(k)
                    if v:
                        parts.append(str(v)[:100])
                extra = " ".join(parts)
            except (json.JSONDecodeError, TypeError):
                pass
        text = f"{assertion} {extra}".strip()[:500]

        # 同步调用 async embed_one
        loop = asyncio.new_event_loop()
        try:
            embedding = loop.run_until_complete(
                self._embedder.embed_one(text)
            )
        finally:
            loop.close()

        self.db_cog.update_fact_embedding(fact_id, embedding)
        logger.debug("eager embed 完成: fact_id=%d", fact_id)

    def _infer_edges_incremental(
        self,
        fact: dict,
        collection: str,
        session_id: str | None,
    ) -> int:
        """增量因果推断 — 只比较新 fact 与最近 facts"""
        if not session_id:
            return 0

        fact_id = fact.get("id")
        if not fact_id:
            return 0

        # 取最近 5 个同 session 的 facts（不含当前）
        recent = self.db_cog.get_session_facts(session_id, collection)
        # 过滤掉当前 fact，取最近 5 个
        recent = [f for f in recent if f.get("id") != fact_id][-5:]
        if not recent:
            return 0

        edges = 0

        # 规则匹配：最近的 fact 作为 prev，新 fact 作为 curr
        prev = recent[-1]
        rule = match_edge_rule(prev, fact)
        if rule:
            relation, confidence = rule
            prev_id = prev.get("id")
            if prev_id and self.db_cog.insert_edge(prev_id, fact_id, relation, confidence, "realtime"):
                edges += 1

        # 三元组规则：倒数第二个、最后一个、新 fact
        if len(recent) >= 2:
            prev2 = recent[-2]
            prev = recent[-1]
            triples = match_triple_rule(prev2, prev, fact)
            if triples:
                for from_id, to_id, relation, confidence in triples:
                    if self.db_cog.insert_edge(from_id, to_id, relation, confidence, "realtime"):
                        edges += 1

        return edges

    def _check_promote(self, collection: str) -> None:
        """周期性检查满足 promote 条件的候选

        条件: confidence ≥ 0.85 + access_count ≥ 3 + 非项目特定
        由 periodic_l0 每 50 次调用一次（避免每次 fact 写入都全表扫描）。
        """
        candidates = self.db_cog.get_universal_candidates(collection)
        for candidate in candidates:
            scope_files = candidate.get("scope_files", "[]")
            try:
                files = json.loads(scope_files) if isinstance(scope_files, str) else scope_files
            except (json.JSONDecodeError, TypeError):
                files = []
            if files:
                continue

            assertion = candidate.get("assertion", "")
            if looks_project_specific(assertion):
                continue

            self.db_cog.promote_to_universal(candidate["id"])
            logger.debug(
                "realtime promote: [%d] %s",
                candidate["id"], assertion[:60],
            )

    def _extract_tactics_l0(self, collection: str) -> int:
        """从跨 session 的重复行为模式自动提取 tactics（与 maintenance 相同逻辑）"""
        conn = self.db_cog.conn
        extracted = 0

        # 规则 1: 重复 root_cause 模式
        try:
            rows = conn.execute("""
                SELECT scope_files, COUNT(*) as cnt,
                       GROUP_CONCAT(assertion, ' | ') as assertions,
                       GROUP_CONCAT(context_json, ' | ') as contexts
                FROM facts
                WHERE collection=? AND status='active'
                  AND category='root_cause'
                  AND scope_files != '[]' AND scope_files != ''
                GROUP BY scope_files
                HAVING cnt >= 2
                ORDER BY cnt DESC
                LIMIT 10
            """, [collection]).fetchall()

            for row in rows:
                scope = row[0]
                count = row[1]
                assertions = row[2] or ""

                existing = self.db_cog.search_tactics(scope, collection, limit=1)
                if existing:
                    self.db_cog.update_tactic_outcome(existing[0]["id"], True)
                    continue

                first_assertion = assertions.split(" | ")[0][:100]
                trigger = f"Error in {scope}"

                contexts_str = row[3] or ""
                steps_parts: list[str] = []
                for ctx_str in contexts_str.split(" | ")[:3]:
                    try:
                        ctx = json.loads(ctx_str)
                        seq = ctx.get("tool_sequence", "")
                        if seq and seq not in steps_parts:
                            steps_parts.append(seq)
                    except (json.JSONDecodeError, TypeError):
                        pass

                steps = " → ".join(steps_parts) if steps_parts else first_assertion

                try:
                    result = self.db_cog.insert_tactic(
                        trigger_pattern=trigger,
                        steps=steps,
                        collection=collection,
                        trigger_keywords=json.dumps(
                            [w for w in scope.replace("/", " ").split() if len(w) > 2][:5],
                        ),
                    )
                    if result is not None:
                        extracted += 1
                except Exception as e:
                    logger.debug("realtime extract tactic 失败: %s", e)

        except Exception as e:
            logger.debug("realtime tactics 规则 1 失败: %s", e)

        # 规则 2: 重复 fix commit 模式
        try:
            rows = conn.execute("""
                SELECT context_json, COUNT(*) as cnt,
                       GROUP_CONCAT(assertion, ' | ') as assertions
                FROM facts
                WHERE collection=? AND status='active'
                  AND category='decision'
                  AND assertion LIKE 'Committed: fix%%'
                  AND context_json LIKE '%%"scope"%%'
                GROUP BY json_extract(context_json, '$.scope')
                HAVING cnt >= 2
                ORDER BY cnt DESC
                LIMIT 10
            """, [collection]).fetchall()

            for row in rows:
                try:
                    ctx = json.loads(row[0])
                except (json.JSONDecodeError, TypeError):
                    continue

                scope = ctx.get("scope", "")
                if not scope:
                    continue

                trigger = f"Fix needed in {scope}"
                existing = self.db_cog.search_tactics(trigger, collection, limit=1)
                if existing:
                    self.db_cog.update_tactic_outcome(existing[0]["id"], True)
                    continue

                assertions_text = (row[2] or "").split(" | ")
                steps = " → ".join(a[:60] for a in assertions_text[:3])

                try:
                    result = self.db_cog.insert_tactic(
                        trigger_pattern=trigger,
                        steps=steps,
                        collection=collection,
                        trigger_keywords=json.dumps([scope]),
                    )
                    if result is not None:
                        extracted += 1
                except Exception as e:
                    logger.debug("realtime extract fix tactic 失败: %s", e)

        except Exception as e:
            logger.debug("realtime tactics 规则 2 失败: %s", e)

        return extracted
