"""
Auto-clustering utility — groups assets by visual similarity via K-Means.

Usage (CLI)::

    python -m scripts.cluster_assets --db path/to/assets.sqlite --k 10

Or call programmatically::

    from scripts.cluster_assets import cluster_assets
    clusters = await cluster_assets(db, k=10)

The script reads all asset embeddings from the ``asset_embeddings`` table,
runs K-Means clustering (scikit-learn or Faiss), and returns a mapping from
cluster label to a list of ``asset_id`` values.  The caller can then use
these clusters to suggest automatic "Collections".

Design notes
------------
* Pure read-only on the DB — clusters are returned, **not** written.
* Both ``scikit-learn`` and ``faiss-cpu`` K-Means are supported.  The
  script auto-selects whichever is installed (Faiss is preferred for large
  datasets because of GPU/AVX2 acceleration).
* Cross-platform: no OS-specific calls.
"""

from __future__ import annotations

import argparse
import asyncio
import json
import sys
from pathlib import Path

# ---------------------------------------------------------------------------
# Allow running as  ``python -m scripts.cluster_assets``  from project root.
# ---------------------------------------------------------------------------
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(_PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(_PROJECT_ROOT))

from mjr_am_backend.config import VECTOR_EMBEDDING_DIM  # noqa: E402
from mjr_am_backend.features.index.vector_service import blob_to_vector  # noqa: E402
from mjr_am_backend.shared import Result, get_logger  # noqa: E402

logger = get_logger(__name__)


# ---------------------------------------------------------------------------
# K-Means implementations
# ---------------------------------------------------------------------------


def _kmeans_faiss(matrix, k: int, *, niter: int = 20, seed: int = 42):
    """Run K-Means via Faiss (fast, SIMD-accelerated)."""
    import faiss
    import numpy as np

    dim = matrix.shape[1]
    kmeans = faiss.Kmeans(dim, k, niter=niter, seed=seed, verbose=False)
    kmeans.train(matrix.astype(np.float32))
    _, labels = kmeans.index.search(matrix.astype(np.float32), 1)
    return [int(lbl) for lbl in labels.flatten()]


def _kmeans_sklearn(matrix, k: int, *, seed: int = 42):
    """Run K-Means via scikit-learn (pure-Python fallback)."""
    from sklearn.cluster import KMeans

    model = KMeans(n_clusters=k, random_state=seed, n_init="auto")
    return model.fit_predict(matrix)


def _run_kmeans(matrix, k: int) -> list[int]:
    """Auto-select the best available K-Means backend."""
    try:
        labels = _kmeans_faiss(matrix, k)
        logger.info("K-Means via Faiss completed (%d clusters)", k)
        return [int(lbl) for lbl in labels]
    except ImportError:
        pass
    try:
        labels = _kmeans_sklearn(matrix, k)
        logger.info("K-Means via scikit-learn completed (%d clusters)", k)
        return [int(lbl) for lbl in labels]
    except ImportError:
        raise RuntimeError("Neither faiss-cpu nor scikit-learn is installed") from None


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


async def cluster_assets(
    db,
    *,
    k: int = 10,
    dim: int | None = None,
) -> Result[dict[int, list[int]]]:
    """Cluster all embedded assets into *k* groups.

    Parameters
    ----------
    db : Sqlite
        Database adapter with ``aquery`` method.
    k : int
        Number of clusters.
    dim : int, optional
        Embedding vector dimensionality (defaults to config value).

    Returns
    -------
    Result[dict[int, list[int]]]
        Mapping ``cluster_label → [asset_id, ...]`` on success.
    """
    dim = dim or VECTOR_EMBEDDING_DIM

    rows_result = await db.aquery(
        "SELECT asset_id, vector FROM vec.asset_embeddings WHERE vector IS NOT NULL"
    )
    if not rows_result.ok or not rows_result.data:
        return Result.Err("NOT_FOUND", "No embeddings found in the database")

    try:
        import numpy as np
    except ImportError:
        return Result.Err("TOOL_MISSING", "numpy is required for clustering")

    ids: list[int] = []
    vectors: list[list[float]] = []
    for row in rows_result.data:
        try:
            aid = int(row["asset_id"])
            vec = blob_to_vector(row["vector"], dim)
            ids.append(aid)
            vectors.append(vec)
        except Exception:
            continue

    if len(ids) < k:
        return Result.Err(
            "INVALID_INPUT",
            f"Need at least {k} embeddings to form {k} clusters (found {len(ids)})",
        )

    matrix = np.array(vectors, dtype=np.float32)

    try:
        labels = await asyncio.to_thread(_run_kmeans, matrix, k)
    except RuntimeError as exc:
        return Result.Err("TOOL_MISSING", str(exc))

    clusters: dict[int, list[int]] = {}
    for aid, label in zip(ids, labels, strict=True):
        clusters.setdefault(label, []).append(aid)

    return Result.Ok(clusters)


# ---------------------------------------------------------------------------
# CLI entry-point
# ---------------------------------------------------------------------------


async def _main(args: argparse.Namespace) -> None:
    from mjr_am_backend.adapters.db.sqlite import Sqlite

    db = Sqlite(args.db)
    result = await cluster_assets(db, k=args.k)

    if not result.ok:
        print(f"Error: [{result.code}] {result.error}", file=sys.stderr)
        sys.exit(1)

    clusters = result.data
    print(json.dumps({str(k): v for k, v in clusters.items()}, indent=2))
    print(f"\n{len(clusters)} clusters formed from {sum(len(v) for v in clusters.values())} assets.")

    await db.aclose()


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Cluster assets by visual similarity using K-Means on CLIP embeddings."
    )
    parser.add_argument(
        "--db",
        type=str,
        required=True,
        help="Path to the SQLite database (e.g. _mjr_index/assets.sqlite)",
    )
    parser.add_argument(
        "--k",
        type=int,
        default=10,
        help="Number of clusters (default: 10)",
    )
    args = parser.parse_args()
    asyncio.run(_main(args))


if __name__ == "__main__":
    main()
