"""Built-in tools and signal exceptions for exploration."""

from __future__ import annotations

import json
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable

import yaml

if TYPE_CHECKING:
    from theow._core._chroma_store import ChromaStore


# Signals that a LLM can raise for theow to intercept and guide.
class ExplorationSignal(Exception):
    """Base for all exploration signals."""

    pass


class GiveUp(ExplorationSignal):
    """LLM determined the problem can't/shouldn't be automated."""

    def __init__(self, reason: str) -> None:
        self.reason = reason
        super().__init__(f"Gave up: {reason}")


class RequestTemplates(ExplorationSignal):
    """LLM is ready to write a rule, needs the syntax."""

    pass


class SubmitRule(ExplorationSignal):
    """LLM finished writing the rule."""

    def __init__(self, rule_file: str, action_file: str | None = None) -> None:
        self.rule_file = rule_file
        self.action_file = action_file
        super().__init__(f"Submitted: {rule_file}")


class Done(ExplorationSignal):
    """LLM finished direct fix, ready for retry."""

    def __init__(self, message: str = "") -> None:
        self.message = message
        super().__init__(f"Done: {message}")


# Signal tool functions (module-level for reuse)
def _give_up(reason: str = "") -> None:
    """Signal that this problem cannot or should not be automated.

    Args:
        reason: Clear explanation of why automation was declined. Omit when
                giving up based on a prior observation to avoid duplicate logs.
    """
    raise GiveUp(reason)


def _request_templates() -> None:
    """Signal that you understand the problem and are ready to write a rule.

    Call this AFTER you have:
    1. Investigated the error thoroughly
    2. Found a fix that works
    3. Determined the pattern is generalizable

    The system will provide rule and action template syntax.
    """
    raise RequestTemplates()


def _submit_rule(rule_file: str, action_file: str | None = None) -> None:
    """Submit your completed rule (and optionally action) for validation.

    Args:
        rule_file: Path to the rule YAML file you created.
        action_file: Path to the action Python file (if you created one).
    """
    raise SubmitRule(rule_file, action_file)


def _done(message: str = "") -> None:
    """Signal that you have completed the task.

    The system will retry the original operation to validate your fix.

    Args:
        message: Brief description of what was done.
    """
    raise Done(message)


# Factory functions for internal tool sets
def make_signal_tools() -> list[Callable[..., Any]]:
    """Signal tools for explorer mode (rule creation)."""
    return [_give_up, _request_templates, _submit_rule]


def make_direct_fix_tools(rules_dir: Path | None = None) -> list[Callable[..., Any]]:
    """Signal tools for direct fix mode (probabilistic rules)."""
    tools: list[Callable[..., Any]] = [_give_up, _done]
    if rules_dir:
        tools.append(_make_failed_rules_tool(rules_dir))
        tools.append(_make_observations_tool(rules_dir))
    return tools


def make_search_tools(
    chroma: ChromaStore, collection: str, rules_dir: Path | None = None
) -> list[Callable[..., Any]]:
    """Create search tools bound to chroma store."""

    def _search_rules(query: str) -> list[dict[str, Any]]:
        """Search existing rules by semantic similarity.

        Returns list of matching rules with their file names (e.g., 'rule_name.rule.yaml').
        """
        results = chroma.query_rules(collection=collection, query_text=query, n_results=5)
        return [
            {"name": name, "file": f"{name}.rule.yaml", "distance": dist, **meta}
            for name, dist, meta in results
        ]

    def _search_actions(query: str) -> list[dict[str, Any]]:
        """Search existing actions by semantic similarity."""
        return chroma.query_actions(query_text=query, n_results=5)

    def _list_rules() -> list[dict[str, str]]:
        """List all rules in the current collection.

        Returns list of dicts with 'name' and 'file' (e.g., 'rule_name.rule.yaml').
        """
        names = chroma.list_rules(collection)
        return [{"name": name, "file": f"{name}.rule.yaml"} for name in names]

    def _list_actions() -> list[str]:
        """List all action names."""
        return chroma.list_actions()

    tools: list[Callable[..., Any]] = [_search_rules, _search_actions, _list_rules, _list_actions]
    if rules_dir:
        tools.append(_make_failed_rules_tool(rules_dir))
        tools.append(_make_observations_tool(rules_dir))
    return tools


