"""GSM8K evaluation for quantized MLX LLMs.

Measures grade-school math accuracy to verify quantized models
still reason correctly, not just produce fluent text.
"""

import os
import re
from dataclasses import dataclass

import numpy as np


# 3-shot exemplars for chain-of-thought prompting
FEW_SHOT_EXAMPLES = [
    {
        "question": "There are 15 trees in the grove. Grove workers will plant trees in the grove today. After they are done, there will be 21 trees. How many trees did the grove workers plant today?",
        "answer": "There are 15 trees originally. Then there were 21 trees after some more were planted. So there must have been 21 - 15 = 6 trees planted.\n#### 6",
    },
    {
        "question": "If there are 3 cars in the parking lot and 2 more cars arrive, how many cars are in the parking lot?",
        "answer": "There are originally 3 cars. 2 more cars arrive. 3 + 2 = 5.\n#### 5",
    },
    {
        "question": "Leah had 32 chocolates and her sister had 42. If they ate 35, how many pieces do they have left in total?",
        "answer": "Originally, Leah had 32 chocolates. Her sister had 42. So in total they had 32 + 42 = 74. After eating 35, they had 74 - 35 = 39.\n#### 39",
    },
]


@dataclass
class GSM8KResult:
    n_correct: int
    n_total: int
    accuracy: float
    per_question: list[dict]


def _build_prompt(question: str, n_shots: int = 3) -> str:
    """Build a few-shot prompt for GSM8K."""
    prompt = ""
    for ex in FEW_SHOT_EXAMPLES[:n_shots]:
        prompt += f"Q: {ex['question']}\nA: {ex['answer']}\n\n"
    prompt += f"Q: {question}\nA:"
    return prompt


def _extract_answer(text: str) -> str | None:
    """Extract the numeric answer after #### from model output."""
    # Look for #### marker
    match = re.search(r"####\s*(-?[\d,]+\.?\d*)", text)
    if match:
        return match.group(1).replace(",", "").strip()

    # Fallback: find the last number in the text
    numbers = re.findall(r"-?[\d,]+\.?\d*", text)
    if numbers:
        return numbers[-1].replace(",", "").strip()

    return None


def _extract_ground_truth(answer_text: str) -> str:
    """Extract numeric answer from GSM8K ground truth (after ####)."""
    match = re.search(r"####\s*(-?[\d,]+\.?\d*)", answer_text)
    if match:
        return match.group(1).replace(",", "").strip()
    return ""


def _normalize_number(s: str) -> float | None:
    """Parse a number string to float for comparison."""
    try:
        return float(s.replace(",", ""))
    except (ValueError, TypeError):
        return None


def evaluate_gsm8k(
    model_path: str,
    n_samples: int = 200,
    n_shots: int = 3,
    max_tokens: int = 256,
    seed: int = 42,
) -> GSM8KResult:
    """Evaluate an MLX LLM on GSM8K test set.

    Args:
        model_path: Path to MLX model directory
        n_samples: Number of test questions to evaluate
        n_shots: Number of few-shot examples in prompt
        max_tokens: Max tokens to generate per question
        seed: Random seed for sample selection

    Returns:
        GSM8KResult with accuracy and per-question details
    """
    import mlx.core as mx
    from mlx_lm import load, generate
    from mlx_lm.sample_utils import make_sampler
    from datasets import load_dataset
    from tqdm import tqdm

    # Resolve to absolute path so mlx_lm treats it as local, not HF repo
    if os.path.isdir(model_path):
        model_path = os.path.abspath(model_path)
    print(f"  Loading model from {model_path}...")
    model, tokenizer = load(model_path)

    print(f"  Loading GSM8K test set...")
    ds = load_dataset("openai/gsm8k", "main", split="test")

    # Sample subset
    rng = np.random.RandomState(seed)
    indices = rng.choice(len(ds), size=min(n_samples, len(ds)), replace=False)
    indices.sort()

    n_correct = 0
    per_question = []

    print(f"  Evaluating {len(indices)} questions ({n_shots}-shot)...")
    for idx in tqdm(indices, desc="  GSM8K eval"):
        item = ds[int(idx)]
        question = item["question"]
        gt_answer = _extract_ground_truth(item["answer"])

        prompt = _build_prompt(question, n_shots=n_shots)

        # Generate with greedy decoding for deterministic math
        sampler = make_sampler(temp=0.0)
        output = generate(
            model, tokenizer,
            prompt=prompt,
            max_tokens=max_tokens,
            verbose=False,
            sampler=sampler,
        )

        predicted = _extract_answer(output)

        gt_num = _normalize_number(gt_answer)
        pred_num = _normalize_number(predicted) if predicted else None

        correct = (gt_num is not None and pred_num is not None
                   and abs(gt_num - pred_num) < 1e-3)
        if correct:
            n_correct += 1

        per_question.append({
            "idx": int(idx),
            "question": question[:100] + "...",
            "ground_truth": gt_answer,
            "predicted": predicted,
            "correct": correct,
        })

    accuracy = n_correct / len(indices) if indices.size > 0 else 0.0

    return GSM8KResult(
        n_correct=n_correct,
        n_total=len(indices),
        accuracy=accuracy,
        per_question=per_question,
    )


def print_gsm8k_report(result: GSM8KResult):
    """Print GSM8K evaluation results."""
    print(f"\n  GSM8K Results")
    print(f"  {'=' * 50}")
    print(f"  Accuracy: {result.n_correct}/{result.n_total} "
          f"({result.accuracy:.1%})")

    # Show some examples
    wrong = [q for q in result.per_question if not q["correct"]]
    right = [q for q in result.per_question if q["correct"]]

    if right:
        print(f"\n  Correct examples:")
        for q in right[:3]:
            print(f"    Q: {q['question']}")
            print(f"    GT: {q['ground_truth']}, Pred: {q['predicted']}")

    if wrong:
        print(f"\n  Incorrect examples:")
        for q in wrong[:3]:
            print(f"    Q: {q['question']}")
            print(f"    GT: {q['ground_truth']}, Pred: {q['predicted']}")
