"""cortex/api/routes/admin/backup.py — Backup and restore endpoints.

Uses the hardened backup module (cortex.storage.backup) which provides:
  - WAL checkpoint before backup
  - SHA-256 checksummed tar.gz archives
  - Integrity check after restore
  - Atomic rollback on restore failure
"""

from __future__ import annotations

import asyncio
import logging
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, List

from fastapi import APIRouter, Depends, HTTPException, status

from cortex.api.deps import PREFIX, get_plugin
from cortex.api.models import (
    AdminBackupRequest,
    AdminBackupResponse,
    AdminRestoreRequest,
    AdminRestoreResponse,
)
from cortex.storage.backup import backup_database, restore_database

logger = logging.getLogger(__name__)


def _register(router: APIRouter, verify_api_key):
    """Register backup and restore routes on the given router."""

    @router.post(
        f"{PREFIX}/admin/backup",
        response_model=AdminBackupResponse,
        status_code=status.HTTP_200_OK,
        summary="Trigger a database backup",
        tags=["admin"],
        operation_id="admin_backup",
    )
    async def admin_backup(
        body: AdminBackupRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> AdminBackupResponse:
        """Create a checksummed, WAL-checkpointed backup via the hardened backup module."""
        storage = getattr(plugin, "storage", None)
        if storage is None:
            raise HTTPException(
                status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                detail="Storage not available",
            )

        db_path_str: str = getattr(plugin.config, "storage_db_path", "cortex.db")
        if hasattr(plugin.config, "db_path_resolved"):
            db_path_str = str(plugin.config.db_path_resolved())

        db_path = Path(db_path_str).resolve()
        # Output directory: use the DB's parent directory for backup archives
        output_dir = db_path.parent / "backups"

        try:
            # backup_database is synchronous — run in executor to avoid blocking
            loop = asyncio.get_event_loop()
            archive_path, checksum_path = await loop.run_in_executor(
                None,
                lambda: backup_database(
                    db_path=db_path,
                    output_dir=output_dir,
                    require_checkpoint=False,  # non-fatal WAL checkpoint failure
                ),
            )
        except Exception as exc:
            logger.error("Admin backup failed: %s", exc, exc_info=True)
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Internal server error",
            ) from exc

        size_bytes = archive_path.stat().st_size
        timestamp = datetime.now(timezone.utc).isoformat(timespec="seconds")

        return AdminBackupResponse(
            path=str(archive_path),
            size_bytes=size_bytes,
            timestamp=timestamp,
        )

    @router.post(
        f"{PREFIX}/admin/restore",
        response_model=AdminRestoreResponse,
        status_code=status.HTTP_200_OK,
        summary="Restore database from a backup",
        tags=["admin"],
        operation_id="admin_restore",
    )
    async def admin_restore(
        body: AdminRestoreRequest,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> AdminRestoreResponse:
        """Restore the database from a backup archive using the hardened restore module.

        The hardened module handles:
          - SHA-256 checksum verification
          - Path traversal protection (CVE-2007-4559)
          - Atomic rollback on integrity check failure
          - WAL/SHM cleanup
        """
        import re as _re
        import aiosqlite

        db_path_str: str = getattr(plugin.config, "storage_db_path", "cortex.db")
        if hasattr(plugin.config, "db_path_resolved"):
            db_path_str = str(plugin.config.db_path_resolved())

        db_path = Path(db_path_str).resolve()

        # --- Path traversal protection (#213) ---
        _safe_filename = _re.compile(r'^[a-zA-Z0-9._-]+$')
        restore_basename = os.path.basename(body.backup_path)
        if '..' in body.backup_path or not _safe_filename.match(restore_basename):
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Invalid backup path: must be a simple filename (alphanumeric, dash, underscore, dot)",
            )
        # Force restore path into the same directory as the source DB
        backup_dir = db_path.parent
        archive_path = backup_dir / "backups" / restore_basename

        # Also check directly in the DB directory for backwards compat
        if not archive_path.exists():
            archive_path = backup_dir / restore_basename

        if not archive_path.exists():
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Backup file not found",
            )

        errors: List[str] = []
        claims_count = 0
        try:
            # restore_database is synchronous — run in executor
            loop = asyncio.get_event_loop()
            await loop.run_in_executor(
                None,
                lambda: restore_database(
                    archive_path=archive_path,
                    db_path=db_path,
                    force=True,  # API context: service manages its own lifecycle
                    skip_checksum=False,
                ),
            )

            # Count claims in restored DB.
            # Support both the current schema table name (semantic_claim) and
            # the legacy/simplified name (claims) that older backups may use.
            async with aiosqlite.connect(str(db_path)) as db:
                for _tbl in ("semantic_claim", "claims"):
                    try:
                        async with db.execute(f"SELECT COUNT(*) FROM {_tbl}") as cur:
                            row = await cur.fetchone()
                            claims_count = row[0] if row else 0
                            break
                    except Exception:
                        continue
        except Exception as exc:
            err = f"Restore failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)
            return AdminRestoreResponse(restored=False, errors=errors)

        return AdminRestoreResponse(restored=True, claims_count=claims_count, errors=errors)
