"""
Credit Card Recommender Engine
Provides grounded recommendations based on JSON card data.

Optionally uses Gemini (via google-generativeai) to generate more natural
chat responses, while still grounding all facts in the JSON data.
"""

import json
import os
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple

try:
    import google.generativeai as genai  # type: ignore
except ImportError:  # Library is optional; app still works without LLM
    genai = None


class CardRecommender:
    def __init__(self, cards_json_path: str = "cards.json"):
        """Initialize recommender with card data"""
        with open(cards_json_path, 'r', encoding='utf-8') as f:
            self.data = json.load(f)
        self.cards = self.data.get('cards', [])
    
    @staticmethod
    def normalize_text(text: str) -> str:
        """Normalize text for comparison"""
        return (text or "").lower().strip()
    
    @staticmethod
    def extract_income_inr(text: str) -> Optional[int]:
        """
        Extract monthly income from text
        Accepts: "1.5L", "150000", "2 lakh", "2l", "₹2,00,000", "2.5 lakhs"
        """
        t = CardRecommender.normalize_text(text).replace(',', ' ').replace('₹', ' ')
        
        # Match lakh/lac patterns
        lakh_match = re.search(r'(\d+(?:\.\d+)?)\s*(lakh|lakhs|lac|lacs|l)\b', t)
        if lakh_match:
            return round(float(lakh_match.group(1)) * 100000)
        
        # Match plain numbers (at least 2 digits)
        num_match = re.search(r'(\d{2,})', t)
        if num_match:
            return int(num_match.group(1))
        
        return None
    
    @staticmethod
    def detect_goal(text: str) -> str:
        """Detect user's primary card goal from their message"""
        t = CardRecommender.normalize_text(text)
        
        # High-signal intents
        if any(word in t for word in ['cashback', 'cash back', 'cb']):
            return "cashback"
        
        if any(word in t for word in ['travel', 'flight', 'hotel', 'miles', 'lounge']):
            return "travel"
        
        if any(word in t for word in ['forex', 'international spend', 'markup']):
            return "travel"
        
        if any(word in t for word in ['tata', 'neu', 'bigbasket', 'croma']):
            return "tata"
        
        if any(word in t for word in ['upi', 'rupay', 'ru pay']):
            return "rupay"
        
        # Generic requests
        if any(word in t for word in ['best', 'recommend', 'suggest']):
            return "unknown"
        
        return "unknown"
    
    def card_eligibility_pass(self, card: Dict, user: Dict) -> bool:
        """Check if user meets card eligibility (only checks income)"""
        monthly_income = user.get('monthlyIncome')
        if isinstance(monthly_income, (int, float)):
            min_income = card.get('eligibility', {}).get('min_monthly_income')
            if isinstance(min_income, (int, float)) and monthly_income < min_income:
                return False
        return True
    
    def score_card(self, card: Dict, user: Dict) -> float:
        """Score a card based on user preferences"""
        score = 0.0
        goal = user.get('goal', 'unknown')
        
        annual_fee = card.get('annual_fee', 0)
        reward_type = card.get('reward_model', {}).get('type')
        has_cashback = card.get('cashback_benefits', {}).get('included', False)
        lounge_dom = card.get('travel_benefits', {}).get('domestic_lounge', {}).get('included', False)
        lounge_intl = card.get('travel_benefits', {}).get('international_lounge', {}).get('included', False)
        forex = card.get('travel_benefits', {}).get('forex_markup_percent')
        
        # Goal-based scoring
        if goal == "cashback":
            if reward_type == "cashback" or has_cashback:
                score += 60
            if annual_fee <= 2000:
                score += 15
            if reward_type in ["points", "miles"]:
                score -= 10
        
        elif goal == "travel":
            if reward_type in ["miles", "points"]:
                score += 35
            if lounge_dom:
                score += 10
            if lounge_intl:
                score += 15
            if isinstance(forex, (int, float)):
                score += max(0, 15 - forex * 3)  # Lower markup = higher score
            if annual_fee > 10000:
                score += 8  # Premium travel cards
        
        elif goal == "tata":
            if 'tata' in self.normalize_text(card.get('name', '')):
                score += 60
            if has_cashback:
                score += 10
        
        elif goal == "rupay":
            if 'rupay' in self.normalize_text(card.get('network', '')):
                score += 60
            if has_cashback:
                score += 10
        
        else:  # unknown
            if has_cashback or reward_type == "cashback":
                score += 25
            if annual_fee <= 2000:
                score += 15
            if lounge_dom:
                score += 5
        
        # Income eligibility bonus
        monthly_income = user.get('monthlyIncome')
        if isinstance(monthly_income, (int, float)):
            min_income = card.get('eligibility', {}).get('min_monthly_income')
            if isinstance(min_income, (int, float)):
                ratio = monthly_income / min_income
                if ratio >= 1.5:
                    score += 6
                elif ratio >= 1.0:
                    score += 3
        
        return score
    
    def build_reasons(self, card: Dict, user: Dict) -> List[str]:
        """
        Build 3-6 grounded reasons for card recommendation.
        These are short factual snippets that the LLM can expand on.
        """
        reasons: List[str] = []
        goal = user.get('goal', 'unknown')

        reward_model = card.get('reward_model', {}) or {}
        travel = card.get('travel_benefits', {}) or {}
        eligibility = card.get('eligibility', {}) or {}
        best_for = card.get('best_for', []) or []

        annual_fee = card.get('annual_fee', 0) or 0
        fee_waiver = card.get('fee_waiver', {}) or {}
        min_income = eligibility.get('min_monthly_income')

        # Goal-specific highlights
        if goal == "cashback":
            rules = reward_model.get('cashback_rules', []) or []
            if rules:
                best_rule = max(rules, key=lambda r: r.get('rate_percent', 0))
                reasons.append(f"{best_rule['rate_percent']}% cashback on {best_rule['category']} spends")
            elif card.get('cashback_benefits', {}).get('included'):
                reasons.append("Cashback-focused rewards on everyday spends")

        elif goal == "travel":
            lounge_dom = travel.get('domestic_lounge', {}).get('included')
            lounge_intl = travel.get('international_lounge', {}).get('included')
            if lounge_dom:
                reasons.append("Access to domestic airport lounges")
            if lounge_intl:
                reasons.append("Access to international airport lounges")
            forex = travel.get('forex_markup_percent')
            if isinstance(forex, (int, float)):
                reasons.append(f"Forex markup around {forex}% on international spends")

        elif goal == "tata":
            rules = reward_model.get('cashback_rules', []) or []
            tata_rule = next(
                (r for r in rules if 'tata' in self.normalize_text(r.get('category', ''))),
                None,
            )
            if tata_rule:
                currency = reward_model.get('currency_name', 'rewards')
                reasons.append(f"{tata_rule['rate_percent']}% back in {currency} on Tata brands")

        elif goal == "rupay":
            reasons.append("RuPay network (UPI-ready if enabled by the issuer)")

        # Generic highlights that are useful for any goal
        reward_type = reward_model.get('type')
        if reward_type:
            reasons.append(f"Rewards structure focused on {reward_type}")

        reasons.append(f"Annual fee around ₹{annual_fee:,}")

        if fee_waiver.get('type') == 'spend_based':
            threshold = fee_waiver.get('threshold')
            period = fee_waiver.get('period', 'year')
            if isinstance(threshold, (int, float)):
                reasons.append(f"Annual fee can be waived on spends of about ₹{threshold:,} per {period}")

        if min_income:
            reasons.append(f"Designed for income from about ₹{min_income:,} per month")

        if best_for:
            reasons.append(f"Works well for: {', '.join(best_for[:2])}")

        # De-duplicate while preserving order
        seen = set()
        unique_reasons: List[str] = []
        for r in reasons:
            if r not in seen:
                seen.add(r)
                unique_reasons.append(r)

        # Cap to 6 concise reasons for readability
        return unique_reasons[:6]
    
    def recommend_one_card(self, user: Dict) -> Dict:
        """
        Recommend the single best card for user
        Returns: {card, reasons, note}
        """
        # Filter eligible cards
        eligible = [c for c in self.cards if self.card_eligibility_pass(c, user)]
        pool = eligible if eligible else self.cards
        
        # Score all cards
        best_card = None
        best_score = float('-inf')
        
        for card in pool:
            score = self.score_card(card, user)
            if score > best_score:
                best_score = score
                best_card = card
        
        # Build reasons
        reasons = self.build_reasons(best_card, user)
        
        # Add note if income was too low
        note = None
        monthly_income = user.get('monthlyIncome')
        if not eligible and isinstance(monthly_income, (int, float)):
            note = f"Note: based on ₹{monthly_income:,}/month, this is the closest fit from available cards."
        
        return {
            'card': best_card,
            'reasons': reasons,
            'note': note
        }


