# ---------------------------------------------------------------------
#  helper: ensure Gemma-3 checkpoints use  language_model.model.…  keys
# ---------------------------------------------------------------------
import os, json, logging
from pathlib import Path
from safetensors.torch import safe_open, save_file

log = logging.getLogger(__name__)


def unwrap_clippable_linears(model) -> int:
    """
    Replace remote-code clippable linear wrappers with their underlying
    torch.nn.Linear modules so vanilla PEFT can inject adapters.

    Some Gemma loaders expose projection modules as Gemma*ClippableLinear
    wrappers with the real Linear stored on `.linear`. PEFT's LoRA injection
    only accepts the inner Linear type.
    """
    count = 0

    for child_name, child in list(model.named_children()):
        count += unwrap_clippable_linears(child)

        inner = getattr(child, "linear", None)
        if inner is None:
            continue
        if child.__class__.__name__.endswith("ClippableLinear"):
            setattr(model, child_name, inner)
            count += 1

    return count

def fix_gemma3_checkpoint(ckpt_dir: str | Path) -> None:
    """
    If `ckpt_dir` is a Gemma-3 checkpoint whose tensor keys look like
        language_model.embed_tokens.weight
    instead of
        language_model.model.embed_tokens.weight
    rewrite the shards and index file in-place.

    No-op when:
      • model_type ≠ 'gemma3'
      • keys are already correct
      • required files are missing
    """
    ckpt_dir = Path(ckpt_dir)
    index_file = ckpt_dir / "model.safetensors.index.json"
    config_file = ckpt_dir / "config.json"
    if not index_file.is_file() or not config_file.is_file():
        return  # nothing to do

    # ── guard: only patch Gemma-3 checkpoints ───────────────────────────
    try:
        with open(config_file) as f:
            cfg = json.load(f)
        if (cfg.get("model_type") or "").lower() != "gemma3":
            return
    except Exception as e:
        log.warning("Could not read %s (%s); skipping fix.", config_file, e)
        return

    # ── scan weight map ─────────────────────────────────────────────────
    with open(index_file) as f:
        idx = json.load(f)

    wm = idx["weight_map"]
    broken = [
        k for k in wm
        if k.startswith("language_model.") and not k.startswith("language_model.model.")
    ]
    if not broken:
        return                                      # already fine

    log.info("Repairing Gemma-3 key prefixes in %s", ckpt_dir)

    def _fixed(k: str) -> str:
        if k.startswith("language_model.") and not k.startswith("language_model.model."):
            return "language_model.model." + k[len("language_model."):]
        return k

    # ── rewrite every shard exactly once ────────────────────────────────
    repaired_shards = set()
    for old_key, shard_name in wm.items():
        wm[_fixed(old_key)] = wm.pop(old_key)       # update key in dict
        if shard_name in repaired_shards:
            continue
        repaired_shards.add(shard_name)

        src = ckpt_dir / shard_name
        tmp = ckpt_dir / (shard_name + ".tmp")

        fixed_tensors = {}
        with safe_open(src, framework="pt", device="cpu") as f:
            for k in f.keys():
                fixed_tensors[_fixed(k)] = f.get_tensor(k)

        save_file(fixed_tensors, tmp, metadata={"format": "pt"})
        tmp.replace(src)                            # atomic overwrite

    # ── write new index ────────────────────────────────────────────────
    with open(index_file, "w") as f:
        json.dump(idx, f, indent=2)

    log.info("✓ Gemma-3 checkpoint repaired.")


