"""FC-3h: /api/system/backup — create/list/restore/delete/tag for
DB + image backups.

Read endpoints are public on FC (operator-facing internal API; same
posture as /api/system/activity). Write endpoints take a typed
`confirm` body field that must match a server-generated token for
that backup row, to prevent click-to-destroy by stale browser tabs
or accidental cURL.
"""
from __future__ import annotations

from quart import Blueprint, jsonify, request
from sqlalchemy import desc, select

from ..extensions import get_session
from ..models import BackupRun, ImportSettings
from ._responses import error_response as _bad

system_backup_bp = Blueprint(
    "system_backup", __name__, url_prefix="/api/system/backup",
)

_KINDS = frozenset({"db", "images"})
_TAG_MAX_LEN = 64
_BACKUP_SETTINGS_FIELDS = (
    "backup_db_nightly_enabled",
    "backup_db_nightly_hour_utc",
    "backup_db_keep_last_n",
    "backup_images_keep_last_n",
)


def _row_to_dict(r: BackupRun) -> dict:
    return {
        "id": r.id,
        "kind": r.kind,
        "status": r.status,
        "tag": r.tag,
        "triggered_by": r.triggered_by,
        "started_at": r.started_at.isoformat() if r.started_at else None,
        "finished_at": r.finished_at.isoformat() if r.finished_at else None,
        "duration_seconds": (
            int((r.finished_at - r.started_at).total_seconds())
            if r.finished_at and r.started_at else None
        ),
        "sql_path": r.sql_path,
        "tar_path": r.tar_path,
        "size_bytes": r.size_bytes,
        "error": r.error,
        "restored_from_id": r.restored_from_id,
        "manifest": r.manifest or {},
    }


def _validate_tag(tag):
    if tag is None:
        return None
    if not isinstance(tag, str):
        return _bad("invalid_tag", detail="tag must be string or null")
    tag = tag.strip()
    if not tag:
        return None
    if len(tag) > _TAG_MAX_LEN:
        return _bad("invalid_tag", detail=f"tag too long (max {_TAG_MAX_LEN})")
    return tag


def _validate_backup_settings_patch(body: dict):
    if "backup_db_nightly_enabled" in body and not isinstance(
        body["backup_db_nightly_enabled"], bool,
    ):
        return _bad("invalid_value", detail="backup_db_nightly_enabled must be bool")
    if "backup_db_nightly_hour_utc" in body:
        v = body["backup_db_nightly_hour_utc"]
        if not isinstance(v, int) or isinstance(v, bool) or not (0 <= v <= 23):
            return _bad("invalid_value", detail="backup_db_nightly_hour_utc must be 0..23")
    if "backup_db_keep_last_n" in body:
        v = body["backup_db_keep_last_n"]
        if not isinstance(v, int) or isinstance(v, bool) or not (1 <= v <= 365):
            return _bad("invalid_value", detail="backup_db_keep_last_n must be 1..365")
    if "backup_images_keep_last_n" in body:
        v = body["backup_images_keep_last_n"]
        if not isinstance(v, int) or isinstance(v, bool) or not (1 <= v <= 100):
            return _bad("invalid_value", detail="backup_images_keep_last_n must be 1..100")
    return None


@system_backup_bp.route("/db", methods=["POST"])
async def trigger_db_backup():
    body = await request.get_json(silent=True) or {}
    tag = _validate_tag(body.get("tag"))
    if isinstance(tag, tuple):
        return tag
    from ..tasks.backup import backup_db_task
    backup_db_task.delay(tag=tag, triggered_by="manual")
    return jsonify({"status": "dispatched"}), 202


@system_backup_bp.route("/images", methods=["POST"])
async def trigger_images_backup():
    body = await request.get_json(silent=True) or {}
    tag = _validate_tag(body.get("tag"))
    if isinstance(tag, tuple):
        return tag
    from ..tasks.backup import backup_images_task
    backup_images_task.delay(tag=tag, triggered_by="manual")
    return jsonify({"status": "dispatched"}), 202


