#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
"""Search Source: Capture Log (N0 — Conversational Memory Recall)

Tails the last N lines from each agent's capture log to surface recent
conversational context — decisions, architectural choices, pricing discussions.

Scoring:
  keyword_overlap * 0.4 + decision_boost * 2.5 + recency_weight
  Decision patterns ("we decided", "going with", "let's do", "I chose") → +2.5
  High query overlap (≥3 words) → high salience
  Most-recent logs get slight recency boost.
"""
from __future__ import annotations

import logging
import os
import time
from pathlib import Path

log = logging.getLogger("pulse.search.capture_log")

# Decision-like trigger phrases — lines containing these get a big salience boost
_DECISION_TRIGGERS = (
    "we decided", "going with", "let's do", "let's use", "i'll use",
    "i chose", "we chose", "i'm going", "we're going", "we'll use",
    "the plan is", "we agreed", "final answer", "the solution",
    "confirmed:", "decision:", "✓", "→ using", "→ go with",
    "pricing", "architecture", "approach:", "strategy:",
)

_TAIL_LINES    = 400   # lines to tail per file (keep it cheap)
_MIN_SCORE     = 0.25  # minimum salience to include result
_MAX_PER_LOG   = 5     # cap hits from one capture file
_RECENCY_FLOOR = 10    # always include last N lines from most-recent log (bypass keyword gate)


def _tail_file(path: Path, n: int) -> list[str]:
    """Efficiently tail last n lines of a file without reading the whole thing."""
    try:
        size = os.path.getsize(path)
        if size == 0:
            return []
        chunk = min(n * 3000, size)  # ~1100-2600 chars/line (capture logs are verbose)
        with path.open("rb") as fh:
            fh.seek(max(0, size - chunk))
            raw = fh.read()
        lines = raw.decode("utf-8", errors="replace").splitlines()
        return lines[-n:]
    except OSError:
        return []


def _score_line(line_lower: str, query_words: set[str]) -> float:
    """Return a salience score for a single capture log line."""
    score = 0.0

    # Keyword overlap
    line_words = set(line_lower.split())
    overlap = len(query_words & line_words)
    if overlap == 0:
        return 0.0
    score += overlap * 0.4

    # Decision phrase boost
    for trigger in _DECISION_TRIGGERS:
        if trigger in line_lower:
            score += 2.5
            break

    return score


def search_capture_log(query: str, config: dict = None) -> list[dict]:
    """Search recent capture logs for conversational context matching query.

    Tails the last _TAIL_LINES from every pulse_captured_<agent>.log in STATE_DIR.
    Returns hits sorted by salience — decision-like lines score highest.
    """
    try:
        from pulse.core.brain_utils import STATE_DIR
    except ImportError:
        return []

    query_lower = query.lower().strip()
    if not query_lower:
        return []

    query_words = set(query_lower.split())
    if len(query_words) < 2:
        return []

    cap_files = sorted(
        STATE_DIR.glob("pulse_captured_*.log"),
        key=lambda f: f.stat().st_mtime,
        reverse=True,   # most recent first
    )
    if not cap_files:
        return []

    scored_hits: list[dict] = []
    recency_hits: list[dict] = []
    now = time.time()

    for _cap_idx, cap_file in enumerate(cap_files[:6]):  # limit to 6 most recent agent logs
        try:
            file_age_days = (now - cap_file.stat().st_mtime) / 86400
        except OSError:
            continue

        lines = _tail_file(cap_file, _TAIL_LINES)
        agent_tag = cap_file.stem.replace("pulse_captured_", "")
        file_hits: list[dict] = []
        seen_texts: set[str] = set()

        # Recency floor: always surface last _RECENCY_FLOOR lines from the most-recent
        # log. Kept separate so the sort+cap on scored items can't bury them.
        guaranteed_recency: list[dict] = []
        if _cap_idx == 0:
            recency_lines = [l.strip() for l in lines[-_RECENCY_FLOOR:] if len(l.strip()) >= 15]
            for r_line in recency_lines:
                dedup_key = r_line[:200]
                if dedup_key not in seen_texts:
                    seen_texts.add(dedup_key)
                    guaranteed_recency.append({
                        "text": f"[RECENT] {r_line[:200]}",
                        "title": f"[CAPTURE:{agent_tag}] {r_line[:60]}",
                        "source": "capture_log",
                        "agent": agent_tag,
                        "salience": 0.3,
                        "confidence": 0.6,
                        "is_decision": False,
                    })

        for i, line in enumerate(lines):
            line_stripped = line.strip()
            if len(line_stripped) < 15:
                continue

            line_lower = line_stripped.lower()
            score = _score_line(line_lower, query_words)
            if score < _MIN_SCORE:
                continue

            # Slight recency boost: recent lines in a recent file score higher
            recency_bonus = max(0.0, 0.3 - file_age_days * 0.05)
            # Lines near the end of the file are more recent
            position_bonus = (i / max(len(lines), 1)) * 0.15
            final_score = round(min(score + recency_bonus + position_bonus, 5.0), 3)

            dedup_key = line_stripped[:200]
            if dedup_key in seen_texts:
                continue
            seen_texts.add(dedup_key)
            is_decision = any(t in line_lower for t in _DECISION_TRIGGERS)
            label = "[DECISION] " if is_decision else "[CONTEXT] "
            file_hits.append({
                "text": f"{label}{line_stripped[:200]}",
                "title": f"[CAPTURE:{agent_tag}] {line_stripped[:60]}",
                "source": "capture_log",
                "agent": agent_tag,
                "salience": final_score,
                "confidence": 0.7 if is_decision else 0.55,
                "is_decision": is_decision,
            })

        file_hits.sort(key=lambda h: h["salience"], reverse=True)
        scored_hits.extend(file_hits[:_MAX_PER_LOG])
        recency_hits.extend(guaranteed_recency)

    # Scored hits sorted by salience, recency hits always appended after
    scored_hits.sort(key=lambda h: h["salience"], reverse=True)
    combined = scored_hits[:10] + recency_hits  # top 10 scored + all guaranteed recent
    return combined
