#!/usr/bin/env python3

"""Utility script for deleting models installed for ComfyUI.

Mirrors `download_models.py` selection logic (`list_models`) but removes files
instead of downloading. Safe to run with --dry-run / --list to preview.

Usage:
    python delete_models.py [destination] [options]
    python delete_models.py /path/to/ComfyUI --all --dry-run
    python delete_models.py /path/to/ComfyUI --all --verbose
    python delete_models.py /path/to/ComfyUI --sd15 --controlnet --prune
"""

import sys
from argparse import ArgumentParser
from pathlib import Path

sys.path.append(str(Path(__file__).parent.parent))

# Ensure websockets check in ai_diffusion/__init__.py passes outside Krita
import importlib.util

if not importlib.util.find_spec(".websockets.src", "ai_diffusion"):
    ws_dir = Path(__file__).parent.parent / "ai_diffusion" / "websockets" / "src"
    ws_dir.mkdir(parents=True, exist_ok=True)
    (ws_dir / "__init__.py").touch(exist_ok=True)

from ai_diffusion.backend import resources
from ai_diffusion.backend.resources import Arch, ModelRequirements, default_checkpoints

# Reuse selection logic from download_models to stay in sync
try:
    from download_models import detect_backend, list_models
except ImportError:
    # Fallback: import directly if download_models not importable as module
    import importlib.machinery
    import importlib.util as iu

    spec = iu.spec_from_file_location(
        "download_models", str(Path(__file__).parent / "download_models.py")
    )
    assert spec and spec.loader
    mod = iu.module_from_spec(spec)
    spec.loader.exec_module(mod)  # type: ignore
    list_models = mod.list_models  # type: ignore
    detect_backend = mod.detect_backend  # type: ignore


def _is_within(destination: Path, target: Path) -> bool:
    try:
        target.resolve().relative_to(destination.resolve())
        return True
    except Exception:
        return False


def delete_models(
    destination: Path,
    models,
    verbose=False,
    dry_run=False,
    prune=False,
) -> tuple[int, int, int]:
    removed = 0
    missing = 0
    errors = 0
    empty_dirs: set[Path] = set()

    for model in sorted(models, key=lambda m: m.name):
        for file in model.files:
            # Just the models: skip prefetch/custom_nodes files
            if file.path.parts[0] != "models":
                if verbose:
                    print(f"SKIP (not a model): {file.path}")
                continue
            target = destination / file.path
            # Safety: only delete inside destination
            if not _is_within(destination, target):
                print(f"SKIP (outside destination): {target}")
                errors += 1
                continue
            if verbose or dry_run:
                print(f"{'[dry-run] ' if dry_run else ''}Checking {target}")
            if not target.exists():
                if verbose:
                    print(f"  missing: {target}")
                missing += 1
                continue
            if not target.is_file():
                print(f"SKIP (not a file): {target}")
                errors += 1
                continue
            if dry_run:
                print(f"  would delete: {target} ({model.name})")
                removed += 1
            else:
                try:
                    target.unlink()
                    print(f"Deleted {target} ({model.name})")
                    removed += 1
                    empty_dirs.add(target.parent)
                except Exception as e:
                    print(f"Error deleting {target}: {e}")
                    errors += 1

            # Also remove .part leftover if present
            part = target.with_suffix(".part")
            if part.exists() and _is_within(destination, part):
                if dry_run:
                    print(f"  would delete: {part} (leftover)")
                else:
                    try:
                        part.unlink()
                        print(f"Deleted leftover {part}")
                    except Exception as e:
                        print(f"Error deleting {part}: {e}")

    if prune and not dry_run:
        # Remove empty parent dirs up to destination (but not destination itself)
        for d in sorted(empty_dirs, key=lambda p: len(p.parts), reverse=True):
            cur = d
            while cur != destination and cur.is_relative_to(destination):
                try:
                    if cur.exists() and cur.is_dir() and not any(cur.iterdir()):
                        cur.rmdir()
                        print(f"Pruned empty dir {cur}")
                        cur = cur.parent
                    else:
                        break
                except Exception as e:
                    print(f"Error pruning {cur}: {e}")
                    break

    return removed, missing, errors


