"""
LLM service.
Supports multiple providers: OpenRouter, OpenAI, Google Gemini, DeepSeek, Grok.
Kept separate from AnalysisService to avoid circular imports.
"""
import json
import os
import requests
from typing import Dict, Any, Optional, List
from enum import Enum

from app.utils.logger import get_logger
from app.config import APIKeys
from app.utils.config_loader import load_addon_config

logger = get_logger(__name__)


class LLMProvider(Enum):
    """Supported LLM providers"""
    OPENROUTER = "openrouter"
    OPENAI = "openai"
    GOOGLE = "google"
    DEEPSEEK = "deepseek"
    GROK = "grok"


# Provider configurations
PROVIDER_CONFIGS = {
    LLMProvider.OPENROUTER: {
        "base_url": "https://openrouter.ai/api/v1",
        "default_model": "openai/gpt-4o",
        "fallback_model": "openai/gpt-4o-mini",
    },
    LLMProvider.OPENAI: {
        "base_url": "https://api.openai.com/v1",
        "default_model": "gpt-4o",
        "fallback_model": "gpt-4o-mini",
    },
    LLMProvider.GOOGLE: {
        "base_url": "https://generativelanguage.googleapis.com/v1beta",
        "default_model": "gemini-1.5-flash",
        "fallback_model": "gemini-1.5-flash",
    },
    LLMProvider.DEEPSEEK: {
        "base_url": "https://api.deepseek.com/v1",
        "default_model": "deepseek-chat",
        "fallback_model": "deepseek-chat",
    },
    LLMProvider.GROK: {
        "base_url": "https://api.x.ai/v1",
        "default_model": "grok-beta",
        "fallback_model": "grok-beta",
    },
}