# fully detie lm_head from embeddings so safetensors can flatten
def detie_lm_head(model):
    """
    Untie the logits projection from the input embeddings and register it
    *exactly* where the model (and loaders like vLLM) expect it.

    Works with HF models whose output head is either `lm_head` or some
    nested attribute (e.g. `language_model.output_projection` in Gemma-3).
    """
    import torch
    from types import SimpleNamespace

    emb = model.get_input_embeddings()          # nn.Embedding
    old_head = model.get_output_embeddings()    # whatever Linear HF exposes

    # nothing to do if they are already separate tensors
    if old_head.weight.data_ptr() != emb.weight.data_ptr():
        return

    vocab_size, hidden_size = emb.weight.shape
    new_head = torch.nn.Linear(hidden_size, vocab_size, bias=False)
    new_head.weight = torch.nn.Parameter(emb.weight.detach().clone())
    new_head.to(next(model.parameters()).dtype)

    # ------------------------------------------------------------------
    # find the *attribute path* of the existing output head
    # ------------------------------------------------------------------
    path = None
    for name, module in model.named_modules():
        if module is old_head:
            path = name            # e.g. "lm_head" or "language_model.output_projection"
            break
    if path is None:               # very unusual, but fall back to "lm_head"
        path = "lm_head"

    print('!!', name)

    # ------------------------------------------------------------------
    # install the new head at that path
    # ------------------------------------------------------------------
    def set_by_path(root, dotted_name, value):
        parts = dotted_name.split(".")
        parent = root
        for p in parts[:-1]:
            parent = getattr(parent, p)
        setattr(parent, parts[-1], value)

    set_by_path(model, path, new_head)

    # HF convenience: if the public attribute `lm_head` *is not* the main path,
    # mirror it so code expecting `model.lm_head` still works. This does *not*
    # duplicate weights – both names reference the same nn.Linear instance.
    #if path != "lm_head":
    #    model.lm_head = new_head

    model.config.tie_word_embeddings = False


# --------------------------------------------------------------------
#  restore Gemma-3’s weight-tying and ensure only the embed_tokens
#  key lands in the safetensors index (no lm_head, no duplication)
# --------------------------------------------------------------------
# ---------------------------------------------------------------
#  Gemma-3: keep weight-tying *and* give vLLM the path it wants
# ---------------------------------------------------------------
def retie_gemma3_and_prune_alias(model):
    """
    Re-establish tying between embeddings and logits projection and ensure
    the projection is reachable at `language_model.output_projection`.
    Removes the top-level `lm_head` alias so the serializer never emits
    an `lm_head.*` key.

    Call just before `save_pretrained(...)`.
    """
    import torch.nn as nn

    if (getattr(model.config, "model_type", "") or "").lower() != "gemma3":
        return  # skip for anything that isn't Gemma-3

    emb  = model.get_input_embeddings()          # nn.Embedding
    proj = getattr(model, "lm_head", None)       # HF always defines this

    if proj is None or not isinstance(proj, nn.Linear):
        raise RuntimeError("Could not find lm_head on Gemma-3 model")

    # ── tie weights if they were detied earlier ─────────────────────────
    if proj.weight.data_ptr() != emb.weight.data_ptr():
        proj.weight = emb.weight      # share storage again
    model.config.tie_word_embeddings = True

    # ── ensure wrapper + attribute for vLLM ────────────────────────────
    # 1. make / fetch `model.language_model`
    if not hasattr(model, "language_model"):
        wrapper = nn.Module()
        model.add_module("language_model", wrapper)
    else:
        wrapper = model.language_model

    # 2. register projection inside wrapper
    wrapper.add_module("output_projection", proj)

    # ── drop the top-level alias so it won't be serialised ─────────────
    if hasattr(model, "lm_head"):
        delattr(model, "lm_head")


# ------------------------------------------------------------------
#  Gemma-3 helper: detie + relabel head for vLLM + safetensors
# ------------------------------------------------------------------
def prepare_gemma3_for_save(model):
    """
    • Makes the output projection an independent tensor if it still shares
      storage with the embeddings.
    • Registers it at `language_model.lm_head` (the path vLLM uses).
    • Deletes the top-level `lm_head` alias so no `lm_head.*` key is saved.
    • Sets `tie_word_embeddings=False` so Transformers knows they’re untied.
    """
    import torch.nn as nn, torch

    if (getattr(model.config, "model_type", "") or "").lower() != "gemma3":
        return

    emb  = model.get_input_embeddings()
    head = model.get_output_embeddings()           # this is model.lm_head

    # 1. Detie if they still share storage
    if head.weight.data_ptr() == emb.weight.data_ptr():
        vocab, hidden = emb.weight.shape
        new_head = nn.Linear(hidden, vocab, bias=False)
        new_head.weight = nn.Parameter(emb.weight.detach().clone())
        new_head.to(next(model.parameters()).dtype)
        head = new_head

    # 2. Ensure `language_model` wrapper exists
    if not hasattr(model, "language_model"):
        model.add_module("language_model", nn.Module())

    # 3. Register under vLLM path
    #model.language_model.add_module("lm_head", head)

    # 4. Drop the alias so no `lm_head.*` key lands in the state-dict
    if hasattr(model, "lm_head"):
        delattr(model, "lm_head")

    model.config.tie_word_embeddings = False
