"""
Claude responses processor for generating AI responses using Anthropic's Claude API.

Supports MCP tools, file uploads (images and documents), extended thinking,
and automatic citation extraction.
"""

import asyncio
import base64
import json
import logging
from collections.abc import AsyncGenerator
from pathlib import Path
from typing import Any, Final
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse

from anthropic import AsyncAnthropic, AsyncAnthropicFoundry

from appkit_assistant.backend.database.models import (
    MCPServer,
)
from appkit_assistant.backend.processors.mcp_mixin import MCPCapabilities
from appkit_assistant.backend.processors.processor_base import mcp_oauth_redirect_uri
from appkit_assistant.backend.processors.streaming_base import StreamingProcessorBase
from appkit_assistant.backend.schemas import (
    AIModel,
    Chunk,
    ChunkType,
    MCPAuthType,
    Message,
    MessageType,
)
from appkit_assistant.backend.services.citation_handler import ClaudeCitationHandler
from appkit_assistant.backend.services.file_validation import FileValidationService
from appkit_assistant.backend.services.system_prompt_builder import SystemPromptBuilder

logger = logging.getLogger(__name__)
default_oauth_redirect_uri: Final[str] = mcp_oauth_redirect_uri()

# Beta headers required for advanced features
ADVANCED_TOOL_USE_BETA_HEADER: Final[str] = "advanced-tool-use-2025-11-20"
MCP_BETA_HEADER: Final[str] = "mcp-client-2025-11-20"
FILES_BETA_HEADER: Final[str] = "files-api-2025-04-14"

# Extended thinking budget (fixed at 10k tokens)
THINKING_BUDGET_TOKENS: Final[int] = 10000


def _append_query_param(url: str, key: str, value: str) -> str:
    """Safely append a query parameter to a URL.

    Handles URLs with existing query parameters and fragments.
    """
    parsed = urlparse(url)
    params = parse_qs(parsed.query, keep_blank_values=True)
    params[key] = [value]
    new_query = urlencode(params, doseq=True)
    return urlunparse(parsed._replace(query=new_query))


