#!/usr/bin/env python3
# Copyright (c) 2026 PULSE Contributors. Proprietary. All rights reserved.
# See LICENSE file for details.
"""
PULSE Worker Tools — tool functions, definitions, dispatch, and capture.

Imported by pulse_worker.py and worker_llm.py.
"""
from __future__ import annotations

import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path

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

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

_CMD_ALLOWLIST = {
    "git", "python", "pytest", "pip", "node", "npm", "npx",
    "mkdir", "type", "cat", "echo", "ls", "dir", "tsc", "eslint", "prettier",
}


def _guard_path(path_str: str) -> Path:
    """Ensure path stays within project root."""
    p = Path(path_str).resolve()
    if not str(p).startswith(str(ROOT_DIR)):
        raise PermissionError(f"Path outside project root: {path_str}")
    return p


# Tool implementations

def tool_read_file(path: str) -> str:
    p = _guard_path(path)
    if not p.exists():
        return f"ERROR: File not found: {path}"
    return p.read_text(encoding="utf-8", errors="replace")[:50000]


def tool_write_file(path: str, content: str) -> str:
    # Pre-action gate: check brain for known mistakes before writing
    try:
        from pulse.brain.integrity.gate_checks import gate1_anti_pattern, _extract_keywords
        from pulse.core.brain_db import get_conn
        kw = _extract_keywords(content)
        if kw:
            blocked, reason = gate1_anti_pattern(kw, content, get_conn(), file_path=path)
            if blocked:
                return f"BLOCKED BY BRAIN: {reason}"
    except Exception:
        pass
    p = _guard_path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content, encoding="utf-8")
    return f"OK: Wrote {len(content)} chars to {path}"


_PYTHON_BLOCKED_FLAGS = {"-c", "-m", "--command"}


def tool_run_command(cmd: str) -> str:
    parts = cmd.strip().split()
    if not parts:
        return "ERROR: Empty command"
    base = Path(parts[0]).stem.lower()
    if base not in _CMD_ALLOWLIST:
        return f"ERROR: Command '{base}' not in allowlist: {sorted(_CMD_ALLOWLIST)}"
    # Block python -c / -m to prevent arbitrary code execution
    if base == "python" and len(parts) > 1 and parts[1] in _PYTHON_BLOCKED_FLAGS:
        return f"ERROR: '{parts[1]}' flag blocked for security. Use a .py file instead."
    try:
        result = subprocess.run(parts, capture_output=True, text=True, timeout=120, cwd=str(ROOT_DIR),
                               encoding="utf-8", errors="replace")
        return f"EXIT {result.returncode}\n{(result.stdout + result.stderr)[:10000]}"
    except subprocess.TimeoutExpired:
        return "ERROR: Command timed out (120s)"
    except Exception as e:
        return f"ERROR: {e}"



# Extended tools (extracted to keep file under 300 lines)
from pulse.workers.worker_tools_ext import (  # noqa: F401
    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,
)

def tool_edit_file(path: str, old_text: str, new_text: str) -> str:
    # Pre-action gate: check brain for known mistakes
    try:
        from pulse.brain.integrity.gate_checks import gate1_anti_pattern, _extract_keywords
        from pulse.core.brain_db import get_conn
        kw = _extract_keywords(new_text)
        if kw:
            blocked, reason = gate1_anti_pattern(kw, new_text, get_conn(), file_path=path)
            if blocked:
                return f"BLOCKED BY BRAIN: {reason}"
    except Exception:
        pass
    p = _guard_path(path)
    if not p.exists():
        return f"ERROR: File not found: {path}"
    content = p.read_text(encoding="utf-8", errors="replace")
    count = content.count(old_text)
    if count == 0:
        return f"ERROR: old_text not found in {path}"
    if count > 1:
        idx = content.index(old_text)
        content = content[:idx] + new_text + content[idx + len(old_text):]
        p.write_text(content, encoding="utf-8")
        return f"WARNING: {count} occurrences found, replaced first only in {path}"
    p.write_text(content.replace(old_text, new_text, 1), encoding="utf-8")
    return f"OK: Replaced in {path}"

def _prop(desc: str, typ: str = "string") -> dict:
    return {"type": typ, "description": desc}

