"""Import admin API: trigger scan, list tasks, retry, clear."""

from datetime import UTC, datetime, timedelta

from quart import Blueprint, jsonify, request
from sqlalchemy import delete, select, update

from ..extensions import get_session
from ..models import ImportBatch, ImportTask

import_admin_bp = Blueprint("import_admin", __name__, url_prefix="/api/import")


@import_admin_bp.route("/trigger", methods=["POST"])
async def trigger_scan():
    body = await request.get_json(silent=True) or {}
    mode = body.get("mode", "quick")
    if mode not in ("quick", "deep", "verify"):
        return jsonify({"error": f"mode {mode!r} not supported; use 'quick', 'deep', or 'verify'"}), 400

    # 'verify' is a library task — short-circuit the import_root walk
    # (no ImportBatch, no per-file ImportTasks).
    if mode == "verify":
        from ..tasks.maintenance import verify_integrity

        async_result = verify_integrity.delay()
        return jsonify({"celery_task_id": async_result.id, "mode": mode}), 202

    from ..tasks.scan import scan_directory

    async_result = scan_directory.delay(triggered_by="manual", mode=mode)
    return jsonify({"celery_task_id": async_result.id, "mode": mode}), 202


@import_admin_bp.route("/status", methods=["GET"])
async def status():
    async with get_session() as session:
        # Active batch = running batch that still has outstanding work.
        # Plain "most recent running" picks freshly-created scans that
        # enqueued zero new files and hides the older batch that's
        # actually being processed. Mirrors the EXISTS predicate
        # /api/system/stats already uses (api/settings.py:145-160).
        # Audit 2026-06-02 — /api/import/status and /api/system/stats
        # used to disagree on the active-batch predicate; the UI banner
        # said "Scanning…" indefinitely while the stats card said idle.
        active = (
            await session.execute(
                select(ImportBatch)
                .where(
                    ImportBatch.status == "running",
                    select(ImportTask.id)
                    .where(
                        ImportTask.batch_id == ImportBatch.id,
                        ImportTask.status.in_(["pending", "queued", "processing"]),
                    )
                    .exists(),
                )
                .order_by(ImportBatch.started_at.desc())
                .limit(1)
            )
        ).scalar_one_or_none()
        payload = {"active_batch": None}
        if active:
            payload["active_batch"] = {
                "id": active.id,
                "source_path": active.source_path,
                "scan_mode": active.scan_mode,
                "total_files": active.total_files,
                "imported": active.imported,
                "skipped": active.skipped,
                "failed": active.failed,
                "refreshed": active.refreshed,
                "started_at": active.started_at.isoformat(),
            }
        return jsonify(payload)


@import_admin_bp.route("/tasks", methods=["GET"])
async def list_tasks():
    status_filter = request.args.get("status")
    try:
        limit = min(int(request.args.get("limit", "50")), 200)
    except ValueError:
        return jsonify({"error": "limit must be an integer"}), 400
    cursor_raw = request.args.get("cursor")
    cursor_id = int(cursor_raw) if cursor_raw else None

    async with get_session() as session:
        stmt = select(ImportTask).order_by(ImportTask.created_at.desc(), ImportTask.id.desc())
        if status_filter:
            stmt = stmt.where(ImportTask.status == status_filter)
        if cursor_id is not None:
            stmt = stmt.where(ImportTask.id < cursor_id)
        stmt = stmt.limit(limit + 1)
        rows = (await session.execute(stmt)).scalars().all()

    has_more = len(rows) > limit
    rows = rows[:limit]
    return jsonify({
        "tasks": [
            {
                "id": t.id,
                "batch_id": t.batch_id,
                "source_path": t.source_path,
                "task_type": t.task_type,
                "status": t.status,
                "result_image_id": t.result_image_id,
                "error": t.error,
                "size_bytes": t.size_bytes,
                "created_at": t.created_at.isoformat(),
                "started_at": t.started_at.isoformat() if t.started_at else None,
                "finished_at": t.finished_at.isoformat() if t.finished_at else None,
            }
            for t in rows
        ],
        "next_cursor": rows[-1].id if has_more and rows else None,
    })


@import_admin_bp.route("/retry-failed", methods=["POST"])
async def retry_failed():
    # Fold SELECT into UPDATE…WHERE…RETURNING — the prior SELECT-then-
    # UPDATE-WHERE-id-IN pattern blew past psycopg's 65535-parameter
    # ceiling once failed_ids exceeded ~65k rows.
    async with get_session() as session:
        result = await session.execute(
            update(ImportTask)
            .where(ImportTask.status == "failed")
            .values(
                status="queued", error=None,
                started_at=None, finished_at=None,
            )
            .returning(ImportTask.id, ImportTask.task_type)
        )
        failed = result.all()
        if not failed:
            return jsonify({"retried": 0})
        await session.commit()

    from ..tasks.import_file import enqueue_import
    for tid, task_type in failed:
        enqueue_import(tid, task_type)

    return jsonify({"retried": len(failed)})


