"""System, calculation, and AI consultation tools for Timmy agents.

Provides:
- Safe AST-based calculator
- consult_grok (xAI frontier reasoning)
- web_fetch (content extraction)
- Toolkit factories for Forge (code), Mace (security), Helm (devops)
"""

from __future__ import annotations

import ast
import logging
import math
import subprocess
from pathlib import Path

from timmy.tools._base import (
    _AGNO_TOOLS_AVAILABLE,
    _ImportError,
    FileTools,
    PythonTools,
    ShellTools,
    Toolkit,
)
from timmy.tools.file_tools import _make_smart_read_file

logger = logging.getLogger(__name__)

# Max characters of user query included in Lightning invoice memo
_INVOICE_MEMO_MAX_LEN = 50


def _safe_eval(node, allowed_names: dict):
    """Walk an AST and evaluate only safe numeric operations."""
    if isinstance(node, ast.Expression):
        return _safe_eval(node.body, allowed_names)
    if isinstance(node, ast.Constant):
        if isinstance(node.value, (int, float, complex)):
            return node.value
        raise ValueError(f"Unsupported constant: {node.value!r}")
    if isinstance(node, ast.UnaryOp):
        operand = _safe_eval(node.operand, allowed_names)
        if isinstance(node.op, ast.UAdd):
            return +operand
        if isinstance(node.op, ast.USub):
            return -operand
        raise ValueError(f"Unsupported unary op: {type(node.op).__name__}")
    if isinstance(node, ast.BinOp):
        left = _safe_eval(node.left, allowed_names)
        right = _safe_eval(node.right, allowed_names)
        ops = {
            ast.Add: lambda a, b: a + b,
            ast.Sub: lambda a, b: a - b,
            ast.Mult: lambda a, b: a * b,
            ast.Div: lambda a, b: a / b,
            ast.FloorDiv: lambda a, b: a // b,
            ast.Mod: lambda a, b: a % b,
            ast.Pow: lambda a, b: a**b,
        }
        op_fn = ops.get(type(node.op))
        if op_fn is None:
            raise ValueError(f"Unsupported binary op: {type(node.op).__name__}")
        return op_fn(left, right)
    if isinstance(node, ast.Name):
        if node.id in allowed_names:
            return allowed_names[node.id]
        raise ValueError(f"Unknown name: {node.id!r}")
    if isinstance(node, ast.Attribute):
        value = _safe_eval(node.value, allowed_names)
        # Only allow attribute access on the math module
        if value is math:
            attr = getattr(math, node.attr, None)
            if attr is not None:
                return attr
        raise ValueError(f"Attribute access not allowed: .{node.attr}")
    if isinstance(node, ast.Call):
        func = _safe_eval(node.func, allowed_names)
        if not callable(func):
            raise ValueError(f"Not callable: {func!r}")
        args = [_safe_eval(a, allowed_names) for a in node.args]
        kwargs = {kw.arg: _safe_eval(kw.value, allowed_names) for kw in node.keywords}
        return func(*args, **kwargs)
    raise ValueError(f"Unsupported syntax: {type(node).__name__}")


def calculator(expression: str) -> str:
    """Evaluate a mathematical expression and return the exact result.

    Use this tool for ANY arithmetic: multiplication, division, square roots,
    exponents, percentages, logarithms, trigonometry, etc.

    Args:
        expression: A valid Python math expression, e.g. '347 * 829',
                    'math.sqrt(17161)', '2**10', 'math.log(100, 10)'.

    Returns:
        The exact result as a string.
    """
    allowed_names = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
    allowed_names["math"] = math
    allowed_names["abs"] = abs
    allowed_names["round"] = round
    allowed_names["min"] = min
    allowed_names["max"] = max
    try:
        tree = ast.parse(expression, mode="eval")
        result = _safe_eval(tree, allowed_names)
        return str(result)
    except Exception as e:  # broad catch intentional: arbitrary code execution
        return f"Error evaluating '{expression}': {e}"


