"""Base agent class with LangChain integration."""

from typing import Any, Callable, Optional, Dict
import logging

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableConfig, RunnableSerializable

from thetable_poc.config import LLMProvider, get_provider
from ..participant import Participant, ParticipantConfig

logger = logging.getLogger(__name__)


def _fuzzy_match_candidate(selected: str, candidates: list[str]) -> str | None:
    """Fuzzy match candidate name with flexible matching.

    Args:
        selected: Selected name from LLM
        candidates: List of available candidate names

    Returns:
        Matched candidate name or None if no match
    """
    selected_lower = selected.lower()

    # 1. Exact matching
    for c in candidates:
        if c.lower() == selected_lower:
            return c

    # 2. Contains matching (bidirectional)
    for c in candidates:
        if c.lower() in selected_lower or selected_lower in c.lower():
            return c

    # 3. Normalized matching (remove dots and spaces)
    # This handles cases like "mryong" → "Mr.Yong"
    selected_normalized = selected_lower.replace('.', '').replace(' ', '')
    for c in candidates:
        c_normalized = c.lower().replace('.', '').replace(' ', '')
        if selected_normalized == c_normalized or \
           selected_normalized in c_normalized or \
           c_normalized in selected_normalized:
            return c

    # 4. First word matching (Sarah Lee → Sarah)
    selected_first = selected_lower.split()[0] if ' ' in selected_lower else selected_lower
    for c in candidates:
        if c.lower().startswith(selected_first):
            return c

    return None


def _get_recent_speakers(messages: list, lookback: int = 10) -> set[str]:
    """Get unique speakers from recent messages.

    Args:
        messages: List of messages
        lookback: Number of recent messages to check

    Returns:
        Set of speaker names
    """
    speakers = set()
    for msg in messages[-lookback:]:
        if hasattr(msg, 'name') and msg.name:
            speakers.add(msg.name)
    return speakers


def _get_last_speaker(context: dict[str, Any]) -> str | None:
    """Get the last speaker from context.

    Args:
        context: Context dictionary with recent_messages

    Returns:
        Name of last speaker or None
    """
    recent_messages = context.get("recent_messages", [])
    if recent_messages:
        last_msg = recent_messages[-1]
        if hasattr(last_msg, 'name'):
            return last_msg.name
    return None


