"""Custom tools for the CLI agent."""

import ipaddress
import logging
import shutil
import socket
import subprocess
from pathlib import Path
from typing import Any, Literal
from urllib.parse import urlparse

import requests
from markdownify import markdownify
from tavily import TavilyClient

from ieum.config import settings

_logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# SSRF protection: block requests to private / internal networks
# ---------------------------------------------------------------------------

_BLOCKED_NETWORKS = [
    ipaddress.ip_network("10.0.0.0/8"),
    ipaddress.ip_network("172.16.0.0/12"),
    ipaddress.ip_network("192.168.0.0/16"),
    ipaddress.ip_network("127.0.0.0/8"),
    ipaddress.ip_network("169.254.0.0/16"),
    ipaddress.ip_network("::1/128"),
    ipaddress.ip_network("fc00::/7"),
    ipaddress.ip_network("fe80::/10"),
]


def is_safe_url(url: str) -> bool:
    """Check whether *url* resolves to a publicly routable address.

    Returns ``False`` if the hostname resolves to any private, link-local,
    loopback, or otherwise internal IP address.  This prevents Server-Side
    Request Forgery (SSRF) attacks that attempt to reach internal services.

    Args:
        url: The URL to validate.

    Returns:
        ``True`` if the URL resolves exclusively to public addresses,
        ``False`` otherwise (including DNS resolution failures).
    """
    parsed = urlparse(url)
    hostname = parsed.hostname
    if not hostname:
        return False
    try:
        addr_info = socket.getaddrinfo(hostname, None)
        for family, type_, proto, canonname, sockaddr in addr_info:
            ip = ipaddress.ip_address(sockaddr[0])
            for network in _BLOCKED_NETWORKS:
                if ip in network:
                    _logger.warning(
                        "SSRF blocked: %s resolves to private address %s",
                        hostname,
                        ip,
                    )
                    return False
    except socket.gaierror:
        _logger.warning("SSRF check: DNS resolution failed for %s", hostname)
        return False
    return True

# Initialize Tavily client if API key is available
tavily_client = TavilyClient(api_key=settings.tavily_api_key) if settings.has_tavily else None


def http_request(
    url: str,
    method: str = "GET",
    headers: dict[str, str] | None = None,
    data: str | dict | None = None,
    params: dict[str, str] | None = None,
    timeout: int = 30,
) -> dict[str, Any]:
    """Make HTTP requests to APIs and web services.

    Args:
        url: Target URL
        method: HTTP method (GET, POST, PUT, DELETE, etc.)
        headers: HTTP headers to include
        data: Request body data (string or dict)
        params: URL query parameters
        timeout: Request timeout in seconds

    Returns:
        Dictionary with response data including status, headers, and content
    """
    # SSRF protection: reject requests targeting private/internal networks
    if not is_safe_url(url):
        return {
            "success": False,
            "status_code": 0,
            "headers": {},
            "content": "Blocked: URL resolves to a private or internal network address",
            "url": url,
        }

    try:
        kwargs = {"url": url, "method": method.upper(), "timeout": timeout}

        if headers:
            kwargs["headers"] = headers
        if params:
            kwargs["params"] = params
        if data:
            if isinstance(data, dict):
                kwargs["json"] = data
            else:
                kwargs["data"] = data

        response = requests.request(**kwargs)

        try:
            content = response.json()
        except (ValueError, requests.exceptions.JSONDecodeError):
            content = response.text

        return {
            "success": response.status_code < 400,
            "status_code": response.status_code,
            "headers": dict(response.headers),
            "content": content,
            "url": response.url,
        }

    except requests.exceptions.Timeout:
        return {
            "success": False,
            "status_code": 0,
            "headers": {},
            "content": f"Request timed out after {timeout} seconds",
            "url": url,
        }
    except requests.exceptions.RequestException as e:
        return {
            "success": False,
            "status_code": 0,
            "headers": {},
            "content": f"Request error: {e!s}",
            "url": url,
        }
    except Exception as e:
        return {
            "success": False,
            "status_code": 0,
            "headers": {},
            "content": f"Error making request: {e!s}",
            "url": url,
        }