@import_admin_bp.route("/tasks/<int:task_id>/refetch", methods=["POST"])
async def refetch_task(task_id: int):
    """Layer-2 one-shot re-download: delete the (corrupt) file behind a
    failed import task and re-run its source's downloader to fetch a
    fresh copy. Only works for files that resolve to an enabled,
    real-URL subscription Source; filesystem-only imports return
    no_source.

    Returns one of: refetch_queued (+source_id) / no_source /
    already_refetched / not_found / not_failed.
    """
    async with get_session() as session:
        result = await session.run_sync(_refetch_task_sync, task_id)
    if result["status"] == "not_found":
        return jsonify(result), 404
    if result["status"] == "not_failed":
        return jsonify(result), 400
    return jsonify(result)


def _refetch_task_sync(session, task_id: int) -> dict:
    from pathlib import Path

    from ..models import ImportSettings
    from ..services.refetch_service import attempt_refetch

    task = session.get(ImportTask, task_id)
    if task is None:
        return {"status": "not_found"}
    if task.status != "failed":
        return {"status": "not_failed"}
    settings = ImportSettings.load_sync(session)
    return attempt_refetch(session, task, Path(settings.import_scan_path))


@import_admin_bp.route("/clear-stuck", methods=["POST"])
async def clear_stuck():
    """Force any non-terminal ImportTask (status in pending/queued/
    processing) to 'failed' AND finalize any ImportBatch that ends up
    with no active children. Escape hatch for the operator when the
    automatic recover_interrupted_tasks sweep keeps re-queueing the
    same stuck row forever (e.g., underlying file is genuinely broken
    and the import keeps OSError-looping at PIL load).

    Idempotent + non-destructive: rows survive as 'failed' so the
    Retry-Failed button can re-attempt them once whatever was broken
    is fixed. Banked 2026-05-25 — operator hit 3 large PNGs that
    autoretry-looped for 2 days after a corrupt-data PIL OSError.
    """
    async with get_session() as session:
        # Fold SELECT into UPDATE…WHERE — see /retry-failed for the
        # 65535-parameter ceiling rationale. rowcount is enough here
        # because we don't need the ids afterward (no .delay()).
        clear_result = await session.execute(
            update(ImportTask)
            .where(
                ImportTask.status.in_(["pending", "queued", "processing"])
            )
            .values(
                status="failed",
                finished_at=datetime.now(UTC),
                error=(
                    "manually cleared via /api/import/clear-stuck "
                    "— stuck in non-terminal state; retry once "
                    "underlying cause (corrupt file, missing model, "
                    "etc.) is resolved"
                ),
            )
        )
        tasks_failed = clear_result.rowcount or 0

        # Finalize any 'running' ImportBatch that no longer has any
        # active children. The "Scanning..." banner is driven by
        # /api/import/status finding a running batch; left untouched,
        # it would persist forever after the stuck-task clear.
        running_batches = (
            await session.execute(
                select(ImportBatch.id).where(ImportBatch.status == "running")
            )
        ).scalars().all()
        finalized_batches = 0
        for batch_id in running_batches:
            still_active = (
                await session.execute(
                    select(ImportTask.id)
                    .where(ImportTask.batch_id == batch_id)
                    .where(ImportTask.status.in_(
                        ["pending", "queued", "processing"]
                    ))
                    .limit(1)
                )
            ).scalar_one_or_none()
            if still_active is None:
                await session.execute(
                    update(ImportBatch)
                    .where(ImportBatch.id == batch_id)
                    .values(
                        status="complete",
                        finished_at=datetime.now(UTC),
                    )
                )
                finalized_batches += 1
        await session.commit()

    return jsonify({
        "tasks_failed": tasks_failed,
        "batches_finalized": finalized_batches,
    })


@import_admin_bp.route("/clear-completed", methods=["POST"])
async def clear_completed():
    body = await request.get_json(silent=True) or {}
    age_days = body.get("age_days", 0)
    status_filter = body.get("status", ["complete", "skipped"])
    if not isinstance(status_filter, list):
        return jsonify({"error": "status must be a list"}), 400

    cutoff = (
        datetime.now(UTC) - timedelta(days=int(age_days)) if age_days else None
    )

    async with get_session() as session:
        stmt = delete(ImportTask).where(ImportTask.status.in_(status_filter))
        if cutoff is not None:
            stmt = stmt.where(ImportTask.finished_at < cutoff)
        result = await session.execute(stmt)
        await session.commit()

    return jsonify({"deleted": result.rowcount or 0})