_TOOL_SPECS = [
    ("read_file", "Read a file's contents. Path relative to project root.",
     {"path": _prop("File path relative to project root")}, ["path"]),
    ("write_file", "Write content to a file. Path relative to project root.",
     {"path": _prop("File path relative to project root"), "content": _prop("Content to write")}, ["path", "content"]),
    ("run_command", f"Run a shell command. Allowed: {', '.join(sorted(_CMD_ALLOWLIST))}.",
     {"cmd": _prop("Shell command to run")}, ["cmd"]),
    ("brain_query", "Search the PULSE brain for knowledge, lessons, and anti-patterns.",
     {"query": _prop("Search query")}, ["query"]),
    ("list_files", "List files matching a glob pattern (e.g. '*.py'). Up to 100 results.",
     {"pattern": _prop("Glob pattern"), "path": _prop("Directory (default: '.')")}, ["pattern"]),
    ("search_code", "Search code for a regex pattern with context. Uses git grep with Python fallback.",
     {"pattern": _prop("Regex pattern"), "path": _prop("Directory (default: '.')"),
      "context_lines": _prop("Context lines (0-5, default: 2)", "integer")}, ["pattern"]),
    ("edit_file", "Surgical find-and-replace in a file (first occurrence).",
     {"path": _prop("File path relative to project root"),
      "old_text": _prop("Exact text to find"), "new_text": _prop("Replacement text")}, ["path", "old_text", "new_text"]),
    ("submit_task", "Post a new task to the PULSE task board for other workers.",
     {"title": _prop("Task title"), "domain": _prop("Task domain (default: 'general')"),
      "complexity": _prop("low/medium/high (default: 'medium')"), "description": _prop("Detailed description")}, ["title"]),
    ("complete_task", "Signal task completion with outcome summary.",
     {"outcome": _prop("Summary of outcome")}, ["outcome"]),
    ("extract_knowledge_batch", "Extract engineering knowledge (lessons, failures, patterns) from a batch of chat interactions. Used for [EXTRACT_KNOWLEDGE] memory tasks.",
     {"task_data": _prop("JSON string with keys: batch (list of interactions), agent_name, source_log, batch_num")}, ["task_data"]),
    ("clean_intents_batch", "Distill raw want/dont_want entries into clean actionable preferences. Used for [CLEAN_INTENTS] memory tasks.",
     {"task_data": _prop("JSON string with keys: batch (list of items), key ('want' or 'dont_want'), batch_num")}, ["task_data"]),
    ("check_antipattern", "Check code against known anti-patterns using semantic LLM analysis. Used for [CHECK_ANTIPATTERN] immune system tasks.",
     {"text": _prop("Code or text to check against anti-patterns")}, ["text"]),
]

TOOL_DEFINITIONS_ANTHROPIC = [
    {"name": n, "description": d, "input_schema": {"type": "object", "properties": p, "required": r}}
    for n, d, p, r in _TOOL_SPECS
]

TOOL_DISPATCH = {
    "read_file": lambda a: tool_read_file(a["path"]),
    "write_file": lambda a: tool_write_file(a["path"], a["content"]),
    "run_command": lambda a: tool_run_command(a["cmd"]),
    "brain_query": lambda a: tool_brain_query(a["query"]),
    "list_files": lambda a: tool_list_files(a["pattern"], a.get("path", ".")),
    "search_code": lambda a: tool_search_code(a["pattern"], a.get("path", "."), a.get("context_lines", 2)),
    "edit_file": lambda a: tool_edit_file(a["path"], a["old_text"], a["new_text"]),
    "submit_task": lambda a: tool_submit_task(a["title"], a.get("domain", "general"), a.get("complexity", "medium"), a.get("description", "")),
    "complete_task": lambda a: a.get("outcome", "").strip() or "Task completed without detailed output.",
    "extract_knowledge_batch": lambda a: tool_extract_knowledge_batch(a["task_data"]),
    "clean_intents_batch": lambda a: tool_clean_intents_batch(a["task_data"]),
    "check_antipattern": lambda a: tool_check_antipattern(a["text"]),
}



# ---------------------------------------------------------------------------
# Re-exports from worker_tools_submit (preserves public API)
# ---------------------------------------------------------------------------
from .worker_tools_submit import (  # noqa: F401
    tool_submit_task, compute_salience, _worker_sign,
)