def web_search(
    query: str,
    max_results: int = 5,
    topic: Literal["general", "news", "finance"] = "general",
    include_raw_content: bool = False,
):
    """Search the web using Tavily for current information and documentation.

    This tool searches the web and returns relevant results. After receiving results,
    you MUST synthesize the information into a natural, helpful response for the user.

    Args:
        query: The search query (be specific and detailed)
        max_results: Number of results to return (default: 5)
        topic: Search topic type - "general" for most queries, "news" for current events
        include_raw_content: Include full page content (warning: uses more tokens)

    Returns:
        Dictionary containing:
        - results: List of search results, each with:
            - title: Page title
            - url: Page URL
            - content: Relevant excerpt from the page
            - score: Relevance score (0-1)
        - query: The original search query

    IMPORTANT: After using this tool:
    1. Read through the 'content' field of each result
    2. Extract relevant information that answers the user's question
    3. Synthesize this into a clear, natural language response
    4. Cite sources by mentioning the page titles or URLs
    5. NEVER show the raw JSON to the user - always provide a formatted response
    """
    if tavily_client is None:
        return {
            "error": "Tavily API key not configured. Please set TAVILY_API_KEY environment variable.",
            "query": query,
        }

    try:
        return tavily_client.search(
            query,
            max_results=max_results,
            include_raw_content=include_raw_content,
            topic=topic,
        )
    except Exception as e:
        return {"error": f"Web search error: {e!s}", "query": query}


def fetch_url(url: str, timeout: int = 30) -> dict[str, Any]:
    """Fetch content from a URL and convert HTML to markdown format.

    This tool fetches web page content and converts it to clean markdown text,
    making it easy to read and process HTML content. After receiving the markdown,
    you MUST synthesize the information into a natural, helpful response for the user.

    Args:
        url: The URL to fetch (must be a valid HTTP/HTTPS URL)
        timeout: Request timeout in seconds (default: 30)

    Returns:
        Dictionary containing:
        - success: Whether the request succeeded
        - url: The final URL after redirects
        - markdown_content: The page content converted to markdown
        - status_code: HTTP status code
        - content_length: Length of the markdown content in characters

    IMPORTANT: After using this tool:
    1. Read through the markdown content
    2. Extract relevant information that answers the user's question
    3. Synthesize this into a clear, natural language response
    4. NEVER show the raw markdown to the user unless specifically requested
    """
    # SSRF protection: reject requests targeting private/internal networks
    if not is_safe_url(url):
        return {
            "error": "Blocked: URL resolves to a private or internal network address",
            "url": url,
        }

    try:
        response = requests.get(
            url,
            timeout=timeout,
            headers={"User-Agent": "Mozilla/5.0 (compatible; IEUM/1.0)"},
        )
        response.raise_for_status()

        # Convert HTML content to markdown
        markdown_content = markdownify(response.text)

        return {
            "url": str(response.url),
            "markdown_content": markdown_content,
            "status_code": response.status_code,
            "content_length": len(markdown_content),
        }
    except Exception as e:
        return {"error": f"Fetch URL error: {e!s}", "url": url}