def _make_failed_rules_tool(rules_dir: Path) -> Callable[..., Any]:
    """Create a tool that lists archived failed rules."""
    failed_dir = rules_dir / "failed"

    def _list_failed_rules() -> list[dict[str, Any]]:
        """List previously failed rules and their error context.

        Use this to learn what approaches were already tried and avoid repeating them.
        Returns rule names, descriptions, and the errors that caused rejection.
        """
        if not failed_dir.exists():
            return []

        results = []
        for path in sorted(failed_dir.glob("*.rule.yaml")):
            entry: dict[str, Any] = {"name": path.stem, "file": path.name}
            sidecar = path.with_suffix(path.suffix + ".json")
            if sidecar.exists():
                try:
                    entry["metadata"] = json.loads(sidecar.read_text())
                except Exception:
                    pass
            results.append(entry)
        return results

    return _list_failed_rules


def _make_observations_tool(rules_dir: Path) -> Callable[..., Any]:
    """Create a tool that searches past LLM attempt observations."""
    observations_path = rules_dir.parent / "observations.jsonl"

    def _search_observations(query: str = "") -> list[dict[str, Any]]:
        """Search past LLM attempt observations for patterns and lessons learned.

        Returns entries where the query substring matches any value.
        Each entry has an "outcome" field ("success" or "failure").
        Use this to learn from previous attempts and avoid repeating mistakes.

        Args:
            query: Substring to search for across all observation fields. Empty returns all.
        """
        if not observations_path.exists():
            return []

        results = []
        for line in observations_path.read_text().splitlines():
            line = line.strip()
            if not line:
                continue
            try:
                entry = json.loads(line)
            except Exception:
                continue
            if not query or any(query.lower() in str(v).lower() for v in entry.values()):
                results.append(entry)
        return results

    return _search_observations


def make_validation_tools(
    rules_dir: Path, context: dict[str, Any], action_registry: Any = None
) -> list[Callable[..., Any]]:
    """Create tools for validating rules against current context."""
    from theow._core._models import Rule

    actions_dir = rules_dir.parent / "actions"

    def _test_rule_match(rule_file: str) -> dict[str, Any]:
        """Test if a rule's when-facts match the current error context.

        Call this BEFORE submit_rule() to verify your patterns are correct.
        Validates both fact matching AND action file correctness.

        Args:
            rule_file: Path to the rule file to test.
        """
        path = Path(rule_file)
        if not path.exists():
            return {"error": f"Rule file not found: {rule_file}"}

        try:
            rule = Rule.from_yaml(path)
        except Exception as e:
            return {"error": f"Failed to parse rule: {e}"}

        results = []
        all_match = True

        for fact in rule.when:
            value = context.get(fact.fact)
            match_result = fact.matches(value)

            if match_result is None:
                all_match = False
                condition = fact.equals or fact.contains or fact.regex
                actual = str(value)[:200] if value else "(missing)"
                results.append(
                    {
                        "fact": fact.fact,
                        "status": "FAIL",
                        "expected": condition,
                        "actual": actual + "..." if value and len(str(value)) > 200 else actual,
                    }
                )
            else:
                results.append(
                    {
                        "fact": fact.fact,
                        "status": "OK",
                        "captures": match_result if match_result else None,
                    }
                )

        # Validate actions can be loaded
        action_errors = []
        if action_registry:
            for rule_action in rule.then:
                action_path = actions_dir / f"{rule_action.action}.py"
                if not action_path.exists():
                    action_errors.append(f"Action file not found: {action_path}")
                    continue
                action_registry._load_action_file(action_path)
                if not action_registry.exists(rule_action.action):
                    action_errors.append(
                        f'Action "{rule_action.action}" not registered after loading '
                        f'{action_path.name} - check @action("{rule_action.action}") decorator'
                    )

        ok = all_match and not action_errors
        result: dict[str, Any] = {"matches": ok, "facts": results}
        if action_errors:
            result["action_errors"] = action_errors
        if not ok:
            result["hint"] = "Fix errors before calling submit_rule()"
        return result

    return [_test_rule_match]


