#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
PULSE Worker Tools (Extended) — search tools, capture, and memory tools.

Extracted from worker_tools.py to keep both files under 300 lines.
Contains: tool_brain_query, tool_list_files, tool_search_code,
          capture_to_bridge_log, save_session_json,
          tool_extract_knowledge_batch, tool_clean_intents_batch, tool_check_antipattern
"""
from __future__ import annotations

import json
import re
import subprocess
import sys
from pathlib import Path

from pulse.core.brain_utils import now_iso as _now_iso, INTENTS_DIR

SCRIPT_DIR = Path(__file__).resolve().parent
ROOT_DIR = SCRIPT_DIR.parent.parent
TOOLS_DIR = ROOT_DIR / "pulse" / "brain"
STATE_DIR = ROOT_DIR / "state"


ROOT_DIR_EXT = ROOT_DIR  # alias used by _guard_path


def _guard_path(path_str: str):
    """Ensure path stays within project root."""
    from pathlib import Path as _Path
    p = _Path(path_str).resolve()
    if not str(p).startswith(str(ROOT_DIR)):
        raise PermissionError(f'Path outside project root: {path_str}')
    return p

def tool_brain_query(query: str) -> str:
    try:
        result = subprocess.run(
            [sys.executable, str(TOOLS_DIR / "brain_query.py"), "--query", query],
            capture_output=True, text=True, timeout=30, cwd=str(ROOT_DIR),
            encoding="utf-8", errors="replace",
        )
        return result.stdout[:5000] if result.stdout else "No brain results."
    except Exception as e:
        return f"Brain query error: {e}"


def tool_list_files(pattern: str, path: str = ".") -> str:
    try:
        base = _guard_path(path)
    except PermissionError as e:
        return f"ERROR: {e}"
    if not base.exists():
        return f"ERROR: Path not found: {path}"
    matches = []
    try:
        for p in base.rglob(pattern):
            if p.is_file():
                matches.append(str(p.relative_to(ROOT_DIR)))
                if len(matches) >= 100:
                    break
    except Exception as e:
        return f"ERROR: {e}"
    return f"Found {len(matches)} file(s):\n" + "\n".join(matches) if matches else f"No files matching '{pattern}' in {path}"


def tool_search_code(pattern: str, path: str = ".", context_lines: int = 2) -> str:
    try:
        base = _guard_path(path)
    except PermissionError as e:
        return f"ERROR: {e}"
    context_lines = min(max(int(context_lines), 0), 5)
    # Try git grep first
    try:
        cmd = ["git", "grep", "-n", f"-C{context_lines}", "--max-count=20", "-E", pattern]
        if path != ".":
            cmd.append(str(base.relative_to(ROOT_DIR)))
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, cwd=str(ROOT_DIR),
                               encoding="utf-8", errors="replace")
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout[:15000]
    except Exception:
        pass
    # Python fallback
    matches = []
    try:
        compiled = re.compile(pattern)
    except re.error as e:
        return f"ERROR: Invalid regex: {e}"
    _SKIP = (".pyc", ".pyo", ".so", ".dll", ".exe", ".bin", ".png", ".jpg", ".gif")
    try:
        for fp in base.rglob("*"):
            if not fp.is_file() or fp.stat().st_size > 500_000 or fp.suffix in _SKIP:
                continue
            try:
                lines = fp.read_text(encoding="utf-8", errors="replace").splitlines()
            except Exception:
                continue
            for i, line in enumerate(lines):
                if compiled.search(line):
                    rel = str(fp.relative_to(ROOT_DIR))
                    s, e = max(0, i - context_lines), min(len(lines), i + context_lines + 1)
                    snippet = "\n".join(f"  {j+1}: {lines[j]}" for j in range(s, e))
                    matches.append(f"{rel}:{i+1}\n{snippet}")
                    if len(matches) >= 20:
                        return f"Found {len(matches)}+ matches:\n\n" + "\n\n".join(matches)
    except Exception as e:
        return f"ERROR scanning files: {e}"
    return f"Found {len(matches)} match(es):\n\n" + "\n\n".join(matches) if matches else f"No matches for '{pattern}'"



def capture_to_bridge_log(worker_id: str, events: list[dict], task_id: str,
                          task_title: str = ""):
    """Write signed conversation to bridge-compatible capture log.

    Writes a synthetic USER line (task description) followed by ASSISTANT lines
    so the bridge can pair them as a conversation and flush to knowledge routing.
    """
    capture_file = STATE_DIR / f"pulse_captured_{worker_id}.log"
    # Lazy import: resolve signer from sibling at call-time (not import-time)
    import sys as _sys
    _sib = Path(__file__).stem[:-4]  # strip _ext suffix
    _pkg = __package__ or "pulse.workers"
    _sib_mod = _sys.modules.get(_pkg + "." + _sib)
    if _sib_mod is None:
        import importlib as _il
        _sib_mod = _il.import_module(_pkg + "." + _sib)
    _sign_fn = getattr(_sib_mod, "_worker_sign", None) or (lambda line, wid: line)
    try:
        with open(capture_file, "a", encoding="utf-8") as f:
            # Synthetic USER line so bridge can pair user+agent for flush
            ts_start = _now_iso()[:19].replace("T", " ")
            user_line = f"[{ts_start}] USER: [TASK {task_id}] {task_title or 'worker task'}"
            f.write(_sign_fn(user_line, worker_id) + "\n")

            for ev in events:
                ts = ev.get("timestamp", _now_iso())[:19].replace("T", " ")
                etype, content = ev.get("type", ""), ev.get("content", "")[:500]
                if etype == "reasoning":
                    line = f"[{ts}] ASSISTANT: {content}"
                elif etype == "tool_call":
                    line = f"[{ts}] ASSISTANT: [TOOL] {ev.get('tool', '')}({json.dumps(ev.get('args', {}))[:200]})"
                elif etype == "tool_result":
                    line = f"[{ts}] ASSISTANT: [RESULT] {ev.get('tool', '')} -> {content[:300]}"
                elif etype == "error":
                    line = f"[{ts}] ASSISTANT: [ERROR] {content}"
                elif etype == "complete":
                    line = f"[{ts}] ASSISTANT: [DONE] Task {task_id}: {content}"
                else:
                    continue
                f.write(_sign_fn(line, worker_id) + "\n")
    except Exception as e:
        import logging
        logging.getLogger("pulse.workers").warning(
            "Capture log write failed for %s task %s: %s", worker_id, task_id, e)


def save_session_json(worker_id: str, model: str, tier: str, task_id: str,
                      task: dict, events: list[dict], outcome: str,
                      success: bool, tokens_in: int, tokens_out: int, cost: float,
                      skills_used: list[str] | None = None):
    """Save rich structured session for deep analysis."""
    session_dir = STATE_DIR / "worker_sessions"
    session_dir.mkdir(parents=True, exist_ok=True)
    session = {
        "worker_id": worker_id, "task_id": task_id,
        "title": task.get("title", ""), "domain": task.get("domain", "general"),
        "model": model, "tier": tier,
        "started_at": events[0]["timestamp"] if events else _now_iso(),
        "completed_at": _now_iso(), "success": success,
        "outcome": outcome[:1000],
        "tokens_in": tokens_in, "tokens_out": tokens_out, "cost_usd": cost,
        "iterations": len([e for e in events if e["type"] == "reasoning"]),
        "tools_used": list({e["tool"] for e in events if e.get("tool")}),
        "skills_used": skills_used or [],
        "errors": [e["content"] for e in events if e["type"] == "error"],
        "events": events,
    }
    # Layer 3: Sign worker session for integrity tracking
    from pulse.core.pulse_crypto import sign_item
    session = sign_item(session, worker_id)
    try:
        (session_dir / f"{task_id}_{worker_id}.json").write_text(
            json.dumps(session, indent=2, default=str), encoding="utf-8")
    except Exception:
        pass

    # Procedural cache: record habit on success, penalize on failure
    try:
        from pulse.brain.procedural_cache import (
            record_procedure, record_failure, extract_tool_sequence
        )
        _domain = task.get("domain", "general")
        _title = task.get("title", "")
        _desc = task.get("description", _title)
        if success:
            tool_seq = extract_tool_sequence(events)
            record_procedure(_domain, _title, _desc, tool_seq)
        else:
            record_failure(_domain, _title, _desc)
    except Exception:
        pass


try:
    from pulse.browser.core import browser_goto, browser_get_content, browser_fill, browser_click
except ImportError:
    browser_goto = browser_get_content = browser_fill = browser_click = None



# === SWARM MEMORY TOOLS (Brain V4) ===
# Uses the same extraction pipeline as worker_memory.py fast-path.

def tool_extract_knowledge_batch(task_data: str) -> str:
    """Extract engineering knowledge from a batch of chat interactions.
    Called when processing an [EXTRACT_KNOWLEDGE] task."""
    try:
        from pulse.core.llm_router import dispatch, extract_json
        from pulse.admin.retroactive.retroactive_llm_extractor import SCRIBE_PROMPT, write_to_brain

        data = json.loads(task_data)
        batch_text = data.get("batch_text", "")
        agent = data.get("agent_name", "unknown")
        timestamp = data.get("timestamp", _now_iso())
        batch_num = data.get("batch_num", 0)

        if not batch_text:
            return "Empty batch — skipped"

        raw = dispatch(SCRIBE_PROMPT + batch_text, task_type="fast")
        items = extract_json(raw)
        if not isinstance(items, dict):
            return f"LLM returned non-dict: {type(items).__name__}"

        written = write_to_brain(items, source_agent=agent, timestamp=timestamp)
        total = sum(written.values())
        return (f"Batch {batch_num}: {written['lessons']}L/{written['failures']}F/"
                f"{written['patterns']}P ({total} items)")
    except Exception as e:
        return f"ERROR in extract_knowledge_batch: {e}"


def tool_clean_intents_batch(task_data: str) -> str:
    """Distill raw want/dont_want entries into clean actionable preferences.
    Called when processing a [CLEAN_INTENTS] task."""
    try:
        from pulse.core.llm_router import dispatch, extract_json
        from pulse.admin.retroactive.retroactive_intents_cleaner import WANTS_PROMPT, DONT_WANTS_PROMPT
        from pulse.brain.save_writers import append_to_json_list

        data = json.loads(task_data)
        batch = data.get("batch", [])
        intent_type = data.get("intent_type", data.get("key", "want"))
        file_path = data.get("file", "")
        batch_num = data.get("batch_num", 0)

        if not batch:
            return "Empty batch — skipped"

        prompt_template = WANTS_PROMPT if intent_type == "want" else DONT_WANTS_PROMPT
        raw = dispatch(prompt_template + json.dumps(batch, indent=None), task_type="fast")
        parsed = extract_json(raw)

        if isinstance(parsed, list):
            clean_items = parsed
        elif isinstance(parsed, dict):
            clean_items = parsed.get("wants", parsed.get("dont_wants", []))
            if not isinstance(clean_items, list):
                clean_items = []
        else:
            clean_items = []

        clean_items = [s.strip() for s in clean_items if isinstance(s, str) and len(s.strip()) >= 15]

        written = 0
        key = "want" if intent_type == "want" else "dont_want"
        ts = _now_iso()
        target = Path(file_path) if file_path else (INTENTS_DIR / f"{key}s.json")

        for item_text in clean_items:
            entry = {key: item_text, "domain": "general", "visibility": "public",
                     "timestamp": ts, "source": "swarm_cleaned"}
            if append_to_json_list(target, entry, dedup_key=key):
                written += 1

        return f"Batch {batch_num}: {len(batch)} raw -> {len(clean_items)} clean -> {written} written"
    except Exception as e:
        return f"ERROR in clean_intents_batch: {e}"


def tool_check_antipattern(text: str) -> str:
    """Check text/code against known anti-patterns via LLM (V5 Immune System)."""
    try:
        from pulse.admin.patterns.anti_pattern_similarity import check_against_anti_patterns
        violations = check_against_anti_patterns(text)
        if not violations:
            return "CLEAN: No anti-pattern violations detected."
        summary = json.dumps(violations, indent=2)
        return f"VIOLATIONS FOUND ({len(violations)}):\n{summary}"
    except Exception as e:
        return f"ERROR in check_antipattern: {e}"
