"""
timss_math_affect_predictor.py
================================
A compact 7-item predictor for TIMSS 2023 Grade 8
Mathematics Affective Scales (SLM / SCM / SVM).

Trained on N = 232,970 students from 44 countries.
Method: Multinomial Logistic Regression (sklearn, lbfgs)
Reference: [Author] (2025). Cross-construct optimization framework
           for TIMSS mathematics affective scales. [Journal].

Theoretical basis:
  Di Martino & Zan (2010) Three-dimensional Model of Attitude (TMA)
    - Emotional Dimension (ED)  → SLM
    - Perceived Competence (PC) → SCM
    - Vision of Mathematics (VM)→ SVM

Usage:
    from timss_math_affect_predictor import MathAffectPredictor
    pred = MathAffectPredictor()
    result = pred.predict({'BSBM23E':1,'BSBM23A':2,'BSBM19D':1,
                           'BSBM23H':2,'BSBM22A':1,'BSBM22B':3,'BSBM19A':1})
    print(result)
"""

import numpy as np
from typing import Dict, List, Union

# ── Instrument definition ─────────────────────────────────────
BATTERY = {
    'BSBM23E': {
        'origin': 'SVM',
        'tma':    'Vision of Mathematics',
        'content':'I would like a job that involves using mathematics.',
        'reverse': False,
        'predicts':['SLM','SCM','SVM'],
    },
    'BSBM23A': {
        'origin': 'SVM',
        'tma':    'Vision of Mathematics',
        'content':'Learning mathematics will help me in my daily life.',
        'reverse': False,
        'predicts':['SLM','SCM','SVM'],
    },
    'BSBM19D': {
        'origin': 'SLM',
        'tma':    'Emotional Dimension',
        'content':'I learn many interesting things in mathematics.',
        'reverse': False,
        'predicts':['SLM','SCM'],
    },
    'BSBM23H': {
        'origin': 'SVM',
        'tma':    'Vision of Mathematics',
        'content':'My parents think it is important that I do well in mathematics.',
        'reverse': False,
        'predicts':['SLM','SCM'],
    },
    'BSBM22A': {
        'origin': 'SCM',
        'tma':    'Perceived Competence',
        'content':'I usually do well in mathematics.',
        'reverse': False,
        'predicts':['SLM','SCM'],
    },
    'BSBM22B': {
        'origin': 'SCM',
        'tma':    'Perceived Competence',
        'content':'Mathematics is harder for me than for many of my classmates.',
        'reverse': True,   # score = 5 - raw_response
        'predicts':['SLM','SCM'],
    },
    'BSBM19A': {
        'origin': 'SLM',
        'tma':    'Emotional Dimension',
        'content':'I enjoy learning mathematics.',
        'reverse': False,
        'predicts':['SLM'],
    },
}

ITEM_ORDER = ['BSBM23E','BSBM23A','BSBM19D','BSBM23H','BSBM22A','BSBM22B','BSBM19A']

RESPONSE_SCALE = {
    1: 'Agree a lot',
    2: 'Agree a little',
    3: 'Disagree a little',
    4: 'Disagree a lot',
}

LEVEL_LABELS = {1:'High', 2:'Intermediate', 3:'Low'}

# ── Model coefficients (trained on N=232,970, 44 countries) ───
# Multinomial LR, solver=lbfgs, C=1.0
# Rows = classes [1=High, 2=Intermediate, 3=Low]
# Cols = items in ITEM_ORDER
MODEL_PARAMS = {
    'SLM': {
        'performance': {'AUROC': 0.939, 'Kappa': 0.850},
        'classes':    [1, 2, 3],
        'intercept':  [9.398899, 0.737085, -10.135984],
        'coef': [
            # High
            [-0.561028, -0.163709, -1.802742,  0.007129, -0.569753, -0.120569, -2.575046],
            # Intermediate
            [-0.008063,  0.033451,  0.187334,  0.059356, -0.005784,  0.000074,  0.195676],
            # Low
            [ 0.569092,  0.130259,  1.615408, -0.066485,  0.575537,  0.120495,  2.379369],
        ],
    },
    'SCM': {
        'performance': {'AUROC': 0.917, 'Kappa': 0.791},
        'classes':    [1, 2, 3],
        'intercept':  [7.622287, 0.891611, -8.513898],
        'coef': [
            # High
            [-0.273786,  0.044643, -0.130473, -0.043756, -2.066540, -1.864495, -0.550070],
            # Intermediate
            [ 0.019133,  0.004921,  0.021457,  0.033329,  0.112676,  0.052577, -0.008140],
            # Low
            [ 0.254654, -0.049563,  0.109016,  0.010427,  1.953864,  1.811918,  0.558209],
        ],
    },
    'SVM': {
        'performance': {'AUROC': 0.915, 'Kappa': 0.792},
        'classes':    [1, 2, 3],
        'intercept':  [9.320556, 0.574244, -9.894800],
        'coef': [
            # High
            [-1.358341, -1.725101, -0.247757, -1.281894, -0.011517,  0.009270, -0.093864],
            # Intermediate
            [ 0.043192,  0.225279,  0.029142,  0.162000, -0.035749,  0.028690, -0.015519],
            # Low
            [ 1.315149,  1.499822,  0.218615,  1.119894,  0.047266, -0.037960,  0.109383],
        ],
    },
}


