# parrot/advisors/questions.py
"""
Discriminant Question Generation for Product Selection.

This module handles:
- Analysis of product catalogs to identify discriminating features
- LLM-based generation of natural language questions
- Question prioritization based on elimination power
- Response mapping to filter criteria
"""
from typing import List, Dict, Any, Optional, Union, Tuple, Set
from enum import Enum
from datetime import datetime
from pydantic import BaseModel, Field, computed_field
import json
from collections import Counter, defaultdict
import math

from navconfig.logging import logging
from .models import ProductSpec, FeatureType


# ─────────────────────────────────────────────────────────────────────────────
# Enums and Constants
# ─────────────────────────────────────────────────────────────────────────────

class AnswerType(str, Enum):
    """Type of expected answer from user."""
    SINGLE_CHOICE = "single_choice"   # Pick one option
    MULTI_CHOICE = "multi_choice"     # Pick multiple
    NUMERIC = "numeric"               # Number input
    NUMERIC_RANGE = "numeric_range"   # "between X and Y"
    BOOLEAN = "boolean"               # Yes/No
    FREE_TEXT = "free_text"           # Open-ended (rare)


class QuestionCategory(str, Enum):
    """Categories of discriminant questions."""
    USE_CASE = "use_case"           # What will you use it for?
    SPACE = "space"                 # Space/dimension constraints
    BUDGET = "budget"               # Price constraints
    FEATURE = "feature"             # Specific feature requirements
    PREFERENCE = "preference"       # Style/material preferences
    TIMELINE = "timeline"           # When do you need it?


# ─────────────────────────────────────────────────────────────────────────────
# Pydantic Models for Structured Output
# ─────────────────────────────────────────────────────────────────────────────

class AnswerOption(BaseModel):
    """A single answer option for choice-type questions."""
    label: str = Field(description="Human-readable option label")
    value: Any = Field(description="Internal value for filtering")
    description: Optional[str] = Field(None, description="Optional explanation")
    icon: Optional[str] = Field(None, description="Optional emoji/icon")


class ValueMapping(BaseModel):
    """Maps user responses to filter criteria."""
    response_pattern: str = Field(
        description="Pattern to match in user response (can be regex)"
    )
    criteria_key: str = Field(
        description="The filter criteria key to set"
    )
    criteria_value: Any = Field(
        description="The value to set for the criteria"
    )
    is_range: bool = Field(
        default=False, 
        description="Whether this creates a range filter"
    )