def make_ephemeral_tools(rules_dir: Path) -> list[Callable[..., Any]]:
    """Create tools for accessing ephemeral rules from current/previous attempts."""
    ephemeral_dir = rules_dir / "ephemeral"

    def _list_ephemeral_rules() -> list[dict[str, Any]]:
        """List ephemeral rules from current or previous exploration attempts.

        Returns list of ephemeral rules with name, file, tags, description, and notes.
        Check this at the start of exploration to see prior work you can continue from.
        """
        if not ephemeral_dir.exists():
            return []

        results = []
        for path in ephemeral_dir.glob("*.rule.yaml"):
            try:
                content = path.read_text()
                data = yaml.safe_load(content)
                results.append(
                    {
                        "name": data.get("name", path.stem),
                        "file": str(path),
                        "tags": data.get("tags", []),
                        "description": data.get("description", "")[:200],
                        "notes": data.get("notes", "")[:500] if data.get("notes") else None,
                    }
                )
            except Exception:
                continue
        return results

    def _read_ephemeral_rule(name: str) -> str:
        """Read the full content of an ephemeral rule to continue from.

        Args:
            name: Rule name (without .rule.yaml extension).

        Returns the full YAML content of the rule file.
        """
        path = ephemeral_dir / f"{name}.rule.yaml"
        if not path.exists():
            return f"Error: Ephemeral rule '{name}' not found"
        return path.read_text()

    def _write_rule(name: str, content: str) -> dict[str, str]:
        """Write a rule file to the ephemeral folder.

        Args:
            name: Rule name (without extension). Will be saved as <name>.rule.yaml
            content: YAML content of the rule.

        Returns dict with 'path' for use with test_rule_match() and submit_rule().
        """
        ephemeral_dir.mkdir(parents=True, exist_ok=True)
        path = ephemeral_dir / f"{name}.rule.yaml"
        path.write_text(content)
        return {"path": str(path), "message": f"Rule written to {path}"}

    def _write_action(name: str, content: str) -> dict[str, str]:
        """Write an action file to the actions folder.

        Args:
            name: Action name (without extension). Will be saved as <name>.py
            content: Python content of the action.

        Returns dict with 'path' for use with submit_rule().
        """
        actions_dir = rules_dir.parent / "actions"
        actions_dir.mkdir(parents=True, exist_ok=True)
        path = actions_dir / f"{name}.py"
        path.write_text(content)
        return {"path": str(path), "message": f"Action written to {path}"}

    return [_list_ephemeral_rules, _read_ephemeral_rule, _write_rule, _write_action]


# Theow external tools for consumers. Not registered by default, consumers can choose to register them.
def read_file(path: str) -> str:
    """Read file contents."""
    return Path(path).read_text()


def write_file(path: str, content: str) -> str:
    """Write content to file, creating directories if needed."""
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content)
    return f"Written {len(content)} bytes to {path}"


def run_command(cmd: str, cwd: str | None = None) -> dict[str, Any]:
    """Run shell command and return output."""
    result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
    return {
        "returncode": result.returncode,
        "stdout": result.stdout,
        "stderr": result.stderr,
    }


def list_directory(path: str) -> list[str]:
    """List files and directories at path."""
    return [p.name for p in Path(path).iterdir()]
