"""
Extract ALL intelligence from GUI and make it API-accessible
"""
from typing import Dict, Any, List, Optional
import pandas as pd
from fmatch.core.engine import (
    determine_optimal_blocking_strategy,
    auto_select_id_column,
    suggest_mappings,
    AutoMapper,
    BlockingMode,
    _get_semantic_type,
    detect_optimal_variants,
    validate_blocking_candidates,
)
from fmatch.preprocessing.core.column_detector import ColumnDetector

class ConfigurationService:
    """
    One-stop shop for intelligent configuration.
    Extracts all GUI intelligence for API use.
    """
    
    def auto_configure(
        self, 
        source_df: pd.DataFrame, 
        ref_df: Optional[pd.DataFrame] = None, 
        mode: str = "match",
        optimization_goal: str = "balanced"
    ) -> Dict[str, Any]:
        """
        Complete intelligent configuration in one call.
        """
        config = {}
        
        # 1. ID columns with enterprise system detection
        config["source_id"] = self._detect_id_column(source_df)
        config["ref_id"] = self._detect_id_column(ref_df) if ref_df else None
        
        # 2. Intelligent mappings with weights
        config["mappings"] = self._generate_mappings(source_df, ref_df)
        
        # 3. Optimal blocking strategy
        config["blocking"] = self._optimize_blocking(
            source_df, 
            ref_df, 
            config["mappings"],
            optimization_goal
        )
        
        # 4. Algorithm selection per column
        config["algorithms"] = self._select_algorithms(
            source_df, 
            ref_df, 
            config["mappings"]
        )
        
        # 5. Threshold optimization
        config["threshold"] = self._calculate_threshold(
            source_df, 
            ref_df, 
            optimization_goal
        )
        
        # 6. Weight optimization
        config["weights"] = self._optimize_weights(
            config["mappings"], 
            source_df, 
            ref_df
        )
        
        # 7. Preprocessing rules
        config["preprocessing"] = self._detect_preprocessing_needs(
            source_df, 
            ref_df
        )
        
        return config
    
    def _detect_id_column(self, df: pd.DataFrame) -> Optional[str]:
        """Use ColumnDetector for enterprise-aware ID detection."""
        if df is None or df.empty:
            return None
            
        result = ColumnDetector.detect_id_column(df)
        if result:
            return result.column_name
        return None
    
    def _generate_mappings(
        self, 
        source_df: pd.DataFrame, 
        ref_df: Optional[pd.DataFrame]
    ) -> List[Dict[str, Any]]:
        """Generate intelligent mappings with confidence scores."""
        if ref_df is None:
            # Dedupe mode - identity mappings
            id_col = self._detect_id_column(source_df)
            return [
                {
                    "source": col,
                    "weight": 1.0,
                    "algorithm": self._get_column_algorithm(source_df[col])
                }
                for col in source_df.columns
                if col != id_col
            ]
        
        # Match mode - use AutoMapper
        mapper = AutoMapper()
        mappings = mapper.suggest(source_df, ref_df, threshold=75)
        
        # Enhance with algorithm selection
        for mapping in mappings:
            source_col = source_df[mapping['source']]
            ref_col = ref_df[mapping['ref']]
            
            # Detect semantic type
            source_semantic = _get_semantic_type(mapping['source'], source_col)
            ref_semantic = _get_semantic_type(mapping['ref'], ref_col)
            
            # Select optimal algorithm
            mapping['algorithm'] = self._select_algorithm_for_pair(
                source_semantic, 
                ref_semantic,
                source_col,
                ref_col
            )
            
            # Add confidence
            mapping['confidence'] = self._calculate_mapping_confidence(
                mapping, 
                source_col, 
                ref_col
            )
        
        return mappings
    
    def _optimize_blocking(
        self, 
        source_df: pd.DataFrame,
        ref_df: Optional[pd.DataFrame],
        mappings: List[Dict],
        optimization_goal: str
    ) -> Dict[str, Any]:
        """Determine optimal blocking with goal-specific weights."""
        
        # Map goal to weight profile
        weight_profile = {
            "speed": "high_recall",
            "accuracy": "high_precision",
            "balanced": "balanced",
            "messy_data": "messy_data",
            "exotic": "exotic_aware"
        }.get(optimization_goal, "balanced")
        
        mode = BlockingMode.MATCH if ref_df is not None else BlockingMode.DEDUPE
        
        # Run full blocking analysis
        strategy = determine_optimal_blocking_strategy(
            source_df,
            ref_df,
            mappings,
            mode,
            "default",
            weight_profile=weight_profile
        )
        
        # Validate the strategy
        if strategy.get("apply_blocking"):
            validation = validate_blocking_candidates(
                [(strategy['source_block_col'], strategy.get('ref_block_col'))],
                source_df,
                ref_df,
                mode
            )
            strategy['validation'] = validation[0] if validation else {}
        
        # Add preprocessing variants
        if strategy.get('source_block_col'):
            strategy['variants'] = detect_optimal_variants(
                source_df[strategy['source_block_col']],
                strategy['source_block_col'],
                strategy.get('validation', {}).get('src_metrics', {})
            )
        
        return strategy
    
    def _select_algorithm_for_pair(
        self,
        source_semantic: str,
        ref_semantic: str,
        source_series: pd.Series,
        ref_series: pd.Series
    ) -> str:
        """Select optimal algorithm based on semantic types."""
        
        # Domain/email matching
        if source_semantic == "email" or ref_semantic == "email":
            return "JaroWinkler"
        
        # Phone matching
        if source_semantic == "phone" or ref_semantic == "phone":
            return "WRatio"
        
        # Company names
        if source_semantic == "name_company" or ref_semantic == "name_company":
            # Companies often have variations
            return "TokenSetRatio"
        
        # Person names
        if source_semantic == "name_person" or ref_semantic == "name_person":
            return "JaroWinkler"
        
        # Product codes/IDs
        if source_semantic == "id" or ref_semantic == "id":
            # IDs should match exactly
            return "Exact"
        
        # Addresses
        if "address" in source_semantic or "address" in ref_semantic:
            return "WRatio"
        
        # Default based on string length
        avg_len = (source_series.astype(str).str.len().mean() + 
                  ref_series.astype(str).str.len().mean()) / 2
        
        if avg_len < 10:
            return "JaroWinkler"  # Better for short strings
        elif avg_len > 50:
            return "TokenSetRatio"  # Better for long text
        else:
            return "WRatio"  # Good general purpose
    
    def _calculate_threshold(
        self,
        source_df: pd.DataFrame,
        ref_df: Optional[pd.DataFrame],
        optimization_goal: str
    ) -> int:
        """Calculate optimal threshold based on goal and data."""
        
        base_threshold = {
            "speed": 75,
            "accuracy": 85,
            "balanced": 80,
            "messy_data": 70,
            "exotic": 75
        }.get(optimization_goal, 80)
        
        # Adjust based on data quality
        if source_df is not None:
            # Check for null ratios
            null_ratio = source_df.isnull().sum().sum() / source_df.size
            if null_ratio > 0.1:
                base_threshold -= 5
            
            # Check for inconsistent data
            string_cols = source_df.select_dtypes(include=['object']).columns
            if len(string_cols) > 0:
                # Sample for variance
                sample = source_df[string_cols].head(1000)
                # High variance in lengths suggests messy data
                length_variance = sample.applymap(
                    lambda x: len(str(x)) if pd.notna(x) else 0
                ).var().mean()
                
                if length_variance > 100:
                    base_threshold -= 5
        
        return max(60, min(95, base_threshold))
    
    def _optimize_weights(
        self,
        mappings: List[Dict],
        source_df: pd.DataFrame,
        ref_df: Optional[pd.DataFrame]
    ) -> Dict[str, float]:
        """Optimize field weights based on data characteristics."""
        
        weights = {}
        
        for mapping in mappings:
            source_col = mapping['source']
            ref_col = mapping.get('ref', source_col)
            
            # Base weight from name similarity
            base_weight = mapping.get('weight', 1.0)
            
            # Adjust for data quality
            if source_col in source_df.columns:
                # Penalize high null ratio
                null_ratio = source_df[source_col].isnull().sum() / len(source_df)
                quality_factor = 1.0 - (null_ratio * 0.5)
                
                # Boost unique columns (likely IDs)
                unique_ratio = source_df[source_col].nunique() / len(source_df)
                if unique_ratio > 0.9:
                    quality_factor *= 1.2
                
                # Penalize low-variance columns
                if unique_ratio < 0.01:
                    quality_factor *= 0.5
                
                weights[source_col] = base_weight * quality_factor
            else:
                weights[source_col] = base_weight
        
        # Normalize weights to sum to 1
        total = sum(weights.values())
        if total > 0:
            weights = {k: v/total for k, v in weights.items()}
        
        return weights
    
    def _detect_preprocessing_needs(
        self,
        source_df: pd.DataFrame,
        ref_df: Optional[pd.DataFrame]
    ) -> List[str]:
        """Detect what preprocessing is needed."""
        
        rules = []
        
        # Check for mixed case
        string_cols = source_df.select_dtypes(include=['object']).columns
        if len(string_cols) > 0:
            sample = source_df[string_cols].head(1000)
            has_mixed_case = sample.applymap(
                lambda x: str(x) != str(x).lower() if pd.notna(x) else False
            ).any().any()
            
            if has_mixed_case:
                rules.append("lowercase")
        
        # Check for extra whitespace
        has_extra_spaces = sample.applymap(
            lambda x: '  ' in str(x) or str(x) != str(x).strip() if pd.notna(x) else False
        ).any().any()
        
        if has_extra_spaces:
            rules.append("trim_whitespace")
            rules.append("collapse_spaces")
        
        # Check for special characters
        has_special = sample.applymap(
            lambda x: bool(re.search(r'[^\w\s]', str(x))) if pd.notna(x) else False
        ).any().any()
        
        if has_special:
            rules.append("remove_punctuation")
        
        return rules
    
    def _get_column_algorithm(self, series: pd.Series) -> str:
        """Get optimal algorithm for a single column."""
        
        semantic = _get_semantic_type(series.name, series)
        
        algorithm_map = {
            "email": "JaroWinkler",
            "phone": "WRatio", 
            "name_person": "JaroWinkler",
            "name_company": "TokenSetRatio",
            "id": "Exact",
            "address_street": "WRatio",
            "city": "JaroWinkler",
            "state_province": "Exact",
            "zip_postal": "Exact",
            "country": "Exact",
            "date": "Exact",
            "website_domain": "JaroWinkler"
        }
        
        return algorithm_map.get(semantic, "WRatio")
    
    def _calculate_mapping_confidence(
        self,
        mapping: Dict,
        source_series: pd.Series,
        ref_series: pd.Series
    ) -> float:
        """Calculate confidence score for a mapping."""
        
        # Name similarity
        from rapidfuzz import fuzz
        name_score = fuzz.WRatio(mapping['source'], mapping['ref']) / 100
        
        # Data type compatibility
        type_compatible = (
            pd.api.types.is_numeric_dtype(source_series) == 
            pd.api.types.is_numeric_dtype(ref_series)
        )
        type_score = 1.0 if type_compatible else 0.5
        
        # Semantic type match
        source_semantic = _get_semantic_type(mapping['source'], source_series)
        ref_semantic = _get_semantic_type(mapping['ref'], ref_series)
        semantic_score = 1.0 if source_semantic == ref_semantic else 0.7
        
        # Value overlap (sample-based)
        source_sample = set(source_series.dropna().head(100))
        ref_sample = set(ref_series.dropna().head(100))
        
        if len(source_sample) > 0 and len(ref_sample) > 0:
            overlap = len(source_sample & ref_sample) / min(len(source_sample), len(ref_sample))
            overlap_score = min(1.0, overlap * 2)  # Boost small overlaps
        else:
            overlap_score = 0.5
        
        # Weighted average
        confidence = (
            name_score * 0.3 +
            type_score * 0.2 +
            semantic_score * 0.3 +
            overlap_score * 0.2
        )
        
        return round(confidence, 2)