class ClaudeResponsesProcessor(StreamingProcessorBase, MCPCapabilities):
    """Claude processor using the Messages API with MCP tools and file uploads.

    Supports both standard Anthropic API and Azure Foundry deployments.
    """

    def __init__(
        self,
        models: dict[str, AIModel],
        api_key: str | None = None,
        base_url: str | None = None,
        oauth_redirect_uri: str = default_oauth_redirect_uri,
        on_azure: bool = False,
    ) -> None:
        StreamingProcessorBase.__init__(self, models, "claude_responses")
        MCPCapabilities.__init__(self, oauth_redirect_uri, "claude_responses")

        self.api_key = api_key
        self.base_url = base_url
        self._on_azure = on_azure
        self.client: AsyncAnthropic | AsyncAnthropicFoundry | None = None

        if self.api_key:
            self.client = self._create_client()
        else:
            logger.warning("No Claude API key found. Processor will not work.")

        # Services
        self._file_validator = FileValidationService()
        self._citation_handler = ClaudeCitationHandler()
        self._system_prompt_builder = SystemPromptBuilder()

        # State
        self._uploaded_file_ids: list[str] = []
        self._current_tool_context: dict[str, Any] | None = None
        self._needs_text_separator: bool = False
        # Tool name tracking: tool_id -> (tool_name, server_label)
        self._tool_name_map: dict[str, tuple[str, str | None]] = {}
        # Warnings to display to the user (e.g. disabled tools)
        self._mcp_warnings: list[str] = []

    def _create_client(self) -> AsyncAnthropic | AsyncAnthropicFoundry | None:
        """Create Claude client based on configuration.

        Args:
            None

        Returns:
            AsyncAnthropic for standard API, AsyncAnthropicFoundry for Azure,
            or None if API key is not available.
        """
        if not self.api_key:
            return None

        if self._on_azure:
            # Use Azure Foundry client for Azure deployments
            if self.base_url:
                # Extract resource name from Azure URL if needed
                # Format: https://{resource}.services.ai.azure.com/anthropic/
                return AsyncAnthropicFoundry(
                    api_key=self.api_key,
                    base_url=f"{self.base_url}/anthropic",
                )
            return AsyncAnthropicFoundry(api_key=self.api_key)

        # Use standard Anthropic API client
        if self.base_url:
            return AsyncAnthropic(
                api_key=self.api_key,
                base_url=self.base_url,
            )
        return AsyncAnthropic(api_key=self.api_key)

    def get_supported_models(self) -> dict[str, AIModel]:
        """Return supported models if API key is available."""
        return self.models if self.api_key else {}

    async def process(
        self,
        messages: list[Message],
        model_id: str,
        files: list[str] | None = None,
        mcp_servers: list[MCPServer] | None = None,
        payload: dict[str, Any] | None = None,
        user_id: int | None = None,
        cancellation_token: asyncio.Event | None = None,
    ) -> AsyncGenerator[Chunk, None]:
        """Process messages using Claude Messages API with streaming."""
        if not self.client:
            raise ValueError("Claude Client not initialized.")

        if model_id not in self.models:
            msg = f"Model {model_id} not supported by Claude processor"
            raise ValueError(msg)

        model = self.models[model_id]
        self.current_user_id = user_id
        self.clear_pending_auth_servers()
        self._uploaded_file_ids = []
        self._tool_name_map.clear()  # Clear tool tracking for new request
        self._mcp_warnings = []  # Clear warnings for new request
        self._reset_statistics(model_id)

        try:
            # Upload files if provided
            file_content_blocks = []
            if files:
                file_content_blocks = await self._process_files(files)

            # Create the request
            stream = await self._create_messages_request(
                messages,
                model,
                mcp_servers,
                payload,
                user_id,
                file_content_blocks,
            )

            # Yield warnings if any (e.g. disabled tools)
            if self._mcp_warnings:
                for warning in self._mcp_warnings:
                    yield self.chunk_factory.text(f"⚠️ {warning}\n\n")

            try:
                # Process streaming events
                async with stream as response:
                    async for event in response:
                        if cancellation_token and cancellation_token.is_set():
                            logger.info("Processing cancelled by user")
                            break
                        chunk = self._handle_event(event)
                        if chunk:
                            yield chunk
            except Exception as e:
                error_msg = str(e)
                logger.error("Error during Claude response processing: %s", error_msg)
                # Only yield error chunk if NOT an auth error
                is_auth_related = (
                    self.auth_detector.is_auth_error(error_msg)
                    or self.pending_auth_servers
                )
                if not is_auth_related:
                    yield self.chunk_factory.error(
                        f"Ein Fehler ist aufgetreten: {error_msg}",
                        error_type=type(e).__name__,
                    )

            # Yield any pending auth requirements
            async for auth_chunk in self.yield_pending_auth_chunks():
                yield auth_chunk

        except Exception as e:
            logger.error("Critical error in Claude processor: %s", e)
            raise

    def _get_event_handlers(self) -> dict[str, Any]:
        """Get the event handler mapping for Claude API events."""
        return {
            "message_start": self._handle_message_start,
            "message_delta": self._handle_message_delta,
            "message_stop": self._handle_message_stop,
            "content_block_start": self._handle_content_block_start,
            "content_block_delta": self._handle_content_block_delta,
            "content_block_stop": self._handle_content_block_stop,
        }

    def _handle_message_start(self, _: Any) -> Chunk | None:
        """Handle message_start event."""
        return self.chunk_factory.lifecycle("created", {"stage": "created"})

    def _handle_message_delta(self, event: Any) -> Chunk | None:
        """Handle message_delta event (contains stop_reason)."""
        delta = getattr(event, "delta", None)
        if not delta:
            return None
        stop_reason = getattr(delta, "stop_reason", None)
        if not stop_reason:
            return None
        return self.chunk_factory.lifecycle(
            f"stop_reason: {stop_reason}", {"stop_reason": stop_reason}
        )

    def _handle_message_stop(self, event: Any) -> Chunk | None:
        """Handle message_stop event."""
        message = getattr(event, "message", None)
        if message:
            usage = getattr(message, "usage", None)
            if usage:
                self._update_statistics(
                    input_tokens=getattr(usage, "input_tokens", 0),
                    output_tokens=getattr(usage, "output_tokens", 0),
                )

        # Aggregate tool usage
        for tool_name, server_name in self._tool_name_map.values():
            self._update_statistics(tool_use=(tool_name, server_name))

        stats = self._get_statistics()
        logger.debug("Completion statistics: %s", stats)
        return self.chunk_factory.completion(
            status="response_complete",
            statistics=stats,
        )

    def _handle_content_block_start(self, event: Any) -> Chunk | None:
        """Handle content_block_start event."""
        content_block = getattr(event, "content_block", None)
        if not content_block:
            return None

        block_type = getattr(content_block, "type", None)

        # Use dispatch map to reduce branches
        handlers = {
            "text": self._handle_text_block_start,
            "thinking": self._handle_thinking_block_start,
            "tool_use": self._handle_tool_use_block_start,
            "mcp_tool_use": self._handle_mcp_tool_use_block_start,
            "mcp_tool_result": self._handle_mcp_tool_result_block_start,
        }

        handler = handlers.get(block_type)
        if handler:
            return handler(content_block)

        return None

    def _handle_text_block_start(self, _: Any) -> Chunk | None:
        """Handle start of text content block."""
        if not self._needs_text_separator:
            return None
        self._needs_text_separator = False
        return self.chunk_factory.text("\n\n", {"separator": "true"})

    def _handle_thinking_block_start(self, content_block: Any) -> Chunk:
        """Handle start of thinking content block."""
        thinking_id = getattr(content_block, "id", "thinking")
        self.current_reasoning_session = thinking_id
        self._needs_text_separator = True
        return self.chunk_factory.thinking(
            "Denke nach...", reasoning_id=thinking_id, status="starting"
        )

    def _handle_tool_use_common(
        self,
        tool_name: str,
        tool_id: str,
        server_label: str | None,
        tool_display_name: str,
    ) -> Chunk:
        """Common handler for tool use start."""
        self._current_tool_context = {
            "tool_name": tool_name,
            "tool_id": tool_id,
            "server_label": server_label,
        }
        # Store for result lookup
        self._tool_name_map[tool_id] = (tool_name, server_label)
        return self.chunk_factory.tool_call(
            tool_display_name,
            tool_name=tool_name,
            tool_id=tool_id,
            server_label=server_label,
            status="starting",
            reasoning_session=self.current_reasoning_session,
        )

    def _handle_tool_use_block_start(self, content_block: Any) -> Chunk:
        """Handle start of tool_use content block."""
        tool_name = getattr(content_block, "name", "unknown_tool")
        tool_id = getattr(content_block, "id", "unknown_id")
        return self._handle_tool_use_common(
            tool_name, tool_id, None, f"Benutze Werkzeug: {tool_name}"
        )

    def _handle_mcp_tool_use_block_start(self, content_block: Any) -> Chunk:
        """Handle start of mcp_tool_use content block."""
        tool_name = getattr(content_block, "name", "unknown_tool")
        tool_id = getattr(content_block, "id", "unknown_id")
        server_name = getattr(content_block, "server_name", "unknown_server")
        return self._handle_tool_use_common(
            tool_name,
            tool_id,
            server_name,
            f"Benutze Werkzeug: {server_name}.{tool_name}",
        )

    def _handle_mcp_tool_result_block_start(self, content_block: Any) -> Chunk:
        """Handle start of mcp_tool_result content block."""
        self._needs_text_separator = True
        tool_use_id = getattr(content_block, "tool_use_id", "unknown_id")
        is_error = bool(getattr(content_block, "is_error", False))
        content = getattr(content_block, "content", "")

        # Look up tool name and server from map
        tool_info = self._tool_name_map.get(tool_use_id, ("unknown_tool", None))
        tool_name, server_label = tool_info

        logger.debug(
            "MCP tool result - tool_use_id: %s, tool: %s, server: %s, is_error: %s",
            tool_use_id,
            tool_name,
            server_label,
            is_error,
        )

        result_text = self._extract_mcp_result_text(content)
        status = "error" if is_error else "completed"
        return self.chunk_factory.tool_result(
            result_text or ("Werkzeugfehler" if is_error else "Erfolgreich"),
            tool_id=tool_use_id,
            tool_name=tool_name,
            server_label=server_label,
            status=status,
            is_error=is_error,
            reasoning_session=self.current_reasoning_session,
        )

    def _extract_mcp_result_text(self, content: Any) -> str:
        """Extract text from MCP tool result content."""
        if not content:
            return ""
        if not isinstance(content, list):
            return str(content)
        parts = []
        for item in content:
            if isinstance(item, dict):
                parts.append(item.get("text", str(item)))
            else:
                parts.append(getattr(item, "text", str(item)))
        return "".join(parts)

    def _handle_content_block_delta(self, event: Any) -> Chunk | None:
        """Handle content_block_delta event."""
        delta = getattr(event, "delta", None)
        if not delta:
            return None

        delta_type = getattr(delta, "type", None)

        if delta_type == "text_delta":
            text = getattr(delta, "text", "")
            # Extract citations using the citation handler
            citations = self._citation_handler.extract_citations(delta)
            metadata: dict[str, Any] = {"delta": text}
            if citations:
                metadata["citations"] = json.dumps(
                    [self._citation_handler.to_dict(c) for c in citations]
                )
            return self.chunk_factory.text(text, metadata)

        if delta_type == "thinking_delta":
            thinking_text = getattr(delta, "thinking", "")
            return self.chunk_factory.thinking(
                thinking_text,
                reasoning_id=self.current_reasoning_session,
                status="in_progress",
                delta=thinking_text,
            )

        if delta_type == "input_json_delta":
            partial_json = getattr(delta, "partial_json", "")
            # Include tool context in streaming chunks
            tool_name = "unknown_tool"
            tool_id = "unknown_id"
            server_label = None
            if self._current_tool_context:
                tool_name = self._current_tool_context.get("tool_name", "unknown_tool")
                tool_id = self._current_tool_context.get("tool_id", "unknown_id")
                server_label = self._current_tool_context.get("server_label")
            return self.chunk_factory.tool_call(
                partial_json,
                tool_name=tool_name,
                tool_id=tool_id,
                server_label=server_label,
                status="arguments_streaming",
                reasoning_session=self.current_reasoning_session,
            )

        logger.debug("Unhandled delta type in stream: %s", delta_type)
        return None

    def _handle_content_block_stop(self, _: Any) -> Chunk | None:
        """Handle content_block_stop event."""
        if self.current_reasoning_session:
            reasoning_id = self.current_reasoning_session
            self.current_reasoning_session = None
            return self.chunk_factory.create(
                ChunkType.THINKING_RESULT,
                "beendet.",
                {"reasoning_id": reasoning_id, "status": "completed"},
            )

        if self._current_tool_context:
            ctx = self._current_tool_context
            self._current_tool_context = None
            return self.chunk_factory.tool_call(
                "Werkzeugargumente vollständig",
                tool_name=ctx.get("tool_name"),
                tool_id=ctx.get("tool_id"),
                server_label=ctx.get("server_label"),
                status="arguments_complete",
            )

        return None

    async def _process_files(self, files: list[str]) -> list[dict[str, Any]]:
        """Process and upload files for use in messages.

        Args:
            files: List of file paths to process

        Returns:
            List of content blocks for file attachments
        """
        content_blocks = []

        for file_path in files:
            is_valid, error_msg = self._file_validator.validate_file(file_path)
            if not is_valid:
                logger.warning("Skipping invalid file %s: %s", file_path, error_msg)
                continue

            try:
                content_block = await self._create_file_content_block(file_path)
                if content_block:
                    content_blocks.append(content_block)
            except Exception as e:
                logger.error("Failed to process file %s: %s", file_path, e)
                continue
            finally:
                # Clean up local file after upload attempt
                try:
                    Path(file_path).unlink(missing_ok=True)  #  noqa: ASYNC240
                except Exception as e:
                    logger.warning("Failed to delete local file %s: %s", file_path, e)

        return content_blocks

    async def _create_file_content_block(self, file_path: str) -> dict[str, Any] | None:
        """Create a content block for a file.

        For images, uses base64 encoding directly.
        For documents, uploads via Files API.

        Args:
            file_path: Path to the file

        Returns:
            Content block dict or None if failed
        """
        path = Path(file_path)

        # Read file content
        file_data = path.read_bytes()  # noqa: ASYNC240

        media_type = self._file_validator.get_media_type(file_path)

        if self._file_validator.is_image_file(file_path):
            # For images, use base64 encoding directly in the message
            base64_data = base64.standard_b64encode(file_data).decode("utf-8")
            return {
                "type": "image",
                "source": {
                    "type": "base64",
                    "media_type": media_type,
                    "data": base64_data,
                },
            }

        # For documents, upload via Files API and reference
        try:
            file_upload = await self.client.beta.files.upload(
                file=(path.name, file_data, media_type),
            )
            self._uploaded_file_ids.append(file_upload.id)
            return {
                "type": "document",
                "source": {
                    "type": "file",
                    "file_id": file_upload.id,
                },
                "citations": {"enabled": True},
            }
        except Exception as e:
            logger.error("Failed to upload file %s: %s", file_path, e)
            return None

    async def _create_messages_request(
        self,
        messages: list[Message],
        model: AIModel,
        mcp_servers: list[MCPServer] | None = None,
        payload: dict[str, Any] | None = None,
        user_id: int | None = None,
        file_content_blocks: list[dict[str, Any]] | None = None,
    ) -> Any:
        """Create a Claude Messages API request with streaming.

        Args:
            messages: List of conversation messages
            model: AI model configuration
            mcp_servers: Optional list of MCP servers for tools
            payload: Optional additional parameters
            user_id: Optional user ID for OAuth token lookup
            file_content_blocks: Optional list of file content blocks

        Returns:
            Streaming response object
        """
        # Configure MCP tools and servers
        tools, mcp_server_configs, mcp_prompt = await self._configure_mcp_tools(
            mcp_servers, user_id
        )

        # Convert messages to Claude format
        claude_messages = await self._convert_messages_to_claude_format(
            messages, file_content_blocks
        )

        # Build system prompt
        system_prompt = await self._system_prompt_builder.build(mcp_prompt)

        # Determine which beta features to enable
        # Advanced tool use is always enabled for better tool handling
        betas = [ADVANCED_TOOL_USE_BETA_HEADER]
        if mcp_servers:
            betas.append(MCP_BETA_HEADER)
        if file_content_blocks:
            betas.append(FILES_BETA_HEADER)

        # Build request parameters
        # max_tokens must be > thinking.budget_tokens when thinking is enabled
        params: dict[str, Any] = {
            "model": model.model,
            "max_tokens": 32000,
            "messages": claude_messages,
        }

        # Add system prompt
        if system_prompt:
            params["system"] = system_prompt

        # Add MCP servers if configured
        if mcp_server_configs:
            params["mcp_servers"] = mcp_server_configs

        # Add tools if configured
        if tools:
            params["tools"] = tools

        # Add extended thinking (always enabled with fixed budget)
        params["thinking"] = {
            "type": "enabled",
            "budget_tokens": THINKING_BUDGET_TOKENS,
        }

        # Add temperature
        if model.temperature is not None:
            params["temperature"] = model.temperature

        # Merge any additional payload (excluding internal keys)
        if payload:
            internal_keys = {"thread_uuid"}
            filtered_payload = {
                k: v for k, v in payload.items() if k not in internal_keys
            }
            params.update(filtered_payload)

        # Create streaming request with beta headers
        return self.client.beta.messages.stream(
            betas=betas,
            **params,
        )

    def _parse_mcp_headers(
        self,
        server: MCPServer,
    ) -> tuple[str | None, str]:
        """Parse MCP server headers and extract auth token + query params.

        Claude's MCP connector only supports authorization_token (Bearer token).
        Custom headers like X-Project-ID are converted to URL query parameters.

        Args:
            server: MCP server configuration

        Returns:
            Tuple of (authorization_token, url_suffix_with_query_params)
        """
        auth_token = None
        query_suffix = ""

        if not server.headers or server.headers == "{}":
            return auth_token, query_suffix

        try:
            headers = json.loads(server.headers)
        except json.JSONDecodeError as e:
            logger.error(
                "Failed to parse headers JSON for server %s: %s",
                server.name,
                e,
            )
            return auth_token, query_suffix

        # Extract Bearer token from Authorization header
        auth_header = headers.get("Authorization", "")
        if auth_header.startswith("Bearer "):
            auth_token = auth_header[7:]  # Remove "Bearer " prefix
            logger.debug(
                "Extracted Bearer token from headers for server %s", server.name
            )
        elif auth_header:
            auth_token = auth_header
            logger.debug("Using raw Authorization header for server %s", server.name)

        # Convert non-auth headers to URL query parameters
        query_params = []
        for key, value in headers.items():
            if key.lower() == "authorization":
                continue
            # Convert header name: X-Project-ID -> project_id
            param_name = key.lower()
            if param_name.startswith("x-"):
                param_name = param_name[2:]
            param_name = param_name.replace("-", "_")
            query_params.append(f"{param_name}={value}")

        if query_params:
            query_suffix = "&".join(query_params)
            logger.info(
                "Converted headers to query params for server %s: %s",
                server.name,
                query_params,
            )

        return auth_token, query_suffix

    async def _configure_mcp_tools(
        self,
        mcp_servers: list[MCPServer] | None,
        user_id: int | None = None,
    ) -> tuple[list[dict[str, Any]], list[dict[str, Any]], str]:
        """Configure MCP servers and tools for the request.

        Args:
            mcp_servers: List of MCP server configurations
            user_id: Optional user ID for OAuth token lookup

        Returns:
            Tuple of (tools list, mcp_servers list, concatenated prompts)
        """
        if not mcp_servers:
            return [], [], ""

        tools = []
        server_configs = []
        prompts = []

        for server in mcp_servers:
            # Parse headers to get auth token and query params
            auth_token, query_suffix = self._parse_mcp_headers(server)

            # Check if tool requires unsupported headers (converted to query suffix).
            # Claude currently does not support custom headers for MCP servers.
            if query_suffix:
                warning_msg = (
                    f"Der MCP-Server '{server.name}' wurde deaktiviert, "
                    "da er HTTP-Header benötigt, die von der Claude API "
                    "nicht unterstützt werden."
                )
                self._mcp_warnings.append(warning_msg)
                continue

            # Build MCP server configuration
            server_config: dict[str, Any] = {
                "type": "url",
                "name": server.name,
            }

            if auth_token:
                server_config["authorization_token"] = auth_token

            # Inject OAuth token if required (overrides static header token)
            if server.auth_type == MCPAuthType.OAUTH_DISCOVERY and user_id is not None:
                token = await self.get_valid_token(server, user_id)
                if token:
                    server_config["authorization_token"] = token.access_token
                    logger.debug("Injected OAuth token for server %s", server.name)
                else:
                    # Track for potential auth flow
                    self.add_pending_auth_server(server)
                    logger.debug(
                        "No valid token for OAuth server %s, auth may be required",
                        server.name,
                    )

            # Set the final URL, injecting x-user-id as query param if enabled
            url = server.url
            if server.inject_user_id and self.current_user_id is not None:
                url = _append_query_param(url, "x-user-id", str(self.current_user_id))
            server_config["url"] = url
            server_configs.append(server_config)

            # Add MCP toolset for this server
            tools.append(
                {
                    "type": "mcp_toolset",
                    "mcp_server_name": server.name,
                }
            )

            # Collect prompts
            if server.prompt:
                prompts.append(f"- {server.prompt}")

        prompt_string = "\n".join(prompts) if prompts else ""
        return tools, server_configs, prompt_string

    async def _convert_messages_to_claude_format(
        self,
        messages: list[Message],
        file_content_blocks: list[dict[str, Any]] | None = None,
    ) -> list[dict[str, Any]]:
        """Convert messages to Claude API format."""
        claude_messages = []
        last_idx = len(messages) - 1

        for i, msg in enumerate(messages):
            if msg.type == MessageType.SYSTEM:
                continue

            role = "user" if msg.type == MessageType.HUMAN else "assistant"
            content: list[dict[str, Any]] = []

            # Attach files to last user message
            if role == "user" and i == last_idx and file_content_blocks:
                content.extend(file_content_blocks)

            content.append({"type": "text", "text": msg.text})

            claude_messages.append(
                {
                    "role": role,
                    "content": content if len(content) > 1 else msg.text,
                }
            )

        return claude_messages