def ripgrep(
    pattern: str,
    path: str = ".",
    *,
    ignore_case: bool = False,
    file_type: str | None = None,
    max_count: int | None = None,
    context_lines: int = 0,
    include_hidden: bool = False,
    fixed_strings: bool = False,
) -> dict[str, Any]:
    """Search for patterns in files using ripgrep (rg).

    ripgrep is a fast, recursive search tool that respects .gitignore files.
    Use this tool for searching code, logs, or any text files.

    Args:
        pattern: The regex pattern to search for
        path: Directory or file to search in (default: current directory)
        ignore_case: Perform case-insensitive search
        file_type: Filter by file type (e.g., "py", "js", "ts", "md")
        max_count: Maximum matches per file
        context_lines: Number of context lines before and after match
        include_hidden: Include hidden files and directories
        fixed_strings: Treat pattern as literal string, not regex

    Returns:
        Dictionary containing:
        - success: Whether the search succeeded
        - matches: List of matches with file, line number, and content
        - match_count: Total number of matches found
        - error: Error message if search failed

    Examples:
        ripgrep("def main", "src/")           # Find "def main" in src/
        ripgrep("TODO", file_type="py")       # Find TODO in Python files
        ripgrep("error", ignore_case=True)    # Case-insensitive search
    """
    # Check if ripgrep is available
    rg_path = shutil.which("rg")
    if not rg_path:
        import sys
        if sys.platform == "win32":
            install_cmd = "winget install BurntSushi.ripgrep.MSVC  # 또는: scoop install ripgrep"
        elif sys.platform == "darwin":
            install_cmd = "brew install ripgrep"
        else:
            install_cmd = "apt install ripgrep  # 또는: cargo install ripgrep"
        return {
            "success": False,
            "matches": [],
            "match_count": 0,
            "error": f"ripgrep (rg) 미설치. 설치 명령어:\n  {install_cmd}",
        }

    # Build command
    cmd = [rg_path, "--json"]

    if ignore_case:
        cmd.append("-i")
    if file_type:
        cmd.extend(["-t", file_type])
    if max_count:
        cmd.extend(["-m", str(max_count)])
    if context_lines > 0:
        cmd.extend(["-C", str(context_lines)])
    if include_hidden:
        cmd.append("--hidden")
    if fixed_strings:
        cmd.append("-F")

    cmd.append(pattern)
    cmd.append(path)

    try:
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=60,
            cwd=Path.cwd(),
        )

        # Parse JSON output
        import json

        matches = []
        for line in result.stdout.strip().split("\n"):
            if not line:
                continue
            try:
                data = json.loads(line)
                if data.get("type") == "match":
                    match_data = data.get("data", {})
                    matches.append({
                        "file": match_data.get("path", {}).get("text", ""),
                        "line_number": match_data.get("line_number", 0),
                        "content": match_data.get("lines", {}).get("text", "").strip(),
                    })
            except json.JSONDecodeError:
                continue

        return {
            "success": True,
            "matches": matches,
            "match_count": len(matches),
            "error": None,
        }

    except subprocess.TimeoutExpired:
        return {
            "success": False,
            "matches": [],
            "match_count": 0,
            "error": "Search timed out after 60 seconds",
        }
    except Exception as e:
        return {
            "success": False,
            "matches": [],
            "match_count": 0,
            "error": f"Search error: {e!s}",
        }


# =============================================================================
# Python REPL Tool
# =============================================================================

import logging
import re as _repl_re
import signal as _repl_signal

# Security: Audit logger for Python REPL executions
_repl_audit_logger = logging.getLogger("ieum.security.python_repl")

# Maximum output size in bytes (100 KB)
_REPL_MAX_OUTPUT_BYTES = 100_000

# Default execution timeout in seconds
_REPL_DEFAULT_TIMEOUT = 30

# Security: Patterns that are clearly dangerous in a REPL context.
# These block shell-out attempts and destructive filesystem operations.
# The list is intentionally conservative -- this is a coding agent that needs
# broad Python access for legitimate development tasks.
_REPL_BLOCKED_PATTERNS: list[tuple[_repl_re.Pattern[str], str]] = [
    # Shell-out via os.system
    (_repl_re.compile(r"\bos\s*\.\s*system\s*\("), "os.system() is blocked -- use the shell tool instead"),
    # subprocess calls
    (_repl_re.compile(r"\bsubprocess\s*\.\s*(Popen|run|call|check_call|check_output)\s*\("),
     "subprocess calls are blocked -- use the shell tool instead"),
    # eval(input()) -- interactive RCE
    (_repl_re.compile(r"\beval\s*\(\s*input\s*\("), "eval(input()) is blocked for security"),
    # exec(open()) -- arbitrary file execution
    (_repl_re.compile(r"\bexec\s*\(\s*open\s*\("), "exec(open()) is blocked for security"),
    # shutil.rmtree on root or very broad paths
    (_repl_re.compile(r"\bshutil\s*\.\s*rmtree\s*\(\s*['\"]\/['\"]"),
     "shutil.rmtree('/') is blocked -- recursive delete of root directory"),
    (_repl_re.compile(r"\bshutil\s*\.\s*rmtree\s*\(\s*['\"]\/\*['\"]"),
     "shutil.rmtree('/*') is blocked -- recursive delete of root directory"),
    # os.remove / os.unlink on root paths
    (_repl_re.compile(r"\bos\s*\.\s*(remove|unlink)\s*\(\s*['\"]\/['\"]"),
     "Deleting root path is blocked"),
]


def _check_repl_code_safety(code: str) -> str | None:
    """Check Python code against blocked patterns.

    Args:
        code: The Python code to validate.

    Returns:
        A reason string if blocked, or None if the code is allowed.
    """
    for pattern, reason in _REPL_BLOCKED_PATTERNS:
        if pattern.search(code):
            return reason
    return None


# Global state for Python REPL (persists across calls)
_python_repl_globals: dict[str, Any] = {}
_python_repl_locals: dict[str, Any] = {}


