import os
import secrets
from pathlib import Path

from fastapi import Depends, FastAPI, Request, Response, status
from fastapi.staticfiles import StaticFiles
from redis.exceptions import RedisError
from sqlalchemy import text
from sqlalchemy.exc import SQLAlchemyError

from .cache import get_cache
from .settings import get_settings
from .db import engine
from .routers import domains, accounts, aliases, redirects, reputation, admin, operations, stats
from .routers import topology_api, auth as auth_router
from .routers import mailbox_import as mailbox_import_router
from .routers import autoresponders as autoresponders_router
from .routers import jmap_router
from .routers import contacts as contacts_router
from .security import require_admin
import logging

logger = logging.getLogger(__name__)

settings = get_settings()

# OpenAPI/docs stay off by default in production; enable with LIMRISTEM_MAIL_ENABLE_API_DOCS=yes.
_enable_api_docs = os.getenv("LIMRISTEM_MAIL_ENABLE_API_DOCS", "no").lower() in {"1", "true", "yes", "on"}
app = FastAPI(
    title="Limristem eMail API",
    version=settings.app_version,
    docs_url="/docs" if _enable_api_docs else None,
    redoc_url="/redoc" if _enable_api_docs else None,
    openapi_url="/openapi.json" if _enable_api_docs else None,
)
_static_dir = Path(__file__).resolve().parent / "static"
if _static_dir.is_dir():
    app.mount("/panel/assets", StaticFiles(directory=str(_static_dir)), name="panel-assets")

app.include_router(auth_router.router)
app.include_router(domains.router)
app.include_router(accounts.router)
app.include_router(aliases.router)
app.include_router(redirects.router)
app.include_router(reputation.router)
app.include_router(operations.router)
app.include_router(stats.router)
app.include_router(topology_api.router)
app.include_router(mailbox_import_router.router)
app.include_router(autoresponders_router.router)
app.include_router(contacts_router.router)
# Experimental and incomplete (see Settings.enable_jmap): only mounted when the
# operator opts in with LIMRISTEM_MAIL_ENABLE_JMAP=yes.
if settings.enable_jmap:
    app.include_router(jmap_router.router)
app.include_router(admin.router)


@app.on_event("startup")
def _start_mailbox_import_worker() -> None:
    """Start the background IMAP import worker when the API process boots."""
    try:
        from .mailbox_import import ensure_schema, ensure_worker_started

        ensure_schema()
        ensure_worker_started()
    except Exception as exc:
        # Schema may be unavailable during install; worker starts on first job.
        logger.warning("_start_mailbox_import_worker: suppressed %s: %s", type(exc).__name__, exc)
    try:
        from .db import SessionLocal
        from .mailbox_ops import ensure_primary_hostname_domain

        db = SessionLocal()
        try:
            ensure_primary_hostname_domain(db)
        finally:
            db.close()
    except Exception as exc:
        logger.warning("_start_mailbox_import_worker: suppressed %s: %s", type(exc).__name__, exc)


@app.middleware("http")
async def add_security_headers(request: Request, call_next):
    """Add security hardening headers to every response (panel CSP uses per-request nonces)."""
    csp_nonce = secrets.token_urlsafe(16)
    request.state.csp_nonce = csp_nonce
    response: Response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-XSS-Protection"] = "0"
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
    response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
    response.headers["Cross-Origin-Resource-Policy"] = "same-origin"
    if request.url.path.startswith("/auth") or (
        request.url.path.startswith("/panel") and not request.url.path.startswith("/panel/assets/")
    ):
        # Authentication secrets and private keys must not survive in caches.
        response.headers["Cache-Control"] = "no-store"
        response.headers["Pragma"] = "no-cache"
    if request.url.path.startswith("/panel"):
        # The panel never frames itself, so no framing at all is allowed: an admin
        # console is a prime clickjacking target and same-origin framing buys nothing.
        response.headers["X-Frame-Options"] = "DENY"
        # Nonce-based CSP: no 'unsafe-inline'. External assets under /panel/assets/.
        response.headers["Content-Security-Policy"] = (
            "default-src 'self'; "
            f"script-src 'self' 'nonce-{csp_nonce}'; "
            f"script-src-elem 'self' 'nonce-{csp_nonce}'; "
            "script-src-attr 'unsafe-inline'; "
            "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; "
            f"style-src-elem 'self' 'unsafe-inline' 'nonce-{csp_nonce}' https://fonts.googleapis.com; "
            "style-src-attr 'unsafe-inline'; "
            "font-src 'self' https://fonts.gstatic.com data:; "
            "img-src 'self' data:; "
            "connect-src 'self'; "
            "frame-ancestors 'none'; "
            "base-uri 'self'; "
            "form-action 'self'; "
            "object-src 'none'"
        )
    else:
        response.headers["X-Frame-Options"] = "DENY"
        response.headers.setdefault(
            "Content-Security-Policy",
            "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'",
        )
    if settings.ssl_mode != "plain":
        response.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains; preload"
    return response


def collect_health() -> dict:
    checks = {
        "database": {"status": "ok"},
        "redis": {"status": "ok"},
    }
    overall = "ok"

    try:
        with engine.connect() as connection:
            connection.execute(text("SELECT 1"))
    except SQLAlchemyError as exc:
        checks["database"] = {"status": "error", "detail": exc.__class__.__name__}
        overall = "degraded"

    try:
        get_cache().ping()
    except RedisError as exc:
        checks["redis"] = {"status": "error", "detail": exc.__class__.__name__}
        overall = "degraded"

    return {"status": overall, "checks": checks}


@app.get("/health")
def health(response: Response):
    """Public liveness probe.

    Deliberately reports only the overall status: which backend is degraded (and
    therefore whether the fail-closed auth store is currently down) is operational
    detail for authenticated callers. Full output stays at /health/details.
    """
    payload = collect_health()
    if payload["status"] != "ok":
        response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
    return {"status": payload["status"]}


@app.get("/health/details")
def health_details(response: Response, _: str = Depends(require_admin)):
    payload = collect_health()
    payload["hostname"] = settings.hostname
    payload["mail_home"] = str(settings.mail_home)
    payload["dkim_keys_dir"] = str(settings.dkim_keys_dir)
    if payload["status"] != "ok":
        response.status_code = status.HTTP_503_SERVICE_UNAVAILABLE
    return payload
