# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""PULSE Bridge Daemon (V7): Raw Logs → Structured Knowledge."""
import time, os, sys
import io
from pathlib import Path
if os.name == 'nt' and __name__ == '__main__':
    if hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(encoding='utf-8', errors='replace')
    if hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(encoding='utf-8', errors='replace')
SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent.parent
if str(ROOT_DIR) not in sys.path:
    sys.path.insert(0, str(ROOT_DIR))
from pulse.brain.extraction.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.capture.auto_capture import capture as auto_capture
from pulse.daemons.bridge.bridge_routing import append_to_evidence, route_knowledge
from pulse.daemons.bridge.reflex_arc import check_reflex, detect_mistakes
from concurrent.futures import ThreadPoolExecutor, TimeoutError as _FuturesTimeout
_PARSE_EXECUTOR = ThreadPoolExecutor(max_workers=2, thread_name_prefix="wants_parser")
from pulse.core.brain_analysis import compute_item_salience, compute_item_confidence

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"

_BASE_LOGS = [LOG_CLAUDE, LOG_GEMINI, LOG_CODEX]
def _get_all_log_paths():
    return _BASE_LOGS + [p.with_suffix('.log.1') for p in _BASE_LOGS if p.with_suffix('.log.1').exists()]
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 _log(self, msg):
        if self.verbose: print(f"[BRIDGE] {msg}")

    def _handle_shutdown(self, signum, frame):
        self._log("Shutdown — 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()
        self._log("Goodbye.")
        sys.exit(0)

    def _load_state(self):
        default_pos = {str(f): 0 for f in [LOG_CLAUDE, LOG_GEMINI, LOG_CODEX]}
        data = load_json_dict(STATE_FILE)
        if not data:
            return {"positions": default_pos}
        positions = data.get("positions", {})
        for k, v in default_pos.items():
            positions.setdefault(k, v)
        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:
            self._log(f"Dropping unsigned line from {source}")
            return
        content_part, signature = line.rsplit(" | SIG: ", 1)
        daemon_id = self.source_map.get(source)
        if not daemon_id and source.startswith("WORKER_"):
            daemon_id = source.lower()
        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()
        if re.match(r"^\s*\|", content): return  # Filter markdown table rows
        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"])
        self._log(f"Parsing interaction from {source}...")
        try:
            _fut = _PARSE_EXECUTOR.submit(parse_full, user_text, agent_text)
            parsed = _fut.result(timeout=5.0)
        except _FuturesTimeout:
            self._log(f"wants_parser timed out — using empty parse for {source}")
            parsed = {"timestamp": "", "primary_domain": "general",
                      "wants": [], "dont_wants": [], "emotional_intensity": 0.0}
        append_to_evidence(parsed, user_text=user_text, agent_text=agent_text, verbose=self.verbose)
        combined_text = f"{user_text}\n{agent_text}" if agent_text else user_text
        try:
            knowledge_items = []
            ts, domain = parsed.get("timestamp", ""), parsed.get("primary_domain", "general")
            emul = 1.0 + parsed.get("emotional_intensity", 0.0)
            for dw in parsed.get("dont_wants", []):
                desc = dw.get("dont_want", "").strip()
                if desc and len(desc) > 15:
                    rp = float(dw.get("priority", 1)) 
                    sal = compute_item_salience(desc, priority=rp, emotional_intensity=emul - 1.0)
                    sal *= 1.5  # Apply Asymmetric Negativity Bias
                    conf = compute_item_confidence(desc, source_type="bridge")
                    knowledge_items.append({"type": "FAILURE", "description": desc,
                        "domain": domain, "timestamp": ts, "confidence": conf, "salience": sal,
                        "emotional_intensity": emul - 1.0})
            for want in parsed.get("wants", []):      
                desc = want.get("want", "").strip()   
                if desc and len(desc) > 15 and want.get("priority", 0) >= 2:
                    rp = float(want.get("priority", 1))
                    sal = compute_item_salience(desc, priority=rp, emotional_intensity=emul - 1.0)
                    conf = compute_item_confidence(desc, source_type="bridge")
                    knowledge_items.append({"type": "LEARNING", "description": desc,
                        "domain": domain, "timestamp": ts, "confidence": conf, "salience": sal,
                        "emotional_intensity": emul - 1.0})
            # Agent text extraction disabled: regex patterns designed for user text
            # produce garbage when fed agent responses (JSON, code, markdown).
            # LLM extraction fallback below handles agent text properly.
            if knowledge_items:
                route_knowledge(knowledge_items, source=f"bridge_{source.lower()}", verbose=self.verbose)
        except Exception as e:
            self._log(f"route_knowledge error: {e}")

        try:
            auto_capture(user_text, agent_text, session_id=source.lower())
        except Exception as e:
            self._log(f"auto_capture error: {e}")

        try:
            from pulse.brain.session.session_lifecycle import _record_interaction as record_interaction
            record_interaction(source.lower(), user_text, agent_text,
                             domain=parsed.get('primary_domain', 'general'), timestamp=parsed.get('timestamp', ''))
        except Exception:
            pass

        regex_yield = len(knowledge_items)
        if regex_yield < 2 and len(combined_text) > 200:
            try:
                from pulse.daemons.bridge.llm_extraction import extract_with_llm
                llm_items = extract_with_llm(combined_text, domain, ts, verbose=self.verbose)
                if llm_items:
                    knowledge_items.extend(llm_items)
                    route_knowledge(llm_items, source=f"bridge_{source.lower()}_llm", verbose=self.verbose)
            except Exception as e:
                self._log(f"LLM extraction fallback: {e}")

        extracted_facts_count = 0
        try:
            from pulse.brain.extraction.fact_parser import parse_facts
            from pulse.brain.persistence.fact_store import store_facts as _store_facts
            facts = parse_facts(combined_text)
            extracted_facts_count = len(facts)
            if facts:
                _store_facts(facts, source=f"bridge_{source.lower()}")
        except Exception:
            pass

        extracted_decisions_count = 0
        try:
            from pulse.brain.extraction.decision_parser import parse_decisions
            from pulse.brain.persistence.decision_store import store_decision, check_anti_pattern_conflict
            decisions = parse_decisions(user_text, agent_text)
            for dec in decisions:
                dec.update(participants=[source.lower()], source_session=source.lower())
                dec.setdefault("domain", domain)
                dec["salience"] = compute_item_salience(dec["choice"], priority=2, emotional_intensity=emul - 1.0)
                dec["confidence"] = compute_item_confidence(dec["choice"], source_type="bridge")
                conflict = check_anti_pattern_conflict(dec["choice"])
                if conflict:
                    dec["_ap_conflict"] = conflict.get("description", "")[:80]
                if store_decision(dec):
                    extracted_decisions_count += 1
            if extracted_decisions_count and self.verbose:
                self._log(f"Stored {extracted_decisions_count} decisions")
        except Exception as e:
            self._log(f"decision_extraction error: {e}")

        try:
            from pulse.brain.index.episodic_index import append_entry
            from pulse.brain.session.context_envelope import get_context_envelope
            append_entry(agent=source.lower(), user_text=user_text, agent_text=agent_text,
                         wants_count=len(parsed.get("wants", [])), facts_count=extracted_facts_count,
                         emotional_intensity=parsed.get("emotional_intensity", 0.0), context=get_context_envelope())
        except Exception:
            pass

        regex_hits = []
        try:
            regex_hits = detect_mistakes(combined_text)
            check_reflex(user_text, agent_text, source)
        except Exception as e:
            self._log(f"reflex_arc error: {e}")
        try:
            from pulse.daemons.bridge.reflex_llm import enqueue_for_llm
            enqueue_for_llm(user_text, agent_text, source, regex_hits)
        except Exception as e:
            self._log(f"reflex_llm error: {e}")

        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):
        found, known = [], {str(p) for p in _BASE_LOGS} | {str(p.with_suffix(".log.1")) for p in _BASE_LOGS}
        for f in STATE_DIR.glob("pulse_captured_*.log"):
            if str(f) in known: continue
            src = f.stem.replace("pulse_captured_", "").upper()
            if src not in self.buffers: self.buffers[src] = {"user": None, "agent": []}; self.last_activity[src] = 0.0
            self.state["positions"].setdefault(str(f), 0); found.append((f, src))
        return found

    def run(self):
        print(f"[BRIDGE] Watching: {LOG_CLAUDE.name}, {LOG_GEMINI.name}, {LOG_CODEX.name} + workers")
        while self.running:
            for log_path in _get_all_log_paths():
                stem = log_path.stem.replace(".log", "")
                source = 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(log_path), 0)
                self._tail_file(log_path, source)
            for log_path, source in self._discover_worker_logs():
                self._tail_file(log_path, source)
            now = time.time()
            for source, buf in self.buffers.items():
                last = self.last_activity.get(source, 0.0)
                if buf["user"] and buf["agent"] and last > 0 and (now - last) >= self.idle_timeout_seconds:
                    self._log(f"Idle flush for {source} ({now - last:.0f}s idle)")
                    self._flush_interaction(source)
                    self.last_activity[source] = 0.0
            try:
                from pulse.brain.pending_failures import flush_to_sqlite
                flush_to_sqlite()
            except Exception: pass
            time.sleep(POLL_INTERVAL)

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