@system_backup_bp.route("/runs", methods=["GET"])
async def list_runs():
    try:
        limit = min(int(request.args.get("limit", "50")), 200)
    except ValueError:
        return _bad("invalid_limit")
    if limit < 1:
        return _bad("invalid_limit")
    kind = request.args.get("kind")
    if kind is not None and kind not in _KINDS:
        return _bad("invalid_kind", detail=f"kind must be one of {sorted(_KINDS)}")
    before_id_raw = request.args.get("before_id")
    before_id = int(before_id_raw) if before_id_raw else None

    async with get_session() as session:
        stmt = select(BackupRun).order_by(desc(BackupRun.id))
        if kind:
            stmt = stmt.where(BackupRun.kind == kind)
        if before_id is not None:
            stmt = stmt.where(BackupRun.id < before_id)
        stmt = stmt.limit(limit + 1)
        rows = (await session.execute(stmt)).scalars().all()

    has_more = len(rows) > limit
    rows = rows[:limit]
    return jsonify({
        "runs": [_row_to_dict(r) for r in rows],
        "next_cursor": rows[-1].id if has_more and rows else None,
    })


@system_backup_bp.route("/runs/<int:run_id>", methods=["GET"])
async def get_run(run_id: int):
    async with get_session() as session:
        row = await session.get(BackupRun, run_id)
    if row is None:
        return _bad("not_found", status=404)
    return jsonify(_row_to_dict(row))


@system_backup_bp.route("/runs/<int:run_id>", methods=["PATCH"])
async def patch_run(run_id: int):
    body = await request.get_json(silent=True) or {}
    if "tag" not in body:
        return _bad("invalid_body", detail="tag required")
    tag = _validate_tag(body["tag"])
    if isinstance(tag, tuple):
        return tag

    async with get_session() as session:
        row = await session.get(BackupRun, run_id)
        if row is None:
            return _bad("not_found", status=404)
        row.tag = tag
        await session.commit()
        await session.refresh(row)
    return jsonify(_row_to_dict(row))


@system_backup_bp.route("/runs/<int:run_id>/restore", methods=["POST"])
async def trigger_restore(run_id: int):
    body = await request.get_json(silent=True) or {}
    supplied = body.get("confirm", "")

    async with get_session() as session:
        row = await session.get(BackupRun, run_id)
        if row is None:
            return _bad("not_found", status=404)
        if row.status != "ok":
            return _bad(
                "not_restorable",
                detail=f"source backup status={row.status!r}; only 'ok' rows are restorable",
            )
        expected = f"restore-{row.kind}-{row.id}"
        if supplied != expected:
            return _bad(
                "confirm_mismatch",
                detail=f"confirm must equal {expected!r}",
                expected=expected,
            )
        kind = row.kind

    if kind == "db":
        from ..tasks.backup import restore_db_task
        restore_db_task.delay(source_backup_run_id=run_id)
    else:  # 'images' (the only other value _KINDS allows via the trigger path)
        from ..tasks.backup import restore_images_task
        restore_images_task.delay(source_backup_run_id=run_id)
    return jsonify({"status": "dispatched", "kind": kind}), 202


@system_backup_bp.route("/runs/<int:run_id>", methods=["DELETE"])
async def delete_run(run_id: int):
    body = await request.get_json(silent=True) or {}
    supplied = body.get("confirm", "")

    async with get_session() as session:
        row = await session.get(BackupRun, run_id)
        if row is None:
            return _bad("not_found", status=404)
        expected = f"delete-{row.kind}-{row.id}"
        if supplied != expected:
            return _bad(
                "confirm_mismatch",
                detail=f"confirm must equal {expected!r}",
                expected=expected,
            )
        from ..services import backup_service
        backup_service.unlink_artifact_files(
            sql_path=row.sql_path, tar_path=row.tar_path,
            manifest_path=(row.manifest or {}).get("manifest_path"),
        )
        await session.delete(row)
        await session.commit()
    return "", 204


@system_backup_bp.route("/settings", methods=["GET"])
async def get_settings():
    async with get_session() as session:
        row = await ImportSettings.load(session)
    return jsonify({
        "backup_db_nightly_enabled": row.backup_db_nightly_enabled,
        "backup_db_nightly_hour_utc": row.backup_db_nightly_hour_utc,
        "backup_db_keep_last_n": row.backup_db_keep_last_n,
        "backup_images_keep_last_n": row.backup_images_keep_last_n,
    })


@system_backup_bp.route("/settings", methods=["PATCH"])
async def patch_settings():
    body = await request.get_json(silent=True)
    if not isinstance(body, dict):
        return _bad("invalid_body", detail="body must be a JSON object")

    err = _validate_backup_settings_patch(body)
    if err is not None:
        return err

    async with get_session() as session:
        row = await ImportSettings.load(session)
        for field in _BACKUP_SETTINGS_FIELDS:
            if field in body:
                setattr(row, field, body[field])
        await session.commit()
    return await get_settings()