class MathAffectPredictor:
    """
    Predicts TIMSS 3-level mathematics affective classification
    (High / Intermediate / Low) for SLM, SCM, and SVM simultaneously
    from 7 cross-construct bridge items.

    Parameters
    ----------
    None (coefficients are embedded from the trained model)

    Notes
    -----
    - Training data: TIMSS 2023 Grade 8, N=232,970, 44 countries
    - Model: Multinomial Logistic Regression (sklearn)
    - Caution: Lower accuracy expected for SAU, ROM, TUR, UZB
      (bridge consistency index ≤ .20 in these countries)
    """

    def __init__(self):
        self.battery    = BATTERY
        self.item_order = ITEM_ORDER
        self.params     = MODEL_PARAMS

    # ── Core prediction ───────────────────────────────────────
    def _softmax(self, logits: np.ndarray) -> np.ndarray:
        e = np.exp(logits - np.max(logits))
        return e / e.sum()

    def _predict_one_scale(self, x: np.ndarray, scale: str) -> Dict:
        p = self.params[scale]
        coef      = np.array(p['coef'])       # (n_classes, n_features)
        intercept = np.array(p['intercept'])  # (n_classes,)
        logits    = coef @ x + intercept      # (n_classes,)
        probs     = self._softmax(logits)

        pred_idx  = int(np.argmax(probs))
        pred_cls  = p['classes'][pred_idx]

        return {
            'predicted_class':  pred_cls,
            'predicted_label':  LEVEL_LABELS[pred_cls],
            'probabilities': {
                LEVEL_LABELS[cls]: round(float(prob), 4)
                for cls, prob in zip(p['classes'], probs)
            }
        }

    def predict(
        self,
        responses: Dict[str, int],
        verbose: bool = True
    ) -> Dict:
        """
        Predict SLM / SCM / SVM affective level from 7-item responses.

        Parameters
        ----------
        responses : dict
            Keys   = item codes (e.g. 'BSBM23E')
            Values = raw responses on 4-point scale (1–4)
                     1=Agree a lot, 2=Agree a little,
                     3=Disagree a little, 4=Disagree a lot

        verbose : bool
            If True, print a formatted summary.

        Returns
        -------
        dict with keys 'SLM', 'SCM', 'SVM', each containing:
            'predicted_label'  : 'High' | 'Intermediate' | 'Low'
            'probabilities'    : {'High':p1, 'Intermediate':p2, 'Low':p3}
        """
        # ── Validate ──────────────────────────────────────────
        missing = [i for i in self.item_order if i not in responses]
        if missing:
            raise ValueError(f"Missing items: {missing}")

        invalid = {i:v for i,v in responses.items()
                   if i in self.item_order and v not in [1,2,3,4]}
        if invalid:
            raise ValueError(
                f"Invalid responses (must be 1–4): {invalid}")

        # ── Reverse scoring ───────────────────────────────────
        scored = {}
        for item in self.item_order:
            raw = responses[item]
            scored[item] = (5 - raw) if self.battery[item]['reverse'] else raw

        x = np.array([scored[i] for i in self.item_order], dtype=float)

        # ── Predict all three scales ──────────────────────────
        results = {
            scale: self._predict_one_scale(x, scale)
            for scale in ['SLM','SCM','SVM']
        }

        if verbose:
            self._print_results(responses, scored, results)

        return results

    def predict_batch(
        self,
        df,
        item_cols: Dict[str, str] = None
    ):
        """
        Predict for a pandas DataFrame of students.

        Parameters
        ----------
        df : pd.DataFrame
            One row per student.

        item_cols : dict, optional
            Mapping {item_code: column_name_in_df}.
            If None, assumes column names match item codes.

        Returns
        -------
        pd.DataFrame with columns:
            SLM_pred, SLM_High, SLM_Intermediate, SLM_Low,
            SCM_pred, SCM_High, SCM_Intermediate, SCM_Low,
            SVM_pred, SVM_High, SVM_Intermediate, SVM_Low
        """
        import pandas as pd

        col_map = item_cols or {i: i for i in self.item_order}
        records = []

        for _, row in df.iterrows():
            resp = {item: int(row[col]) for item, col in col_map.items()}
            res  = self.predict(resp, verbose=False)
            rec  = {}
            for scale in ['SLM','SCM','SVM']:
                rec[f'{scale}_pred'] = res[scale]['predicted_label']
                for lv in ['High','Intermediate','Low']:
                    rec[f'{scale}_{lv}'] = res[scale]['probabilities'][lv]
            records.append(rec)

        return pd.DataFrame(records, index=df.index)

    # ── Display ───────────────────────────────────────────────
    def _print_results(self, raw, scored, results):
        print("\n" + "="*60)
        print("  TIMSS Mathematics Affective Profile")
        print("="*60)

        print("\n[Responses entered]")
        for item in self.item_order:
            info = self.battery[item]
            r    = raw[item]
            s    = scored[item]
            rev  = " (R→{})".format(s) if info['reverse'] else ""
            print(f"  {item} [{info['origin']}] = {r}{rev}  "
                  f"| {RESPONSE_SCALE[r]}")

        print("\n[Predictions]")
        scale_names = {
            'SLM': 'Students Like Learning Math (SLM)',
            'SCM': 'Students Confident in Math   (SCM)',
            'SVM': 'Students Value Mathematics   (SVM)',
        }
        for scale in ['SLM','SCM','SVM']:
            r   = results[scale]
            lbl = r['predicted_label']
            p   = r['probabilities']
            bar = {
                'High':         '█' * int(p['High']*20),
                'Intermediate': '█' * int(p['Intermediate']*20),
                'Low':          '█' * int(p['Low']*20),
            }
            print(f"\n  {scale_names[scale]}")
            print(f"  → Predicted: {lbl}")
            for lv in ['High','Intermediate','Low']:
                marker = " ◄" if lv == lbl else ""
                print(f"     {lv:13s}: {p[lv]:.3f}  {bar[lv]}{marker}")
        print("\n" + "="*60)

    def show_battery(self):
        """Print the 7-item battery with descriptions."""
        print("\n" + "="*70)
        print("  7-item Cross-construct Battery")
        print("  (Di Martino & Zan, 2010 TMA framework)")
        print("="*70)
        header = f"{'No.':<4}{'Item':<12}{'Scale':<6}{'TMA Dimension':<25}{'R':<3}Content"
        print(header)
        print("-"*70)
        for i, item in enumerate(self.item_order, 1):
            info = self.battery[item]
            rev  = "R" if info['reverse'] else ""
            print(f"{i:<4}{item:<12}{info['origin']:<6}"
                  f"{info['tma']:<25}{rev:<3}{info['content']}")
        print("\nResponse scale: 1=Agree a lot  2=Agree a little"
              "  3=Disagree a little  4=Disagree a lot")
        print("R = reverse-scored (5 − raw response)")

    def show_performance(self):
        """Print model performance metrics."""
        print("\n[Model Performance  |  N=232,970  |  44 countries]")
        print(f"{'Scale':<8}{'AUROC':>8}{'Kappa':>8}  Note")
        print("-"*50)
        notes = {
            'SLM': '71% cross-scale items in plateau set',
            'SCM': '67% cross-scale items; SCM most cross-construct dependent',
            'SVM': '0% cross-scale; value is most independent construct',
        }
        for scale in ['SLM','SCM','SVM']:
            perf = self.params[scale]['performance']
            print(f"{scale:<8}{perf['AUROC']:>8.3f}{perf['Kappa']:>8.3f}"
                  f"  {notes[scale]}")