class DiscriminantQuestion(BaseModel):
    """
    A question designed to filter/discriminate between products.
    
    Generated by LLM analysis of the product catalog.
    """
    # Identity
    question_id: str = Field(description="Unique identifier for this question")
    
    # The question itself
    question_text: str = Field(
        description="The natural language question to ask the user"
    )
    question_text_voice: Optional[str] = Field(
        None,
        description="Voice-optimized version of the question (shorter, more conversational)"
    )
    
    # Categorization
    category: QuestionCategory = Field(
        description="Category of this question"
    )
    
    # Answer configuration
    answer_type: AnswerType = Field(
        description="Type of answer expected"
    )
    options: Optional[List[AnswerOption]] = Field(
        None,
        description="Options for choice-type questions"
    )
    numeric_config: Optional[Dict[str, Any]] = Field(
        None,
        description="Config for numeric questions: {min, max, unit, step}"
    )
    
    # Mapping to product attributes
    maps_to_feature: str = Field(
        description="Primary feature/attribute this question maps to"
    )
    value_mappings: List[ValueMapping] = Field(
        default_factory=list,
        description="How to map user responses to filter criteria"
    )
    
    # Discrimination metrics
    priority: int = Field(
        default=50,
        description="Priority score (0-100). Higher = asked earlier"
    )
    discrimination_power: float = Field(
        default=0.5,
        description="Expected fraction of products eliminated (0-1)"
    )
    unique_values_count: int = Field(
        default=0,
        description="Number of distinct values in catalog for this feature"
    )
    
    # Conditional logic
    ask_if: Optional[Dict[str, Any]] = Field(
        None,
        description="Only ask if these criteria are already set"
    )
    skip_if: Optional[Dict[str, Any]] = Field(
        None,
        description="Skip this question if these criteria are set"
    )
    depends_on: Optional[List[str]] = Field(
        None,
        description="Question IDs that should be asked before this one"
    )
    
    # UX helpers
    follow_up_text: Optional[str] = Field(
        None,
        description="Text to say after user answers (e.g., 'Great choice!')"
    )
    clarification_hint: Optional[str] = Field(
        None,
        description="Help text if user seems confused"
    )
    example_answers: Optional[List[str]] = Field(
        None,
        description="Example valid answers for free-text questions"
    )
    
    # ─────────────────────────────────────────────────────────────────────────
    # Methods
    # ─────────────────────────────────────────────────────────────────────────
    
    def format_for_voice(self) -> str:
        """Format question for voice interaction (concise)."""
        base = self.question_text_voice or self.question_text
        
        if self.answer_type == AnswerType.SINGLE_CHOICE and self.options:
            if len(self.options) <= 4:
                labels = [opt.label for opt in self.options]
                options_text = ", ".join(labels[:-1]) + f", or {labels[-1]}"
                return f"{base} Your options are: {options_text}."
        
        return base
    
    def format_for_text(self) -> str:
        """Format question for text chat (can be more detailed)."""
        parts = [self.question_text]
        
        if self.answer_type == AnswerType.SINGLE_CHOICE and self.options:
            options_list = "\n".join(
                f"  • **{opt.label}**" + (f" - {opt.description}" if opt.description else "")
                for opt in self.options
            )
            parts.append(options_list)
        
        elif self.answer_type == AnswerType.NUMERIC and self.numeric_config:
            unit = self.numeric_config.get("unit", "")
            min_val = self.numeric_config.get("min", "")
            max_val = self.numeric_config.get("max", "")
            if min_val or max_val:
                parts.append(f"  _(Enter a number{' in ' + unit if unit else ''})_")
        
        return "\n".join(parts)
    
    def parse_response(self, user_response: str) -> Optional[Dict[str, Any]]:
        """
        Parse user response and return filter criteria.
        
        Returns:
            Dict with criteria to apply, or None if couldn't parse
        """
        response_lower = user_response.lower().strip()
        
        # Try direct option matching first
        if self.options:
            for opt in self.options:
                if (opt.label.lower() in response_lower or 
                    str(opt.value).lower() in response_lower):
                    return {self.maps_to_feature: opt.value}
        
        # Try value mappings
        for mapping in self.value_mappings:
            import re
            if re.search(mapping.response_pattern, response_lower, re.IGNORECASE):
                if mapping.is_range:
                    # Extract numbers for range
                    numbers = re.findall(r'\d+\.?\d*', response_lower)
                    if numbers:
                        return {mapping.criteria_key: {"max": float(numbers[0])}}
                else:
                    return {mapping.criteria_key: mapping.criteria_value}
        
        # Try numeric extraction for numeric questions
        if self.answer_type in (AnswerType.NUMERIC, AnswerType.NUMERIC_RANGE):
            import re
            numbers = re.findall(r'\d+\.?\d*', response_lower)
            if numbers:
                value = float(numbers[0])
                # Check for dimension patterns like "10x12" or "10 by 12"
                dim_match = re.search(r'(\d+\.?\d*)\s*[xX×by]\s*(\d+\.?\d*)', response_lower)
                if dim_match:
                    width, depth = float(dim_match.group(1)), float(dim_match.group(2))
                    return {
                        "available_space": {"width": width, "depth": depth},
                        "max_footprint": width * depth
                    }
                return {self.maps_to_feature: value}
        
        # Boolean detection
        if self.answer_type == AnswerType.BOOLEAN:
            if any(word in response_lower for word in ["yes", "yeah", "yep", "sure", "definitely"]):
                return {self.maps_to_feature: True}
            if any(word in response_lower for word in ["no", "nope", "nah", "not"]):
                return {self.maps_to_feature: False}
        
        return None


