"""Real LoRA fine-tuning on an NVIDIA GPU via Unsloth (fast, memory-efficient QLoRA —
chosen specifically because it targets single consumer GPUs with limited VRAM, unlike
plain PEFT+TRL). Like mlx_backend.py, this module is a thin subprocess wrapper: torch
and unsloth are never imported here, only inside finetune_scripts/cuda_train.py and
cuda_fuse.py, which this module runs as subprocesses. That keeps those huge,
GPU-specific dependencies out of the main FastAPI/Celery process's import graph
entirely — the same lesson learned when a top-level `huggingface_hub` import in the
old finetune.py crashed the whole app on hardware that didn't have it installed.
"""

import json
import sys
from pathlib import Path
from typing import Callable

from app.services.finetune.shared import patch_tokenizer_class, run_streaming

_SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "finetune_scripts"


def run_lora_training(
    base_model: str, data_dir: Path, adapter_path: Path, config: dict, on_line: Callable[[str], None]
) -> None:
    cmd = [
        sys.executable,
        str(_SCRIPTS_DIR / "cuda_train.py"),
        "--model",
        base_model,
        "--data",
        str(data_dir),
        "--adapter-path",
        str(adapter_path),
        "--iters",
        str(config["iters"]),
        "--batch-size",
        str(config["batch_size"]),
        "--learning-rate",
        str(config["learning_rate"]),
        "--lora-r",
        str(config.get("lora_r", 16)),
        "--lora-alpha",
        str(config.get("lora_alpha", 16)),
        "--lora-dropout",
        str(config.get("lora_dropout", 0.0)),
        "--steps-per-report",
        str(config.get("steps_per_report", 5)),
        "--steps-per-eval",
        str(config.get("steps_per_eval", 20)),
    ]
    run_streaming(cmd, on_line)


def run_fuse(base_model: str, adapter_path: Path, fused_dir: Path) -> None:
    cmd = [
        sys.executable,
        str(_SCRIPTS_DIR / "cuda_fuse.py"),
        "--model",
        base_model,
        "--adapter-path",
        str(adapter_path),
        "--save-path",
        str(fused_dir),
    ]
    run_streaming(cmd, lambda _line: None)
    patch_tokenizer_class(fused_dir)


def parse_progress_line(line: str) -> dict | None:
    # cuda_train.py prints one JSON object per progress event — no regex needed since
    # this project owns that script's output format entirely (unlike mlx_lm's CLI,
    # whose text output mlx_backend.py has to pattern-match).
    line = line.strip()
    if not line.startswith("{"):
        return None
    try:
        event = json.loads(line)
    except json.JSONDecodeError:
        return None
    if event.get("type") in ("train", "val"):
        return event
    return None
