"""Topology wizard + cluster node management (API + internode endpoints)."""

from __future__ import annotations

import secrets
from typing import Any, Literal, Optional  # noqa: F401 — Any used in file_payload

from fastapi import APIRouter, Depends, Header, HTTPException, Request, status
from pydantic import BaseModel, ConfigDict, Field, field_validator
from sqlalchemy.orm import Session

from .. import models
from ..db import get_db
from ..security import require_admin
from ..settings import get_settings
from ..topology import (
    MODE_DESCRIPTIONS,
    MODE_LABELS,
    cluster_storage_backend,
    internode_request,
    load_topology_file,
    local_server_id,
    routed_db_strategy,
    save_topology_file,
    topology_mode,
    topology_summary,
    validate_api_base_url,
    validate_server_id,
    verify_internode_token,
    verify_sync_signature,
)

router = APIRouter(tags=["topology"])
settings = get_settings()


class TopologyWizard(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    mode: Literal["standalone", "routed", "cluster"]
    local_server_id: Optional[str] = None
    display_name: Optional[str] = None
    # routed
    routed_db_strategy: Optional[Literal["shared", "synced"]] = "shared"
    # cluster
    cluster_storage: Optional[Literal["nfs", "ceph", "s3"]] = "nfs"
    cluster_storage_path: Optional[str] = None
    s3_endpoint: Optional[str] = None
    s3_bucket: Optional[str] = None
    s3_region: Optional[str] = None
    s3_access_key_id: Optional[str] = None
    s3_secret_access_key: Optional[str] = None
    s3_cache_ttl_seconds: int = Field(default=3600, ge=60, le=86400 * 30)
    s3_cache_max_mb: int = Field(default=10240, ge=128, le=1024 * 1024)
    # shared internode
    generate_internode_token: bool = True
    internode_token: Optional[str] = Field(default=None, min_length=32, max_length=256)
    peer_api_base_url: Optional[str] = None  # join existing mesh/cluster

    @field_validator("local_server_id")
    @classmethod
    def _sid(cls, value: Optional[str]) -> Optional[str]:
        if value is None or value == "":
            return None
        return validate_server_id(value)

    @field_validator("peer_api_base_url")
    @classmethod
    def _peer(cls, value: Optional[str]) -> Optional[str]:
        if value is None or value == "":
            return None
        return validate_api_base_url(value)


class ClusterServerIn(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    server_id: str
    display_name: str = Field(min_length=1, max_length=128)
    hostname: str = Field(min_length=1, max_length=255)
    api_base_url: str
    smtp_host: str = Field(min_length=1, max_length=255)
    smtp_port: int = Field(default=25, ge=1, le=65535)
    role: Literal["edge", "backend", "both"] = "both"
    is_active: bool = True

    @field_validator("server_id")
    @classmethod
    def _sid(cls, value: str) -> str:
        return validate_server_id(value)

    @field_validator("api_base_url")
    @classmethod
    def _url(cls, value: str) -> str:
        return validate_api_base_url(value)


def _require_internode(
    authorization: Optional[str] = Header(default=None),
    x_limristem_internode: Optional[str] = Header(default=None),
) -> str:
    token = x_limristem_internode
    if not token and authorization and authorization.lower().startswith("bearer "):
        token = authorization.split(" ", 1)[1].strip()
    if not verify_internode_token(token):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid internode token")
    return token or ""


def _write_env_topology(payload: TopologyWizard, internode_token: str | None) -> None:
    """Best-effort env persistence via manage-topology helper when available."""
    from ..admin_ops import run_root_script

    args = [
        "apply-mode",
        payload.mode,
        "--server-id",
        payload.local_server_id or local_server_id(),
    ]
    if payload.mode == "routed":
        args.extend(["--routed-db", payload.routed_db_strategy or "shared"])
    if payload.mode == "cluster":
        args.extend(["--storage", payload.cluster_storage or "nfs"])
        if payload.cluster_storage_path:
            args.extend(["--storage-path", payload.cluster_storage_path])
        if payload.cluster_storage == "s3":
            args.extend(
                [
                    "--s3-cache-ttl",
                    str(payload.s3_cache_ttl_seconds),
                    "--s3-cache-max-mb",
                    str(payload.s3_cache_max_mb),
                ]
            )
    if internode_token:
        args.extend(["--internode-token", internode_token])
    try:
        run_root_script("manage-topology.sh", *args)
    except HTTPException:
        # Dev / unprivileged: topology.json still holds operator intent.
        pass


@router.get("/topology")
def get_topology(_: str = Depends(require_admin), db: Session = Depends(get_db)):
    servers = db.query(models.ClusterServer).order_by(models.ClusterServer.server_id.asc()).all()
    return {
        **topology_summary(),
        "modes": [
            {"id": key, "label": MODE_LABELS[key], "description": MODE_DESCRIPTIONS[key]}
            for key in ("standalone", "routed", "cluster")
        ],
        "servers": [
            {
                "id": s.id,
                "server_id": s.server_id,
                "display_name": s.display_name,
                "hostname": s.hostname,
                "api_base_url": s.api_base_url,
                "smtp_host": s.smtp_host,
                "smtp_port": s.smtp_port,
                "is_local": bool(s.is_local),
                "is_active": bool(s.is_active),
                "role": s.role,
                "last_seen_at": s.last_seen_at,
            }
            for s in servers
        ],
        "file_config": {k: v for k, v in load_topology_file().items() if "secret" not in k.lower() and "token" not in k.lower() and "password" not in k.lower() and "key" not in k.lower()},
    }


@router.post("/topology/wizard")
def topology_wizard(payload: TopologyWizard, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    sid = payload.local_server_id or local_server_id()
    token = payload.internode_token
    if payload.mode != "standalone":
        if payload.generate_internode_token and not token:
            token = secrets.token_urlsafe(48)
        if not token or len(token) < 32:
            raise HTTPException(status_code=400, detail="Internode token required (min 32 chars) for multi-node modes")
    else:
        token = token if token else None

    file_payload: dict[str, Any] = {
        "mode": payload.mode,
        "local_server_id": sid,
        "display_name": payload.display_name or sid,
        "routed_db_strategy": payload.routed_db_strategy or "shared",
        "cluster_storage": payload.cluster_storage or "nfs",
        "cluster_storage_path": payload.cluster_storage_path or "",
        "s3_endpoint": payload.s3_endpoint or "",
        "s3_bucket": payload.s3_bucket or "",
        "s3_region": payload.s3_region or "",
        "s3_access_key_id": payload.s3_access_key_id or "",
        "s3_cache_ttl_seconds": payload.s3_cache_ttl_seconds,
        "s3_cache_max_mb": payload.s3_cache_max_mb,
    }
    if payload.s3_secret_access_key:
        file_payload["s3_secret_access_key"] = payload.s3_secret_access_key
    save_topology_file(file_payload)
    _write_env_topology(payload, token)

    # Ensure local server row exists for multi-node modes.
    if payload.mode in {"routed", "cluster"}:
        existing = db.query(models.ClusterServer).filter(models.ClusterServer.server_id == sid).first()
        api_url = f"https://{settings.hostname}"
        if existing:
            existing.display_name = payload.display_name or sid
            existing.hostname = settings.hostname
            existing.api_base_url = api_url
            existing.smtp_host = settings.hostname
            existing.is_local = True
            existing.is_active = True
        else:
            db.add(
                models.ClusterServer(
                    server_id=sid,
                    display_name=payload.display_name or sid,
                    hostname=settings.hostname,
                    api_base_url=api_url,
                    smtp_host=settings.hostname,
                    smtp_port=25,
                    is_local=True,
                    is_active=True,
                    role="both",
                )
            )
        db.commit()

    # Optional: join peer mesh by registering ourselves.
    peer_result = None
    if payload.peer_api_base_url and token and payload.mode != "standalone":
        try:
            peer_result = internode_request(
                payload.peer_api_base_url,
                "/internode/v1/register",
                method="POST",
                payload={
                    "server_id": sid,
                    "display_name": payload.display_name or sid,
                    "hostname": settings.hostname,
                    "api_base_url": f"https://{settings.hostname}",
                    "smtp_host": settings.hostname,
                    "smtp_port": 25,
                    "role": "both",
                },
            )
        except Exception as exc:
            peer_result = {"status": "error", "detail": str(exc)}

    return {
        "status": "ok",
        "topology": topology_summary(),
        "internode_token": token if payload.mode != "standalone" else None,
        "internode_token_note": "Store this token now; only a hash/meta is kept on disk where possible."
        if token
        else None,
        "peer_registration": peer_result,
    }


@router.get("/topology/servers")
def list_servers(db: Session = Depends(get_db), _: str = Depends(require_admin)):
    return get_topology(_, db)["servers"]


@router.post("/topology/servers")
def add_server(payload: ClusterServerIn, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    if topology_mode() == "standalone":
        raise HTTPException(status_code=400, detail="Switch topology mode away from standalone before adding peers")
    existing = db.query(models.ClusterServer).filter(models.ClusterServer.server_id == payload.server_id).first()
    if existing:
        raise HTTPException(status_code=409, detail="server_id already exists")
    row = models.ClusterServer(
        server_id=payload.server_id,
        display_name=payload.display_name,
        hostname=payload.hostname.lower(),
        api_base_url=payload.api_base_url,
        smtp_host=payload.smtp_host.lower(),
        smtp_port=payload.smtp_port,
        is_local=False,
        is_active=payload.is_active,
        role=payload.role,
    )
    db.add(row)
    db.commit()
    db.refresh(row)
    return {"id": row.id, "server_id": row.server_id}


@router.delete("/topology/servers/{server_pk}")
def delete_server(server_pk: int, db: Session = Depends(get_db), _: str = Depends(require_admin)):
    row = db.query(models.ClusterServer).filter(models.ClusterServer.id == server_pk).first()
    if not row:
        raise HTTPException(status_code=404, detail="Server not found")
    if row.is_local:
        raise HTTPException(status_code=400, detail="Cannot delete the local server row")
    db.delete(row)
    db.commit()
    return {"deleted": True, "server_id": row.server_id}


# ---- Internode protocol (Bearer internode token, not admin basic auth) ----


@router.get("/internode/v1/hello")
def internode_hello(_: str = Depends(_require_internode)):
    return {
        "status": "ok",
        "server_id": local_server_id(),
        "mode": topology_mode(),
        "hostname": settings.hostname,
    }


@router.post("/internode/v1/register")
def internode_register(
    payload: ClusterServerIn,
    db: Session = Depends(get_db),
    _: str = Depends(_require_internode),
):
    existing = db.query(models.ClusterServer).filter(models.ClusterServer.server_id == payload.server_id).first()
    if existing:
        existing.display_name = payload.display_name
        existing.hostname = payload.hostname.lower()
        existing.api_base_url = payload.api_base_url
        existing.smtp_host = payload.smtp_host.lower()
        existing.smtp_port = payload.smtp_port
        existing.role = payload.role
        existing.is_active = payload.is_active
        existing.is_local = False
        db.commit()
        return {"status": "updated", "server_id": existing.server_id}
    row = models.ClusterServer(
        server_id=payload.server_id,
        display_name=payload.display_name,
        hostname=payload.hostname.lower(),
        api_base_url=payload.api_base_url,
        smtp_host=payload.smtp_host.lower(),
        smtp_port=payload.smtp_port,
        is_local=False,
        is_active=payload.is_active,
        role=payload.role,
    )
    db.add(row)
    db.commit()
    return {"status": "created", "server_id": row.server_id}


@router.post("/internode/v1/sync")
async def internode_sync(
    request: Request,
    db: Session = Depends(get_db),
    _: str = Depends(_require_internode),
    x_limristem_signature: Optional[str] = Header(default=None),
):
    body = await request.body()
    if not verify_sync_signature(body, x_limristem_signature):
        raise HTTPException(status_code=401, detail="Invalid sync signature")
    try:
        payload = await request.json()
    except Exception as exc:
        raise HTTPException(status_code=400, detail="Invalid JSON body") from exc
    if not isinstance(payload, dict):
        raise HTTPException(status_code=400, detail="Invalid payload")
    entity_type = str(payload.get("entity_type") or "")
    operation = str(payload.get("operation") or "")
    entity_key = str(payload.get("entity_key") or "")[:320]
    if entity_type not in {"account", "domain", "alias", "redirect", "server"}:
        raise HTTPException(status_code=400, detail="Invalid entity_type")
    if operation not in {"upsert", "delete"}:
        raise HTTPException(status_code=400, detail="Invalid operation")
    # Enqueue for local application (worker processes queue).
    row = models.ClusterSyncQueue(
        entity_type=entity_type,
        entity_key=entity_key or "unknown",
        operation=operation,
        payload=payload,
        status="pending",
    )
    db.add(row)
    db.commit()
    return {"status": "queued", "id": row.id}
