"""Canonical CEFR / IELTS band / IRT theta mapper.

This is the SINGLE SOURCE OF TRUTH for band ↔ CEFR ↔ theta conversions across
the platform. All modules MUST import from here rather than hand-rolling their
own mapping tables. See §21 of docs/PROJECT_SUMMARY.md for the rationale.
"""


# IRT theta band thresholds (ability parameter → CEFR level)
THETA_TO_CEFR = [
    (-4.0, -2.5, 'pre_a1'),
    (-2.5, -1.5, 'a1'),
    (-1.5, -0.5, 'a2'),
    (-0.5, 0.5, 'b1'),
    (0.5, 1.5, 'b2'),
    (1.5, 2.5, 'c1'),
    (2.5, 4.0, 'c2'),
]

# CEFR code → canonical IELTS band centre value
CEFR_TO_BAND = {
    'pre_a1': 2.0,
    'a1': 3.0,
    'a2': 4.0,
    'b1': 5.0,
    'b2': 6.5,
    'c1': 7.5,
    'c2': 9.0,
}

# Lowercase CEFR code → human-readable label
CEFR_LABELS = {
    'pre_a1': 'Pre-A1 (Beginner)',
    'a1': 'A1 (Elementary)',
    'a2': 'A2 (Pre-Intermediate)',
    'b1': 'B1 (Intermediate)',
    'b2': 'B2 (Upper-Intermediate)',
    'c1': 'C1 (Advanced)',
    'c2': 'C2 (Proficient)',
}

# Monotonic (highest band → lowest) thresholds used by band_to_cefr.
# Picked to align CEFR_TO_BAND with widely-published IELTS/CEFR crosswalks
# (Cambridge, Council of Europe): 8.0+ → C2, 7.0-7.9 → C1, 5.5-6.9 → B2, etc.
_BAND_THRESHOLDS = [
    (8.0, 'c2'),
    (7.0, 'c1'),
    (5.5, 'b2'),
    (4.5, 'b1'),
    (3.5, 'a2'),
    (2.5, 'a1'),
    (0.0, 'pre_a1'),
]


def band_to_cefr(band):
    """Map an IELTS band (0-9, may be float) to a canonical CEFR code."""
    if band is None:
        return None
    try:
        b = float(band)
    except (TypeError, ValueError):
        return None
    for threshold, level in _BAND_THRESHOLDS:
        if b >= threshold:
            return level
    return 'pre_a1'


def theta_to_cefr(theta):
    """Map an IRT theta ability parameter (typically -4..+4) to a CEFR code."""
    try:
        t = float(theta)
    except (TypeError, ValueError):
        return 'pre_a1'
    for low, high, level in THETA_TO_CEFR:
        if low <= t < high:
            return level
    return 'c2' if t >= 2.5 else 'pre_a1'


def theta_to_band(theta):
    """Interpolate an IRT theta to an IELTS band, rounded to nearest 0.5."""
    try:
        t = float(theta)
    except (TypeError, ValueError):
        return 0.0
    cefr = theta_to_cefr(t)
    base_band = CEFR_TO_BAND.get(cefr, 5.0)
    for low, high, level in THETA_TO_CEFR:
        if level == cefr:
            width = high - low
            position = (t - low) / width if width > 0 else 0.5
            cefr_list = list(CEFR_TO_BAND.keys())
            idx = cefr_list.index(cefr)
            next_band = CEFR_TO_BAND.get(
                cefr_list[min(idx + 1, len(cefr_list) - 1)], base_band + 1.0
            )
            band = base_band + position * (next_band - base_band)
            return round(band * 2) / 2
    return base_band


def cefr_to_band(cefr):
    """Return the canonical centre-of-range band for a CEFR code."""
    if not cefr:
        return None
    return CEFR_TO_BAND.get(str(cefr).strip().lower().replace('-', '_'))


def normalize_cefr(label):
    """Canonicalise a CEFR label to the uppercase display form (e.g. 'A2', 'Pre-A1').

    Accepts 'a2', 'A2', 'pre_a1', 'Pre-A1', 'pre a1' etc. Returns ``None``
    if the input is falsy; returns the input unchanged if unrecognised.
    """
    if not label:
        return None
    s = str(label).strip().lower().replace('-', '_').replace(' ', '_')
    return {
        'pre_a1': 'Pre-A1',
        'a1': 'A1',
        'a2': 'A2',
        'b1': 'B1',
        'b2': 'B2',
        'c1': 'C1',
        'c2': 'C2',
    }.get(s, label)


def cefr_label(cefr_code):
    """Return the human-readable label (e.g. 'B2 (Upper-Intermediate)') for a code."""
    return CEFR_LABELS.get(cefr_code, cefr_code)


class CefrMapper:
    """Class-style facade kept for backward compatibility with ``encoach_placement``."""

    THETA_TO_CEFR = THETA_TO_CEFR
    CEFR_TO_BAND = CEFR_TO_BAND
    CEFR_LABELS = CEFR_LABELS

    theta_to_cefr = staticmethod(theta_to_cefr)
    theta_to_band = staticmethod(theta_to_band)
    band_to_cefr = staticmethod(band_to_cefr)

    @staticmethod
    def get_cefr_label(cefr_code):
        return cefr_label(cefr_code)
