from __future__ import annotations

import argparse
from collections import OrderedDict
import json
from pathlib import Path
import sys
import torch

_THIS_DIR = Path(__file__).resolve().parent
_REPO_ROOT = _THIS_DIR.parent.parent
if str(_REPO_ROOT) not in sys.path:
    sys.path.insert(0, str(_REPO_ROOT))

from mmgp import safetensors2

from models.magi_human.checkpoint_schema import convert_transformer_state_dict_to_split_experts, preprocess_magi_lora_state_dict


def _load_state_dict_and_metadata(path: str):
    with safetensors2.safe_open(path, framework="pt", device="cpu", writable_tensors=False) as reader:
        metadata = reader.metadata() or {}
        state_dict = OrderedDict((key, reader.get_tensor(key)) for key in reader.keys())
    return state_dict, metadata


def _load_sharded_state_dict_and_metadata(index_path: str):
    with open(index_path, "r", encoding="utf-8") as reader:
        index_data = json.load(reader)
    shard_to_keys = OrderedDict()
    for key, shard_name in index_data["weight_map"].items():
        shard_to_keys.setdefault(shard_name, []).append(key)
    state_dict = OrderedDict()
    metadata = dict(index_data.get("metadata") or {})
    base_dir = Path(index_path).resolve().parent
    for shard_name, keys in shard_to_keys.items():
        shard_path = base_dir / shard_name
        with safetensors2.safe_open(str(shard_path), framework="pt", device="cpu", writable_tensors=False) as reader:
            if not metadata:
                metadata.update(reader.metadata() or {})
            for key in keys:
                state_dict[key] = reader.get_tensor(key)
    return state_dict, metadata


def _resolve_input_path(path: str):
    input_path = Path(path)
    if input_path.is_dir():
        index_path = input_path / "model.safetensors.index.json"
        if index_path.exists():
            return str(index_path)
        safetensors_path = input_path / "model.safetensors"
        if safetensors_path.exists():
            return str(safetensors_path)
    return str(input_path)


def _cast_state_dict_dtype(state_dict: Mapping[str, object], dtype: torch.dtype | None):
    if dtype is None:
        return OrderedDict(state_dict.items())
    casted = OrderedDict()
    for key, value in state_dict.items():
        if torch.is_tensor(value) and value.is_floating_point() and value.dtype != dtype:
            casted[key] = value.to(dtype)
        else:
            casted[key] = value
    return casted


def _load_any_state_dict_and_metadata(path: str, dtype: torch.dtype | None = None):
    resolved_path = _resolve_input_path(path)
    if resolved_path.endswith(".index.json"):
        state_dict, metadata = _load_sharded_state_dict_and_metadata(resolved_path)
    else:
        state_dict, metadata = _load_state_dict_and_metadata(resolved_path)
    return _cast_state_dict_dtype(state_dict, dtype), metadata


def convert_transformer_checkpoint(input_path: str, output_path: str, dtype: torch.dtype | None = torch.bfloat16) -> None:
    state_dict, metadata = _load_any_state_dict_and_metadata(input_path, dtype=dtype)
    config = metadata.get("config")
    converted = convert_transformer_state_dict_to_split_experts(state_dict)
    safetensors2.torch_write_file(converted, output_path, config=config)


def convert_lora_checkpoint(input_path: str, output_path: str, dtype: torch.dtype | None = None) -> None:
    state_dict, metadata = _load_any_state_dict_and_metadata(input_path, dtype=dtype)
    converted = preprocess_magi_lora_state_dict(state_dict)
    extra_meta = {k: v for k, v in metadata.items() if k not in {"config", "format"}}
    safetensors2.torch_write_file(converted, output_path, config=metadata.get("config"), extra_meta=extra_meta or None)


def main() -> None:
    parser = argparse.ArgumentParser(description="Convert Magi Human checkpoints or LoRAs to the split-expert schema.")
    parser.add_argument("input", type=str, help="Source safetensors path")
    parser.add_argument("output", type=str, help="Converted safetensors path")
    parser.add_argument("--kind", choices=("transformer", "lora"), default="transformer")
    parser.add_argument("--dtype", choices=("keep", "bf16", "fp16"), default="bf16")
    args = parser.parse_args()

    Path(args.output).parent.mkdir(parents=True, exist_ok=True)
    dtype = None if args.dtype == "keep" else (torch.bfloat16 if args.dtype == "bf16" else torch.float16)
    if args.kind == "transformer":
        convert_transformer_checkpoint(args.input, args.output, dtype=dtype)
    else:
        convert_lora_checkpoint(args.input, args.output, dtype=dtype if args.dtype != "keep" else None)


if __name__ == "__main__":
    main()
