#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
PULSE Bridge Daemon (V7)
The Digestive System: Converts Raw Logs -> Structured Knowledge.

Watches: pulse_captured_claude.log, pulse_captured_gemini.log, pulse_captured_codex.log
Routing logic in bridge_routing.py (split for Law 7).
"""
import time
import os
import sys
import io
from pathlib import Path

if os.name == 'nt' and __name__ == '__main__':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent

# When run directly, ensure pulse package is importable
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))

from pulse.brain.wants_parser import parse_full
from pulse.core.pulse_crypto import PulseCrypto
from pulse.core.brain_utils import save_json, load_json_dict
from pulse.core.brain_io import _acquire
from pulse.brain.auto_capture import capture as auto_capture
from pulse.daemons.bridge.bridge_routing import append_to_evidence
from pulse.daemons.bridge.reflex_arc import check_reflex, detect_mistakes

# Config
STATE_DIR = ROOT_DIR / "state"
LOG_CLAUDE = STATE_DIR / "pulse_captured_claude.log"
LOG_GEMINI = STATE_DIR / "pulse_captured_gemini.log"
LOG_CODEX = STATE_DIR / "pulse_captured_codex.log"
STATE_FILE = STATE_DIR / "pulse_bridge_state.json"
POLL_INTERVAL = 1.0


class PulseBridge:
    def __init__(self, verbose=False):
        self.verbose = verbose
        self.running = True
        self.state = self._load_state()
        self.crypto = PulseCrypto()

        import signal
        signal.signal(signal.SIGTERM, self._handle_shutdown)
        signal.signal(signal.SIGINT, self._handle_shutdown)

        self.source_map = {
            "CLAUDE": "claude_daemon",
            "GEMINI": "gemini_daemon",
            "CODEX": "codex_daemon"
        }
        self.buffers = {
            "CLAUDE": {"user": None, "agent": []},
            "GEMINI": {"user": None, "agent": []},
            "CODEX": {"user": None, "agent": []}
        }
        self.last_activity = {"CLAUDE": 0.0, "GEMINI": 0.0, "CODEX": 0.0}
        self.idle_timeout_seconds = 30

    def _handle_shutdown(self, signum, frame):
        if self.verbose:
            print("[BRIDGE] Shutdown signal received — flushing buffers...")
        self.running = False
        for source in self.buffers:
            if self.buffers[source]["user"] and self.buffers[source]["agent"]:
                self._flush_interaction(source)
        self._save_state()
        if self.verbose:
            print("[BRIDGE] Buffers flushed. Goodbye.")
        sys.exit(0)

    def _load_state(self):
        default = {"positions": {
            str(LOG_CLAUDE): 0, str(LOG_GEMINI): 0, str(LOG_CODEX): 0
        }}
        data = load_json_dict(STATE_FILE)
        if not data:
            return default
        positions = data.get("positions", {})
        positions.setdefault(str(LOG_CLAUDE), 0)
        positions.setdefault(str(LOG_GEMINI), 0)
        positions.setdefault(str(LOG_CODEX), 0)
        # Only keep "positions" — strip orphaned flat keys from old code
        return {"positions": positions}

    def _save_state(self):
        with _acquire(STATE_FILE):
            save_json(STATE_FILE, self.state)

    def _process_line(self, line, source):
        line = line.strip()
        if not line:
            return
        if " | SIG: " not in line:
            if self.verbose:
                print(f"[BRIDGE] Dropping unsigned line from {source}")
            return

        content_part, signature = line.rsplit(" | SIG: ", 1)
        daemon_id = self.source_map.get(source)
        if not self.crypto.verify_string(content_part, signature, daemon_id):
            print(f"[BRIDGE] INVALID SIGNATURE from {source}! POSSIBLE SPOOFING.")
            return

        import re
        match = re.match(r"^\[(.*?)\] (.*?): (.*)$", content_part)
        if not match:
            return

        ts, role, content = match.groups()
        buffer = self.buffers[source]
        self.last_activity[source] = time.time()

        if role == "USER":
            if buffer["user"] and buffer["agent"]:
                self._flush_interaction(source)
            buffer["user"] = content
            buffer["agent"] = []
        elif role in ["CLAUDE", "GEMINI", "CODEX", "ASSISTANT"]:
            if buffer["user"]:
                buffer["agent"].append(content)

    def _flush_interaction(self, source):
        buffer = self.buffers[source]
        if not buffer["user"]:
            return
        user_text = buffer["user"]
        agent_text = "\n".join(buffer["agent"])

        if self.verbose:
            print(f"[BRIDGE] Parsing interaction from {source}...")

        parsed = parse_full(user_text, agent_text)
        append_to_evidence(parsed, user_text=user_text, agent_text=agent_text,
                           verbose=self.verbose)

        try:
            auto_capture(user_text, agent_text, session_id=source.lower())
        except Exception as e:
            if self.verbose:
                print(f"[BRIDGE] auto_capture error: {e}")

        # Reflex Arc: instant anti-pattern detection (<10ms)
        regex_hits = []
        try:
            combined = f"{user_text}\n{agent_text}" if agent_text else user_text
            regex_hits = detect_mistakes(combined)
            check_reflex(user_text, agent_text, source)
        except Exception as e:
            if self.verbose:
                print(f"[BRIDGE] reflex_arc error: {e}")

        # Reflex LLM: async deep analysis (non-blocking enqueue)
        try:
            from pulse.daemons.bridge.reflex_llm import enqueue_for_llm
            enqueue_for_llm(user_text, agent_text, source, regex_hits)
        except Exception:
            pass  # never crash bridge

        buffer["user"] = None
        buffer["agent"] = []

    def _tail_file(self, filepath, source, chunk_size=500):
        if not filepath.exists():
            return
        key = str(filepath)
        pos = self.state["positions"].get(key, 0)
        current_size = filepath.stat().st_size
        if current_size < pos:
            pos = 0
        if current_size == pos:
            return
        with open(filepath, "r", encoding="utf-8") as f:
            f.seek(pos)
            count = 0
            while True:
                line = f.readline()
                if not line:
                    break
                self._process_line(line, source)
                count += 1
                if count % chunk_size == 0:
                    self.state["positions"][key] = f.tell()
                    self._save_state()
            self.state["positions"][key] = f.tell()
        self._save_state()

    def _discover_worker_logs(self):
        worker_logs = []
        for f in STATE_DIR.glob("pulse_captured_worker_*.log"):
            source = f.stem.replace("pulse_captured_", "").upper()
            if source not in self.buffers:
                self.buffers[source] = {"user": None, "agent": []}
                self.last_activity[source] = 0.0
            self.state["positions"].setdefault(str(f), 0)
            worker_logs.append((f, source))
        return worker_logs

    def run(self):
        print(f"[BRIDGE] Watching logs for digestion...")
        print(f"         {LOG_CLAUDE.name}")
        print(f"         {LOG_GEMINI.name}")
        print(f"         {LOG_CODEX.name}")
        print(f"         + worker capture logs (auto-discovered)")

        while self.running:
            self._tail_file(LOG_CLAUDE, "CLAUDE")
            self._tail_file(LOG_GEMINI, "GEMINI")
            self._tail_file(LOG_CODEX, "CODEX")
            for log_path, source in self._discover_worker_logs():
                self._tail_file(log_path, source)

            now = time.time()
            for source in self.buffers:
                last = self.last_activity.get(source, 0.0)
                buf = self.buffers[source]
                if buf["user"] and buf["agent"] and last > 0:
                    if (now - last) >= self.idle_timeout_seconds:
                        if self.verbose:
                            print(f"[BRIDGE] Idle flush for {source} ({now - last:.0f}s idle)")
                        self._flush_interaction(source)
                        self.last_activity[source] = 0.0

            time.sleep(POLL_INTERVAL)

if __name__ == "__main__":
    v = "--verbose" in sys.argv
    PulseBridge(verbose=v).run()
