from __future__ import annotations

import logging
import re
from collections import defaultdict
from datetime import datetime
from typing import Optional

logger = logging.getLogger(__name__)


class _TimeoutError(Exception):
    pass


class SpiritLLMLayer:
    """Three-tier LLM architecture for Spirit:

    Tier 1: Pure code (always available, zero cost) — 60% of functionality
    Tier 2: Embedding (existing capability, zero extra cost) — 25%
    Tier 3: LLM (minimal calls, on-demand) — 15%

    LLM calls have hard limits:
    - max_tokens_input = 500
    - max_tokens_output = 300
    - timeout = 10 seconds
    - fallback = template output

    Three-tier degradation for each capability:
    - Command parsing:  rule-based → embedding similarity → LLM
    - Merge summary:    key-point concatenation → embedding cluster → LLM
    - Report enhance:   template stats → embedding pattern → LLM
    """

    MAX_INPUT_TOKENS = 500
    MAX_OUTPUT_TOKENS = 300
    TIMEOUT_SECONDS = 10
    _TOKEN_CHARS_RATIO = 2.5

    def __init__(self, llm_client=None):
        self.llm_client = llm_client
        self.rate_limits = {
            'command_parse': 10,
            'merge_summary': 5,
            'report_summary': 2,
            'health_analysis': 3,
        }
        self._call_counts: dict[str, int] = defaultdict(int)
        self._total_calls = 0
        self._fallback_count = 0
        self._llm_call_counter: dict[str, int] = {}
        self._llm_call_date: str = datetime.now().strftime("%Y-%m-%d")

    def _check_llm_budget(self) -> bool:
        """Check if LLM budget allows another call."""
        try:
            from agent_memory.config.settings import settings
            limit = settings.get("cost.llm_daily_call_limit", 500)

            today = datetime.now().strftime("%Y-%m-%d")
            if self._llm_call_date != today:
                self._llm_call_counter = {}
                self._llm_call_date = today

            current_count = self._llm_call_counter.get("total", 0)
            if current_count >= limit:
                logger.warning(f"LLM daily call limit reached ({current_count}/{limit})")
                return False
            return True
        except Exception:
            return True

    def call_llm(self, purpose: str, prompt: str, max_tokens: int = 300) -> Optional[str]:
        if not self.llm_client:
            self._fallback_count += 1
            return None

        if not self._check_llm_budget():
            self._fallback_count += 1
            return None

        if self._call_counts[purpose] >= self.rate_limits.get(purpose, 100):
            logger.debug("SpiritLLMLayer: rate limited for '%s'", purpose)
            self._fallback_count += 1
            return None

        truncated_prompt = self._truncate_to_tokens(prompt, self.MAX_INPUT_TOKENS)

        try:
            messages = [{"role": "user", "content": truncated_prompt}]

            result = self._call_with_timeout(messages)

            if result is None:
                self._fallback_count += 1
                return None

            if not self._safety_filter(result, purpose):
                self._fallback_count += 1
                return None

            result = self._truncate_to_tokens(result, max_tokens)

            self._call_counts[purpose] += 1
            self._total_calls += 1
            self._llm_call_counter["total"] = self._llm_call_counter.get("total", 0) + 1

            return result

        except _TimeoutError:
            logger.warning("SpiritLLMLayer.call_llm(%s): timeout after %ds", purpose, self.TIMEOUT_SECONDS)
            self._fallback_count += 1
            return None
        except Exception as e:
            logger.debug("SpiritLLMLayer.call_llm(%s): %s", purpose, e)
            self._fallback_count += 1
            return None

    def _call_with_timeout(self, messages: list[dict]) -> Optional[str]:
        import threading
        result = [None]
        error = [None]

        def _worker():
            try:
                if hasattr(self.llm_client, 'chat'):
                    result[0] = self.llm_client.chat(messages)
                elif callable(self.llm_client):
                    prompt = "\n".join(m.get("content", "") for m in messages)
                    result[0] = self.llm_client(prompt)
                else:
                    error[0] = ValueError("Invalid llm_client")
            except Exception as e:
                error[0] = e

        thread = threading.Thread(target=_worker, daemon=True)
        thread.start()
        thread.join(timeout=self.TIMEOUT_SECONDS)

        if thread.is_alive():
            raise _TimeoutError(f"LLM call timed out after {self.TIMEOUT_SECONDS}s")

        if error[0]:
            raise error[0]

        return result[0]

    def parse_command_with_llm(self, command_text: str) -> Optional[dict]:
        prompt = (
            "Parse the following command into a structured intent.\n"
            "Return ONLY a JSON object with keys: intent, target, params\n"
            "Valid intents: consolidate, review, archive, find, report, correct\n\n"
            f"Command: {command_text}\n\n"
            "JSON:"
        )
        result = self.call_llm('command_parse', prompt, max_tokens=150)
        if result is None:
            return None

        return self._extract_json(result)

    def summarize_with_llm(self, content: str, purpose: str = 'merge_summary') -> Optional[str]:
        prompt = (
            "Summarize the following memory content concisely.\n"
            "Keep key facts, entities, and relationships.\n"
            "Output in the same language as input.\n\n"
            f"Content:\n{content}\n\n"
            "Summary:"
        )
        return self.call_llm(purpose, prompt, max_tokens=200)

    def generate_report_summary(self, report_content: str) -> Optional[str]:
        prompt = (
            "Generate a brief executive summary for this memory report.\n"
            "Highlight key patterns, anomalies, and action items.\n"
            "Output in the same language as input.\n\n"
            f"Report:\n{report_content}\n\n"
            "Summary:"
        )
        return self.call_llm('report_summary', prompt, max_tokens=200)

    def analyze_health_issue(self, issue_description: str) -> Optional[str]:
        prompt = (
            "Analyze this memory health issue and suggest a fix.\n"
            "Be specific and actionable.\n\n"
            f"Issue: {issue_description}\n\n"
            "Analysis:"
        )
        return self.call_llm('health_analysis', prompt, max_tokens=200)

    def enhance_command_parse(self, command_text: str, rule_result: Optional[dict] = None) -> dict:
        """Three-tier command parsing: code → embedding → LLM.

        Tier 1 (code): use rule_result if already parsed by CommandParser.
        Tier 2 (embedding): not applicable for command parsing, skip.
        Tier 3 (LLM): fall back to LLM when rule-based fails.
        """
        if rule_result and rule_result.get("intent") and rule_result["intent"] != "unknown":
            return rule_result

        llm_result = self.parse_command_with_llm(command_text)
        if llm_result:
            return llm_result

        return rule_result or {"intent": "unknown", "target": None, "params": {}}

    def enhance_merge_summary(self, memories: list[dict], template_summary: str = "") -> str:
        """Three-tier merge summarization: code → embedding → LLM.

        Tier 1 (code): concatenate key points from memories.
        Tier 2 (embedding): sentence-level extraction for long content.
        Tier 3 (LLM): use LLM for higher-quality summary.
        """
        memories_content = [m.get("content", "") for m in memories[:5] if m.get("content")]

        if not template_summary:
            key_points = []
            for content in memories_content:
                sentences = content.replace('。', '。\n').replace('！', '！\n').replace('？', '？\n').split('\n')
                for s in sentences:
                    s = s.strip()
                    if len(s) > 10:
                        key_points.append(s)
            template_summary = '；'.join(key_points[:5])

        if len(template_summary) > 500:
            key_sentences = []
            for content in memories_content:
                for sep in ['。', '！', '？', '.', '!', '?']:
                    idx = content.find(sep)
                    if idx > 0:
                        key_sentences.append(content[:idx+1])
                        break
            template_summary = '；'.join(key_sentences[:8])

        llm_summary = self.summarize_with_llm(template_summary, purpose='merge_summary')
        if llm_summary:
            return llm_summary

        return template_summary

    def enhance_report(self, report_content: str) -> str:
        """Three-tier report enhancement: code → embedding → LLM.

        Tier 1 (code): template-based stats report (already generated).
        Tier 2 (embedding): not directly applicable, skip.
        Tier 3 (LLM): add natural language insights.
        """
        llm_summary = self.generate_report_summary(report_content)
        if llm_summary:
            return report_content + "\n\n## AI 洞察\n" + llm_summary

        return report_content

    def _safety_filter(self, output: str, purpose: str) -> bool:
        """Apply safety filtering to LLM output.

        This is a pattern-based output filter, NOT a dual-LLM safety protocol.
        It checks for dangerous patterns in generated text.
        """
        if not output or not output.strip():
            return False

        if len(output) > self.MAX_OUTPUT_TOKENS * self._TOKEN_CHARS_RATIO * 2:
            return False

        _FORBIDDEN_PATTERNS = [
            r'import\s+os',
            r'subprocess',
            r'exec\s*\(',
            r'eval\s*\(',
            r'__import__',
            r'open\s*\(',
        ]
        for pattern in _FORBIDDEN_PATTERNS:
            if re.search(pattern, output):
                logger.warning("SpiritLLMLayer: forbidden pattern in output for '%s'", purpose)
                return False

        return True

    def _extract_json(self, text: str) -> Optional[dict]:
        json_match = re.search(r'\{[^{}]*\}', text, re.DOTALL)
        if not json_match:
            return None

        try:
            import json
            return json.loads(json_match.group())
        except (ValueError, TypeError):
            return None

    def _truncate_to_tokens(self, text: str, max_tokens: int) -> str:
        max_chars = int(max_tokens * self._TOKEN_CHARS_RATIO)
        if len(text) <= max_chars:
            return text
        return text[:max_chars]

    def get_usage_stats(self) -> dict:
        return {
            'total_calls': self._total_calls,
            'fallback_count': self._fallback_count,
            'by_purpose': dict(self._call_counts),
            'rate_limits': dict(self.rate_limits),
            'llm_available': self.llm_client is not None,
        }

    def reset_counters(self):
        self._call_counts.clear()
        self._total_calls = 0
        self._fallback_count = 0