def consult_grok(query: str) -> str:
    """Consult Grok (xAI) for frontier reasoning on complex questions.

    Use this tool when a question requires advanced reasoning, real-time
    knowledge, or capabilities beyond the local model. Grok is a premium
    cloud backend — use sparingly and only for high-complexity queries.

    Args:
        query: The question or reasoning task to send to Grok.

    Returns:
        Grok's response text, or an error/status message.
    """
    from config import settings
    from timmy.backends import get_grok_backend, grok_available

    if not grok_available():
        return (
            "Grok is not available. Enable with GROK_ENABLED=true "
            "and set XAI_API_KEY in your .env file."
        )

    backend = get_grok_backend()

    # Log to Spark if available
    try:
        from spark.engine import spark_engine

        spark_engine.on_tool_executed(
            agent_id="default",
            tool_name="consult_grok",
            success=True,
        )
    except (ImportError, AttributeError) as exc:
        logger.warning("Tool execution failed (consult_grok logging): %s", exc)

    # Generate Lightning invoice for monetization (unless free mode)
    invoice_info = ""
    if not settings.grok_free:
        try:
            from lightning.factory import get_backend as get_ln_backend

            ln = get_ln_backend()
            sats = min(settings.grok_max_sats_per_query, settings.grok_sats_hard_cap)
            inv = ln.create_invoice(sats, f"Grok query: {query[:_INVOICE_MEMO_MAX_LEN]}")
            invoice_info = f"\n[Lightning invoice: {sats} sats — {inv.payment_request[:40]}...]"
        except (ImportError, OSError, ValueError) as exc:
            logger.error("Lightning invoice creation failed: %s", exc)
            return "Error: Failed to create Lightning invoice. Please check logs."

    result = backend.run(query)

    response = result.content
    if invoice_info:
        response += invoice_info

    return response


def web_fetch(url: str, max_tokens: int = 4000) -> str:
    """Fetch a web page and return its main text content.

    Downloads the URL, extracts readable text using trafilatura, and
    truncates to a token budget.  Use this to read full articles, docs,
    or blog posts that web_search only returns snippets for.

    Args:
        url: The URL to fetch (must start with http:// or https://).
        max_tokens: Maximum approximate token budget (default 4000).
                    Text is truncated to max_tokens * 4 characters.

    Returns:
        Extracted text content, or an error message on failure.
    """
    if not url or not url.startswith(("http://", "https://")):
        return f"Error: invalid URL — must start with http:// or https://: {url!r}"

    try:
        import requests as _requests
    except ImportError:
        return "Error: 'requests' package is not installed. Install with: pip install requests"

    try:
        import trafilatura
    except ImportError:
        return (
            "Error: 'trafilatura' package is not installed. Install with: pip install trafilatura"
        )

    try:
        resp = _requests.get(
            url,
            timeout=15,
            headers={"User-Agent": "TimmyResearchBot/1.0"},
        )
        resp.raise_for_status()
    except _requests.exceptions.Timeout:
        return f"Error: request timed out after 15 seconds for {url}"
    except _requests.exceptions.HTTPError as exc:
        return f"Error: HTTP {exc.response.status_code} for {url}"
    except _requests.exceptions.RequestException as exc:
        return f"Error: failed to fetch {url} — {exc}"

    text = trafilatura.extract(resp.text, include_tables=True, include_links=True)
    if not text:
        return f"Error: could not extract readable content from {url}"

    char_budget = max_tokens * 4
    if len(text) > char_budget:
        text = text[:char_budget] + f"\n\n[…truncated to ~{max_tokens} tokens]"

    return text