def python_repl(
    code: str,
    timeout: int | None = _REPL_DEFAULT_TIMEOUT,
    reset: bool = False,
) -> dict[str, Any]:
    """Execute Python code in a persistent REPL environment.

    This tool allows you to run Python code directly. Variables and imports
    persist between calls, allowing for interactive development and testing.

    Args:
        code: Python code to execute. Can be single statements or multi-line code.
        timeout: Maximum execution time in seconds (default: 30). Set to None to disable.
        reset: If True, reset the REPL state (clear all variables and imports)

    Returns:
        Dictionary containing:
        - success: Whether execution succeeded
        - output: Captured stdout output (print statements)
        - result: Return value of the last expression (if any)
        - error: Error message if execution failed

    Examples:
        python_repl("print('Hello, World!')")
        python_repl("import pandas as pd")
        python_repl("df = pd.DataFrame({'a': [1,2,3]})")
        python_repl("df.describe()")
        python_repl("", reset=True)  # Reset REPL state

    IMPORTANT:
    - Use print() to see output
    - Variables persist between calls
    - Be careful with infinite loops
    - File I/O is relative to current working directory
    """
    import re
    import sys
    from io import StringIO

    global _python_repl_globals, _python_repl_locals

    # Handle reset
    if reset:
        _python_repl_globals = {}
        _python_repl_locals = {}
        return {
            "success": True,
            "output": "",
            "result": None,
            "error": None,
            "message": "REPL state reset successfully",
        }

    # Sanitize input - remove markdown code fences if present
    code = re.sub(r"^(\s|`)*(?i:python)?\s*", "", code)
    code = re.sub(r"(\s|`)*$", "", code)

    if not code.strip():
        return {
            "success": True,
            "output": "",
            "result": None,
            "error": None,
        }

    # Security: Audit log every code execution
    _repl_audit_logger.info(
        "Python REPL executed | code_length=%d | code_preview=%s",
        len(code),
        code[:200].replace("\n", "\\n"),
    )

    # Security: Check for blocked patterns
    block_reason = _check_repl_code_safety(code)
    if block_reason is not None:
        _repl_audit_logger.warning(
            "Python REPL BLOCKED | reason=%s | code_preview=%s",
            block_reason,
            code[:200].replace("\n", "\\n"),
        )
        return {
            "success": False,
            "output": "",
            "result": None,
            "error": f"Security policy: {block_reason}",
        }

    # Capture stdout/stderr
    old_stdout = sys.stdout
    old_stderr = sys.stderr
    sys.stdout = mystdout = StringIO()
    sys.stderr = mystderr = StringIO()

    result = None
    error = None

    # Timeout handler (Unix only -- signal.SIGALRM is not available on Windows)
    use_timeout = timeout is not None and timeout > 0 and sys.platform != "win32"

    def _timeout_handler(signum, frame):
        raise TimeoutError(f"Code execution timed out after {timeout} seconds")

    if use_timeout:
        old_handler = _repl_signal.signal(_repl_signal.SIGALRM, _timeout_handler)
        _repl_signal.alarm(timeout)

    try:
        # Try to eval first (for expressions that return values)
        try:
            result = eval(code, _python_repl_globals, _python_repl_locals)
        except SyntaxError:
            # If eval fails, use exec (for statements)
            exec(code, _python_repl_globals, _python_repl_locals)
    except TimeoutError as e:
        error = str(e)
        _repl_audit_logger.warning("Python REPL timeout | timeout=%s", timeout)
    except Exception as e:
        error = f"{type(e).__name__}: {e}"
    finally:
        if use_timeout:
            _repl_signal.alarm(0)  # Cancel the alarm
            _repl_signal.signal(_repl_signal.SIGALRM, old_handler)
        sys.stdout = old_stdout
        sys.stderr = old_stderr

    output = mystdout.getvalue()
    if mystderr.getvalue():
        output += "\n[stderr]\n" + mystderr.getvalue()

    # Security: Limit output size to prevent memory exhaustion
    if len(output) > _REPL_MAX_OUTPUT_BYTES:
        output = output[:_REPL_MAX_OUTPUT_BYTES]
        output += f"\n\n... Output truncated at {_REPL_MAX_OUTPUT_BYTES} bytes."

    return {
        "success": error is None,
        "output": output,
        "result": repr(result) if result is not None else None,
        "error": error,
    }
