"""Generator that converts tool_extraction suggestions into @tool function code."""

from __future__ import annotations

import logging

import litellm

from sage_evaluator.exceptions import SuggestionError
from sage_evaluator.models import GeneratedTool, Suggestion, SuggestionCategory

logger = logging.getLogger(__name__)

_TOOL_GENERATION_PROMPT_TEMPLATE = """\
You are an expert Python developer working with the Apollo Agent SDK.

A code review has identified the following opportunity to extract a reusable tool
from an agent's system prompt:

Title: {title}
Description: {description}
Original inline text:
{before}

Proposed replacement hint:
{after}

## Task

Generate a complete Python function using the Apollo Agent @tool decorator pattern.
The function must:
1. Import `tool` from `sage.tools.decorator`
2. Be decorated with `@tool(description="...")`
3. Have a clear, snake_case function name derived from the title
4. Include a docstring
5. Implement realistic, deterministic logic (no placeholders such as `pass` or `...`)
6. Use only Python standard library imports (no third-party packages unless
   critically necessary and widely available)
7. Have typed parameters and a typed return value

Return ONLY the Python source code — no markdown fences, no explanation.

Example output:
from sage.tools.decorator import tool


@tool(description="Validate that an email address is syntactically correct.")
def validate_email_format(email: str) -> str:
    \"\"\"Check whether the provided string is a valid RFC 5322 email address.

    Args:
        email: The email address to validate.

    Returns:
        "valid" if the address is syntactically correct, otherwise an error message.
    \"\"\"
    import re

    pattern = r"^[\\w.+-]+@[\\w-]+\\.[\\w.-]+$"
    if re.match(pattern, email):
        return "valid"
    return f"invalid: '{{email}}' does not match expected email format"
"""


def _extract_function_name(source_code: str) -> str:
    """Extract the function name from generated source code.

    Scans the source for the first ``def <name>(`` pattern and returns the name.
    Falls back to ``"generated_tool"`` if no function definition is found.
    """
    for line in source_code.splitlines():
        stripped = line.strip()
        if stripped.startswith("def "):
            # "def some_name(..." -> "some_name"
            remainder = stripped[4:]
            paren_idx = remainder.find("(")
            if paren_idx > 0:
                return remainder[:paren_idx].strip()
    return "generated_tool"


class ToolGenerator:
    """Generates Python ``@tool`` functions from tool_extraction suggestions.

    Only suggestions whose category is
    :attr:`~sage_evaluator.models.SuggestionCategory.TOOL_EXTRACTION` are
    processed; all others are silently ignored.

    Example usage::

        generator = ToolGenerator(model="azure_ai/claude-opus-4-6")
        tools = await generator.generate_tools(report.suggestions)
        for t in tools:
            print(t.source_code)
    """

    def __init__(self, model: str) -> None:
        self.model = model

    async def generate_tools(self, suggestions: list[Suggestion]) -> list[GeneratedTool]:
        """Generate @tool function source code from tool_extraction suggestions.

        Args:
            suggestions: Full list of suggestions from the analyzer. Non-
                ``tool_extraction`` entries are ignored.

        Returns:
            A list of :class:`~sage_evaluator.models.GeneratedTool` objects,
            one per processed suggestion. The ``category`` field is set to
            ``"extracted"``.

        Raises:
            SuggestionError: If the LLM call fails for any suggestion.
        """
        candidates = [s for s in suggestions if s.category == SuggestionCategory.TOOL_EXTRACTION]
        if not candidates:
            logger.debug("No tool_extraction suggestions to process.")
            return []

        logger.info("Generating tools from %d tool_extraction suggestion(s)", len(candidates))
        generated: list[GeneratedTool] = []
        for suggestion in candidates:
            tool = await self._generate_single_tool(suggestion)
            generated.append(tool)

        logger.info("Tool generation complete: %d tool(s) generated", len(generated))
        return generated

    async def _generate_single_tool(self, suggestion: Suggestion) -> GeneratedTool:
        """Call the LLM to produce source code for a single tool suggestion."""
        prompt = _TOOL_GENERATION_PROMPT_TEMPLATE.format(
            title=suggestion.title,
            description=suggestion.description,
            before=suggestion.before or "(not specified)",
            after=suggestion.after or "(not specified)",
        )

        logger.debug("Generating tool for suggestion: '%s'", suggestion.title)

        try:
            response = await litellm.acompletion(
                model=self.model,
                messages=[{"role": "user", "content": prompt}],
                temperature=0.0,
            )
        except Exception as exc:
            raise SuggestionError(
                f"LLM call failed while generating tool for '{suggestion.title}': {exc}"
            ) from exc

        source_code: str = response.choices[0].message.content or ""
        # Strip accidental markdown code fences if the model ignores our instruction.
        source_code = _strip_code_fences(source_code)

        name = _extract_function_name(source_code)
        logger.debug("Generated tool '%s' (%d chars)", name, len(source_code))

        return GeneratedTool(
            name=name,
            description=suggestion.description,
            source_code=source_code,
            category="extracted",
        )


def _strip_code_fences(text: str) -> str:
    """Remove leading/trailing markdown code fence lines if present.

    Handles both ````` ```python ``` ````` and plain ``` ``` ``` fences.
    """
    lines = text.strip().splitlines()
    if not lines:
        return text

    start = 0
    end = len(lines)

    if lines[0].startswith("```"):
        start = 1
    if end > start and lines[-1].strip() == "```":
        end -= 1

    return "\n".join(lines[start:end]).strip()
