from __future__ import annotations

from collections import OrderedDict
import re
from typing import Mapping


MODALITY_NAMES = ("video", "audio", "text")
MM_LAYERS = frozenset((0, 1, 2, 3, 36, 37, 38, 39))
Q_SIZE = 5120
KV_SIZE = 8 * 128
G_SIZE = 5120 // 128
EXPERT_LINEAR_NAMES = frozenset((
    "attention.linear_q",
    "attention.linear_k",
    "attention.linear_v",
    "attention.linear_g",
    "attention.linear_proj",
    "mlp.up_gate_proj",
    "mlp.down_proj",
))
_EXPERT_KEY_RE = re.compile(r"^(block\.layers\.(\d+)\.(" + "|".join(re.escape(name) for name in sorted(EXPERT_LINEAR_NAMES)) + r"))(\..+)$")
_EXPERT_SUFFIX_RE = re.compile(r"^(block\.layers\.(\d+)\.(" + "|".join(re.escape(name) for name in sorted(EXPERT_LINEAR_NAMES)) + r"))_(video|audio|text)(\..+)$")
_QKVG_KEY_RE = re.compile(r"^(block\.layers\.(\d+)\.attention\.linear_qkv)(\..+)$")
_ROW_SPLIT_SUFFIXES = (".weight", ".bias", ".lora_B.weight", ".lora_up.weight", ".diff_b")
_SHARED_SUFFIXES = (".lora_A.weight", ".lora_down.weight", ".alpha", ".dora_scale", ".diff")


def _match_expert_key(key: str):
    match = _EXPERT_KEY_RE.match(key)
    if match is None:
        return None
    layer_idx = int(match.group(2))
    if layer_idx not in MM_LAYERS:
        return None
    return match


def _is_already_split(key: str) -> bool:
    return _EXPERT_SUFFIX_RE.match(key) is not None


def _split_tensor_rows(value, parts: int):
    if value.shape[0] % parts != 0:
        raise ValueError(f"Cannot split tensor with shape {tuple(value.shape)} into {parts} equal row chunks.")
    return value.chunk(parts, dim=0)


def _iter_qkvg_targets(layer_idx: int, suffix: str, value):
    num_modality = len(MODALITY_NAMES) if layer_idx in MM_LAYERS else 1
    offset = 0
    for modality_idx in range(num_modality):
        modality_name = MODALITY_NAMES[modality_idx]
        modality_suffix = f"_{modality_name}" if num_modality > 1 else ""
        for proj_name, proj_size in (("linear_q", Q_SIZE), ("linear_k", KV_SIZE), ("linear_v", KV_SIZE), ("linear_g", G_SIZE)):
            chunk = value.narrow(0, offset, proj_size)
            offset += proj_size
            yield f"block.layers.{layer_idx}.attention.{proj_name}{modality_suffix}{suffix}", chunk


def convert_transformer_state_dict_to_split_experts(state_dict: Mapping[str, object]) -> OrderedDict[str, object]:
    new_state_dict: OrderedDict[str, object] = OrderedDict()
    for key, value in state_dict.items():
        qkvg_match = _QKVG_KEY_RE.match(key)
        if qkvg_match is not None:
            layer_idx = int(qkvg_match.group(2))
            suffix = qkvg_match.group(3)
            if suffix in (".weight", ".bias"):
                for new_key, chunk in _iter_qkvg_targets(layer_idx, suffix, value):
                    new_state_dict[new_key] = chunk
                continue
        match = _match_expert_key(key)
        if match is None or _is_already_split(key):
            new_state_dict[key] = value
            continue
        base_key, suffix = match.group(1), match.group(4)
        if suffix not in (".weight", ".bias"):
            new_state_dict[key] = value
            continue
        chunks = _split_tensor_rows(value, len(MODALITY_NAMES))
        for modality_name, chunk in zip(MODALITY_NAMES, chunks):
            new_state_dict[f"{base_key}_{modality_name}{suffix}"] = chunk
    return new_state_dict


def preprocess_magi_lora_state_dict(state_dict: Mapping[str, object]) -> OrderedDict[str, object]:
    first_key = next(iter(state_dict), None)
    if first_key is None or _is_already_split(first_key):
        return OrderedDict(state_dict.items())

    new_state_dict: OrderedDict[str, object] = OrderedDict()
    for key, value in state_dict.items():
        qkvg_match = _QKVG_KEY_RE.match(key)
        if qkvg_match is not None:
            layer_idx = int(qkvg_match.group(2))
            suffix = qkvg_match.group(3)
            if suffix in _ROW_SPLIT_SUFFIXES:
                for new_key, chunk in _iter_qkvg_targets(layer_idx, suffix, value):
                    new_state_dict[new_key] = chunk
                continue
            if suffix in _SHARED_SUFFIXES:
                num_modality = len(MODALITY_NAMES) if layer_idx in MM_LAYERS else 1
                for modality_idx in range(num_modality):
                    modality_name = MODALITY_NAMES[modality_idx]
                    modality_suffix = f"_{modality_name}" if num_modality > 1 else ""
                    for proj_name in ("linear_q", "linear_k", "linear_v", "linear_g"):
                        new_state_dict[f"block.layers.{layer_idx}.attention.{proj_name}{modality_suffix}{suffix}"] = value
                continue
        match = _match_expert_key(key)
        if match is None:
            new_state_dict[key] = value
            continue
        base_key, suffix = match.group(1), match.group(4)
        if suffix in _ROW_SPLIT_SUFFIXES:
            chunks = _split_tensor_rows(value, len(MODALITY_NAMES))
            for modality_name, chunk in zip(MODALITY_NAMES, chunks):
                new_state_dict[f"{base_key}_{modality_name}{suffix}"] = chunk
        elif suffix in _SHARED_SUFFIXES:
            for modality_name in MODALITY_NAMES:
                new_state_dict[f"{base_key}_{modality_name}{suffix}"] = value
        else:
            new_state_dict[key] = value
    return new_state_dict
