#!/usr/bin/env python3
"""Verify pinned Hugging Face ModernBERT -> MLX backbone parity."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Sequence

import numpy as np

from deep_contract import DEEP_VARIANTS, validate_variant_config
from purpose_data import DataError
from train_deep_mlx import _configure_mlx_device, _resolve_source


def verify(variant_name: str, local_model: Path | None, device: str) -> float:
    try:
        import mlx.core as mx
        import torch
        from transformers import ModernBertForMaskedLM

        from deep_model_mlx import (
            ModernBertForPurposeClassification,
            ModernBertPurposeConfig,
            load_pretrained_weights,
        )
    except ImportError as exc:
        raise DataError(
            "deep parity requires PyTorch, Transformers, and MLX"
        ) from exc

    _configure_mlx_device(mx, device)
    variant = DEEP_VARIANTS[variant_name]
    source = _resolve_source(variant, local_model)
    try:
        config_json = json.loads(
            (source / "config.json").read_text(encoding="utf-8")
        )
    except (OSError, json.JSONDecodeError) as exc:
        raise DataError(f"cannot read ModernBERT config: {exc}") from exc
    validate_variant_config(config_json, variant)

    torch.set_num_threads(1)
    reference = ModernBertForMaskedLM.from_pretrained(
        source,
        local_files_only=True,
        attn_implementation="eager",
    )
    reference.eval()
    candidate = ModernBertForPurposeClassification(
        ModernBertPurposeConfig.from_hugging_face(
            config_json, gradient_checkpointing=False
        )
    )
    report = load_pretrained_weights(candidate, source / "model.safetensors")
    candidate.eval()

    # 96 tokens crosses the local layer's 64-token half-window, so this catches
    # both full-attention and sliding-window-mask parity without a slow 512-token
    # CPU reference pass.
    rng = np.random.default_rng(20260731)
    input_ids = rng.integers(
        3, int(config_json["vocab_size"]) - 1, size=(2, 96), dtype=np.int32
    )
    input_ids[:, 0] = int(config_json["bos_token_id"])
    input_ids[:, -1] = int(config_json["eos_token_id"])
    attention_mask = np.ones_like(input_ids, dtype=np.int32)
    attention_mask[0, -11:] = 0
    input_ids[0, -11:] = int(config_json["pad_token_id"])

    with torch.inference_mode():
        sequence = reference.model(
            input_ids=torch.from_numpy(input_ids.astype(np.int64)),
            attention_mask=torch.from_numpy(attention_mask.astype(np.int64)),
        ).last_hidden_state
        reference_pooled = reference.head(sequence[:, 0]).cpu().numpy()
    candidate_sequence = candidate.model(
        mx.array(input_ids), mx.array(attention_mask)
    )
    candidate_pooled = candidate.head(candidate_sequence[:, 0])
    mx.eval(candidate_pooled)
    error = float(
        np.max(np.abs(reference_pooled - np.asarray(candidate_pooled)))
    )
    print(
        f"{variant.model_id}@{variant.revision}: "
        f"loaded={report['loaded']} ignored={report['ignored']} "
        f"pooled_max_abs_error={error:.3g}",
        flush=True,
    )
    if not np.isfinite(error) or error > 5e-4:
        raise DataError(
            f"ModernBERT MLX parity failed: max abs error {error:.6g} > 0.0005"
        )
    return error


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--variant", choices=tuple(DEEP_VARIANTS), default="base")
    parser.add_argument("--model", type=Path)
    parser.add_argument("--device", choices=("metal", "cpu"), default="metal")
    return parser


def main(argv: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    try:
        verify(args.variant, args.model, args.device)
    except (DataError, OSError, RuntimeError, ValueError) as exc:
        print(f"error: {exc}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
