from __future__ import annotations

import argparse
import fnmatch
import json
import os
import sys
from pathlib import Path

from huggingface_hub import HfApi

from common import ROOT, iter_model_card_paths, localize_workspace_path, read_yaml_mapping

DEFAULT_IGNORE_PATTERNS = [
    "*.msgpack",
    "*.h5",
    "*.ot",
    "*.tflite",
    "*.onnx",
    "*.pb",
    "*.ckpt",
    "flax_model*",
    "tf_model*",
    "rust_model*",
]


def matches_any(name: str, patterns: list[str]) -> bool:
    return any(fnmatch.fnmatch(name, pattern) for pattern in patterns)


def desired_patterns(card: dict) -> list[str]:
    if card.get("format") == "gguf" and card.get("filename"):
        return [str(card["filename"]), "README.md", "*.json", "*.txt", "*.model"]
    if card.get("upstream_id") == "Qwen/Qwen-Image-Edit":
        return [
            "README.md",
            "LICENSE.md",
            "model_index.json",
            "processor/*",
            "scheduler/*.json",
            "tokenizer/*",
            "text_encoder/*.json",
            "text_encoder/*.safetensors",
            "text_encoder/*.safetensors.index.json",
            "transformer/*.json",
            "transformer/*.safetensors",
            "transformer/*.safetensors.index.json",
            "vae/*.json",
            "vae/*.safetensors",
        ]
    if card.get("format") == "diffusers" or card.get("type") == "image-diffusion-model":
        upstream_id = str(card.get("upstream_id") or "")
        # Many SDXL community repos publish standard .safetensors names instead of *fp16.safetensors.
        # Keep the narrow fp16 defaults for repos that provide them, and widen only for known SDXL variants
        # we want to support in the current runtime.
        if upstream_id in {
            "cagliostrolab/animagine-xl-4.0",
            "glides/illustriousxl",
        }:
            return [
                "README.md",
                "LICENSE.md",
                "model_index.json",
                "scheduler/*.json",
                "tokenizer/*",
                "tokenizer_2/*",
                "text_encoder/*.json",
                "text_encoder/*.safetensors",
                "text_encoder_2/*.json",
                "text_encoder_2/*.safetensors",
                "unet/*.json",
                "unet/*.safetensors",
                "vae/*.json",
                "vae/*.safetensors",
            ]
        return [
            "README.md",
            "LICENSE.md",
            "model_index.json",
            "scheduler/*.json",
            "tokenizer/*",
            "tokenizer_2/*",
            "text_encoder/*.json",
            "text_encoder/*fp16.safetensors",
            "text_encoder_2/*.json",
            "text_encoder_2/*fp16.safetensors",
            "unet/*.json",
            "unet/*fp16.safetensors",
            "vae/*.json",
            "vae/*fp16.safetensors",
        ]
    model_type = str(card.get("type") or "")
    if model_type == "speech-model":
        return ["*.safetensors", "*.bin", "*.json", "*.txt", "*.model", "*.md"]
    if model_type in {"translation-model", "vision-language-model", "base-model"}:
        return ["*.safetensors", "*.bin", "*.json", "*.txt", "*.model", "*.jinja", "*.md"]
    return ["*.safetensors", "*.bin", "*.gguf", "*.json", "*.txt", "*.model", "*.md"]


def remote_plan(card_path: Path, *, include_ready: bool = False) -> dict | None:
    card = read_yaml_mapping(card_path)
    repo_id = card.get("upstream_id")
    if not repo_id:
        return None

    raw_storage_path = str(card.get("storage_path") or "")
    if os.environ.get("LLM_USE_RAW_MODEL_PATH") == "1" and raw_storage_path.startswith("/"):
        storage_path = Path(raw_storage_path)
    else:
        storage_path = localize_workspace_path(raw_storage_path)
    api = HfApi()
    info = api.model_info(repo_id=repo_id, files_metadata=True)
    allow = desired_patterns(card)
    files = []
    for sibling in info.siblings:
        name = sibling.rfilename
        if name == ".gitattributes":
            continue
        if matches_any(name, DEFAULT_IGNORE_PATTERNS):
            continue
        if not matches_any(name, allow):
            continue
        size = int(sibling.size or 0)
        local_path = storage_path / name
        local_size = local_path.stat().st_size if local_path.exists() else 0
        files.append(
            {
                "name": name,
                "size_bytes": size,
                "local_size_bytes": local_size,
                "missing_bytes": max(size - local_size, 0),
                "complete": size > 0 and local_size == size,
            }
        )

    missing_bytes = sum(item["missing_bytes"] for item in files if not item["complete"])
    local_ready = bool(files) and missing_bytes == 0
    if local_ready and not include_ready:
        return None

    return {
        "id": card.get("id"),
        "repo_id": repo_id,
        "storage_path": str(storage_path),
        "local_ready": local_ready,
        "allow_patterns": allow,
        "missing_bytes": missing_bytes,
        "missing_gb": round(missing_bytes / 1024**3, 2),
        "files": files,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Plan Hugging Face downloads for incomplete model cards.")
    parser.add_argument("--include-ready", action="store_true")
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    plans = []
    for card_path in iter_model_card_paths():
        try:
            plan = remote_plan(card_path, include_ready=args.include_ready)
        except Exception as exc:  # noqa: BLE001 - report per model and continue.
            print(f"WARN {card_path.relative_to(ROOT)}: {exc}", file=sys.stderr)
            continue
        if plan:
            plans.append(plan)

    if args.json:
        print(json.dumps({"plans": plans}, ensure_ascii=False, indent=2))
        return 0

    total = sum(plan["missing_bytes"] for plan in plans)
    print(f"Models needing downloads: {len(plans)} | missing: {round(total / 1024**3, 2)} GB")
    for plan in plans:
        missing_files = [item for item in plan["files"] if not item["complete"]]
        print(f"{plan['id']}: {plan['missing_gb']} GB missing -> {plan['storage_path']}")
        for item in missing_files[:12]:
            print(f"  - {item['name']} {round(item['missing_bytes'] / 1024**3, 2)} GB")
        if len(missing_files) > 12:
            print(f"  ... {len(missing_files) - 12} more file(s)")
    return 0


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