import os
import json
from datetime import date
from typing import List, Dict, Any, Optional, Tuple
from .base import BaseLLM
from health.utils.logging_config import setup_logger

logger = setup_logger(__name__)

# Base system prompt for health assistant
BUTLER_SYSTEM_INSTRUCTION_BASE = """You are Butler, a personal health assistant for tracking and analyzing health data.

You help the user manage:
- Sleep quality and patterns (from Garmin watch)
- Exercise data: steps, activities, heart rate
- Diet logs: meals, supplements, fasting modes (OMAD, PSMF)
- Diet logs: meals, supplements, fasting modes (OMAD, PSMF)
- Body feelings and symptoms

CRITICAL ANTI-HALLUCINATION RULES:
1. **TOOL-FIRST POLICY**: When user asks about CURRENT or SPECIFIC health data (e.g., "how did I sleep last night?", "what was my steps yesterday?"), you MUST call the appropriate tool FIRST. NEVER answer based on conversation history or memory.
2. **NO DATA INVENTION**: If a tool returns "No data found" or null values, YOU MUST SAY "I cannot find data for this date" or "No data available". Do NOT guess, estimate, or invent numbers.
3. **VISION LIMITATIONS**: When analyzing images, if unclear or unreadable, say "I cannot identify the food in this image". Do NOT guess ingredients you do not see.
4. **EXPLICIT UNCERTAINTY**: If you're unsure whether data exists, call the tool to check. Say "Let me check your data" rather than guessing.

Guidelines:
- Be concise and data-driven
- When user wants to log something (diet, supplement, etc), use the appropriate tool
- Provide actionable insights based ONLY on retrieved data
- Support both English and Chinese

CONVERSATIONAL CONTEXT HANDLING:
- When user gives vague confirmation replies like "好的，可以记录" / "ok, log it" / "yes, record that":
  1. FIRST check the conversation history to see what they were discussing
  2. If they mentioned food/diet in recent messages, extract the details and call `log_diet` with:
     - description: the food items they mentioned
     - meal_type: infer from context (breakfast/lunch/dinner/snack)
     - target_date: today unless specified otherwise
  3. If you cannot extract clear details, politely ask: "请问要记录什么内容？可以告诉我具体吃了什么吗？"
- NEVER return empty responses. If unsure what to do, ask a clarifying question.

WEB SEARCH USAGE:
- You have access to real-time web search via `search_web`.
- USE IT WHEN:
  1. User asks for "latest" research, news, or biohacking trends.
  2. You need to verify external facts (e.g. "benefits of berberine").
  3. User specifically asks to check online.
- DO NOT USE IT WHEN:
  1. User asks for PERSONAL data (steps, sleep). Use `health_read` tools.

Available data sources:
- Garmin health metrics (sleep, steps, heart rate, etc.)
- Manual logs (diet, supplements, fasting, body feelings)
- Real-time Web Search
- Obsidian daily notes (can be synced)

5. **ACTION COMMANDS**: If the user asks to "sync", "update", or "fetch" data, you MUST call the corresponding tool (e.g., `sync_garmin`). Do NOT say "done" without calling the tool. If the tool call fails or you cannot find the tool, report the error.

IMPORTANT: Today's date is {today}. When logging data without a specified date, use today's date."""


from health.utils.time_utils import get_current_time_str

def get_system_instruction() -> str:
    """Get system instruction with current date and health profile."""
    # Use UTC+8 string directly
    base_prompt = BUTLER_SYSTEM_INSTRUCTION_BASE.format(today=get_current_time_str())
    
    # Try to load custom health profile
    try:
        health_md_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "health.md")
        if os.path.exists(health_md_path):
            with open(health_md_path, "r", encoding="utf-8") as f:
                health_profile = f.read()
            return f"{base_prompt}\n\n=== USER HEALTH PROTOCOLS ===\n{health_profile}"
    except Exception as e:
        logger.error(f"Failed to load health.md: {e}")
    
    return base_prompt


