"""Minimal training smoke test: 2 batches forward+backward.

Verifies end-to-end that MutuallyExclusiveSampler + InfoNCELoss +
per-sample caption masking compose correctly for training.
"""
import torch
from torch.utils.data import DataLoader

from src.datasets.gtauav_dataset import GTAUAVDataset, collate_gtauav_batch
from src.datasets.mutually_exclusive_sampler import MutuallyExclusiveSampler
from src.losses.hard_negatives import NegativeMemoryBank
from src.losses.multi_infonce import InfoNCELoss
from src.models.asymmetric_encoder import AsymmetricEncoder, get_dino_transform

CKPT = "out/gtauav/with_text/ckpt_epoch005.pt"


def main() -> None:
    model, _ = AsymmetricEncoder.load_checkpoint(CKPT, device="cuda")
    model.train()

    tf = get_dino_transform(image_size=256)
    ds = GTAUAVDataset(
        pair_json="meta/train_80.json",
        filter_meta="meta/seg_filter.json",
        drone_transform=tf,
        sat_transform=tf,
    )

    sampler = MutuallyExclusiveSampler(
        [e["sat_candidates"] for e in ds.entries],
        batch_size=8, shuffle=True, seed=42,
    )
    sampler.set_epoch(0)
    loader = DataLoader(
        ds, batch_sampler=sampler, num_workers=2,
        collate_fn=collate_gtauav_batch, pin_memory=True,
    )

    loss_fn = InfoNCELoss(
        temperature_init=0.07, learnable_temperature=True,
        label_smoothing=0.1, weight_q2g=0.6, weight_g2q=0.4,
        tau_min=0.01, tau_max=0.1,
        hard_mining_k=512,
    ).to("cuda")
    neg_bank = NegativeMemoryBank(size=4096, dim=model.embed_dim).to("cuda")

    trainable = [p for p in model.trainable_parameters()] + list(loss_fn.parameters())
    opt = torch.optim.AdamW(trainable, lr=1e-4)

    it = iter(loader)
    for step in range(3):
        batch = next(it)
        opt.zero_grad()
        emb = model(
            drone_img=batch["drone_img"].to("cuda", non_blocking=True),
            sat_img=batch["sat_img"].to("cuda", non_blocking=True),
            caption_l1=batch["caption_l1"], caption_l2=batch["caption_l2"], caption_l3=batch["caption_l3"],
            sat_caption_l1=batch["sat_caption_l1"], sat_caption_l2=batch["sat_caption_l2"], sat_caption_l3=batch["sat_caption_l3"],
        )
        queue = neg_bank.get_queue()
        out = loss_fn(emb, epoch=0, total_epochs=10, queue_negatives=queue)
        out["total"].backward()
        opt.step()
        neg_bank.enqueue(emb["gallery"].detach())

        # Verify mutual exclusion in batch
        batch_sats = [set(ds.entries[i]["sat_candidates"]) for i in batch.get("__indices__", range(8))]
        # We can also check via sat_names (one sat per drone sampled)
        sat_names = batch["sat_names"]
        print(
            f"  step {step}: loss={out['total'].item():.4f}  "
            f"tau={out['temperature'].item():.4f}  "
            f"gate_q={out['gate_q'].item():.3f}  gate_g={out['gate_g'].item():.3f}  "
            f"queue_size={queue.shape[0] if queue is not None else 0}  "
            f"mining_k={loss_fn.hard_mining_k if queue is not None and queue.shape[0] > loss_fn.hard_mining_k else 'full'}"
        )
        assert torch.isfinite(out["total"]).all(), "Loss not finite!"

    print("OK: 3 train steps completed with finite loss (hard mining K=512)")


if __name__ == "__main__":
    main()
