"""RBS REGAL — Global Error Tracking & System Health module.

Stores both backend AND frontend errors in the `error_logs` collection so they
can be reviewed from a single admin dashboard.

Collections:
  - error_logs:
      {
        _id,
        ts (ISO),
        source ("backend" | "frontend"),
        kind ("exception" | "http_5xx" | "http_4xx" | "react" | "promise" | "console"),
        path (request path or page URL),
        method (HTTP verb, optional),
        status (HTTP status, optional),
        message (str),
        stack (str, optional, truncated to 4 KB),
        user_id (optional),
        company_id (optional),
        user_agent (optional, browser only),
        meta (free-form dict, optional)
      }

API:
  POST /api/errors/log              — frontend reports an error (auth optional)
  GET  /api/errors/list             — admin: paginated list with filters
  GET  /api/errors/stats            — admin: aggregate counts by kind/source/path
  POST /api/errors/clear            — admin: prune old errors (default: > 30 days)
  GET  /api/admin/health            — admin: aggregate system health snapshot
"""
from __future__ import annotations

import os
import time
import logging
from datetime import datetime, timezone, timedelta
from typing import Optional

from fastapi import APIRouter, Request, Depends, HTTPException, Query, status
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from starlette.middleware.base import BaseHTTPMiddleware

from auth import get_current_user, require_admin

logger = logging.getLogger("rbs.errors")
router = APIRouter(prefix="/api/errors", tags=["errors"])
admin_health_router = APIRouter(prefix="/api/admin", tags=["admin-health"])

# How long to keep error logs by default
DEFAULT_RETENTION_DAYS = int(os.environ.get("ERROR_LOG_RETENTION_DAYS", "30"))
MAX_STACK_LEN = 4096


def _trim(text: str | None, n: int = MAX_STACK_LEN) -> str:
    if not text:
        return ""
    s = str(text)
    return s if len(s) <= n else s[: n - 3] + "..."


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


# ============================================================================
# Pydantic models
# ============================================================================

class FrontendErrorIn(BaseModel):
    kind: str = Field(default="frontend", description="react | promise | console | manual")
    message: str
    stack: Optional[str] = None
    path: Optional[str] = None      # page URL
    user_agent: Optional[str] = None
    meta: Optional[dict] = None


class ErrorOut(BaseModel):
    id: str
    ts: str
    source: str
    kind: str
    path: Optional[str] = None
    method: Optional[str] = None
    status: Optional[int] = None
    message: str
    stack: Optional[str] = None
    user_id: Optional[str] = None
    company_id: Optional[str] = None
    user_agent: Optional[str] = None
    meta: Optional[dict] = None


# ============================================================================
# Recording helpers (callable from anywhere in the backend)
# ============================================================================

async def record_error(
    db,
    *,
    source: str,
    kind: str,
    message: str,
    stack: str | None = None,
    path: str | None = None,
    method: str | None = None,
    status_code: int | None = None,
    user_id: str | None = None,
    company_id: str | None = None,
    user_agent: str | None = None,
    meta: dict | None = None,
) -> None:
    """Insert a single error log row. Best-effort — never raises."""
    try:
        await db.error_logs.insert_one({
            "ts": _now_iso(),
            "source": source,
            "kind": kind,
            "path": path,
            "method": method,
            "status": status_code,
            "message": _trim(message, 1000),
            "stack": _trim(stack),
            "user_id": user_id,
            "company_id": company_id,
            "user_agent": _trim(user_agent, 512),
            "meta": meta or {},
        })
    except Exception:
        logger.exception("record_error failed")


# ============================================================================
# FastAPI middleware — captures backend exceptions + 5xx automatically
# ============================================================================

class ErrorCaptureMiddleware(BaseHTTPMiddleware):
    """
    Captures every unhandled backend exception + every 5xx response and
    persists it to `error_logs`. Already-handled HTTP exceptions (4xx) are
    NOT captured to keep noise low (validation errors, auth failures etc.).
    """

    async def dispatch(self, request: Request, call_next):
        start = time.monotonic()
        path = str(request.url.path)
        method = request.method
        db = getattr(request.app.state, "db", None)

        try:
            response = await call_next(request)
        except Exception as exc:
            # Best-effort persist, then re-raise (FastAPI will return 500)
            if db is not None:
                import traceback
                user_id = None
                try:
                    cookies = request.cookies
                    user_id = cookies.get("user_id") or None
                except Exception:
                    user_id = None
                await record_error(
                    db,
                    source="backend",
                    kind="exception",
                    message=f"{type(exc).__name__}: {exc}",
                    stack=traceback.format_exc(),
                    path=path,
                    method=method,
                    status_code=500,
                    user_id=user_id,
                    user_agent=request.headers.get("user-agent"),
                )
            raise

        # Capture 5xx responses (from raise HTTPException or explicit returns)
        if response.status_code >= 500 and db is not None:
            await record_error(
                db,
                source="backend",
                kind="http_5xx",
                message=f"HTTP {response.status_code} on {method} {path}",
                path=path,
                method=method,
                status_code=response.status_code,
                user_agent=request.headers.get("user-agent"),
                meta={"duration_ms": int((time.monotonic() - start) * 1000)},
            )
        return response


