"""ML admin API: settings, backfill trigger, centroid recompute trigger."""

from quart import Blueprint, jsonify, request

from ..extensions import get_session
from ..models import MLSettings

ml_admin_bp = Blueprint("ml_admin", __name__, url_prefix="/api/ml")


_EDITABLE = (
    "suggestion_threshold_character",
    "suggestion_threshold_general",
    "centroid_similarity_threshold",
    "min_reference_images",
    "tagger_store_floor",
    "video_frame_interval_seconds",
    "video_max_frames",
    "video_min_tag_frames",
)


@ml_admin_bp.route("/settings", methods=["GET"])
async def get_settings():
    from sqlalchemy import select

    async with get_session() as session:
        s = (
            await session.execute(select(MLSettings).where(MLSettings.id == 1))
        ).scalar_one()
    return jsonify(
        {
            "suggestion_threshold_character": s.suggestion_threshold_character,
            "suggestion_threshold_general": s.suggestion_threshold_general,
            "centroid_similarity_threshold": s.centroid_similarity_threshold,
            "min_reference_images": s.min_reference_images,
            "tagger_store_floor": s.tagger_store_floor,
            "video_frame_interval_seconds": s.video_frame_interval_seconds,
            "video_max_frames": s.video_max_frames,
            "video_min_tag_frames": s.video_min_tag_frames,
            "tagger_model_version": s.tagger_model_version,
            "embedder_model_version": s.embedder_model_version,
        }
    )


@ml_admin_bp.route("/settings", methods=["PATCH"])
async def patch_settings():
    from sqlalchemy import select

    body = await request.get_json()
    if not isinstance(body, dict):
        return jsonify({"error": "body must be an object"}), 400
    async with get_session() as session:
        s = (
            await session.execute(select(MLSettings).where(MLSettings.id == 1))
        ).scalar_one()

        # Merge the patch over current values, then validate the result as a
        # whole — the store-floor invariant couples three fields, so they
        # can't be checked one at a time.
        proposed = {f: getattr(s, f) for f in _EDITABLE}
        for field in _EDITABLE:
            if field in body:
                proposed[field] = body[field]

        err = _validate(proposed)
        if err is not None:
            return jsonify({"error": err}), 400

        for field in _EDITABLE:
            setattr(s, field, proposed[field])
        await session.commit()
    return await get_settings()


def _validate(p: dict) -> str | None:
    """Returns an error string if the proposed settings are invalid, else None.

    Invariant (plan-task #764): the per-category suggestion thresholds can't
    drop below tagger_store_floor — nothing below the floor is stored, so a
    lower threshold would silently surface nothing in that gap. The UI clamps
    the sliders to the floor; this is the server-side backstop.
    """
    floor = p["tagger_store_floor"]
    if not (0.0 <= floor <= 1.0):
        return "tagger_store_floor must be between 0 and 1"
    for cat in ("character", "general"):
        if p[f"suggestion_threshold_{cat}"] < floor:
            return (
                f"suggestion_threshold_{cat} cannot be below tagger_store_floor "
                f"({floor}) — predictions below the floor are not stored"
            )
    # Video tagging (#747).
    if p["video_frame_interval_seconds"] <= 0:
        return "video_frame_interval_seconds must be > 0"
    if p["video_max_frames"] < 1:
        return "video_max_frames must be >= 1"
    if p["video_min_tag_frames"] < 1:
        return "video_min_tag_frames must be >= 1"
    if p["video_min_tag_frames"] > p["video_max_frames"]:
        return "video_min_tag_frames cannot exceed video_max_frames"
    return None


@ml_admin_bp.route("/backfill", methods=["POST"])
async def trigger_backfill():
    from ..tasks.ml import backfill

    r = backfill.delay()
    return jsonify({"celery_task_id": r.id}), 202


@ml_admin_bp.route("/recompute-centroids", methods=["POST"])
async def trigger_recompute():
    from ..tasks.ml import recompute_centroids

    r = recompute_centroids.delay()
    return jsonify({"celery_task_id": r.id}), 202
