"""Resolver for macro injection patterns in chat messages.

This module handles:
1. Resolving @fragment references by calling the template render API
2. Resolving @text references by fetching text content from the content API
3. Resolving @json references by fetching structured content from the content API
4. Resolving @schema references by fetching schema definitions from the schema API
5. Replacing patterns in the original message with resolved content
"""

import json
import re
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from types import TracebackType
from typing import Any, cast

from cli.infrastructure.injection.parser import (
    ContentReference,
    FragmentReference,
    SchemaReference,
    parse_injections,
)
from alloy_runtime_sdk.api_client.client import ApiClient
from alloy_runtime_types.dtos.templates import (
    RenderTemplateRequest,
)
from alloy_runtime_sdk.exceptions.errors import (
    AuthenticationError,
    ForbiddenError,
    NotFoundError,
    ServerError,
    ValidationError,
)


def _empty_details() -> dict[str, Any]:
    return {}


def _empty_str_list() -> list[str]:
    return []


def _empty_error_list() -> list["InjectionError"]:
    return []


@dataclass
class InjectionError:
    """An error that occurred during injection resolution."""

    pattern: str  # The pattern that failed (e.g., @fragment(name))
    error_type: str  # Type of error (e.g., "not_found", "permission_denied")
    message: str  # Human-readable error message
    details: dict[str, Any] = field(default_factory=_empty_details)


@dataclass
class ResolvedMessage:
    """Result of resolving all injection patterns in a message."""

    text: str  # Final message with all patterns resolved
    fragments_used: list[str] = field(default_factory=_empty_str_list)
    content_used: list[str] = field(default_factory=_empty_str_list)
    schemas_used: list[str] = field(default_factory=_empty_str_list)
    errors: list[InjectionError] = field(default_factory=_empty_error_list)

    @property
    def has_errors(self) -> bool:
        """Check if any errors occurred during resolution."""
        return len(self.errors) > 0

    @property
    def is_resolved(self) -> bool:
        """Check if message was successfully resolved without errors."""
        return not self.has_errors