# ============================================================================
# Routes
# ============================================================================

@router.post("/log")
async def log_frontend_error(payload: FrontendErrorIn, request: Request):
    """Anyone can post a frontend error — even unauthenticated users
    (the login page itself may crash and we want that captured)."""
    db = request.app.state.db
    user_id = None
    company_id = None
    try:
        # Optional user context — DON'T raise if anonymous
        import jwt as _jwt
        from auth import get_jwt_secret, JWT_ALGORITHM
        token = request.cookies.get("access_token") or request.cookies.get("rbs_access_token")
        if token:
            decoded = _jwt.decode(token, get_jwt_secret(), algorithms=[JWT_ALGORITHM])
            user_id = decoded.get("sub")
            company_id = decoded.get("company_id")
    except Exception:
        user_id = None
        company_id = None

    await record_error(
        db,
        source="frontend",
        kind=payload.kind or "frontend",
        message=payload.message,
        stack=payload.stack,
        path=payload.path,
        user_id=user_id,
        company_id=company_id,
        user_agent=payload.user_agent or request.headers.get("user-agent"),
        meta=payload.meta,
    )
    return {"ok": True}


@router.get("/list")
async def list_errors(
    request: Request,
    source: Optional[str] = None,
    kind: Optional[str] = None,
    path: Optional[str] = None,
    since_hours: int = Query(default=168, ge=1, le=24 * 30),
    limit: int = Query(default=100, ge=1, le=500),
    skip: int = Query(default=0, ge=0),
    user=Depends(require_admin),
):
    db = request.app.state.db
    since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
    q: dict = {"ts": {"$gte": since}}
    if source:
        q["source"] = source
    if kind:
        q["kind"] = kind
    if path:
        q["path"] = {"$regex": path, "$options": "i"}
    cursor = db.error_logs.find(q).sort("ts", -1).skip(skip).limit(limit)
    rows = []
    async for d in cursor:
        rows.append({
            "id": str(d.get("_id")),
            "ts": d.get("ts"),
            "source": d.get("source"),
            "kind": d.get("kind"),
            "path": d.get("path"),
            "method": d.get("method"),
            "status": d.get("status"),
            "message": d.get("message"),
            "stack": d.get("stack"),
            "user_id": d.get("user_id"),
            "company_id": d.get("company_id"),
            "user_agent": d.get("user_agent"),
            "meta": d.get("meta") or {},
        })
    total = await db.error_logs.count_documents(q)
    return {"items": rows, "total": total, "since_hours": since_hours}


@router.get("/stats")
async def error_stats(
    request: Request,
    since_hours: int = Query(default=24, ge=1, le=24 * 30),
    user=Depends(require_admin),
):
    db = request.app.state.db
    since = (datetime.now(timezone.utc) - timedelta(hours=since_hours)).isoformat()
    match = {"$match": {"ts": {"$gte": since}}}

    by_kind = []
    async for d in db.error_logs.aggregate([match, {"$group": {"_id": "$kind", "n": {"$sum": 1}}}, {"$sort": {"n": -1}}]):
        by_kind.append({"kind": d["_id"] or "unknown", "count": d["n"]})

    by_source = []
    async for d in db.error_logs.aggregate([match, {"$group": {"_id": "$source", "n": {"$sum": 1}}}, {"$sort": {"n": -1}}]):
        by_source.append({"source": d["_id"] or "unknown", "count": d["n"]})

    by_path = []
    async for d in db.error_logs.aggregate([match, {"$group": {"_id": "$path", "n": {"$sum": 1}}}, {"$sort": {"n": -1}}, {"$limit": 10}]):
        by_path.append({"path": d["_id"] or "unknown", "count": d["n"]})

    total = await db.error_logs.count_documents({"ts": {"$gte": since}})
    return {
        "since_hours": since_hours,
        "total": total,
        "by_kind": by_kind,
        "by_source": by_source,
        "by_path": by_path,
    }


