"""API Communication Service for LLM requests.

Handles API communication with LLM endpoints using the provider system.
Integrates with ProviderRegistry for unified OpenAI/Anthropic/Azure/Custom support.
"""

import asyncio
import json
import logging
import os
import time
from datetime import datetime
from typing import Any, Dict, List, Optional, AsyncIterator

from .profile_manager import LLMProfile
from .providers.registry import ProviderRegistry, create_config_from_profile
from .providers.models import (
    ProviderType,
    UnifiedResponse,
    StreamingResponse,
    UsageInfo,
    TextContent,
    ToolUseContent,
    TextDelta,
    ToolCallDelta,
)
from .providers.transformers import ToolCallAccumulator

logger = logging.getLogger(__name__)


class APICommunicationService:
    """Pure API communication service for LLM requests.

    Handles request formatting, response parsing,
    and error handling for LLM API communication using provider system.
    """

    def __init__(self, config, raw_conversations_dir, profile: LLMProfile):
        """Initialize API communication service.

        Args:
            config: Configuration manager for API settings
            raw_conversations_dir: Directory for raw interaction logs
            profile: LLM profile with configuration (resolves env vars -> config -> defaults)
        """
        self.config = config
        self.raw_conversations_dir = raw_conversations_dir

        # Initialize from profile (resolves env vars through profile's getter methods)
        self.update_from_profile(profile)

        # Streaming (from config, not profile-specific)
        self.enable_streaming = config.get("core.llm.enable_streaming", False)

        # Session tracking for raw log linking
        self.current_session_id: Optional[str] = None

        # Provider-based communication
        self._provider = None  # Initialized in initialize()
        self._provider_error = None  # Error message if provider init failed
        self._use_provider = True  # Always use provider system (no legacy fallback)
        self._initialized = False

        # Request cancellation support
        self.current_request_task = None
        self.cancel_requested = False

        # Token usage tracking
        self.last_token_usage = {}

        # Native tool calling support
        self.last_tool_calls = []  # Tool calls from last response
        self.last_stop_reason = ""  # Stop reason from last response

        # Tool accumulator mode (LEGACY vs EXPLICIT)
        self._use_explicit_accumulation = config.get(
            "core.llm.use_explicit_tool_accumulation", False
        )

        # Tool accumulator for streaming (provider system)
        self._tool_accumulator: Optional[ToolCallAccumulator] = None

        # Resource monitoring and statistics
        self._connection_stats = {
            'total_requests': 0,
            'failed_requests': 0,
            'last_activity': None,
            'session_creation_time': None,
            'connection_errors': 0
        }

        # Store profile for provider creation
        self._profile = profile

        logger.info(f"API service initialized for provider={profile.provider} (profile: {profile.name})")

    def set_session_id(self, session_id: str) -> None:
        """Set current session ID for raw log linking.

        Args:
            session_id: Session identifier from conversation logger
        """
        self.current_session_id = session_id
        logger.debug(f"API service session ID set to: {session_id}")

    def update_from_profile(self, profile: LLMProfile) -> None:
        """Update API settings from a profile.

        Uses profile getter methods that resolve env var -> config -> default.

        Args:
            profile: LLMProfile with configuration
        """
        old_model = getattr(self, 'model', None)

        # Get all values from profile (getter methods resolve env vars)
        self.model = profile.get_model()
        self.temperature = profile.get_temperature()
        self.max_tokens = profile.get_max_tokens()
        self.timeout = profile.get_timeout()

        # Update stored profile for provider recreation
        self._profile = profile

        # Always use provider system (all profiles must have provider field)
        logger.info(f"Profile '{profile.name}' uses provider system: {profile.provider}")

        if old_model:
            logger.info(
                f"API service updated: {old_model} -> {self.model}"
            )

    def cancel_current_request(self):
        """Cancel the current API request."""
        self.cancel_requested = True
        if self.current_request_task and not self.current_request_task.done():
            logger.info("Cancelling current API request")
            self.current_request_task.cancel()
        else:
            logger.debug("No active API request to cancel")

    async def initialize(self) -> bool:
        """Initialize provider with proper error handling.

        Returns:
            True if provider initialized successfully, False if there was
            an error (app can still run, user can fix via /profile)
        """
        if self._initialized:
            return self._provider is not None

        # Initialize provider system (handles its own errors gracefully)
        await self._initialize_provider()

        self._initialized = True
        self._connection_stats['last_activity'] = time.time()

        if self._provider:
            logger.info("API service initialized successfully")
            return True
        else:
            logger.warning(
                f"API service initialized with errors - provider not available. "
                f"Use /profile to fix configuration."
            )
            return False


    async def _initialize_provider(self) -> None:
        """Initialize provider from profile configuration.

        Handles validation errors gracefully - logs warning and allows app
        to continue so user can fix the profile via /profile command.
        """
        try:
            # Convert profile to provider config
            provider_config = create_config_from_profile(
                self._profile.to_dict(),
            )

            # Get or create provider instance
            self._provider = await ProviderRegistry.get_provider(provider_config)

            logger.info(
                f"Provider initialized: {self._provider.provider_name} "
                f"(model={self._provider.model})"
            )

        except ValueError as e:
            # Config validation failed (e.g., wrong API key format for provider)
            self._provider = None
            self._provider_error = str(e)
            logger.warning(
                f"Profile '{self._profile.name}' has configuration error: {e}. "
                f"Use /profile to fix the configuration."
            )

        except Exception as e:
            # Other errors (network, etc.)
            self._provider = None
            self._provider_error = str(e)
            logger.warning(
                f"Failed to initialize provider for profile '{self._profile.name}': {e}. "
                f"Use /profile to check configuration."
            )

    async def shutdown(self):
        """Shutdown provider resources with comprehensive error handling."""
        if not self._initialized:
            return

        try:
            logger.info("Starting API communication service shutdown")

            # Cancel any active requests
            if self.current_request_task and not self.current_request_task.done():
                logger.info("Cancelling active request during shutdown")
                self.current_request_task.cancel()
                try:
                    await self.current_request_task
                except asyncio.CancelledError:
                    pass
                except Exception as e:
                    logger.error(f"Error cancelling request during shutdown: {e}")

            # Note: We don't shutdown provider here as it's a singleton
            # ProviderRegistry.shutdown_all() should be called at application shutdown

            self._initialized = False
            logger.info("API communication service shutdown complete")

        except Exception as e:
            logger.error(f"Error during API service shutdown: {e}")
            # Don't raise - we want cleanup to complete even if there are errors





    async def call_llm(self, conversation_history: List[Dict[str, str]],
                       max_history: int = None, streaming_callback=None,
                       tools: List[Dict[str, Any]] = None) -> str:
        """Make API call to LLM with conversation history and robust error handling.

        Args:
            conversation_history: List of conversation messages
            max_history: Maximum number of messages to send (optional)
            streaming_callback: Optional callback for streaming content chunks
            tools: Optional list of tool definitions for native function calling

        Returns:
            LLM response content

        Raises:
            RuntimeError: If provider not initialized
            asyncio.CancelledError: If request was cancelled
            Exception: For API communication errors
        """
        # Reset cancellation flag
        self.cancel_requested = False

        # Store streaming callback for use in handlers
        self.streaming_callback = streaming_callback

        # Update activity tracking
        self._connection_stats['total_requests'] += 1
        self._connection_stats['last_activity'] = time.time()

        # Prepare messages for API
        messages = self._prepare_messages(conversation_history, max_history)

        # Use provider system (always enabled)
        if not self._provider:
            # Provide helpful error with the actual configuration issue
            if self._provider_error:
                raise RuntimeError(
                    f"LLM provider not available due to configuration error:\n"
                    f"{self._provider_error}\n\n"
                    f"Use /profile to fix the configuration."
                )
            else:
                raise RuntimeError("Provider not initialized. Call initialize() first.")

        try:
            if self.enable_streaming:
                return await self._call_provider_stream(messages, tools, streaming_callback)
            else:
                return await self._call_provider_nonstream(messages, tools)
        except Exception as e:
            logger.error(f"Provider call failed: {e}")
            self._connection_stats['failed_requests'] += 1
            raise

        # Validate session state
        if not None or None.closed:
            raise RuntimeError("HTTP session is not available - failed to initialize")

        # Build request payload using adapter
        payload = None.format_request(
            messages=messages,
            model=self.model,
            temperature=self.temperature,
            stream=self.enable_streaming,
            max_tokens=int(self.max_tokens) if self.max_tokens else None,
            tools=tools  # Native function calling (OpenAI/Anthropic format)
        )

        # Execute request with cancellation support and comprehensive error handling
        self.current_request_task = asyncio.create_task(
            self._execute_request_with_error_handling(payload)
        )

        try:
            return await self._monitor_request()
        except asyncio.CancelledError:
            # Log cancellation to raw logs
            self._log_raw_interaction(payload, cancelled=True)
            raise asyncio.CancelledError("API request cancelled by user")
        except Exception as e:
            self._connection_stats['failed_requests'] += 1
            raise

    async def _call_provider_nonstream(
        self,
        messages: List[Dict[str, Any]],
        tools: Optional[List[Dict[str, Any]]] = None,
    ) -> str:
        """Make non-streaming API call using provider system.

        Args:
            messages: Conversation messages
            tools: Optional tool definitions

        Returns:
            Response content as string

        Raises:
            Exception: If provider call fails
        """
        logger.debug(f"Provider non-streaming call (model={self.model})")

        # Call provider
        response: UnifiedResponse = await self._provider.call(
            messages=messages,
            tools=tools,
        )

        # Extract token usage
        self.last_token_usage = {
            "prompt_tokens": response.usage.prompt_tokens,
            "completion_tokens": response.usage.completion_tokens,
            "total_tokens": response.usage.total_tokens,
        }

        # Extract tool calls for backward compatibility
        self.last_tool_calls = response.get_tool_uses()
        self.last_stop_reason = response.finish_reason or "stop"

        # Extract text content
        content = response.get_text_content()

        logger.debug(
            f"Provider response received (tokens={response.usage.total_tokens}, "
            f"tool_calls={len(self.last_tool_calls)})"
        )

        return content

    async def _call_provider_stream(
        self,
        messages: List[Dict[str, Any]],
        tools: Optional[List[Dict[str, Any]]] = None,
        streaming_callback=None,
    ) -> str:
        """Make streaming API call using provider system.

        Args:
            messages: Conversation messages
            tools: Optional tool definitions
            streaming_callback: Optional callback for streaming chunks

        Returns:
            Complete response content as string

        Raises:
            Exception: If provider stream fails
        """
        logger.debug(f"Provider streaming call (model={self.model})")

        # Initialize tool accumulator with mode from config
        use_legacy = not self._use_explicit_accumulation
        self._tool_accumulator = ToolCallAccumulator(legacy_mode=use_legacy)

        content_parts = []
        final_usage = None
        accumulated_tools = []  # For EXPLICIT mode

        try:
            # Stream provider responses
            async for streaming_response in self._provider.stream(
                messages=messages,
                tools=tools,
            ):
                # Check for cancellation
                if self.cancel_requested:
                    raise asyncio.CancelledError("Streaming request cancelled")

                # Handle delta types
                delta = streaming_response.delta

                # Text content
                if isinstance(delta, TextDelta):
                    content_chunk = delta.content
                    content_parts.append(content_chunk)

                    # Call streaming callback if provided
                    if streaming_callback:
                        await streaming_callback(content_chunk)

                # Tool call delta
                elif isinstance(delta, ToolCallDelta):
                    # Accumulate tool calls
                    if delta.tool_call_id:
                        completed_tools = self._tool_accumulator.add_delta(
                            tool_call_id=delta.tool_call_id,
                            name=delta.tool_name,
                            arguments_delta=delta.tool_arguments_delta,
                        )

                        # EXPLICIT mode: add_delta returns completed tools immediately
                        if self._use_explicit_accumulation and completed_tools:
                            accumulated_tools.extend(completed_tools)
                            logger.debug(
                                f"EXPLICIT mode: {len(completed_tools)} tools completed "
                                f"({len(accumulated_tools)} total)"
                            )

                # Capture final usage
                if streaming_response.is_final and streaming_response.usage:
                    final_usage = streaming_response.usage

            # Combine content
            content = "".join(content_parts)

            # Set token usage
            if final_usage:
                self.last_token_usage = {
                    "prompt_tokens": final_usage.prompt_tokens,
                    "completion_tokens": final_usage.completion_tokens,
                    "total_tokens": final_usage.total_tokens,
                }
            else:
                # Fallback to zero usage if not provided
                self.last_token_usage = {
                    "prompt_tokens": 0,
                    "completion_tokens": 0,
                    "total_tokens": 0,
                }

            # Extract completed tool calls
            if self._use_explicit_accumulation:
                # EXPLICIT mode: tools already accumulated during streaming
                self.last_tool_calls = accumulated_tools
                logger.debug(f"EXPLICIT mode: {len(accumulated_tools)} tools accumulated")
            else:
                # LEGACY mode: batch retrieval at end
                self.last_tool_calls = self._tool_accumulator.get_completed_tools()
                logger.debug(
                    f"LEGACY mode: {len(self.last_tool_calls)} tools retrieved"
                )

            self.last_stop_reason = "stop"  # Default for streaming

            logger.debug(
                f"Provider streaming complete (tokens={self.last_token_usage.get('total_tokens', 0)}, "
                f"tool_calls={len(self.last_tool_calls)})"
            )

            return content

        finally:
            # Reset tool accumulator
            if self._tool_accumulator:
                self._tool_accumulator.reset()
    
    def _prepare_messages(self, conversation_history: List[Any], 
                         max_history: Optional[int]) -> List[Dict[str, str]]:
        """Prepare conversation messages for API request.
        
        Args:
            conversation_history: Raw conversation history
            max_history: Maximum messages to include
            
        Returns:
            List of formatted messages for API
        """
        # Apply history limit if specified
        if max_history:
            recent_messages = conversation_history[-max_history:]
        else:
            recent_messages = conversation_history
        
        # Format messages for API
        messages = []
        for msg in recent_messages:
            # Handle both ConversationMessage objects and dicts
            if hasattr(msg, 'role'):
                role, content = msg.role, msg.content
            else:
                role, content = msg["role"], msg["content"]
            
            messages.append({
                "role": role,
                "content": content
            })
        
        return messages






    def get_last_token_usage(self) -> Dict[str, int]:
        """Get token usage from last API call.

        Returns:
            Dictionary with token counts
        """
        return self.last_token_usage

    async def health_check(self) -> Dict[str, Any]:
        """Perform health check on provider service.

        Returns:
            Dictionary with health status information
        """
        health_status = {
            'healthy': True,
            'checks': {},
            'timestamp': time.time()
        }

        # Check if provider is initialized
        provider_healthy = self._provider is not None and self._initialized
        health_status['checks']['provider'] = {
            'healthy': provider_healthy,
            'provider_name': self._provider.provider_name if self._provider else None,
            'error': self._provider_error if not provider_healthy else None,
        }
        if not provider_healthy:
            health_status['healthy'] = False

        return health_status

    def get_provider_error(self) -> Optional[str]:
        """Get provider error message if initialization failed.

        Returns:
            Error message string or None if no error
        """
        return self._provider_error

    def is_provider_available(self) -> bool:
        """Check if provider is available for API calls.

        Returns:
            True if provider is initialized and ready
        """
        return self._provider is not None

    async def reinitialize_provider(self, profile: LLMProfile) -> bool:
        """Reinitialize provider with a new profile.

        Used when user changes profile via /profile command.

        Args:
            profile: New LLM profile configuration

        Returns:
            True if provider initialized successfully
        """
        # Update stored profile
        self._profile = profile
        self._provider_error = None

        # Update settings from new profile
        self.update_from_profile(profile)

        # Reinitialize provider
        await self._initialize_provider()

        if self._provider:
            logger.info(f"Provider reinitialized for profile '{profile.name}'")
            return True
        else:
            logger.warning(
                f"Provider reinitialization failed for profile '{profile.name}': "
                f"{self._provider_error}"
            )
            return False

    def get_api_stats(self) -> Dict[str, Any]:
        """Get API communication statistics.

        Returns:
            Dictionary with API statistics
        """
        return {
            "model": self.model,
            "temperature": self.temperature,
            "timeout": self.timeout,
            "streaming_enabled": self.enable_streaming,
            "connection_stats": self.get_connection_stats()
        }

    def get_api_stats(self) -> Dict[str, Any]:
        """Get API communication statistics.

        Returns:
            Dictionary with API statistics
        """
        return {
            "api_url": self.api_url,
            "model": self.model,
            "temperature": self.temperature,
            "timeout": self.timeout,
            "streaming_enabled": self.enable_streaming,
            "session_active": None is not None,
            "connection_stats": self.get_connection_stats()
        }
    def has_pending_tool_calls(self) -> bool:
        """Check if there are pending tool calls from last response.

        Returns:
            True if there are pending tool calls to execute
        """
        return bool(self.last_tool_calls)