class BaseAgent(Participant):
    """Base class for AI agents with LangChain integration."""

    def __init__(
        self,
        config: ParticipantConfig,
        provider: Optional[LLMProvider] = None,
        llm_settings: Optional[dict] = None,
    ):
        super().__init__(config)

        # Get global settings
        from thetable_poc.config import settings as global_settings
        llm_cfg = global_settings.llm
        
        # Configure LLM settings
        self.temperature = llm_cfg.temperature
        self.language = llm_cfg.language
        self.model_name = llm_cfg.model
        
        if llm_settings:
            self.temperature = llm_settings.get("temperature", self.temperature)
            self.language = llm_settings.get("language", self.language)
            self.model_name = llm_settings.get("model", self.model_name)
            
        self._system_prompt = self._build_system_prompt()
        self._llm = self._init_langchain_model()

    def _init_langchain_model(self) -> ChatOpenAI:
        """Initialize LangChain ChatModel."""
        # Using ChatOpenAI as the standard interface
        # This will work with OpenAI compatible endpoints (like Ollama, OpenRouter)
        from config import settings as global_settings
        
        api_key = global_settings.llm.openrouter_api_key or "dummy"
        base_url = global_settings.llm.openrouter_url

        return ChatOpenAI(
            model=self.model_name,
            temperature=self.temperature,
            openai_api_key=api_key,
            openai_api_base=base_url
        )

    def _build_system_prompt(self) -> str:
        """Build system prompt for this agent."""
        if self.config.prompt_template:
            return self.config.prompt_template

        # Language instruction
        language_instruction = ""
        if self.language == "ko":
            language_instruction = "\n\n**IMPORTANT: You must respond in Korean (한국어). Always use Korean for all your responses.**"
        elif self.language == "en":
            language_instruction = "\n\n**IMPORTANT: You must respond in English. Always use English for all your responses.**"

        return f"""You are {self.name}, a {self.role.value} in a project team meeting.

Your role: {self.role.value}
Your responsibilities: {self._get_role_description()}

Guidelines:
- Stay focused on your role and responsibilities
- Be concise and professional
- Collaborate with other team members

{language_instruction}
"""

    def _get_role_description(self) -> str:
        """Get description of this agent's role."""
        return f"Fulfill the responsibilities of a {self.role.value}"

    def generate_response(self, context: dict[str, Any]) -> str:
        """Generate response using LangChain Runnable.
        
        This method is compatible with LangGraph nodes.
        """
        chain = self._build_runnable()
        
        # The prompt builder expects 'context' to generate the user message part
        user_prompt_content = self._build_prompt(context)
        
        return chain.invoke({"user_message": user_prompt_content})

    def speak(self, context: dict[str, Any]) -> str:
        """Legacy method for backward compatibility."""
        return self.generate_response(context)

    def speak_stream(
        self,
        context: dict[str, Any],
        typing_callback: Optional[Callable[[str], None]] = None,
    ) -> str:
        """Generate streaming response using LangChain."""
        chain = self._build_runnable()
        user_prompt_content = self._build_prompt(context)
        
        full_response = []
        for chunk in chain.stream({"user_message": user_prompt_content}):
            if typing_callback:
                typing_callback(chunk)
            full_response.append(chunk)
            
        return "".join(full_response)

    def _build_runnable(self) -> RunnableSerializable:
        """Build the LangChain runnable."""
        prompt = ChatPromptTemplate.from_messages([
            ("system", self._system_prompt),
            ("human", "{user_message}"),
        ])
        
        return prompt | self._llm | StrOutputParser()

    def _build_prompt(self, context: dict[str, Any]) -> str:
        """Build the user message part of the prompt from context.
        
        Reuse existing logic to format the dynamic context into a string.
        """
        parts = []
        
        # Add participants info if available
        if "participants" in context and context["participants"]:
            parts.append("**Meeting Participants:**")
            for p in context["participants"]:
                role = p.get("role", "")
                name = p.get("name", "Unknown")
                is_human = not p.get("is_ai", True)
                human_marker = " [Human]" if is_human else ""
                parts.append(f"- {name} ({role}){human_marker}")
            parts.append("")  # Empty line after participants

        # Add current agenda items if available
        if "agenda_items" in context and context["agenda_items"]:
            parts.append("**Current Agenda Items:**")
            for item in context["agenda_items"]:
                # Handle both dict and AgendaItemInfo object
                if isinstance(item, dict):
                    title = item.get("title", "Unknown")
                    status = item.get("status", "pending")
                    owner = item.get("owner")
                    decision = item.get("decision")
                else:
                    title = item.title
                    status = item.status
                    owner = item.owner
                    decision = item.decision

                status_emoji = {
                    "pending": "⏳",
                    "in_progress": "🔄",
                    "decided": "✅",
                    "deferred": "⏸️"
                }.get(status, "❓")

                owner_text = f" (담당: {owner})" if owner else ""
                decision_text = f" → \"{decision}\"" if decision else ""
                parts.append(f"- {status_emoji} {title}{owner_text}{decision_text}")
            parts.append("")  # Empty line after agenda

        # Simple default prompt construction
        if "task" in context:
            parts.append(f"Current Task: {context['task']}")
            
        if "phase" in context:
            parts.append(f"Current Phase: {context['phase']}")
            
        if "recent_messages" in context:
            parts.append("\nRecent conversation:")
            human_messages = []
            for msg in context["recent_messages"]:
                # Handle both dict (old) and BaseMessage (new)
                if isinstance(msg, dict):
                    sender = msg.get('sender', 'Unknown')
                    content = msg.get('message', '')
                    parts.append(f"{sender}: {content}")
                elif hasattr(msg, 'content'):  # BaseMessage
                    sender = msg.name or msg.type
                    content = msg.content
                    parts.append(f"{sender}: {content}")
                    # Track human messages for special attention
                    if msg.type == "human":
                        human_messages.append(f"{sender}: {content}")
            
            # Highlight human participant opinions
            if human_messages:
                parts.append("\n⚠️ IMPORTANT - Human participant's opinion (MUST acknowledge and respond to this):")
                for hm in human_messages:
                    parts.append(f"  → {hm}")

        if "user_message" in context and context.get("user_message"):
             parts.append(f"\nUser Input: {context['user_message']}")

        # Fallback/Default question
        if "question" in context:
            parts.append(f"\n{context['question']}")
            
        return "\n".join(parts)

    def select_next_speaker(
        self,
        context: dict[str, Any],
        candidates: list[str],
        prioritize: list[str] = None,
        llm: Optional[RunnableSerializable] = None,
    ) -> str:
        """Select the next speaker based on context.

        This is a special capability usually required by the Host/Moderator agent.
        Returns one name from the candidates list.

        Args:
            context: Context dictionary with conversation history
            candidates: List of available speaker names
            prioritize: List of speakers who should be prioritized (e.g., mentioned participants)
            llm: Optional LLM instance to use (e.g. for using a faster/cheaper model)

        Returns:
            Name of selected speaker or 'FINISH'
        """

        candidates_str = ", ".join(candidates)

        # Identify the last speaker from recent messages
        last_speaker = None
        recent_messages = context.get("recent_messages", [])
        if recent_messages:
            last_msg = recent_messages[-1]
            if hasattr(last_msg, 'name'):
                last_speaker = last_msg.name

        # 1단계: 종료 신호 감지 (프롬프트 개선)
        closing_keywords = ["마치겠습니다", "마무리", "수고하셨습니다", "수고 많으셨습니다",
                           "회의를 종료", "종료하겠습니다", "감사합니다", "이상입니다"]
        has_closing_signal = False
        if recent_messages:
            for msg in recent_messages[-3:]:  # 최근 3개 메시지 체크
                if hasattr(msg, 'content') and msg.content:
                    if any(keyword in msg.content for keyword in closing_keywords):
                        has_closing_signal = True
                        break

        closing_note = ""
        if has_closing_signal:
            closing_note = """
⚠️ **MEETING CLOSING DETECTED:**
A closing statement has already been made in recent messages.
You MUST return 'FINISH' unless there is an urgent unresolved issue or pending mention.
DO NOT select another speaker for additional closing remarks.
"""

        # Identify human participants for context-aware selection
        human_participants = []
        for name in candidates:
            if "Mr." in name or "Ms." in name or "Yong" in name.lower():
                human_participants.append(name)

        human_note = ""
        if human_participants:
            human_names = ", ".join(human_participants)
            human_note = f"""
**Human Participant Note:**
{human_names} is a human participant. Only select them when:
- Their specific expertise or opinion is needed for the current topic
- You explicitly want to ask them a question
- The discussion directly affects their area of responsibility
"""

        last_speaker_note = ""
        if last_speaker:
            last_speaker_note = f"""
**IMPORTANT - Last Speaker:** {last_speaker}
DO NOT select {last_speaker} again - they just spoke. Choose someone else.
If {last_speaker} asked "다른 분들" or "other team members" for opinions, select a DIFFERENT person.
"""

        # Build mention priority note
        pending = context.get("pending_mentions", [])
        mention_note = ""
        if pending:
            mention_note = f"""
⚠️ **CRITICAL - PENDING MENTIONS:**
The following participants were @mentioned but haven't responded yet:
{', '.join(pending)}

**YOU MUST select one of them FIRST unless there's a compelling reason not to.**
Ignoring mentioned participants is disrespectful and breaks meeting flow.
"""

        # Build agenda note for better speaker selection
        agenda_items = context.get("agenda_items", [])
        agenda_note = ""
        if agenda_items:
            in_progress_items = [item for item in agenda_items
                                if (isinstance(item, dict) and item.get("status") == "in_progress")
                                or (hasattr(item, 'status') and item.status == "in_progress")]

            if in_progress_items:
                # Get owners from in-progress items
                owners = []
                for item in in_progress_items:
                    owner = item.get("owner") if isinstance(item, dict) else getattr(item, 'owner', None)
                    if owner:
                        owners.append(owner)

                if owners:
                    agenda_note = f"""
📋 **CURRENT AGENDA CONTEXT:**
The following agenda items are in progress:
{', '.join(set(owners))} are responsible for current agenda items.
Consider prioritizing these participants when selecting speakers.
"""

        # Inject time context after closing_note
        time_context = context.get("time_context", "")

        selection_prompt = f"""You are the moderator of this meeting.
Based on the conversation context, who should speak next?

Candidates: {candidates_str}
{last_speaker_note}{mention_note}{agenda_note}{human_note}{closing_note}{time_context}
**Rules:**
1. NEVER select the same person who just spoke.
2. If someone was @mentioned and hasn't responded, SELECT THEM FIRST (see PENDING MENTIONS above).
3. If a specific person was directly asked a question, select them.
4. If the last speaker asked others for their opinion, select someone who hasn't spoken recently.
5. If the topic is technical and Tech Lead hasn't spoken recently, consider them.
6. If the topic is about status/schedule and PM hasn't spoken recently, consider them.
7. Select 'FINISH' when:
   - ALL mentioned participants have responded, AND
   - The discussion has reached a natural conclusion (closing statements like "마치겠습니다", "마무리", "수고하셨습니다"), AND
   - Key stakeholders have shared their opinions.
   - **If you see MEETING CLOSING DETECTED above, return FINISH immediately unless pending mentions exist.**
8. Return ONLY the name of the selected candidate (or 'FINISH'). No explanation.

Current Context:
{self._build_prompt(context)}
"""

        # Create a simple chain for selection
        prompt = ChatPromptTemplate.from_messages([
            ("system", self._system_prompt),
            ("human", selection_prompt),
        ])

        chain = prompt | (llm or self._llm) | StrOutputParser()

        # Invoke and clean up response
        response = chain.invoke({})
        selected = response.strip().replace(".", "")

        # FINISH validation with multiple conditions
        if "FINISH" in selected.upper():
            # Condition 1: pending mentions must be empty
            if pending:
                logger.info(f"FINISH blocked: pending mentions exist: {pending}")
                logger.info(f"Selecting {pending[0]} instead")
                return pending[0]

            # Time urgency override: if time context exists and >= 80%, skip other validations
            time_context = context.get("time_context", "")
            if time_context and "TIME CRITICAL" in time_context:
                logger.info("FINISH approved: TIME CRITICAL - skipping other validations")
                return "FINISH"

            # Condition 2: all AI participants must have spoken at least once
            speaker_counts = context.get("speaker_counts", {})
            participants = context.get("participants", [])

            # Find AI participants who haven't spoken yet
            silent_ai = [
                p["name"] for p in participants
                if p.get("is_ai", True) and speaker_counts.get(p["name"], 0) == 0
            ]

            if silent_ai:
                logger.info(f"FINISH blocked: {silent_ai} haven't spoken yet")
                logger.info(f"Selecting {silent_ai[0]} to ensure minimum discussion")
                return silent_ai[0]

            # Condition 3: minimum turns check (optional - 2 rounds per AI participant)
            ai_participants = [p for p in participants if p.get("is_ai", True)]
            total_turns = sum(speaker_counts.values())
            min_turns = len(ai_participants) * 2

            if total_turns < min_turns:
                # Not enough discussion yet, select least spoken participant
                least_spoken = min(
                    [p["name"] for p in ai_participants],
                    key=lambda name: speaker_counts.get(name, 0)
                )
                logger.info(f"FINISH blocked: only {total_turns}/{min_turns} turns completed")
                logger.info(f"Selecting {least_spoken} to continue discussion")
                return least_spoken

            # All conditions passed - allow FINISH
            logger.info("FINISH approved: all conditions met")
            return "FINISH"

        # Match selected speaker with fuzzy matching
        matched = _fuzzy_match_candidate(selected, candidates)
        if matched:
            # 2단계: last_speaker 재선택 차단
            if matched == last_speaker:
                logger.info(f"❌ Blocked: LLM selected last speaker '{matched}', forcing fallback")
                matched = None  # fallback 로직으로 이동
            else:
                return matched

        # === Improved Fallback Logic ===

        # 1st priority: pending mentions
        if pending:
            logger.info(f"Name matching failed, selecting from pending mentions: {pending[0]}")
            return pending[0]

        # 2nd priority: participants who haven't spoken recently
        recent_speakers = _get_recent_speakers(context.get("recent_messages", []))
        silent_candidates = [c for c in candidates if c not in recent_speakers]
        if silent_candidates:
            logger.info(f"Selecting from silent participants: {silent_candidates[0]}")
            return silent_candidates[0]

        # 3rd priority: random selection excluding last speaker
        last_speaker = _get_last_speaker(context)
        available = [c for c in candidates if c != last_speaker]
        if available:
            import random
            selected_random = random.choice(available)
            logger.info(f"Random selection (excluding last speaker): {selected_random}")
            return selected_random

        # Last resort: FINISH
        logger.warning("All fallback strategies exhausted, returning FINISH")
        return "FINISH"