if __name__ == "__main__":
    parser = ArgumentParser(
        prog="delete_models.py",
        usage="%(prog)s [options] destination",
        description="Delete models installed for ComfyUI (mirrors download_models.py selection).",
    )
    parser.add_argument(
        "destination",
        type=Path,
        default=Path.cwd(),
        nargs="?",
        help="Path to ComfyUI/server directory containing models/ and custom_nodes/.",
    )
    checkpoint_names = [m.id.identifier for m in resources.default_checkpoints]
    parser.add_argument("-v", "--verbose", action="store_true", help="print filepaths")
    parser.add_argument("-l", "--list", action="store_true", help="list models that would be deleted and exit")
    parser.add_argument("-d", "--dry-run", action="store_true", help="preview without deleting")
    parser.add_argument("--prune", action="store_true", help="remove empty parent dirs after deletion")
    parser.add_argument("-y", "--yes", action="store_true", help="skip confirmation prompt")
    parser.add_argument("-m", "--minimal", action="store_true", help="delete minimum viable set")
    parser.add_argument("-r", "--recommended", action="store_true", help="delete recommended set")
    parser.add_argument("-a", "--all", action="store_true", help="delete ALL models")
    parser.add_argument("--sd15", action="store_true", help="[Workload] SD 1.5")
    parser.add_argument("--sdxl", action="store_true", help="[Workload] SDXL")
    parser.add_argument("--illu", action="store_true", help="[Workload] Illustrious-SDXL")
    parser.add_argument("--flux", action="store_true", help="[Workload] Flux")
    parser.add_argument("--flux2", action="store_true", help="[Workload] Flux 2")
    parser.add_argument("--zimage", action="store_true", help="[Workload] Z-Image")
    parser.add_argument("--checkpoints", action="store_true", dest="checkpoints", help="include checkpoints for selected workloads")
    parser.add_argument("--controlnet", action="store_true", help="include ControlNet models")
    parser.add_argument("--checkpoint", action="append", choices=checkpoint_names, dest="checkpoint_list", help="specific checkpoint (repeatable)")
    parser.add_argument("--upscalers", action="store_true", help="include upscale models")
    parser.add_argument("--prefetch", action="store_true", help="include prefetch models")
    parser.add_argument("--deprecated", action="store_true", help="include deprecated models")
    parser.add_argument("--backend", choices=["auto", "cpu", "cuda", "xpu", "rocm", "mps"], default="auto", help="filter for hardware")
    args = parser.parse_args()

    checkpoints = args.checkpoint_list or []
    if args.checkpoints and args.sd15:
        checkpoints += [m.id.identifier for m in default_checkpoints if m.arch is Arch.sd15]
    if args.checkpoints and args.sdxl:
        checkpoints += [m.id.identifier for m in default_checkpoints if m.arch is Arch.sdxl]
    if args.checkpoints and args.flux:
        checkpoints += [m.id.identifier for m in default_checkpoints if m.arch is Arch.flux]
    if args.checkpoints and args.flux2:
        checkpoints += [m.id.identifier for m in default_checkpoints if m.arch is Arch.flux2_4b]
    if args.checkpoints and args.illu:
        checkpoints += [m.id.identifier for m in default_checkpoints if m.arch is Arch.illu]
        checkpoints += [m.id.identifier for m in default_checkpoints if m.arch is Arch.illu_v]
    if args.checkpoints and args.zimage:
        checkpoints += [m.id.identifier for m in default_checkpoints if m.arch is Arch.zimage]

    print(f"Generative AI for Krita - Model delete - v{resources.version}")

    backend = ModelRequirements.no_cuda
    if args.backend == "auto":
        backend = detect_backend()
    elif args.backend == "cuda":
        backend = ModelRequirements.cuda

    models = list_models(
        sd15=args.sd15,
        sdxl=args.sdxl,
        flux=args.flux,
        flux2=args.flux2,
        illu=args.illu,
        zimage=args.zimage,
        upscalers=args.upscalers,
        checkpoints=checkpoints,
        controlnet=args.controlnet,
        prefetch=args.prefetch,
        deprecated=args.deprecated,
        minimal=args.minimal,
        recommended=args.recommended,
        all=args.all,
        backend=backend,
    )

    if args.list:
        for model in sorted(models, key=lambda m: m.name):
            for file in model.files:
                if file.path.parts[0] != "models":
                    continue
                print(f"{model.name:32} {file.path} ({file.name})")
        sys.exit(0)

    if not models:
        sys.exit(0)

    total_files = sum(len([f for f in m.files if f.path.parts[0] == "models"]) for m in models)
    print(f"Selected {len(models)} model(s) / {total_files} file(s) under {args.destination} (models/ only)")
    if args.dry_run:
        print("Dry-run: no files will be deleted")

    if not args.yes and not args.dry_run:
        resp = input(f"Delete {total_files} file(s) under {args.destination}? [y/N] ")
        if resp.lower() not in ("y", "yes"):
            print("Aborted")
            sys.exit(1)

    removed, missing, errors = delete_models(
        args.destination, models, verbose=args.verbose, dry_run=args.dry_run, prune=args.prune
    )
    print(f"Done: removed={removed} missing={missing} errors={errors}")
    if args.dry_run:
        print("Dry-run complete - no changes made")
