"""
Admin-only routes for managing the auth-provider stack.

Provider _credentials_ (LDAP host, OIDC client id, SAML certs, ...) stay
in environment variables — they require process restart to take effect
and shouldn't be hot-edited via API. What IS editable at runtime is the
group→role mapping, because admins re-evaluate org structure often.

Endpoints:
- GET    /api/v1/auth-config/status               provider enablement + last sync stats
- GET    /api/v1/auth-config/role-mappings        current mapping rules
- PUT    /api/v1/auth-config/role-mappings        replace mapping rules (validated)
- POST   /api/v1/auth-config/ldap/test            test service-account bind + sample search
- POST   /api/v1/auth-config/oidc/test            fetch discovery doc, check signing keys
- POST   /api/v1/auth-config/saml/test            fetch IdP metadata, return entity ID
"""
from __future__ import annotations

import json
import logging
import os
from datetime import datetime
from typing import Dict, List, Optional

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

from app.auth.dependencies import RequireAdmin
from app.auth.role_mapper import SETTINGS_KEY as ROLE_MAP_SETTINGS_KEY
from app.database import get_db
from app.models.audit_log import AuditEventType, AuditLog
from app.models.setting import Setting
from app.models.user import AuthProvider, User, UserRole

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/auth-config", tags=["Auth Config"])

# Whitelist for role-mapping JSON sanitisation
VALID_PROVIDERS = {p.value for p in AuthProvider}
VALID_ROLES = {r.value for r in UserRole}


# ============================================
# Schemas
# ============================================

class RoleMappingRule(BaseModel):
    pattern: str = Field(..., min_length=1, max_length=512)
    role: str

    @field_validator("role")
    @classmethod
    def role_must_be_valid(cls, v: str) -> str:
        v = v.lower().strip()
        if v not in VALID_ROLES:
            raise ValueError(f"role must be one of {sorted(VALID_ROLES)}")
        return v


class RoleMappingsPayload(BaseModel):
    """{provider: [rule, rule, ...], ...}"""
    mappings: Dict[str, List[RoleMappingRule]]

    @field_validator("mappings")
    @classmethod
    def keys_must_be_known_providers(cls, v):
        bad = [k for k in v.keys() if k.lower() not in VALID_PROVIDERS]
        if bad:
            raise ValueError(f"unknown providers: {bad}")
        return {k.lower(): rules for k, rules in v.items()}


class LdapTestRequest(BaseModel):
    sample_username: Optional[str] = Field(
        None,
        description="Optional: search this username after a successful service bind to verify filter + attrs.",
    )


# ============================================
# Helpers
# ============================================

def _env_list(name: str, default: str) -> List[str]:
    return [x.strip().lower() for x in os.getenv(name, default).split(",") if x.strip()]


def _audit(db: Session, user_id: int, event: AuditEventType, desc: str, request: Optional[Request] = None):
    db.add(
        AuditLog(
            user_id=user_id,
            event_type=event,
            event_description=desc[:500],
            ip_address=request.client.host if (request and request.client) else None,
            user_agent=request.headers.get("user-agent") if request else None,
            timestamp=datetime.now(),
        )
    )
    db.commit()


# ============================================
# Status
# ============================================

@router.get("/status")
async def auth_status(
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireAdmin),
):
    """
    Provider-enablement + readiness check. Read-only. Reflects what is
    configured via .env (provider config stays env-controlled).
    """
    enabled = _env_list("AUTH_PROVIDERS", "local")
    lookup_order = _env_list("AUTH_LOOKUP_ORDER", "local,ldap")
    jit = os.getenv("AUTH_JIT_PROVISIONING", "true").lower() == "true"
    default_role = os.getenv("AUTH_JIT_DEFAULT_ROLE", "readonly")

    # User counts per provider (operational signal)
    user_counts: dict[str, int] = {}
    for p in AuthProvider:
        user_counts[p.value] = (
            db.query(User).filter(User.auth_provider == p).count()
        )

    return {
        "providers": [
            {
                "name": p,
                "enabled": p in enabled,
                "configured": _provider_is_configured(p),
                "user_count": user_counts.get(p, 0),
            }
            for p in [pp.value for pp in AuthProvider]
        ],
        "lookup_order": lookup_order,
        "jit_provisioning": jit,
        "default_role": default_role,
        "crypto_key_set": bool(os.getenv("AUTH_PROVIDER_CRYPTO_KEY")),
    }


def _provider_is_configured(provider_name: str) -> bool:
    """Heuristic: minimum env vars to consider the provider 'ready'."""
    if provider_name == "local":
        return True
    if provider_name == "ldap":
        return bool(os.getenv("LDAP_HOST") and os.getenv("LDAP_BIND_DN") and os.getenv("LDAP_USER_SEARCH_BASE"))
    if provider_name == "oidc":
        return bool(
            os.getenv("OIDC_DISCOVERY_URL")
            and os.getenv("OIDC_CLIENT_ID")
            and os.getenv("OIDC_CLIENT_SECRET")
            and os.getenv("OIDC_REDIRECT_URI")
        )
    if provider_name == "saml":
        return bool(
            (os.getenv("SAML_IDP_METADATA_URL") or os.getenv("SAML_IDP_METADATA_PATH"))
            and os.getenv("SAML_SP_ENTITY_ID")
            and os.getenv("SAML_SP_ACS_URL")
            and os.getenv("SAML_SP_CERT_PATH")
            and os.getenv("SAML_SP_PRIVATE_KEY_PATH")
        )
    return False


# ============================================
# Role mappings
# ============================================