class InjectionResolver:
    """Resolves macro injection patterns via API calls.

    Supports the same macro syntax as templates:
    - @fragment(name) - Include a fragment template
    - @text(name) - Insert text from a content part
    - @json(name) - Insert JSON from a content part
    - @schema(name) - Reference a schema definition

    This resolver can work with either:
    1. Direct initialization with api_url/api_key (creates its own client)
    2. Initialization with an existing ApiClient instance

    The TUI should use the client-based initialization to share the client.
    """

    def __init__(
        self,
        api_url: str | None = None,
        api_key: str | None = None,
        client: ApiClient | None = None,
    ):
        """Initialize the resolver.

        Args:
            api_url: Base URL for the API (used if client not provided)
            api_key: API key for authentication (used if client not provided)
            client: Existing ApiClient instance (preferred for TUI)
        """
        if client is not None:
            self._client = client
            self._owns_client = False
        elif api_url and api_key:
            self.api_url = api_url.rstrip("/")
            self.api_key = api_key
            self._client = None
            self._owns_client = True
        else:
            raise ValueError("Must provide either client or both api_url and api_key")

    async def _get_client(self) -> ApiClient:
        """Get or create server client."""
        if self._client is None:
            self._client = ApiClient(
                base_url=self.api_url, api_key=self.api_key, timeout=30.0
            )
            # Enter the async context manager
            await self._client.__aenter__()
        return self._client

    async def close(self) -> None:
        """Close the server client if we own it."""
        if self._owns_client and self._client:
            aexit = cast(
                Callable[
                    [
                        type[BaseException] | None,
                        BaseException | None,
                        TracebackType | None,
                    ],
                    Awaitable[None],
                ],
                self._client.__aexit__,
            )
            await aexit(None, None, None)
            self._client = None

    @staticmethod
    def _is_valid_uuid(value: str) -> bool:
        """Check if a string is a valid UUID format."""
        uuid_pattern = re.compile(
            r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
            re.IGNORECASE,
        )
        return bool(uuid_pattern.match(value))

    async def resolve(self, message: str) -> ResolvedMessage:
        """Resolve all injection patterns in a message.

        Args:
            message: User message with injection patterns

        Returns:
            ResolvedMessage with resolved text and metadata
        """
        # Parse all patterns
        fragment_refs, content_refs, schema_refs = parse_injections(message)

        if not fragment_refs and not content_refs and not schema_refs:
            # No injections found - return original message
            return ResolvedMessage(text=message)

        # Track what we've injected
        fragments_used: list[str] = []
        content_used: list[str] = []
        schemas_used: list[str] = []
        errors: list[InjectionError] = []

        # Build replacement map: (start, end) -> replacement_text
        # We'll sort by position and replace from end to start to maintain positions
        replacements: list[tuple[int, int, str]] = []

        # Resolve fragments
        for ref in fragment_refs:
            try:
                rendered_text = await self._resolve_fragment(ref)
                replacements.append((ref.start, ref.end, rendered_text))
                fragments_used.append(ref.identifier)
            except Exception as e:
                error = self._create_error_from_exception(ref.raw_match, e)
                errors.append(error)
                # Keep the original pattern in the message if it fails
                replacements.append((ref.start, ref.end, ref.raw_match))

        # Resolve content
        for ref in content_refs:
            try:
                content_text = await self._resolve_content(ref)
                replacements.append((ref.start, ref.end, content_text))
                content_used.append(ref.identifier)
            except Exception as e:
                error = self._create_error_from_exception(ref.raw_match, e)
                errors.append(error)
                # Keep the original pattern in the message if it fails
                replacements.append((ref.start, ref.end, ref.raw_match))

        # Resolve schemas
        for ref in schema_refs:
            try:
                schema_definition = await self._resolve_schema(ref)
                replacements.append((ref.start, ref.end, schema_definition))
                schemas_used.append(ref.identifier)
            except Exception as e:
                error = self._create_error_from_exception(ref.raw_match, e)
                errors.append(error)
                # Keep the original pattern in the message if it fails
                replacements.append((ref.start, ref.end, ref.raw_match))

        # Sort replacements by start position (descending) so we replace from end to start
        replacements.sort(key=lambda x: x[0], reverse=True)

        # Apply replacements
        result_text = message
        for start, end, replacement in replacements:
            result_text = result_text[:start] + replacement + result_text[end:]

        return ResolvedMessage(
            text=result_text,
            fragments_used=fragments_used,
            content_used=content_used,
            schemas_used=schemas_used,
            errors=errors,
        )

    async def _resolve_fragment(self, ref: FragmentReference) -> str:
        """Resolve a fragment reference by calling the template render API.

        Fragments are templates with content_type='fragment'. They are rendered
        without variables (fragments don't take variables in the macro syntax).

        Args:
            ref: Fragment reference to resolve

        Returns:
            Rendered fragment text
        """
        client = await self._get_client()

        # The render endpoint requires a UUID, so we need to resolve name -> UUID first
        # Check if identifier is already a UUID
        if self._is_valid_uuid(ref.identifier):
            from uuid import UUID

            template_uuid = UUID(ref.identifier)
        else:
            # Look up template by name to get UUID
            template_uuid = await self._get_template_uuid_by_name(ref.identifier)

        # Prepare render request (no variables for fragments in macro syntax)
        request_data = RenderTemplateRequest(variables={})

        # Call render API using server_client
        render_response = await client.render_template(template_uuid, request_data)
        return render_response.rendered_text

    async def _get_template_uuid_by_name(self, name: str):
        """Look up a template by name and return its UUID.

        Args:
            name: Template name (e.g., "my-fragment" or "public.header")

        Returns:
            Template UUID
        """

        client = await self._get_client()

        # GET /templates/{name} returns template details including UUID
        template_response = await client.get_template(name)
        return template_response.template.id

    async def _resolve_content(self, ref: ContentReference) -> str:
        """Resolve a content reference by fetching from the content API.

        Args:
            ref: Content reference to resolve

        Returns:
            Content text or JSON
        """
        from uuid import UUID

        client = await self._get_client()

        # Content can be referenced by UUID or by searching
        # For now, we only support UUID references
        if not self._is_valid_uuid(ref.identifier):
            raise ValueError(
                f"Invalid content identifier: '{ref.identifier}'. "
                "Content must be referenced by UUID."
            )

        # Fetch content part using server_client
        content_part = await client.get_content_part(UUID(ref.identifier))

        # Return appropriate content based on type
        if ref.is_json:
            # User requested JSON format (@json)
            if content_part.content_structured is not None:
                return json.dumps(content_part.content_structured, indent=2)
            elif content_part.content_text is not None:
                # Fall back to text if structured not available
                return content_part.content_text
            else:
                raise ValueError(
                    f"Content {ref.identifier} has no text or structured content"
                )
        else:
            # User requested text format (@text)
            if content_part.content_text is not None:
                return content_part.content_text
            elif content_part.content_structured is not None:
                # Fall back to JSON if text not available
                return json.dumps(content_part.content_structured, indent=2)
            else:
                raise ValueError(
                    f"Content {ref.identifier} has no text or structured content"
                )

    async def _resolve_schema(self, ref: SchemaReference) -> str:
        """Resolve a schema reference by fetching from the schema API.

        Args:
            ref: Schema reference to resolve

        Returns:
            Schema definition string
        """
        client = await self._get_client()

        # Fetch schema using server_client (supports both name and UUID)
        schema_response = await client.get_schema(ref.identifier)

        return schema_response.schema_definition

    def _create_error_from_exception(
        self, pattern: str, exception: Exception
    ) -> InjectionError:
        """Create an InjectionError from an exception.

        Args:
            pattern: The pattern that failed
            exception: The exception that was raised

        Returns:
            InjectionError with appropriate type and message
        """
        # Handle server_client exceptions
        if isinstance(exception, NotFoundError):
            return InjectionError(
                pattern=pattern,
                error_type="not_found",
                message=f"Fragment or content not found: {exception!s}",
            )
        elif isinstance(exception, ForbiddenError):
            return InjectionError(
                pattern=pattern,
                error_type="permission_denied",
                message=f"Permission denied: {exception!s}",
            )
        elif isinstance(exception, ValidationError):
            return InjectionError(
                pattern=pattern,
                error_type="validation_error",
                message=f"Validation error: {exception!s}",
            )
        elif isinstance(exception, AuthenticationError):
            return InjectionError(
                pattern=pattern,
                error_type="authentication_error",
                message=f"Authentication failed: {exception!s}",
            )
        elif isinstance(exception, ServerError):
            return InjectionError(
                pattern=pattern,
                error_type="api_error",
                message=f"Server error: {exception!s}",
            )
        elif isinstance(exception, ValueError):
            return InjectionError(
                pattern=pattern,
                error_type="invalid_reference",
                message=str(exception),
            )
        else:
            return InjectionError(
                pattern=pattern,
                error_type="unknown_error",
                message=f"Unexpected error: {exception!s}",
            )