@router.post("/clear")
async def clear_errors(
    request: Request,
    older_than_days: int = Query(default=DEFAULT_RETENTION_DAYS, ge=1, le=365),
    user=Depends(require_admin),
):
    db = request.app.state.db
    cutoff = (datetime.now(timezone.utc) - timedelta(days=older_than_days)).isoformat()
    res = await db.error_logs.delete_many({"ts": {"$lt": cutoff}})
    return {"deleted": res.deleted_count, "older_than_days": older_than_days}


# ============================================================================
# System Health Dashboard
# ============================================================================

# Track service start time
_SERVICE_START_TS = datetime.now(timezone.utc)


@admin_health_router.get("/health")
async def system_health(request: Request, user=Depends(require_admin)):
    """Aggregate health snapshot for the System Health Dashboard."""
    db = request.app.state.db
    now = datetime.now(timezone.utc)
    h24 = (now - timedelta(hours=24)).isoformat()
    h1 = (now - timedelta(hours=1)).isoformat()

    # --- DB ping ------------------------------------------------------------
    db_status = "down"
    db_latency_ms: Optional[int] = None
    try:
        t0 = time.monotonic()
        await db.command("ping")
        db_latency_ms = int((time.monotonic() - t0) * 1000)
        db_status = "ok"
    except Exception as e:
        db_status = f"error: {e}"

    # --- Errors -------------------------------------------------------------
    errors_24h = await db.error_logs.count_documents({"ts": {"$gte": h24}})
    errors_1h = await db.error_logs.count_documents({"ts": {"$gte": h1}})
    backend_5xx_24h = await db.error_logs.count_documents({"ts": {"$gte": h24}, "kind": "http_5xx"})

    # --- Top failing paths --------------------------------------------------
    top_paths = []
    try:
        async for d in db.error_logs.aggregate([
            {"$match": {"ts": {"$gte": h24}, "kind": {"$in": ["http_5xx", "exception"]}}},
            {"$group": {"_id": "$path", "n": {"$sum": 1}}},
            {"$sort": {"n": -1}},
            {"$limit": 5},
        ]):
            top_paths.append({"path": d["_id"] or "unknown", "count": d["n"]})
    except Exception:
        pass

    # --- Storage usage (count of biggest collections) ----------------------
    storage = {}
    try:
        collstats = [
            "users", "companies", "items", "parties", "invoices",
            "expenses", "items", "label_templates", "error_logs", "activity_logs",
        ]
        for c in collstats:
            try:
                storage[c] = await db[c].estimated_document_count()
            except Exception:
                storage[c] = 0
    except Exception:
        pass

    # --- Backup status ------------------------------------------------------
    last_backup = None
    try:
        last = await db.backups.find_one({}, sort=[("created_at", -1)])
        if last:
            last_backup = {
                "id": str(last.get("_id")),
                "created_at": last.get("created_at"),
                "kind": last.get("kind"),
                "size_bytes": last.get("size_bytes"),
            }
    except Exception:
        pass

    # --- Active sessions / users -------------------------------------------
    active_users_24h = 0
    try:
        active_users_24h = await db.login_history.count_documents({"ts": {"$gte": h24}})
    except Exception:
        pass

    # --- Performance score (heuristic, 0-100) -------------------------------
    # Base 100, minus penalties:
    #   - 5xx in last 24h:   -2 per error (capped at -40)
    #   - DB latency > 100ms: -5
    #   - DB down:            -50
    score = 100
    score -= min(40, backend_5xx_24h * 2)
    if isinstance(db_latency_ms, int) and db_latency_ms > 100:
        score -= 5
    if db_status != "ok":
        score -= 50
    score = max(0, score)

    uptime_seconds = int((now - _SERVICE_START_TS).total_seconds())

    return {
        "ok": True,
        "now": now.isoformat(),
        "service": {
            "uptime_seconds": uptime_seconds,
            "started_at": _SERVICE_START_TS.isoformat(),
            "python": os.environ.get("PYTHON_VERSION") or "3.11",
            "node_id": os.environ.get("HOSTNAME") or "rbs-backend",
        },
        "database": {
            "status": db_status,
            "latency_ms": db_latency_ms,
            "name": os.environ.get("DB_NAME", ""),
        },
        "errors": {
            "last_1h": errors_1h,
            "last_24h": errors_24h,
            "backend_5xx_24h": backend_5xx_24h,
            "top_failing_paths_24h": top_paths,
        },
        "storage": {"document_counts": storage},
        "backup": {"last": last_backup},
        "activity": {"active_users_24h": active_users_24h},
        "performance_score": score,
    }
