"""cortex/api/routes/admin.py — Admin endpoints (backup, restore, stats).

Deprecated paths (sunset 2026-06-01 — issue #297):
  GET /api/v1/stats    → /stats/dashboard  (simple stats, superseded by dashboard)
  GET /api/v1/requests → /admin/requests   (request log, now properly namespaced)
"""

from __future__ import annotations

import logging
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, Depends, HTTPException, Response, status
from fastapi.responses import JSONResponse

from cortex.api.deps import PREFIX, _serialise, get_plugin
from cortex.api.deprecation import add_deprecation_headers
from cortex.api.models import (
    AdminBackupRequest,
    AdminBackupResponse,
    AdminRestoreRequest,
    AdminRestoreResponse,
    AdminStatsResponse,
    HealthResponse,
    RequestLogResponse,
    StatsResponse,
)

logger = logging.getLogger(__name__)

router = APIRouter()


def _make_routes(verify_api_key):
    """Register admin routes with the given auth dependency."""

    @router.get(
        f"{PREFIX}/health",
        summary="System health",
        tags=["system"],
        operation_id="get_health",
    )
    async def get_health(
        detail: bool = False,
        plugin: Any = Depends(get_plugin),
    ):
        """Return health status for all Cortex modules.

        Without ``?detail=true``, returns the legacy structure
        (``ok``, ``storage``, ``modules``, ``errors``, ``timestamp``) with an added
        ``status`` field for forward-compatibility.

        With ``?detail=true``, also returns full HealthChecker output including
        storage stats, error rates, memory usage, and uptime.
        """
        from cortex.api.health import HealthChecker

        # Legacy plugin.health() for backward-compatible fields
        report = await plugin.health()

        # HealthChecker for richer status string + optional detail
        health_report = await HealthChecker.check(plugin, detail=detail)

        # ok reflects actual availability: True when healthy or degraded,
        # False only when truly unhealthy.
        effective_ok = health_report.status in ("healthy", "degraded")
        is_degraded = health_report.status == "degraded"

        base = {
            "ok": effective_ok,
            "degraded": is_degraded,
            "status": health_report.status,
            "storage": report.storage,
            "modules": report.modules,
            "errors": report.errors,
            "timestamp": report.timestamp,
        }

        if detail:
            base.update({
                "checks": health_report.checks,
                "uptime_seconds": health_report.uptime_seconds,
                "version": health_report.version,
                "detail": health_report.detail,
            })

        return JSONResponse(content=base)

    @router.get(
        f"{PREFIX}/health/ready",
        summary="Readiness probe (Fly.io / k8s)",
        tags=["system"],
        operation_id="get_health_ready",
    )
    async def get_health_ready(
        plugin: Any = Depends(get_plugin),
    ):
        """Readiness probe endpoint.

        Returns 200 only when plugin is fully initialized AND storage is connected.
        Returns 503 during startup or if storage is disconnected.
        """
        from cortex.api.health import HealthChecker

        health_report = await HealthChecker.check(plugin)

        storage_check = health_report.checks.get("storage", {})
        storage_status = (
            storage_check.get("status", "unknown")
            if isinstance(storage_check, dict) else "ok"
        )

        if storage_status == "unhealthy" or health_report.status == "unhealthy":
            return JSONResponse(
                status_code=503,
                content={
                    "ready": False,
                    "reason": "storage unavailable or plugin unhealthy",
                    "status": health_report.status,
                },
            )

        return JSONResponse(content={"ready": True, "status": health_report.status})

    @router.post(
        f"{PREFIX}/admin/dream-cycle",
        summary="Manually trigger a dream cycle (admin)",
        tags=["admin"],
        operation_id="admin_dream_cycle",
    )
    async def admin_dream_cycle(
        owner_id: Optional[str] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """Trigger a full dream cycle manually — for ops/debugging.

        Returns cycle status, memories processed count, and duration_ms.
        """
        import time as _time

        t0 = _time.monotonic()
        errors: List[str] = []
        _owner = owner_id or getattr(getattr(plugin, "config", None), "owner_id", "default")

        try:
            dream_report = await plugin.dream(owner_id=_owner)
            duration_ms = int((_time.monotonic() - t0) * 1000)
            memories_processed = (
                getattr(dream_report, "claims_consolidated", 0)
                or getattr(dream_report, "steps_completed", 0)
                or 0
            )
            return JSONResponse(content={
                "status": "completed",
                "memories_processed": memories_processed,
                "duration_ms": duration_ms,
                "steps_completed": getattr(dream_report, "steps_completed", 0),
                "curiosity_seeds": getattr(dream_report, "curiosity_seeds", 0),
                "journal": getattr(dream_report, "journal", ""),
                "errors": getattr(dream_report, "errors", []),
            })
        except Exception as exc:
            err = f"Dream cycle failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)
            duration_ms = int((_time.monotonic() - t0) * 1000)
            return JSONResponse(
                status_code=500,
                content={
                    "status": "failed",
                    "memories_processed": 0,
                    "duration_ms": duration_ms,
                    "errors": errors,
                },
            )

    @router.get(
        f"{PREFIX}/stats",
        response_model=StatsResponse,
        summary="Memory statistics — DEPRECATED, use /stats/dashboard",
        tags=["system (deprecated)"],
        operation_id="get_stats",
        deprecated=True,
    )
    async def get_stats(
        response: Response,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> StatsResponse:
        """Return aggregate memory statistics from storage.

        .. deprecated::
            Use ``GET /api/v1/stats/dashboard`` for richer statistics.
            Sunset: 2026-06-01  (issue #297)
        """
        add_deprecation_headers(response, f"{PREFIX}/stats/dashboard")
        errors: List[str] = []
        total_claims = 0
        total_sessions = 0
        total_cornerstones = 0
        storage_ok = False

        storage = getattr(plugin, "storage", None)
        if storage is None:
            errors.append("Storage not available")
            return StatsResponse(storage_ok=False, errors=errors)

        owner_id = getattr(getattr(plugin, "config", None), "owner_id", "default")
        try:
            if hasattr(storage, "count_claims"):
                total_claims = await storage.count_claims(owner_id=owner_id)
            if hasattr(storage, "count_sessions"):
                total_sessions = await storage.count_sessions(owner_id=owner_id)
            if hasattr(storage, "get_cornerstones"):
                cs = await storage.get_cornerstones()
                total_cornerstones = len(cs)
            storage_ok = True
        except Exception as exc:
            err = f"Stats fetch failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return StatsResponse(
            total_claims=total_claims,
            total_sessions=total_sessions,
            total_cornerstones=total_cornerstones,
            storage_ok=storage_ok,
            errors=errors,
        )

    @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:
        """Checkpoint the WAL and copy the SQLite database to a backup path."""
        import shutil
        import os
        from datetime import datetime, timezone

        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 = getattr(plugin.config, "storage_db_path", "cortex.db")
        if hasattr(plugin.config, "db_path_resolved"):
            db_path = str(plugin.config.db_path_resolved())

        out_path = body.output_path or (db_path + ".bak")

        # --- Path traversal protection (#213) ---
        import re as _re
        _safe_filename = _re.compile(r'^[a-zA-Z0-9._-]+$')
        output_basename = os.path.basename(out_path)
        if '..' in out_path or not _safe_filename.match(output_basename):
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Invalid backup path: must be a simple filename (alphanumeric, dash, underscore, dot)",
            )
        # Force output into the same directory as the source DB
        backup_dir = os.path.dirname(os.path.abspath(db_path))
        out_path = os.path.join(backup_dir, output_basename)

        try:
            conn = await storage._get_conn()
            await conn.execute("PRAGMA wal_checkpoint(TRUNCATE)")
        except Exception as exc:
            logger.warning("WAL checkpoint failed (non-fatal): %s", exc)

        try:
            shutil.copy2(db_path, out_path)
        except Exception as exc:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Internal server error",
            ) from exc

        size_bytes = os.path.getsize(out_path)
        timestamp = datetime.now(timezone.utc).isoformat(timespec="seconds")

        return AdminBackupResponse(
            path=out_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:
        """Replace the active SQLite database with a backup file."""
        import shutil
        import os
        import aiosqlite

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

        # --- Path traversal protection (#213) ---
        import re as _re
        _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 = os.path.dirname(os.path.abspath(db_path))
        restore_path = os.path.join(backup_dir, restore_basename)

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

        errors: List[str] = []
        claims_count = 0
        try:
            shutil.copy2(restore_path, db_path)

            # 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(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)

    @router.get(
        f"{PREFIX}/admin/stats",
        response_model=AdminStatsResponse,
        summary="Expanded system statistics",
        tags=["admin"],
        operation_id="admin_stats",
    )
    async def admin_stats(
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> AdminStatsResponse:
        """Return comprehensive system statistics."""
        import os

        errors: List[str] = []
        storage = getattr(plugin, "storage", None)

        claims_total = 0
        claims_by_type: Dict[str, int] = {}
        claims_by_confidence: Dict[str, int] = {}

        if storage is not None:
            try:
                if hasattr(storage, "count_claims"):
                    claims_total = await storage.count_claims(
                        owner_id=getattr(plugin.config, "owner_id", "default")
                    )
                if hasattr(storage, "_conn") and storage._conn is not None:
                    async with storage._conn.execute(
                        "SELECT memory_type, COUNT(*) FROM semantic_claim GROUP BY memory_type"
                    ) as cur:
                        async for row in cur:
                            claims_by_type[row[0]] = row[1]
                    async with storage._conn.execute(
                        "SELECT confidence, COUNT(*) FROM semantic_claim GROUP BY confidence"
                    ) as cur:
                        async for row in cur:
                            claims_by_confidence[str(row[0])] = row[1]
            except Exception as exc:
                errors.append(f"Claims stats failed: {exc}")

        cs_count = 0
        cs_drift_scores: List[float] = []
        try:
            cs_list = await plugin.get_cornerstones()
            cs_count = len(cs_list)
            cs_drift_scores = [
                getattr(cs, "drift_score", 0.0) or 0.0
                for cs in cs_list
            ]
        except Exception as exc:
            errors.append(f"Cornerstone stats failed: {exc}")

        entities_total = 0
        if storage is not None:
            try:
                if hasattr(storage, "list_entities"):
                    entities_raw = await storage.list_entities(
                        owner_id=getattr(plugin.config, "owner_id", "default"),
                        limit=1,
                        offset=0,
                    )
                    if isinstance(entities_raw, tuple):
                        entities_total = entities_raw[1]
                    elif hasattr(storage, "_conn") and storage._conn is not None:
                        async with storage._conn.execute(
                            "SELECT COUNT(*) FROM entity"
                        ) as cur:
                            row = await cur.fetchone()
                            entities_total = row[0] if row else 0
                elif hasattr(storage, "_conn") and storage._conn is not None:
                    async with storage._conn.execute(
                        "SELECT COUNT(*) FROM entity"
                    ) as cur:
                        row = await cur.fetchone()
                        entities_total = row[0] if row else 0
            except Exception as exc:
                errors.append(f"Entity stats failed: {exc}")

        sessions_total = 0
        sessions_active = 0
        if storage is not None:
            try:
                if hasattr(storage, "count_sessions"):
                    sessions_total = await storage.count_sessions(
                        owner_id=getattr(plugin.config, "owner_id", "default")
                    )
                if hasattr(storage, "_conn") and storage._conn is not None:
                    async with storage._conn.execute(
                        "SELECT COUNT(*) FROM session_record WHERE ended_at IS NULL"
                    ) as cur:
                        row = await cur.fetchone()
                        sessions_active = row[0] if row else 0
                    if sessions_total == 0:
                        async with storage._conn.execute(
                            "SELECT COUNT(*) FROM session_record"
                        ) as cur:
                            row = await cur.fetchone()
                            sessions_total = row[0] if row else 0
            except Exception as exc:
                errors.append(f"Session stats failed: {exc}")

        brain_nodes = 0
        brain_edges = 0
        if storage is not None:
            try:
                if hasattr(storage, "_conn") and storage._conn is not None:
                    async with storage._conn.execute(
                        "SELECT COUNT(*) FROM brain_graph_node"
                    ) as cur:
                        row = await cur.fetchone()
                        brain_nodes = row[0] if row else 0
                    async with storage._conn.execute(
                        "SELECT COUNT(*) FROM brain_graph_edge"
                    ) as cur:
                        row = await cur.fetchone()
                        brain_edges = row[0] if row else 0
            except Exception as exc:
                errors.append(f"Brain graph stats failed: {exc}")

        db_size_bytes = 0
        embedding_count = 0
        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())
        try:
            if os.path.exists(db_path_str):
                db_size_bytes = os.path.getsize(db_path_str)
            if storage is not None and hasattr(storage, "_conn") and storage._conn is not None:
                async with storage._conn.execute(
                    "SELECT COUNT(*) FROM embedding_index"
                ) as cur:
                    row = await cur.fetchone()
                    embedding_count = row[0] if row else 0
        except Exception as exc:
            errors.append(f"Storage stats failed: {exc}")

        return AdminStatsResponse(
            claims={
                "total": claims_total,
                "by_type": claims_by_type,
                "by_confidence": claims_by_confidence,
            },
            cornerstones={
                "count": cs_count,
                "drift_scores": cs_drift_scores,
            },
            entities={"count": entities_total},
            sessions={"total": sessions_total, "active": sessions_active},
            brain_graph={"nodes": brain_nodes, "edges": brain_edges},
            storage={"db_size_bytes": db_size_bytes, "embedding_count": embedding_count},
            errors=errors,
        )

    # ---- CANONICAL: /admin/requests  (properly namespaced — issue #297) ----
    @router.get(
        f"{PREFIX}/admin/requests",
        response_model=RequestLogResponse,
        summary="Paginated request log",
        tags=["admin"],
        operation_id="admin_get_request_log",
    )
    async def admin_get_request_log(
        request_type: str | None = None,
        limit: int = 50,
        offset: int = 0,
        after: str | None = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> RequestLogResponse:
        """Return a paginated view of the request log.

        Canonical path: ``GET /api/v1/admin/requests``.
        """
        return await _get_request_log_impl(
            request_type=request_type, limit=limit, offset=offset, after=after, plugin=plugin
        )

    # ---- DEPRECATED: /requests  (un-namespaced — issue #297) ----
    @router.get(
        f"{PREFIX}/requests",
        response_model=RequestLogResponse,
        summary="Paginated request log — DEPRECATED, use /admin/requests",
        tags=["system (deprecated)"],
        operation_id="get_request_log",
        deprecated=True,
    )
    async def get_request_log(
        request_type: str | None = None,
        limit: int = 50,
        offset: int = 0,
        after: str | None = None,
        response: Response = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ) -> RequestLogResponse:
        """Return a paginated view of the request log.

        .. deprecated::
            Use ``GET /api/v1/admin/requests``.
            Sunset: 2026-06-01  (issue #297)
        """
        if response is not None:
            add_deprecation_headers(response, f"{PREFIX}/admin/requests")
        return await _get_request_log_impl(
            request_type=request_type, limit=limit, offset=offset, after=after, plugin=plugin
        )

    async def _get_request_log_impl(
        request_type, limit, offset, after, plugin
    ) -> RequestLogResponse:
        """Shared implementation for request-log endpoints."""
        errors: List[str] = []
        items: List[Dict[str, Any]] = []
        total = 0

        storage = getattr(plugin, "storage", None)
        if storage is None:
            errors.append("Storage not available")
            return RequestLogResponse(errors=errors)

        if not hasattr(storage, "get_request_log"):
            errors.append("Request log not supported by storage")
            return RequestLogResponse(errors=errors)

        owner_id = getattr(plugin.config, "owner_id", "default")

        try:
            rows = await storage.get_request_log(
                owner_id=owner_id,
                request_type=request_type,
                limit=limit,
                offset=offset,
            )

            if after is not None:
                from datetime import datetime, timezone
                try:
                    after_dt = datetime.fromisoformat(after.replace("Z", "+00:00"))
                    filtered = []
                    for row in rows:
                        created_at = row.get("created_at")
                        if created_at is None:
                            filtered.append(row)
                            continue
                        if isinstance(created_at, str):
                            try:
                                row_dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
                            except ValueError:
                                filtered.append(row)
                                continue
                        elif isinstance(created_at, (int, float)):
                            row_dt = datetime.fromtimestamp(created_at, tz=timezone.utc)
                        else:
                            filtered.append(row)
                            continue
                        if row_dt > after_dt:
                            filtered.append(row)
                    rows = filtered
                except (ValueError, TypeError) as exc:
                    errors.append(f"Invalid 'after' datetime: {exc}")

            items = [_serialise(row) for row in rows]
            total = len(items)
        except Exception as exc:
            err = f"Request log query failed: {exc}"
            logger.error(err, exc_info=True)
            errors.append(err)

        return RequestLogResponse(items=items, total=total, errors=errors)

    @router.post(
        f"{PREFIX}/admin/prune-logs",
        summary="Prune old event_log and request_log rows",
        tags=["admin"],
    )
    async def prune_logs(
        retention_days: Optional[int] = None,
        _: None = Depends(verify_api_key),
        plugin: Any = Depends(get_plugin),
    ):
        """Delete event_log and request_log rows older than *retention_days*.

        Uses ``config.log_retention_days`` (default 90) when ``retention_days``
        is not supplied.  Returns the row counts deleted from each table.
        """
        storage = plugin.storage
        if storage is None:
            raise HTTPException(status_code=503, detail="Storage unavailable")
        if not hasattr(storage, "prune_old_logs"):
            raise HTTPException(status_code=501, detail="Storage backend does not support log pruning")

        days = retention_days if retention_days is not None else getattr(plugin.config, "log_retention_days", 90)
        try:
            result = await storage.prune_old_logs(retention_days=days)
        except Exception as exc:
            logger.error("prune_logs error: %s", exc, exc_info=True)
            raise HTTPException(status_code=500, detail="Internal server error")

        return {"pruned": result, "retention_days": days}
