"""AI2D evaluation for Vision-Language Models.

Evaluates VQA accuracy on AI2 Diagrams — multiple-choice visual questions
about scientific diagrams (food webs, life cycles, etc.). Uses lmms-lab/ai2d
from HuggingFace Hub.

This is the VLM equivalent of GSM8K: clear right/wrong answers, exercises
genuine visual reasoning, and is sensitive to quantization quality degradation.
"""

import re
from dataclasses import dataclass, field

import numpy as np
from tqdm import tqdm


@dataclass
class AI2DResult:
    n_correct: int
    n_total: int
    accuracy: float
    per_question: list[dict] = field(default_factory=list)


def _build_prompt(question: str, options: list[str]) -> str:
    """Build a multiple-choice VQA prompt."""
    labels = ["A", "B", "C", "D", "E", "F", "G", "H"]
    choices = "\n".join(f"{labels[i]}. {opt}" for i, opt in enumerate(options))
    return (
        f"{question}\n{choices}\n"
        f"Answer with the letter of the correct option."
    )


def _extract_answer_letter(text: str, n_options: int) -> str | None:
    """Extract the answer letter from model output."""
    text = text.strip()
    valid = set("ABCDEFGH"[:n_options])

    # Check if response starts with a valid letter
    if text and text[0].upper() in valid:
        return text[0].upper()

    # Look for patterns like "A.", "A)", "Answer: A", "The answer is A"
    patterns = [
        r"(?:answer|option)\s*(?:is|:)\s*([A-H])",
        r"\b([A-H])\s*[\.\):]",
        r"\b([A-H])\b",
    ]
    for pattern in patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            letter = match.group(1).upper()
            if letter in valid:
                return letter

    return None


def evaluate_ai2d(
    model,
    processor,
    n_samples: int = 50,
    max_new_tokens: int = 32,
    max_image_size: int = 512,
    seed: int = 42,
) -> AI2DResult:
    """Evaluate a VLM on AI2D diagram understanding.

    Args:
        model: PyTorch VLM (e.g., Qwen3-VL)
        processor: Model's processor (tokenizer + image processor)
        n_samples: Number of test questions to evaluate
        max_new_tokens: Max tokens to generate per question
        max_image_size: Max image dimension (limits visual token count for speed)
        seed: Random seed for sample selection

    Returns:
        AI2DResult with accuracy and per-question details
    """
    import torch
    from datasets import load_dataset
    from PIL import Image

    device = next(model.parameters()).device

    print("  Loading AI2D test set from HuggingFace...")
    ds = load_dataset("lmms-lab/ai2d", split="test")

    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)} AI2D questions...")
    for idx in tqdm(indices, desc="  AI2D eval"):
        item = ds[int(idx)]
        img = item["image"]
        if not isinstance(img, Image.Image):
            img = Image.open(img)
        img = img.convert("RGB")

        # Limit image size to control visual token count and inference time
        w, h = img.size
        if max(w, h) > max_image_size:
            scale = max_image_size / max(w, h)
            img = img.resize((int(w * scale), int(h * scale)), Image.BILINEAR)

        question = item["question"]
        options = item["options"]
        gt_idx = int(item["answer"])
        labels = ["A", "B", "C", "D", "E", "F", "G", "H"]
        gt_letter = labels[gt_idx]

        prompt_text = _build_prompt(question, options)

        try:
            # Build multimodal input using chat template
            if hasattr(processor, "apply_chat_template"):
                messages = [
                    {
                        "role": "user",
                        "content": [
                            {"type": "image"},
                            {"type": "text", "text": prompt_text},
                        ],
                    }
                ]
                text = processor.apply_chat_template(
                    messages, tokenize=False, add_generation_prompt=True
                )
                inputs = processor(text=[text], images=[img], return_tensors="pt")
            else:
                inputs = processor(text=prompt_text, images=img, return_tensors="pt")

            inputs = {k: v.to(device) if hasattr(v, 'to') else v
                      for k, v in inputs.items()}

            with torch.no_grad():
                output_ids = model.generate(
                    **inputs,
                    max_new_tokens=max_new_tokens,
                    do_sample=False,
                )

            # Decode only the generated tokens
            input_len = inputs.get("input_ids", torch.tensor([[]])).shape[-1]
            generated = output_ids[0][input_len:]
            output_text = processor.decode(generated, skip_special_tokens=True)

            predicted = _extract_answer_letter(output_text, len(options))
            correct = predicted == gt_letter

        except Exception as e:
            output_text = f"ERROR: {e}"
            predicted = None
            correct = False

        if correct:
            n_correct += 1

        per_question.append({
            "idx": int(idx),
            "question": question[:100] + ("..." if len(question) > 100 else ""),
            "ground_truth": gt_letter,
            "predicted": predicted,
            "output": output_text[:200],
            "correct": correct,
        })

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

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