class GeminiLLM(BaseLLM):
    """Gemini Implementation via OpenAI-compatible API (for local proxy support)."""

    def __init__(self, system_instruction: Optional[str] = None):
        api_key = os.environ.get("GEMINI_API_KEY")
        base_url = os.environ.get("GEMINI_BASE_URL")

        if not api_key:
            logger.error("GEMINI_API_KEY not found in environment.")
            raise ValueError("GEMINI_API_KEY not found")

        # Determine system instruction (default to Health/Butler if not provided)
        self.system_instruction = system_instruction if system_instruction else get_system_instruction()

        # Use OpenAI client for OpenAI-compatible proxies
        try:
            from openai import OpenAI
        except ImportError:
            logger.error("openai package not found. Install with: pip install openai")
            raise

        # Initialize OpenAI client pointing to local proxy or OpenRouter
        if base_url:
            # Check if using OpenRouter (requires special headers)
            is_openrouter = "openrouter.ai" in base_url.lower()

            if is_openrouter:
                # OpenRouter requires HTTP-Referer and X-Title headers
                headers = {
                    "HTTP-Referer": "https://github.com/your-username/butler",
                    "X-Title": "Butler Health Assistant"
                }

                # Add Google API key if available to bypass rate limits
                google_key = os.environ.get("GOOGLE_API_KEY")
                if google_key:
                    headers["X-Google-API-Key"] = google_key
                    logger.info("Using Google API key for OpenRouter")

                self.client = OpenAI(
                    api_key=api_key,
                    base_url=f"{base_url}/v1",
                    default_headers=headers
                )
                logger.info(f"Using OpenRouter: {base_url}")
            else:
                self.client = OpenAI(api_key=api_key, base_url=f"{base_url}/v1")
                logger.info(f"Using proxy: {base_url}")
        else:
            # Fallback: use Google's generativeai for direct connection
            import google.generativeai as genai
            genai.configure(api_key=api_key)
            self.client = None
            self.genai_model = genai.GenerativeModel(
                os.environ.get("GEMINI_MODEL", "gemini-3-flash"),
                system_instruction=self.system_instruction
            )
            logger.info("Using direct Google API")

        self.model_name = os.environ.get("GEMINI_MODEL", "gemini-3-flash")
        self.use_proxy = bool(base_url)

        logger.info(f"Initialized Gemini model: {self.model_name}")

    def get_model_name(self) -> str:
        return self.model_name

    def generate_response(
        self,
        message: str,
        context: List[Dict[str, Any]],
        tools: Optional[List[Dict[str, Any]]] = None,
        images: Optional[List[Dict[str, Any]]] = None
    ) -> Tuple[str, Optional[List[Dict]]]:
        """
        Generate response with optional tool calling and images.

        Args:
            message: Text prompt
            context: Chat history
            tools: Tool definitions
            images: List of dicts {'mime_type': str, 'data': bytes} (for direct API)

        Returns:
            Tuple of (response_text, tool_calls)
        """
        try:
            if self.use_proxy:
                return self._generate_with_openai(message, context, tools, images)
            else:
                return self._generate_with_google(message, context, tools, images)

        except Exception as e:
            logger.error(f"Gemini generation error: {e}")
            return f"[Gemini Error] {str(e)}", None

        return text_response, tool_calls

    def _retry_with_backoff(self, func, *args, **kwargs):
        """Simple retry logic with exponential backoff."""
        import time
        import random
        
        max_retries = 3
        base_delay = 2
        
        for attempt in range(max_retries + 1):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                error_str = str(e)
                # Check for known transient errors
                is_transient = (
                    "Token acquisition timeout" in error_str or
                    "limit" in error_str.lower() or 
                    "busy" in error_str.lower() or
                    "500" in error_str or
                    "503" in error_str or
                    "deadlock" in error_str.lower()
                )
                
                if not is_transient or attempt == max_retries:
                    raise e
                
                delay = base_delay * (2 ** attempt) + random.uniform(0, 1)
                logger.warning(f"API Request failed ({error_str}). Retrying in {delay:.2f}s... (Attempt {attempt+1}/{max_retries})")
                time.sleep(delay)

    def _generate_with_openai(
        self,
        message: str,
        context: List[Dict[str, Any]],
        tools: Optional[List[Dict[str, Any]]],
        images: Optional[List[Dict[str, Any]]] = None
    ) -> Tuple[str, Optional[List[Dict]]]:
        """Generate using OpenAI-compatible API (for proxy)."""
        # Refresh system instruction with current date/time on every request.
        # This prevents the LLM from using a stale date when the bot runs across midnight.
        current_system_instruction = get_system_instruction()
        messages = [{"role": "system", "content": current_system_instruction}]
        messages.extend(context)

        if images:
            import base64

            content_list = []
            if message:
                content_list.append({"type": "text", "text": message})

            for img in images:
                # Convert bytes to base64
                b64_data = base64.b64encode(img["data"]).decode("utf-8")
                mime_type = img["mime_type"]

                content_list.append({
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:{mime_type};base64,{b64_data}"
                    }
                })

            messages.append({"role": "user", "content": content_list})
        else:
            if message:
                messages.append({"role": "user", "content": message})

        # Call API
        kwargs = {
            "model": self.model_name,
            "messages": messages,
        }

        if tools:
            kwargs["tools"] = tools
            # NOTE: Removed tool_choice parameter - some proxies don't handle it correctly
            # kwargs["tool_choice"] = "auto"

        try:
            # DEBUG: Log payload for diagnosis
            import json
            debug_mode = os.environ.get("DEBUG_GEMINI_PAYLOAD", "").lower() in ["true", "1", "yes"]
            if debug_mode:
                payload_preview = json.dumps(kwargs, default=str, ensure_ascii=False)
                logger.info(f"[DEBUG] Payload size: {len(payload_preview)} chars")
                logger.info(f"[DEBUG] Model: {kwargs.get('model')}")
                logger.info(f"[DEBUG] Messages: {len(kwargs.get('messages', []))}")
                logger.info(f"[DEBUG] Tools: {len(kwargs.get('tools', []))}")

                # Save full payload for inspection
                debug_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "debug_request.json")
                with open(debug_file, "w") as f:
                    json.dump(kwargs, f, indent=2, default=str, ensure_ascii=False)
                logger.info(f"[DEBUG] Full payload saved to {debug_file}")

            # Use retry wrapper
            response = self._retry_with_backoff(self.client.chat.completions.create, **kwargs)

            # PROXY WORKAROUND: If response is empty and we provided tools, retry without tools
            message_obj = response.choices[0].message
            text_response = message_obj.content or ""
            has_tool_calls = hasattr(message_obj, 'tool_calls') and message_obj.tool_calls

            if tools and not text_response.strip() and not has_tool_calls:
                logger.warning("Empty response with tools - retrying WITHOUT tools as workaround")
                kwargs_no_tools = {
                    "model": self.model_name,
                    "messages": messages,
                }
                try:
                    response = self._retry_with_backoff(self.client.chat.completions.create, **kwargs_no_tools)
                    message_obj = response.choices[0].message
                    text_response = message_obj.content or ""
                    logger.info(f"Retry without tools: got {len(text_response)} chars")
                except Exception as retry_error:
                    logger.error(f"Retry without tools also failed: {retry_error}")

        except Exception as api_error:
            logger.error(f"OpenAI API Request Failed: {api_error}")
            
            # DEBUG: Dump payload to file for inspection
            try:
                debug_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "debug_failed_payload.json")
                with open(debug_file, "w") as f:
                    import json
                    json.dump(kwargs, f, indent=2, default=str)
                logger.error(f"Dumped failed payload to {debug_file}")
            except Exception as dump_err:
                logger.error(f"Failed to dump payload: {dump_err}")

            # If the API response itself is malformed (e.g. proxy returns bad JSON),
            # the openai client raises a JSONDecodeError (which has 'Extra data' etc).
            error_str = str(api_error)
            if len(error_str) > 200:
                # Check for HTML content in error (common with proxy auth failures)
                if "<html" in error_str.lower() or "<!doctype" in error_str.lower():
                     error_str = error_str[:100] + "... [HTML Error Truncated]"
                else:
                     error_str = error_str[:200] + "... [Truncated]"
            
            return f"[Gemini API Error] {error_str}", None

        try:
            # Extract response
            message_obj = response.choices[0].message
            text_response = message_obj.content or ""

            # DEBUG: Log response details
            debug_mode = os.environ.get("DEBUG_GEMINI_PAYLOAD", "").lower() in ["true", "1", "yes"]
            if debug_mode:
                logger.info(f"[DEBUG] Response text length: {len(text_response)}")
                logger.info(f"[DEBUG] Response has tool_calls: {hasattr(message_obj, 'tool_calls') and message_obj.tool_calls}")
                logger.info(f"[DEBUG] Response preview: {text_response[:200] if text_response else '(empty)'}")

                # Save response for inspection
                debug_file = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "debug_response.json")
                with open(debug_file, "w") as f:
                    import json
                    json.dump({
                        "text": text_response,
                        "has_tool_calls": hasattr(message_obj, 'tool_calls') and bool(message_obj.tool_calls),
                        "tool_calls_count": len(message_obj.tool_calls) if hasattr(message_obj, 'tool_calls') and message_obj.tool_calls else 0,
                        "finish_reason": response.choices[0].finish_reason if hasattr(response.choices[0], 'finish_reason') else None,
                    }, f, indent=2, ensure_ascii=False)
                logger.info(f"[DEBUG] Response details saved to {debug_file}")

            # Extract tool calls
            tool_calls = None
            if hasattr(message_obj, 'tool_calls') and message_obj.tool_calls:
                tool_calls = []
                for tc in message_obj.tool_calls:
                    # Ensure arguments is a string before parsing
                    args_str = tc.function.arguments
                    if not isinstance(args_str, str):
                        logger.warning(f"Tool arguments not a string: {type(args_str)}")
                        args_str = str(args_str)
                    
                    tool_calls.append({
                        "name": tc.function.name,
                        "args": self._safe_parse_json(args_str)
                    })
                    
            return text_response, tool_calls
            
        except Exception as parse_error:
            logger.error(f"Error parsing OpenAI response object: {parse_error}")
            return f"[Gemini Response Parse Error] {str(parse_error)}", None

        return text_response, tool_calls

    def _safe_parse_json(self, json_str: str) -> Dict[str, Any]:
        """Parse JSON with fallback for common LLM formatting errors."""
        if not json_str:
            return {}
            
        try:
            return json.loads(json_str)
        except Exception as e:
            # Log the problematic string for debugging
            logger.warning(f"JSON parse error: {e}. String: {json_str[:100]}...")
            
            # Try to decode just the first valid JSON object using raw_decode
            try:
                decoder = json.JSONDecoder()
                obj, _ = decoder.raw_decode(json_str)
                logger.info("Successfully recovered JSON using raw_decode")
                return obj
            except Exception:
                pass

            # Try finding the first '{'
            try:
                start_idx = json_str.find("{")
                if start_idx != -1:
                    decoder = json.JSONDecoder()
                    obj, _ = decoder.raw_decode(json_str[start_idx:])
                    logger.info("Successfully recovered JSON by skipping prefix")
                    return obj
            except Exception:
                pass
            
            logger.error(f"Failed to recover JSON arguments. Returning empty dict.")
            return {}

    def _generate_with_google(
        self,
        message: str,
        context: List[Dict[str, Any]],
        tools: Optional[List[Dict[str, Any]]],
        images: Optional[List[Dict[str, Any]]] = None
    ) -> Tuple[str, Optional[List[Dict]]]:
        """Generate using Google's generativeai (direct connection)."""
        import google.generativeai as genai

        # Convert context to Gemini format
        history = []
        for msg in context:
            role = "user" if msg["role"] == "user" else "model"
            history.append({"role": role, "parts": [msg["content"]]})

        # Convert tools to Gemini format
        gemini_tools = None
        if tools:
            gemini_tools = self._convert_tools_to_gemini(tools)

        chat = self.genai_model.start_chat(history=history)
        
        # Prepare content parts (Text + Images)
        content_parts = [message]
        if images:
            logger.info(f"Adding {len(images)} images to request")
            for img in images:
                content_parts.append({
                    "mime_type": img["mime_type"],
                    "data": img["data"] 
                })

        # Send message
        try:
            if gemini_tools:
                response = chat.send_message(
                    content_parts,
                    tools=gemini_tools,
                    tool_config={'function_calling_config': 'AUTO'}
                )
            else:
                response = chat.send_message(content_parts)
        except Exception as e:
            # Handle cases where image upload fails or model rejects it
            logger.error(f"Gemini send_message failed (with images={bool(images)}): {e}")
            return f"[Error] Failed to generate response: {e}", None

        # Extract tool calls
        tool_calls = None
        if response.candidates and response.candidates[0].content.parts:
            for part in response.candidates[0].content.parts:
                if hasattr(part, 'function_call') and part.function_call:
                    if tool_calls is None:
                        tool_calls = []
                    fc = part.function_call
                    tool_calls.append({
                        "name": fc.name,
                        "args": dict(fc.args)
                    })

        # Get text (when there are function calls, response.text might fail)
        try:
            text_response = response.text
        except Exception as e:
            # This is normal when response only contains function calls
            logger.debug(f"No text in response (likely only function calls): {e}")
            text_response = ""

        return text_response, tool_calls

    def _convert_tools_to_gemini(self, openai_tools: List[Dict[str, Any]]) -> List:
        """Convert OpenAI-style tools to Gemini format (for direct Google API)."""
        import google.generativeai as genai

        gemini_functions = []
        for tool in openai_tools:
            if tool.get("type") == "function":
                func_def = tool["function"]
                gemini_functions.append(
                    genai.protos.FunctionDeclaration(
                        name=func_def["name"],
                        description=func_def["description"],
                        parameters=genai.protos.Schema(
                            type=genai.protos.Type.OBJECT,
                            properties={
                                k: genai.protos.Schema(
                                    type=self._convert_type(v.get("type", "string")),
                                    description=v.get("description", "")
                                )
                                for k, v in func_def["parameters"]["properties"].items()
                            },
                            required=func_def["parameters"].get("required", [])
                        )
                    )
                )

        if gemini_functions:
            return [genai.protos.Tool(function_declarations=gemini_functions)]
        return None

    def _convert_type(self, openai_type: str):
        """Convert OpenAI type to Gemini type."""
        import google.generativeai as genai

        type_mapping = {
            "string": genai.protos.Type.STRING,
            "number": genai.protos.Type.NUMBER,
            "integer": genai.protos.Type.INTEGER,
            "boolean": genai.protos.Type.BOOLEAN,
            "object": genai.protos.Type.OBJECT,
            "array": genai.protos.Type.ARRAY
        }
        return type_mapping.get(openai_type.lower(), genai.protos.Type.STRING)