class QuestionSet(BaseModel):
    """
    Complete set of discriminant questions for a catalog.
    
    Generated once per catalog and cached.
    """
    # Identity
    catalog_id: str = Field(description="Catalog this question set belongs to")
    version: str = Field(default="1.0", description="Version for cache invalidation")
    
    # Questions
    questions: List[DiscriminantQuestion] = Field(
        default_factory=list,
        description="All generated questions, ordered by priority"
    )
    
    # Metadata
    generated_at: datetime = Field(default_factory=datetime.utcnow)
    generated_by_model: str = Field(default="", description="LLM model used")
    total_products: int = Field(default=0)
    
    # Statistics
    avg_questions_to_narrow: float = Field(
        default=3.0,
        description="Average questions needed to narrow to ≤3 products"
    )
    coverage: Dict[str, int] = Field(
        default_factory=dict,
        description="How many products each question covers"
    )
    
    # ─────────────────────────────────────────────────────────────────────────
    # Methods
    # ─────────────────────────────────────────────────────────────────────────
    
    def get_question(self, question_id: str) -> Optional[DiscriminantQuestion]:
        """Get a question by ID."""
        for q in self.questions:
            if q.question_id == question_id:
                return q
        return None
    
    def get_next_question(
        self,
        asked_ids: List[str],
        current_criteria: Dict[str, Any],
        remaining_products: int
    ) -> Optional[DiscriminantQuestion]:
        """
        Get the next best question to ask.
        
        Logic:
        1. Skip already asked questions
        2. Check ask_if conditions
        3. Check skip_if conditions
        4. Check depends_on requirements
        5. Return highest priority remaining
        
        Args:
            asked_ids: Question IDs already asked
            current_criteria: Criteria already collected
            remaining_products: Number of products still in consideration
            
        Returns:
            Next question to ask, or None if no more questions
        """
        asked_set = set(asked_ids)
        
        for question in self.questions:  # Already sorted by priority
            # Skip if already asked
            if question.question_id in asked_set:
                continue
            
            # Check skip_if conditions
            if question.skip_if:
                should_skip = all(
                    current_criteria.get(k) == v
                    for k, v in question.skip_if.items()
                )
                if should_skip:
                    continue
            
            # Check ask_if conditions
            if question.ask_if:
                should_ask = all(
                    current_criteria.get(k) == v
                    for k, v in question.ask_if.items()
                )
                if not should_ask:
                    continue
            
            # Check depends_on
            if question.depends_on:
                deps_satisfied = all(
                    dep_id in asked_set
                    for dep_id in question.depends_on
                )
                if not deps_satisfied:
                    continue
            
            return question
        
        return None
    
    def get_questions_by_category(
        self, 
        category: QuestionCategory
    ) -> List[DiscriminantQuestion]:
        """Get all questions of a specific category."""
        return [q for q in self.questions if q.category == category]
    
    @computed_field
    @property
    def question_count(self) -> int:
        return len(self.questions)


# ─────────────────────────────────────────────────────────────────────────────
# LLM Output Schema for Question Generation
# ─────────────────────────────────────────────────────────────────────────────

class GeneratedQuestion(BaseModel):
    """Schema for LLM-generated question (subset of DiscriminantQuestion)."""
    question_text: str = Field(
        description="Natural language question to ask the user"
    )
    question_text_voice: str = Field(
        description="Shorter version optimized for voice"
    )
    category: str = Field(
        description="One of: use_case, space, budget, feature, preference"
    )
    answer_type: str = Field(
        description="One of: single_choice, multi_choice, numeric, boolean"
    )
    options: Optional[List[AnswerOption]] = Field(
        None,
        description="For choice questions: list of answer options with label, value, description"
    )
    maps_to_feature: str = Field(
        description="Product attribute this maps to"
    )
    discrimination_power: float = Field(
        description="Expected fraction of products eliminated (0-1)"
    )
    priority_reason: str = Field(
        description="Why this question should be asked early/late"
    )
    follow_up_text: Optional[str] = Field(
        None,
        description="What to say after user answers"
    )
    skip_if_feature: Optional[str] = Field(
        None,
        description="Skip if this feature was already determined"
    )


class QuestionGenerationResponse(BaseModel):
    """Complete response from LLM question generation."""
    questions: List[GeneratedQuestion] = Field(
        description="List of discriminant questions, ordered by priority"
    )
    analysis_summary: str = Field(
        description="Brief summary of catalog analysis"
    )
    key_discriminating_features: List[str] = Field(
        description="Top features that differentiate products"
    )
    recommended_question_order: List[str] = Field(
        description="Recommended order of question categories"
    )

# ─────────────────────────────────────────────────────────────────────────────
# Feature Analysis (Pre-LLM)
# ─────────────────────────────────────────────────────────────────────────────