def evaluate_ai2d_mlx(
    model_path: str,
    n_samples: int = 100,
    max_new_tokens: int = 32,
    max_image_size: int = 512,
    seed: int = 42,
) -> AI2DResult:
    """Evaluate a quantized MLX VLM on AI2D diagram understanding.

    Args:
        model_path: Path to MLX model directory (converted via mlx-vlm).
        n_samples: Number of test questions to evaluate.
        max_new_tokens: Max tokens to generate per question.
        max_image_size: Max image dimension (limits visual token count).
        seed: Random seed for sample selection.

    Returns:
        AI2DResult with accuracy and per-question details.
    """
    import os
    from datasets import load_dataset
    from PIL import Image
    from mlx_vlm import load, generate
    from mlx_vlm.prompt_utils import apply_chat_template
    from mlx_vlm.utils import load_config

    # Resolve to absolute path
    if os.path.isdir(model_path):
        model_path = os.path.abspath(model_path)

    print(f"  Loading MLX VLM from {model_path}...")
    model, processor = load(model_path)
    config = load_config(model_path)

    print("  Loading AI2D test set from HuggingFace...")
    ds = load_dataset("lmms-lab/ai2d", split="test")

    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)} AI2D questions...")
    for idx in tqdm(indices, desc="  AI2D eval"):
        item = ds[int(idx)]
        img = item["image"]
        if not isinstance(img, Image.Image):
            img = Image.open(img)
        img = img.convert("RGB")

        w, h = img.size
        if max(w, h) > max_image_size:
            scale = max_image_size / max(w, h)
            img = img.resize((int(w * scale), int(h * scale)), Image.BILINEAR)

        question = item["question"]
        options = item["options"]
        gt_idx = int(item["answer"])
        labels = ["A", "B", "C", "D", "E", "F", "G", "H"]
        gt_letter = labels[gt_idx]

        prompt_text = _build_prompt(question, options)

        try:
            # Save image to temp file for mlx-vlm generate()
            import tempfile
            with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
                img.save(f, format="PNG")
                temp_path = f.name

            # Build prompt via mlx-vlm chat template
            formatted = apply_chat_template(processor, config, prompt_text, num_images=1)

            result = generate(
                model, processor,
                prompt=formatted,
                image=[temp_path],
                max_tokens=max_new_tokens,
                verbose=False,
                temp=0.0,
            )
            output_text = result.text if hasattr(result, 'text') else str(result)

            os.unlink(temp_path)

            predicted = _extract_answer_letter(output_text, len(options))
            correct = predicted == gt_letter

        except Exception as e:
            output_text = f"ERROR: {e}"
            predicted = None
            correct = False

        if correct:
            n_correct += 1

        per_question.append({
            "idx": int(idx),
            "question": question[:100] + ("..." if len(question) > 100 else ""),
            "ground_truth": gt_letter,
            "predicted": predicted,
            "output": str(output_text)[:200],
            "correct": correct,
        })

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

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


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

    right = [q for q in result.per_question if q["correct"]]
    wrong = [q for q in result.per_question if not 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']}")
            print(f"    Output: {q['output'][:100]}")