class ConversationSession:
    """Manages conversation state for asking max 2 questions"""
    
    def __init__(self):
        self.step = "idle"  # idle, ask_goal, ask_income
        self.asked = 0      # number of questions asked
        self.user = {}      # collected user preferences
        self.recommender = CardRecommender()
        self._llm_configured = False
        self.finished = False  # True after a recommendation is given

    def _build_llm_prompt(self, card: Dict, reasons: List[str], note: Optional[str]) -> str:
        """Build a grounded prompt for the LLM to create a natural-sounding reply."""
        card_lines = [
            f"Name: {card.get('name')}",
            f"Issuer: {card.get('issuer')}",
            f"Network: {card.get('network')}",
            f"Annual fee: ₹{card.get('annual_fee', 0):,}",
        ]
        if note:
            card_lines.append(f"Note: {note}")

        reasons_text = "\n".join(f"- {r}" for r in reasons)

        return (
            "You are a friendly, concise credit card assistant inside a chat UI.\n"
            "ONLY use the factual details given below about the recommended card. "
            "Do NOT invent benefits, fees, limits, or numbers that are not listed. Do not use emojis.\n\n"
            "Recommended card details:\n"
            f"{os.linesep.join(card_lines)}\n\n"
            "Grounded reasons for recommendation:\n"
            f"{reasons_text}\n\n"
            "Write a natural chat reply in a warm, helpful tone:\n"
            "- Start with: Best pick: *<card name>*\n"
            "- Then explain 2–3 key benefits, using or paraphrasing the reasons above\n"
            "- If there is a note, include it in a separate friendly sentence.\n"
            "- You may add 1 short closing sentence inviting the user to ask another question.\n"
            "Use simple markdown where helpful (like *bold* for the card name or key phrases). "
            "Do not add any extra factual details beyond what is provided."
        )

    def _llm_rephrase(self, base_text: str) -> str:
        """
        Ask the LLM to rephrase a simple system-style reply into a more
        natural chat response, while keeping meaning the same.
        Falls back to the original text on any error or if LLM is unavailable.
        """
        if genai is None:
            return base_text

        api_key = os.getenv("GEMINI_API_KEY")
        if not api_key:
            return base_text

        try:
            if not self._llm_configured:
                genai.configure(api_key=api_key)
                self._llm_configured = True

            model = genai.GenerativeModel("gemini-flash-latest")
            prompt = (
                "Rewrite the following assistant reply so it sounds like a natural, friendly chat "
                "message in 1-3 short sentences. Keep the meaning and any important numbers the same. "
                "Do not add new facts or emojis. Here is the reply:\n\n"
                f"\"{base_text}\""
            )
            response = model.generate_content(prompt)
            text = getattr(response, "text", None)
            if not text:
                return base_text
            return text.strip()
        except Exception:
            return base_text

    def _maybe_llm_response(self, card: Dict, reasons: List[str], note: Optional[str]) -> Optional[str]:
        """
        If Gemini is available and configured via GEMINI_API_KEY, ask it to
        craft the final chat response. Falls back to None on any error.
        """
        if genai is None:
            return None

        api_key = os.getenv("GEMINI_API_KEY")
        if not api_key:
            return None

        try:
            if not self._llm_configured:
                genai.configure(api_key=api_key)
                self._llm_configured = True

            model = genai.GenerativeModel("gemini-flash-latest")
            prompt = self._build_llm_prompt(card, reasons, note)
            response = model.generate_content(prompt)
            text = getattr(response, "text", None)
            if not text:
                return None
            return text.strip()
        except Exception:
            # On any issue with the LLM, silently fall back to rule-based text
            return None
    
    def reset(self):
        """Reset conversation state"""
        self.step = "idle"
        self.asked = 0
        self.user = {}
        self.finished = False
    
    def process_message(self, text: str) -> str:
        """
        Process incoming message and return bot response
        Asks max 2 questions before recommending
        """
        normalized = CardRecommender.normalize_text(text)
        
        # Global commands
        if normalized in ["reset", "restart", "start over"]:
            self.reset()
            base = "Reset done. Tell me what you want: cashback, travel, Tata/Neu, RuPay/UPI, or 'best card for me'."
            return self._llm_rephrase(base)
        
        # Handle current step
        if self.step == "ask_goal":
            self.user['goal'] = CardRecommender.detect_goal(text)
            self.step = "idle"
        
        elif self.step == "ask_income":
            income = CardRecommender.extract_income_inr(text)
            if income:
                self.user['monthlyIncome'] = income
                self.step = "idle"
                # Short-circuit if income is below minimum threshold
                if income < 10000:
                    self.finished = True
                    base = "Income is too low for the cards available in this demo."
                    return self._llm_rephrase(base)
            else:
                base = "Please share your *monthly income in INR* (for example, 80000 or 1.2L)."
                return self._llm_rephrase(base)
        
        # If idle, try to infer from current message
        if self.step == "idle":
            inferred_goal = CardRecommender.detect_goal(text)
            if not self.user.get('goal') or self.user.get('goal') == 'unknown':
                self.user['goal'] = inferred_goal
            
            # Ask goal if still unknown (Question #1)
            if self.user.get('goal') == 'unknown':
                if self.asked < 2:
                    self.asked += 1
                    self.step = "ask_goal"
                    base = "Quick one: what do you want most — *cashback* or *travel perks* (lounges and miles)?"
                    return self._llm_rephrase(base)
            
            # Ask income if needed (Question #2)
            needs_income = self.user.get('goal') in ['travel', 'unknown', 'tata', 'rupay', 'cashback']
            if needs_income and not isinstance(self.user.get('monthlyIncome'), (int, float)):
                if self.asked < 2:
                    self.asked += 1
                    self.step = "ask_income"
                    base = "What is your *monthly income in INR*? You can just send a number like 80000 or 1.5L."
                    return self._llm_rephrase(base)
            
            # We have enough info → recommend
            result = self.recommender.recommend_one_card(self.user)
            card = result['card']
            reasons = result['reasons']
            note = result['note']

            # Try LLM-crafted response first (if available)
            llm_answer = self._maybe_llm_response(card, reasons, note)
            if llm_answer:
                self.step = "idle"
                self.finished = True
                return llm_answer

            # Fallback: existing template response
            lines = [f"Best pick: *{card['name']}*"]
            if note:
                lines.append(note)
            for reason in reasons:
                lines.append(f"- {reason}")
            lines.append("")
            lines.append("If you want, tell me your typical spend split (online/offline/travel) and I'll re-check.")

            self.step = "idle"
            self.finished = True
            return "\n".join(lines)
        # Fallback
        base = "Tell me whether you care more about cashback, travel, Tata/Neu, RuPay/UPI, or just say 'best card for me'."
        return self._llm_rephrase(base)
