"""Produce vLLM-loadable quantized checkpoints.

compressor methods (int8/awq/gptq) use llm-compressor `oneshot`.
bnb methods (bnb8/bnb4) use transformers + BitsAndBytesConfig + save_pretrained.
baseline/online (bf16/fp8) need no build.
"""
from __future__ import annotations

from pathlib import Path
from typing import List

from scripts.quant.recipes import METHOD_SPECS


def needs_build(method: str) -> bool:
    return METHOD_SPECS[method]["kind"] in ("compressor", "bnb")


def _is_built(out_dir: Path) -> bool:
    return (out_dir / "config.json").exists()


CALIB_MAX_SEQ_LEN = 8192
# Never quantize the LM head or the vision tower: vision modules are shape-fragile
# (see the FP8 Marlin failure) and contribute little to decode cost.
QUANT_IGNORE = ["re:.*lm_head", "re:.*visual.*", "re:.*vision.*"]


def _calibration_dataset(calib_images: List[Path], processor):
    """HF Dataset of pre-tokenized multimodal samples (batch dim kept), matching
    llm-compressor's multimodal-vision examples; re-tensorized by _data_collator.
    Activation calibration only needs representative forward passes, not the
    exact training prompt."""
    from datasets import Dataset
    from PIL import Image

    samples = []
    for path in calib_images:
        image = Image.open(path).convert("RGB")
        messages = [{
            "role": "user",
            "content": [
                {"type": "image"},
                {"type": "text", "text": "OCR this document."},
            ],
        }]
        prompt = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
        inputs = processor(
            text=[prompt], images=[image],
            padding=False, truncation=True, max_length=CALIB_MAX_SEQ_LEN,
        )
        samples.append({k: (v.tolist() if hasattr(v, "tolist") else v) for k, v in inputs.items()})
    return Dataset.from_list(samples)


def _data_collator(batch):
    import torch

    assert len(batch) == 1
    return {key: torch.tensor(value) for key, value in batch[0].items()}


def _build_compressor(method: str, base_model: str, out_dir: Path, calib_images: List[Path]) -> None:
    from llmcompressor import oneshot
    from llmcompressor.modifiers.quantization import GPTQModifier
    from transformers import AutoProcessor

    try:
        from transformers import AutoModelForImageTextToText as _AutoModel
    except ImportError:  # older transformers
        from transformers import AutoModelForCausalLM as _AutoModel

    spec = METHOD_SPECS[method]
    model = _AutoModel.from_pretrained(base_model, dtype="auto", device_map="auto")
    processor = AutoProcessor.from_pretrained(base_model)
    dataset = _calibration_dataset(calib_images, processor)

    if spec["modifier"] == "awq":
        # llm-compressor main: AWQ is a transform paired with a QuantizationModifier.
        from llmcompressor.modifiers.quantization import QuantizationModifier
        try:
            from llmcompressor.modifiers.transform.awq import AWQModifier
        except ImportError:  # older layouts keep AWQModifier under modifiers.awq
            from llmcompressor.modifiers.awq import AWQModifier
        recipe = [
            AWQModifier(duo_scaling=False),
            QuantizationModifier(scheme=spec["scheme"], ignore=QUANT_IGNORE),
        ]
    else:
        recipe = []
        if spec.get("smoothquant"):
            from llmcompressor.modifiers.smoothquant import SmoothQuantModifier
            recipe.append(SmoothQuantModifier(smoothing_strength=0.8))
        recipe.append(GPTQModifier(targets="Linear", scheme=spec["scheme"], ignore=QUANT_IGNORE))

    oneshot(
        model=model,
        dataset=dataset,
        recipe=recipe,
        max_seq_length=CALIB_MAX_SEQ_LEN,
        num_calibration_samples=len(dataset),
        data_collator=_data_collator,
    )
    out_dir.mkdir(parents=True, exist_ok=True)
    model.save_pretrained(out_dir, save_compressed=True)
    processor.save_pretrained(out_dir)


def _build_bnb(method: str, base_model: str, out_dir: Path) -> None:
    import torch
    from transformers import AutoProcessor, BitsAndBytesConfig

    try:
        from transformers import AutoModelForImageTextToText as _AutoModel
    except ImportError:
        from transformers import AutoModelForCausalLM as _AutoModel

    bits = METHOD_SPECS[method]["bits"]
    if bits == 8:
        config = BitsAndBytesConfig(load_in_8bit=True)
    else:
        config = BitsAndBytesConfig(
            load_in_4bit=True,
            bnb_4bit_quant_type="nf4",
            bnb_4bit_compute_dtype=torch.float16,
            bnb_4bit_use_double_quant=True,
        )
    model = _AutoModel.from_pretrained(base_model, quantization_config=config, device_map="auto")
    model.save_pretrained(out_dir)
    AutoProcessor.from_pretrained(base_model).save_pretrained(out_dir)


def build_model(method: str, base_model: str, out_dir: Path, calib_images: List[Path]) -> Path:
    if not needs_build(method):
        return Path(base_model)
    out_dir = Path(out_dir)
    if _is_built(out_dir):
        return out_dir
    kind = METHOD_SPECS[method]["kind"]
    if kind == "compressor":
        _build_compressor(method, base_model, out_dir, calib_images)
    else:
        _build_bnb(method, base_model, out_dir)
    return out_dir