def create_aider_tool(base_path: Path):
    """Create an Aider tool for AI-assisted coding."""

    class AiderTool:
        """Tool that calls Aider (local AI coding assistant) for code generation."""

        def __init__(self, base_dir: Path):
            self.base_dir = base_dir

        def run_aider(self, prompt: str, model: str = "qwen3:30b") -> str:
            """Run Aider to generate code changes.

            Args:
                prompt: What you want Aider to do (e.g., "add a fibonacci function")
                model: Ollama model to use (default: qwen3:30b)

            Returns:
                Aider's response with the code changes made
            """
            try:
                # Run aider with the prompt
                result = subprocess.run(
                    [
                        "aider",
                        "--no-git",
                        "--model",
                        f"ollama/{model}",
                        "--quiet",
                        prompt,
                    ],
                    capture_output=True,
                    text=True,
                    timeout=120,
                    cwd=str(self.base_dir),
                )

                if result.returncode == 0:
                    return result.stdout if result.stdout else "Code changes applied successfully"
                else:
                    return f"Aider error: {result.stderr}"
            except FileNotFoundError:
                return "Error: Aider not installed. Run: pip install aider"
            except subprocess.TimeoutExpired:
                return "Error: Aider timed out after 120 seconds"
            except (OSError, subprocess.SubprocessError) as e:
                return f"Error running Aider: {str(e)}"

    return AiderTool(base_path)


def create_code_tools(base_dir: str | Path | None = None):
    """Create tools for the code agent (Forge).

    Includes: shell commands, python execution, file read/write, Aider AI assist
    """
    if not _AGNO_TOOLS_AVAILABLE:
        raise ImportError(f"Agno tools not available: {_ImportError}")
    toolkit = Toolkit(name="code")

    # Shell commands (sandboxed)
    shell_tools = ShellTools()
    toolkit.register(shell_tools.run_shell_command, name="shell")

    # Python execution
    python_tools = PythonTools()
    toolkit.register(python_tools.run_python_code, name="python")

    # File operations
    from config import settings

    base_path = Path(base_dir) if base_dir else Path(settings.repo_root)
    file_tools = FileTools(base_dir=base_path)
    toolkit.register(_make_smart_read_file(file_tools), name="read_file")
    toolkit.register(file_tools.save_file, name="write_file")
    toolkit.register(file_tools.list_files, name="list_files")

    # Aider AI coding assistant (local with Ollama)
    aider_tool = create_aider_tool(base_path)
    toolkit.register(aider_tool.run_aider, name="aider")

    return toolkit


def create_security_tools(base_dir: str | Path | None = None):
    """Create tools for the security agent (Mace).

    Includes: shell commands (for scanning), file read
    """
    if not _AGNO_TOOLS_AVAILABLE:
        raise ImportError(f"Agno tools not available: {_ImportError}")
    toolkit = Toolkit(name="security")

    # Shell for running security scans
    shell_tools = ShellTools()
    toolkit.register(shell_tools.run_shell_command, name="shell")

    # File reading for logs/configs
    from config import settings

    base_path = Path(base_dir) if base_dir else Path(settings.repo_root)
    file_tools = FileTools(base_dir=base_path)
    toolkit.register(_make_smart_read_file(file_tools), name="read_file")
    toolkit.register(file_tools.list_files, name="list_files")

    return toolkit


def create_devops_tools(base_dir: str | Path | None = None):
    """Create tools for the DevOps agent (Helm).

    Includes: shell commands, file read/write
    """
    if not _AGNO_TOOLS_AVAILABLE:
        raise ImportError(f"Agno tools not available: {_ImportError}")
    toolkit = Toolkit(name="devops")

    # Shell for deployment commands
    shell_tools = ShellTools()
    toolkit.register(shell_tools.run_shell_command, name="shell")

    # File operations for config management
    from config import settings

    base_path = Path(base_dir) if base_dir else Path(settings.repo_root)
    file_tools = FileTools(base_dir=base_path)
    toolkit.register(_make_smart_read_file(file_tools), name="read_file")
    toolkit.register(file_tools.save_file, name="write_file")
    toolkit.register(file_tools.list_files, name="list_files")

    return toolkit
