import json
import numpy as np
import pandas as pd


class LightweightScorer:
    """
    Lightweight scorer for deployment without autoscore dependency.
    
    This class provides scoring functionality using only numpy and pandas,
    making it suitable for production environments where installing
    the full autoscore package is not desirable.
    
    Parameters:
    -----------
    model_path : str
        Path to the lightweight model JSON file exported from AutoScore
    """
    
    def __init__(self, model_path):
        """
        Initialize lightweight scorer from exported model file.
        
        Parameters:
        -----------
        model_path : str
            Path to the lightweight model JSON file
        """
        # Load model data
        with open(model_path, 'r', encoding='utf-8') as f:
            self.model_data = json.load(f)
        
        # Extract model components
        self.coefs = self.model_data['model']['coefs']
        self.intercept = self.model_data['model']['intercept']
        self.binning_rules = self.model_data['binning_rules']
        self.sc_params = self.model_data['scoring_params']
        self.selected_features = self.sc_params['selected_features']
        self.metadata = self.model_data.get('metadata', {})
        
        print(f"LightweightScorer loaded from: {model_path}")
        print(f"Model version: {self.metadata.get('version', 'unknown')}")
        print(f"Number of features: {len(self.selected_features)}")
    
    def _transform_woe(self, X):
        """
        Transform input data to WOE values.
        
        Parameters:
        -----------
        X : pd.DataFrame
            Input feature matrix
            
        Returns:
        --------
        pd.DataFrame
            Transformed WOE values
        """
        X_woe = X.copy()
        
        for feat in X.columns:
            if feat not in self.binning_rules:
                continue
            
            rule = self.binning_rules[feat]
            
            if rule['type'] == 'numeric':
                # Numeric feature transformation
                cutoffs = rule['cutoffs']
                woe_map = rule['woe_map']
                
                # Ensure column is numeric
                x_col = pd.to_numeric(X[feat], errors='coerce')
                
                def get_bin(x):
                    if pd.isna(x):
                        return -1
                    for i, cutoff in enumerate(cutoffs):
                        if x <= cutoff:
                            return i
                    return len(cutoffs)
                
                X_woe[feat] = x_col.apply(get_bin).map(woe_map)
            
            elif rule['type'] == 'categorical':
                # Categorical feature transformation
                woe_map = rule['woe_map']
                categories = rule['categories']
                
                # Create value to bin mapping
                val_to_bin = {}
                for bin_idx, bin_vals in enumerate(categories):
                    for val in bin_vals:
                        val_to_bin[val] = bin_idx
                
                def get_woe(x):
                    if pd.isna(x):
                        return woe_map.get(-1, woe_map.get('-1', 0))
                    bin_idx = val_to_bin.get(x, -1)
                    return woe_map.get(bin_idx, woe_map.get(str(bin_idx), 0))
                
                X_woe[feat] = X[feat].apply(get_woe)
        
        return X_woe
    
    def predict_proba(self, X):
        """
        Predict default probability.
        
        Parameters:
        -----------
        X : pd.DataFrame
            Input feature matrix
            
        Returns:
        --------
        np.ndarray
            Predicted probabilities (0-1)
        """
        # Transform to WOE
        X_woe = self._transform_woe(X)
        
        # Select features
        X_sel = X_woe[self.selected_features]
        
        # Calculate linear combination
        linear = np.dot(X_sel, self.coefs) + self.intercept
        
        # Sigmoid function
        proba = 1 / (1 + np.exp(-linear))
        
        return proba
    
    def predict_score(self, X):
        """
        Predict credit score.
        
        Parameters:
        -----------
        X : pd.DataFrame
            Input feature matrix
            
        Returns:
        --------
        np.ndarray
            Predicted scores
        """
        # Transform to WOE
        X_woe = self._transform_woe(X)
        
        # Select features
        X_sel = X_woe[self.selected_features]
        
        # Calculate linear combination (log odds)
        log_odds = np.dot(X_sel, self.coefs) + self.intercept
        
        # Calculate score
        score = self.sc_params['offset'] - self.sc_params['factor'] * log_odds
        
        return score
    
    def get_scorecard(self):
        """
        Get scorecard table.
        
        Returns:
        --------
        pd.DataFrame
            Scorecard table with Variable, Bin, WOE, Points columns
        """
        card_data = []
        
        for feature in self.selected_features:
            rule = self.binning_rules[feature]
            coef = self.coefs[self.selected_features.index(feature)]
            
            # For each bin
            stats = rule['stats']
            
            # Base points distribution
            n_features = len(self.selected_features)
            intercept_points = (self.sc_params['offset'] - 
                            self.sc_params['factor'] * self.intercept)
            base_points = intercept_points / n_features
            
            for bin_stat in stats:
                woe = bin_stat['woe']
                points = base_points - self.sc_params['factor'] * coef * woe
                
                # Handle both 'min'/'max' and 'lower'/'upper' key names
                min_val = bin_stat.get('min', bin_stat.get('lower', ''))
                max_val = bin_stat.get('max', bin_stat.get('upper', ''))
                
                card_data.append({
                    'Variable': feature,
                    'Bin': f"[{min_val}, {max_val})",
                    'WOE': woe,
                    'Points': round(points)
                })
        
        return pd.DataFrame(card_data)
    
    def get_feature_importance(self):
        """
        Get feature importance based on absolute coefficients.
        
        Returns:
        --------
        pd.DataFrame
            DataFrame with Feature and Importance columns
        """
        importance_df = pd.DataFrame({
            'Feature': self.selected_features,
            'Coefficient': self.coefs,
            'Importance': np.abs(self.coefs)
        }).sort_values('Importance', ascending=False)
        
        return importance_df
    
    def get_model_info(self):
        """
        Get model information and metadata.
        
        Returns:
        --------
        dict
            Dictionary containing model information
        """
        info = {
            'version': self.metadata.get('version', 'unknown'),
            'created_at': self.metadata.get('created_at', 'unknown'),
            'n_features': len(self.selected_features),
            'features': self.selected_features,
            'pdo': self.sc_params.get('pdo', 20),
            'base_score': self.sc_params.get('base_score', 600),
            'base_odds': self.sc_params.get('base_odds', 50),
            'factor': self.sc_params['factor'],
            'offset': self.sc_params['offset']
        }
        return info