class FeatureAnalysis(BaseModel):
    """Analysis of a single feature across the catalog."""
    feature_name: str
    feature_type: FeatureType
    unique_values: List[Any]
    value_counts: Dict[str, int]
    coverage: float  # Fraction of products that have this feature
    variance: float  # Statistical variance (for numeric)
    discrimination_power: float  # How well it splits products
    suggested_question_type: AnswerType
    
    @property
    def unique_values_count(self) -> int:
        """Number of unique values for this feature."""
        return len(self.unique_values)


class CatalogAnalysis(BaseModel):
    """Complete analysis of a product catalog."""
    catalog_id: str
    total_products: int
    categories: List[str]
    
    # Feature analyses
    feature_analyses: List[FeatureAnalysis]
    
    # Dimension analysis
    dimension_ranges: Dict[str, Dict[str, float]]  # {width: {min, max, avg}}
    
    # Price analysis
    price_range: Dict[str, float]  # {min, max, avg, median}
    price_clusters: List[Dict[str, Any]]  # Price tiers
    
    # Use case analysis
    all_use_cases: List[str]
    use_case_counts: Dict[str, int]
    
    # Top discriminating features (sorted)
    top_discriminators: List[str]


class FeatureAnalyzer:
    """
    Analyzes a product catalog to identify discriminating features.
    
    This runs BEFORE the LLM to provide structured input for question generation.
    """
    
    def __init__(self, products: List[ProductSpec]):
        self.products = products
        self.logger = logging.getLogger("FeatureAnalyzer")
    
    def analyze(self) -> CatalogAnalysis:
        """Run complete catalog analysis."""
        if not self.products:
            raise ValueError("No products to analyze")
        
        # Basic stats
        categories = list(set(p.category for p in self.products if p.category))
        
        # Analyze features
        feature_analyses = self._analyze_features()
        
        # Analyze dimensions
        dimension_ranges = self._analyze_dimensions()
        
        # Analyze prices
        price_analysis = self._analyze_prices()
        
        # Analyze use cases
        use_case_analysis = self._analyze_use_cases()
        
        # Rank discriminators
        top_discriminators = self._rank_discriminators(feature_analyses)
        
        return CatalogAnalysis(
            catalog_id=self.products[0].catalog_id if self.products else "unknown",
            total_products=len(self.products),
            categories=categories,
            feature_analyses=feature_analyses,
            dimension_ranges=dimension_ranges,
            price_range=price_analysis["range"],
            price_clusters=price_analysis["clusters"],
            all_use_cases=use_case_analysis["all"],
            use_case_counts=use_case_analysis["counts"],
            top_discriminators=top_discriminators
        )
    
    def _analyze_features(self) -> List[FeatureAnalysis]:
        """Analyze all features across products."""
        # Collect all features
        feature_values: Dict[str, List[Tuple[Any, FeatureType]]] = defaultdict(list)
        
        for product in self.products:
            for feature in product.features:
                if feature.is_filterable:
                    feature_values[feature.name].append(
                        (feature.value, feature.feature_type)
                    )
        
        analyses = []
        for name, values_types in feature_values.items():
            values = [v[0] for v in values_types]
            feature_type = values_types[0][1] if values_types else FeatureType.TEXT
            
            # Count unique values
            unique_values = list(set(str(v) for v in values))
            value_counts = Counter(str(v) for v in values)
            
            # Calculate coverage
            coverage = len(values) / len(self.products)
            
            # Calculate variance/discrimination
            discrimination = self._calculate_discrimination(values, feature_type)
            
            # Suggest question type
            suggested_type = self._suggest_question_type(
                feature_type, len(unique_values), values
            )
            
            analyses.append(FeatureAnalysis(
                feature_name=name,
                feature_type=feature_type,
                unique_values=unique_values[:20],  # Limit for readability
                value_counts=dict(value_counts.most_common(10)),
                coverage=coverage,
                variance=discrimination["variance"],
                discrimination_power=discrimination["power"],
                suggested_question_type=suggested_type
            ))
        
        # Sort by discrimination power
        analyses.sort(key=lambda x: x.discrimination_power, reverse=True)
        return analyses
    
    def _calculate_discrimination(
        self, 
        values: List[Any], 
        feature_type: FeatureType
    ) -> Dict[str, float]:
        """Calculate how well a feature discriminates between products."""
        if not values:
            return {"variance": 0.0, "power": 0.0}
        
        unique_count = len(set(str(v) for v in values))
        total_count = len(values)
        
        if feature_type == FeatureType.NUMERIC:
            # For numeric: use coefficient of variation
            try:
                numeric_values = [float(v) for v in values if v is not None]
                if numeric_values:
                    mean = sum(numeric_values) / len(numeric_values)
                    variance = sum((x - mean) ** 2 for x in numeric_values) / len(numeric_values)
                    std_dev = math.sqrt(variance)
                    cv = std_dev / mean if mean > 0 else 0
                    # Normalize to 0-1 range
                    power = min(cv, 1.0)
                    return {"variance": variance, "power": power}
            except (ValueError, TypeError):
                pass
        
        # For categorical: use entropy-based measure
        value_counts = Counter(str(v) for v in values)
        entropy = 0.0
        for count in value_counts.values():
            p = count / total_count
            if p > 0:
                entropy -= p * math.log2(p)
        
        # Normalize by max entropy
        max_entropy = math.log2(unique_count) if unique_count > 1 else 1
        normalized_entropy = entropy / max_entropy if max_entropy > 0 else 0
        
        return {"variance": entropy, "power": normalized_entropy}
    
    def _suggest_question_type(
        self,
        feature_type: FeatureType,
        unique_count: int,
        values: List[Any]
    ) -> AnswerType:
        """Suggest the best question type for a feature."""
        if feature_type == FeatureType.BOOLEAN:
            return AnswerType.BOOLEAN
        
        if feature_type == FeatureType.NUMERIC:
            return AnswerType.NUMERIC
        
        if feature_type == FeatureType.CATEGORICAL:
            if unique_count <= 5:
                return AnswerType.SINGLE_CHOICE
            elif unique_count <= 10:
                return AnswerType.MULTI_CHOICE
        
        return AnswerType.FREE_TEXT
    
    def _analyze_dimensions(self) -> Dict[str, Dict[str, float]]:
        """Analyze product dimensions."""
        result = {}
        
        widths = [p.dimensions.width for p in self.products if p.dimensions]
        depths = [p.dimensions.depth for p in self.products if p.dimensions]
        heights = [p.dimensions.height for p in self.products if p.dimensions]
        footprints = [p.dimensions.footprint for p in self.products if p.dimensions]
        
        for name, values in [
            ("width", widths), 
            ("depth", depths), 
            ("height", heights),
            ("footprint", footprints)
        ]:
            if values:
                result[name] = {
                    "min": min(values),
                    "max": max(values),
                    "avg": sum(values) / len(values)
                }
        
        return result
    
    def _analyze_prices(self) -> Dict[str, Any]:
        """Analyze price distribution."""
        prices = [p.price for p in self.products if p.price]
        
        if not prices:
            return {"range": {}, "clusters": []}
        
        sorted_prices = sorted(prices)
        
        range_info = {
            "min": min(prices),
            "max": max(prices),
            "avg": sum(prices) / len(prices),
            "median": sorted_prices[len(sorted_prices) // 2]
        }
        
        # Simple clustering into tiers
        clusters = []
        if len(prices) >= 3:
            q1 = sorted_prices[len(sorted_prices) // 4]
            q2 = sorted_prices[len(sorted_prices) // 2]
            q3 = sorted_prices[3 * len(sorted_prices) // 4]
            
            clusters = [
                {"name": "budget", "max": q1, "count": sum(1 for p in prices if p <= q1)},
                {"name": "mid-range", "min": q1, "max": q3, "count": sum(1 for p in prices if q1 < p <= q3)},
                {"name": "premium", "min": q3, "count": sum(1 for p in prices if p > q3)}
            ]
        
        return {"range": range_info, "clusters": clusters}
    
    def _analyze_use_cases(self) -> Dict[str, Any]:
        """Analyze use cases across products."""
        all_use_cases = []
        for product in self.products:
            all_use_cases.extend(product.use_cases)
        
        counts = Counter(all_use_cases)
        
        return {
            "all": list(set(all_use_cases)),
            "counts": dict(counts.most_common())
        }
    
    def _rank_discriminators(
        self, 
        feature_analyses: List[FeatureAnalysis]
    ) -> List[str]:
        """Rank features by discrimination power."""
        # Filter features with good coverage and discrimination
        good_features = [
            fa for fa in feature_analyses
            if fa.coverage >= 0.5 and fa.discrimination_power >= 0.3
        ]
        
        # Sort by discrimination power
        good_features.sort(key=lambda x: x.discrimination_power, reverse=True)
        
        return [fa.feature_name for fa in good_features[:10]]