@router.get("/role-mappings")
async def get_role_mappings(
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireAdmin),
):
    row = db.query(Setting).filter(Setting.key == ROLE_MAP_SETTINGS_KEY).first()
    if not row or not row.value:
        return {"mappings": {p.value: [] for p in AuthProvider if p != AuthProvider.LOCAL}}
    try:
        data = json.loads(row.value)
    except json.JSONDecodeError:
        return {"mappings": {}}
    return {"mappings": data}


@router.put("/role-mappings")
async def put_role_mappings(
    payload: RoleMappingsPayload,
    request: Request,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireAdmin),
):
    # Serialise back to JSON-storable dict
    out = {
        prov: [rule.model_dump() for rule in rules]
        for prov, rules in payload.mappings.items()
    }
    raw = json.dumps(out)

    row = db.query(Setting).filter(Setting.key == ROLE_MAP_SETTINGS_KEY).first()
    if row:
        row.value = raw
    else:
        row = Setting(
            key=ROLE_MAP_SETTINGS_KEY,
            value=raw,
            description="External group → internal role mapping rules per provider",
        )
        db.add(row)
    db.commit()

    _audit(
        db,
        current_user.id,
        AuditEventType.CONFIG_CHANGE,
        f"auth_role_mappings updated by {current_user.username}: "
        + ", ".join(f"{p}={len(r)}" for p, r in out.items()),
        request,
    )
    return {"mappings": out}


# ============================================
# Provider test endpoints
# ============================================

@router.post("/ldap/test")
async def test_ldap(
    payload: LdapTestRequest,
    db: Session = Depends(get_db),
    current_user: User = Depends(RequireAdmin),
):
    """Verify LDAP config: open server, bind as service account, optionally search a user."""
    if not _provider_is_configured("ldap"):
        raise HTTPException(400, "LDAP is not configured (check LDAP_* env vars)")

    # Import inside endpoint so the dependency only loads when actually used.
    try:
        from app.auth.strategies.ldap_strategy import LdapStrategy
    except ImportError as e:
        raise HTTPException(500, f"ldap3 not available: {e}")

    strategy = LdapStrategy(db)
    try:
        conn = strategy._service_connection()  # type: ignore[attr-defined]
    except Exception as e:
        logger.warning("LDAP test bind failed: %s", e)
        return {"ok": False, "step": "service_bind", "error": str(e)}

    out = {
        "ok": True,
        "step": "service_bind",
        "server": f"{strategy.host}:{strategy.port}",
        "use_ssl": strategy.use_ssl,
        "use_starttls": strategy.use_starttls,
    }
    try:
        if payload.sample_username:
            user_dn, attrs = strategy._search_user(payload.sample_username)  # type: ignore[attr-defined]
            out["sample"] = {
                "dn": user_dn,
                "username_attr_value": str(attrs.get(strategy.attr_username, "<missing>")),
                "email_attr_value": str(attrs.get(strategy.attr_email, "<missing>")),
                "groups_count": len(attrs.get(strategy.attr_groups, []) or []),
            }
    except Exception as e:
        out["sample_error"] = str(e)
    finally:
        try:
            conn.unbind()
        except Exception:
            pass
    return out


@router.post("/oidc/test")
async def test_oidc(
    current_user: User = Depends(RequireAdmin),
):
    """Fetch OIDC discovery doc + JWKS, sanity-check fields."""
    if not _provider_is_configured("oidc"):
        raise HTTPException(400, "OIDC is not configured (check OIDC_* env vars)")

    discovery_url = os.getenv("OIDC_DISCOVERY_URL", "")
    try:
        with httpx.Client(timeout=15) as client:
            r = client.get(discovery_url)
            r.raise_for_status()
            doc = r.json()
            jwks_uri = doc.get("jwks_uri")
            jwks = None
            if jwks_uri:
                jwks_r = client.get(jwks_uri)
                if jwks_r.status_code == 200:
                    jwks = jwks_r.json()
    except httpx.HTTPError as e:
        return {"ok": False, "error": str(e)}

    return {
        "ok": True,
        "issuer": doc.get("issuer"),
        "authorization_endpoint": doc.get("authorization_endpoint"),
        "token_endpoint": doc.get("token_endpoint"),
        "userinfo_endpoint": doc.get("userinfo_endpoint"),
        "jwks_uri": doc.get("jwks_uri"),
        "supported_scopes": doc.get("scopes_supported"),
        "supported_id_token_signing_algs": doc.get("id_token_signing_alg_values_supported"),
        "jwks_key_count": len(jwks.get("keys", [])) if jwks else 0,
    }


@router.post("/saml/test")
async def test_saml(
    current_user: User = Depends(RequireAdmin),
):
    """Fetch IdP metadata, return SSO URL + entity ID + cert thumbprint hint."""
    if not _provider_is_configured("saml"):
        raise HTTPException(400, "SAML is not configured")

    try:
        from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
    except ImportError as e:
        raise HTTPException(500, f"python3-saml not available: {e}")

    url = os.getenv("SAML_IDP_METADATA_URL", "")
    path = os.getenv("SAML_IDP_METADATA_PATH", "")
    try:
        if url:
            data = OneLogin_Saml2_IdPMetadataParser.parse_remote(url, timeout=15)
        else:
            with open(path, "r") as f:
                data = OneLogin_Saml2_IdPMetadataParser.parse(f.read())
    except Exception as e:
        return {"ok": False, "error": str(e)}

    idp = data.get("idp", {})
    return {
        "ok": True,
        "idp_entity_id": idp.get("entityId"),
        "sso_url": (idp.get("singleSignOnService") or {}).get("url"),
        "slo_url": (idp.get("singleLogoutService") or {}).get("url"),
        "x509cert_present": bool(idp.get("x509cert")),
    }