class LLMService:
    """LLM provider wrapper with multi-provider support."""

    def __init__(self, provider: str = None):
        """
        Initialize LLM service.
        
        Args:
            provider: Override the default provider (openrouter, openai, google, deepseek, grok)
        """
        self._provider_override = provider

    @property
    def provider(self) -> LLMProvider:
        """Get the active LLM provider."""
        if self._provider_override:
            try:
                return LLMProvider(self._provider_override.lower())
            except ValueError:
                pass
        
        # Check env/config for provider selection
        config = load_addon_config()
        provider_name = config.get('llm', {}).get('provider') or os.getenv('LLM_PROVIDER', '')
        
        if provider_name:
            try:
                selected = LLMProvider(provider_name.lower())
                # Verify this provider has an API key configured
                if self.get_api_key(selected):
                    return selected
                logger.warning(f"LLM_PROVIDER={provider_name} but no API key configured, auto-detecting...")
            except ValueError:
                pass
        
        # Auto-detect: find any provider with a configured API key
        # Priority: DeepSeek > Grok > OpenAI > Google > OpenRouter
        priority_order = [
            LLMProvider.DEEPSEEK,
            LLMProvider.GROK,
            LLMProvider.OPENAI,
            LLMProvider.GOOGLE,
            LLMProvider.OPENROUTER,
        ]
        
        for p in priority_order:
            if self.get_api_key(p):
                logger.info(f"Auto-detected LLM provider: {p.value}")
                return p
        
        # Fallback to OpenRouter (will fail later if no key)
        return LLMProvider.OPENROUTER

    def get_api_key(self, provider: LLMProvider = None) -> str:
        """Get API key for the specified provider."""
        p = provider or self.provider
        
        key_map = {
            LLMProvider.OPENROUTER: APIKeys.OPENROUTER_API_KEY,
            LLMProvider.OPENAI: APIKeys.OPENAI_API_KEY,
            LLMProvider.GOOGLE: APIKeys.GOOGLE_API_KEY,
            LLMProvider.DEEPSEEK: APIKeys.DEEPSEEK_API_KEY,
            LLMProvider.GROK: APIKeys.GROK_API_KEY,
        }
        return key_map.get(p, "") or ""

    def get_base_url(self, provider: LLMProvider = None) -> str:
        """Get base URL for the specified provider."""
        p = provider or self.provider
        config = load_addon_config()
        
        # Check for custom base URL in config
        provider_config = config.get(p.value, {})
        custom_url = provider_config.get('base_url') or os.getenv(f'{p.value.upper()}_BASE_URL', '').strip()
        
        if custom_url:
            return custom_url.rstrip('/')
        
        return PROVIDER_CONFIGS[p]["base_url"]

    def get_default_model(self, provider: LLMProvider = None) -> str:
        """Get default model for the specified provider."""
        p = provider or self.provider
        config = load_addon_config()
        
        provider_config = config.get(p.value, {})
        custom_model = provider_config.get('model') or os.getenv(f'{p.value.upper()}_MODEL', '').strip()
        
        if custom_model:
            return custom_model
        
        return PROVIDER_CONFIGS[p]["default_model"]

    # Legacy properties for backward compatibility
    @property
    def api_key(self):
        return self.get_api_key()

    @property
    def base_url(self):
        return self.get_base_url()

    def _call_openai_compatible(self, messages: list, model: str, temperature: float, 
                                 api_key: str, base_url: str, timeout: int,
                                 use_json_mode: bool = True) -> str:
        """Call OpenAI-compatible API (OpenAI, DeepSeek, Grok, OpenRouter)."""
        url = f"{base_url}/chat/completions"
        
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        }
        
        # OpenRouter specific headers
        if "openrouter" in base_url:
            headers["HTTP-Referer"] = "https://quantdinger.com"
            headers["X-Title"] = "QuantDinger Analysis"

        data = {
            "model": model,
            "messages": messages,
            "temperature": temperature,
        }
        
        if use_json_mode:
            data["response_format"] = {"type": "json_object"}

        response = requests.post(url, headers=headers, json=data, timeout=timeout)
        response.raise_for_status()
        
        result = response.json()
        if "choices" in result and len(result["choices"]) > 0:
            content = result["choices"][0]["message"]["content"]
            if not content:
                raise ValueError(f"Model {model} returned empty content")
            return content
        else:
            raise ValueError("API response is missing 'choices'")

    def _call_google_gemini(self, messages: list, model: str, temperature: float,
                           api_key: str, base_url: str, timeout: int) -> str:
        """Call Google Gemini API."""
        url = f"{base_url}/models/{model}:generateContent?key={api_key}"
        
        # Convert OpenAI message format to Gemini format
        contents = []
        system_instruction = None
        
        for msg in messages:
            role = msg["role"]
            content = msg["content"]
            
            if role == "system":
                system_instruction = content
            elif role == "user":
                contents.append({"role": "user", "parts": [{"text": content}]})
            elif role == "assistant":
                contents.append({"role": "model", "parts": [{"text": content}]})
        
        data = {
            "contents": contents,
            "generationConfig": {
                "temperature": temperature,
                "responseMimeType": "application/json",
            }
        }
        
        if system_instruction:
            data["systemInstruction"] = {"parts": [{"text": system_instruction}]}
        
        headers = {"Content-Type": "application/json"}
        
        response = requests.post(url, headers=headers, json=data, timeout=timeout)
        response.raise_for_status()
        
        result = response.json()
        if "candidates" in result and len(result["candidates"]) > 0:
            candidate = result["candidates"][0]
            if "content" in candidate and "parts" in candidate["content"]:
                text = candidate["content"]["parts"][0].get("text", "")
                if text:
                    return text
        
        raise ValueError("Gemini API response is missing content")

    def _normalize_model_for_provider(self, model: str, provider: LLMProvider) -> str:
        """
        Normalize model name for the target provider.
        
        Frontend may send OpenRouter-style model names (e.g., 'openai/gpt-4o').
        This converts them to the correct format for each provider.
        """
        if not model:
            return self.get_default_model(provider)
        
        model = model.strip()
        
        # If using OpenRouter, keep the original format
        if provider == LLMProvider.OPENROUTER:
            return model
        
        # For direct providers, extract the model name from OpenRouter format
        # e.g., 'openai/gpt-4o' -> 'gpt-4o'
        #       'google/gemini-1.5-flash' -> 'gemini-1.5-flash'
        #       'deepseek/deepseek-chat' -> 'deepseek-chat'
        #       'x-ai/grok-beta' -> 'grok-beta'
        
        if '/' in model:
            prefix, actual_model = model.split('/', 1)
            prefix_lower = prefix.lower()
            
            # Map OpenRouter prefixes to providers
            prefix_to_provider = {
                'openai': LLMProvider.OPENAI,
                'google': LLMProvider.GOOGLE,
                'deepseek': LLMProvider.DEEPSEEK,
                'x-ai': LLMProvider.GROK,
                'xai': LLMProvider.GROK,
            }
            
            # If the model prefix matches the current provider, use the extracted model name
            matched_provider = prefix_to_provider.get(prefix_lower)
            if matched_provider == provider:
                return actual_model
            
            # If model prefix doesn't match current provider, use provider's default model
            # This prevents sending 'gpt-4o' to DeepSeek, etc.
            logger.warning(f"Model '{model}' doesn't match provider '{provider.value}', using default model")
            return self.get_default_model(provider)
        
        # Model name without prefix - use as is
        return model

    def _detect_provider_from_model(self, model: str) -> Optional[LLMProvider]:
        """
        Detect which provider a model belongs to based on its name.
        Returns None if detection fails.
        """
        if not model or '/' not in model:
            return None
        
        prefix = model.split('/')[0].lower()
        
        prefix_to_provider = {
            'openai': LLMProvider.OPENAI,
            'google': LLMProvider.GOOGLE,
            'deepseek': LLMProvider.DEEPSEEK,
            'x-ai': LLMProvider.GROK,
            'xai': LLMProvider.GROK,
            'anthropic': LLMProvider.OPENROUTER,  # Anthropic only via OpenRouter
            'meta': LLMProvider.OPENROUTER,  # Meta/Llama only via OpenRouter
            'mistral': LLMProvider.OPENROUTER,  # Mistral only via OpenRouter
        }
        
        return prefix_to_provider.get(prefix)

    def call_llm_api(self, messages: list, model: str = None, temperature: float = 0.7, 
                     use_fallback: bool = True, provider: LLMProvider = None,
                     use_json_mode: bool = True) -> str:
        """
        Call LLM API with the specified or default provider.
        
        Args:
            messages: List of message dicts with 'role' and 'content'
            model: Model name (uses provider default if not specified). Supports OpenRouter format (e.g., 'openai/gpt-4o')
            temperature: Sampling temperature
            use_fallback: Whether to try fallback model on failure
            provider: Override the service's default provider
            use_json_mode: Whether to request JSON output format (default True for analysis, False for code generation)
        
        Returns:
            Generated text content
        
        Model Resolution Priority:
            1. If model is specified and matches a direct provider (openai/, google/, deepseek/, x-ai/),
               use that provider directly if its API key is configured
            2. Otherwise, use the configured LLM_PROVIDER with normalized model name
            3. Fall back to provider's default model if model name is incompatible
        """
        # Smart provider detection: if model specifies a provider and we have its API key, use it
        if model and not provider:
            detected_provider = self._detect_provider_from_model(model)
            if detected_provider and detected_provider != LLMProvider.OPENROUTER:
                # Check if we have API key for the detected provider
                if self.get_api_key(detected_provider):
                    provider = detected_provider
                    logger.debug(f"Auto-detected provider '{provider.value}' from model '{model}'")
        
        p = provider or self.provider
        api_key = self.get_api_key(p)
        
        if not api_key:
            raise ValueError(f"API key not configured for provider: {p.value}")
        
        base_url = self.get_base_url(p)
        
        # Normalize model name for the provider
        model = self._normalize_model_for_provider(model, p)
        
        config = load_addon_config()
        timeout = int(config.get(p.value, {}).get('timeout', 120))
        
        # Build model candidates
        models_to_try = [model]
        provider_default_model = PROVIDER_CONFIGS[p]["default_model"]
        if use_fallback:
            fallback = PROVIDER_CONFIGS[p].get("fallback_model")
            if fallback and fallback != model:
                models_to_try.append(fallback)
        
        last_error = None
        
        for current_model in models_to_try:
            try:
                if p == LLMProvider.GOOGLE:
                    return self._call_google_gemini(
                        messages, current_model, temperature,
                        api_key, base_url, timeout
                    )
                else:
                    # OpenAI-compatible providers
                    return self._call_openai_compatible(
                        messages, current_model, temperature,
                        api_key, base_url, timeout,
                        use_json_mode=use_json_mode
                    )
                    
            except requests.exceptions.HTTPError as e:
                error_detail = e.response.text if e.response else str(e)
                logger.error(f"{p.value} API HTTP error ({current_model}): {error_detail}")
                last_error = str(e)
                
                # Check for recoverable errors - try fallback model
                # 402: Payment required, 403: Forbidden (invalid key), 404: Model not found, 429: Rate limit
                if e.response and e.response.status_code in (402, 403, 404, 429):
                    logger.warning(f"{p.value} returned {e.response.status_code} for model {current_model}; trying fallback...")
                    continue
                
                if not use_fallback or current_model == models_to_try[-1]:
                    raise
                    
            except requests.exceptions.RequestException as e:
                logger.error(f"{p.value} API request error ({current_model}): {str(e)}")
                last_error = str(e)
                if not use_fallback or current_model == models_to_try[-1]:
                    raise
                    
            except ValueError as e:
                logger.warning(f"Model {current_model} returned invalid data: {str(e)}")
                last_error = str(e)
                if current_model == models_to_try[-1]:
                    raise
        
        error_msg = f"All model calls failed. Last error: {last_error}"
        logger.error(error_msg)
        raise Exception(error_msg)

    # Legacy method for backward compatibility
    def call_openrouter_api(self, messages: list, model: str = None, temperature: float = 0.7, use_fallback: bool = True) -> str:
        """Call LLM API (legacy method name for backward compatibility)."""
        return self.call_llm_api(messages, model, temperature, use_fallback)

    def safe_call_llm(self, system_prompt: str, user_prompt: str, default_structure: Dict[str, Any], 
                      model: str = None, provider: LLMProvider = None) -> Dict[str, Any]:
        """Safe LLM call with robust JSON parsing and fallback structure."""
        response_text = ""
        try:
            response_text = self.call_llm_api([
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt}
            ], model=model, provider=provider)
            
            # Strip markdown fences if present
            clean_text = response_text.strip()
            if clean_text.startswith("```"):
                first_newline = clean_text.find("\n")
                if first_newline != -1:
                    clean_text = clean_text[first_newline+1:]
                if clean_text.endswith("```"):
                    clean_text = clean_text[:-3]
            clean_text = clean_text.strip()
            
            # Parse JSON
            result = json.loads(clean_text)
            return result
        except json.JSONDecodeError:
            logger.error(f"JSON parse failed. Raw text: {response_text[:200] if response_text else 'N/A'}")
            
            # Try extracting JSON substring
            try:
                if response_text:
                    start = response_text.find('{')
                    end = response_text.rfind('}') + 1
                    if start >= 0 and end > start:
                        result = json.loads(response_text[start:end])
                        return result
            except:
                pass
            
            default_structure['report'] = f"Failed to parse analysis result JSON. Raw output (partial): {response_text[:500] if response_text else 'N/A'}"
            return default_structure
        except Exception as e:
            logger.error(f"LLM call failed: {str(e)}")
            default_structure['report'] = f"Analysis failed: {str(e)}"
            return default_structure

    @classmethod
    def get_available_providers(cls) -> List[Dict[str, Any]]:
        """Get list of available (configured) providers."""
        providers = []
        
        for p in LLMProvider:
            service = cls()
            api_key = service.get_api_key(p)
            providers.append({
                "id": p.value,
                "name": p.value.title(),
                "configured": bool(api_key),
                "default_model": PROVIDER_CONFIGS[p]["default_model"],
            })
        
        return providers