# ── Example usage ─────────────────────────────────────────────
if __name__ == "__main__":

    pred = MathAffectPredictor()

    # Show the battery
    pred.show_battery()

    # Show model performance
    pred.show_performance()

    # Example 1: Single student — high engagement
    print("\n\n[Example 1: High-engagement student]")
    result1 = pred.predict({
        'BSBM23E': 1,   # Agree a lot: would like job involving math
        'BSBM23A': 1,   # Agree a lot: math helps daily life
        'BSBM19D': 1,   # Agree a lot: learn interesting things
        'BSBM23H': 2,   # Agree a little: parents think important
        'BSBM22A': 1,   # Agree a lot: usually do well
        'BSBM22B': 4,   # Disagree a lot: NOT harder than classmates (R)
        'BSBM19A': 1,   # Agree a lot: enjoy learning math
    })

    # Example 2: Low-engagement student
    print("\n\n[Example 2: Low-engagement student]")
    result2 = pred.predict({
        'BSBM23E': 4,
        'BSBM23A': 3,
        'BSBM19D': 4,
        'BSBM23H': 3,
        'BSBM22A': 3,
        'BSBM22B': 1,   # Agree a lot: IS harder than classmates (R→low confidence)
        'BSBM19A': 4,
    })

    # Example 3: Batch prediction
    print("\n\n[Example 3: Batch prediction for 3 students]")
    import pandas as pd
    df_students = pd.DataFrame([
        {'BSBM23E':1,'BSBM23A':1,'BSBM19D':1,'BSBM23H':2,
         'BSBM22A':1,'BSBM22B':4,'BSBM19A':1, 'StudentID':'S001'},
        {'BSBM23E':3,'BSBM23A':3,'BSBM19D':3,'BSBM23H':3,
         'BSBM22A':3,'BSBM22B':2,'BSBM19A':3, 'StudentID':'S002'},
        {'BSBM23E':2,'BSBM23A':1,'BSBM19D':2,'BSBM23H':1,
         'BSBM22A':2,'BSBM22B':3,'BSBM19A':2, 'StudentID':'S003'},
    ]).set_index('StudentID')

    preds_df = pred.predict_batch(df_students)
    print(preds_df.to_string())
