"""
SAML 2.0 routes.

Endpoints:
- GET  /auth/saml/login    — SP-initiated SSO (redirect to IdP)
- POST /auth/saml/acs      — Assertion Consumer Service (IdP posts SAMLResponse here)
                              works for both SP-initiated (with RelayState) and IdP-initiated
- GET  /auth/saml/metadata — SP metadata XML (for IdP configuration)

XSW (XML Signature Wrapping) mitigation is handled by python3-saml's
`strict` mode + xmlsec1: signature must be on the *correct* element
(Response or Assertion) and additional Assertion children are rejected.
"""
from __future__ import annotations

import logging
import os
from datetime import datetime
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse, Response
from sqlalchemy.orm import Session

from app.auth.jwt_handler import (
    JWT_ACCESS_TOKEN_EXPIRE_MINUTES,
    JWT_REFRESH_TOKEN_EXPIRE_DAYS,
    create_access_token,
    create_refresh_token,
)
from app.auth.orchestrator import AuthOrchestrator
from app.auth.strategies.base import AuthError
from app.auth.strategies.saml_strategy import SamlStrategy
from app.database import get_db
from app.models.audit_log import AuditEventType, AuditLog

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/auth/saml", tags=["Authentication"])

COOKIE_SAMESITE = os.getenv("AUTH_COOKIE_SAMESITE", "lax")
COOKIE_SECURE = os.getenv("AUTH_COOKIE_SECURE", "true").lower() == "true"


def _saml_settings() -> dict:
    """
    Build python3-saml settings dict. Loads IdP metadata at startup or
    from local file. Strict mode is non-negotiable.
    """
    sp_entity = os.getenv("SAML_SP_ENTITY_ID", "").strip()
    sp_acs = os.getenv("SAML_SP_ACS_URL", "").strip()
    sp_slo = os.getenv("SAML_SP_SLO_URL", "").strip()
    sp_cert_path = os.getenv("SAML_SP_CERT_PATH", "").strip()
    sp_key_path = os.getenv("SAML_SP_PRIVATE_KEY_PATH", "").strip()
    idp_metadata_url = os.getenv("SAML_IDP_METADATA_URL", "").strip()
    idp_metadata_path = os.getenv("SAML_IDP_METADATA_PATH", "").strip()

    if not (sp_entity and sp_acs and sp_cert_path and sp_key_path):
        raise HTTPException(404, "SAML SP is not fully configured")

    def _read(path: str) -> str:
        with open(path, "r") as f:
            return f.read().strip()

    sp_cert = _read(sp_cert_path) if sp_cert_path else ""
    sp_key = _read(sp_key_path) if sp_key_path else ""

    # Load IdP descriptor
    from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser

    if idp_metadata_url:
        idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(idp_metadata_url, timeout=15)
    elif idp_metadata_path:
        idp_data = OneLogin_Saml2_IdPMetadataParser.parse(_read(idp_metadata_path))
    else:
        raise HTTPException(500, "No IdP metadata source configured")

    return {
        "strict": True,                  # XSW + signature/audience/replay enforcement
        "debug": False,
        "sp": {
            "entityId": sp_entity,
            "assertionConsumerService": {
                "url": sp_acs,
                "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
            },
            "singleLogoutService": {
                "url": sp_slo or sp_acs,
                "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
            },
            "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
            "x509cert": sp_cert,
            "privateKey": sp_key,
        },
        "idp": idp_data.get("idp", {}),
        "security": {
            "authnRequestsSigned": True,
            "wantMessagesSigned": True,
            "wantAssertionsSigned": True,
            "signMetadata": True,
            "wantAssertionsEncrypted": False,
            "wantNameId": True,
            "wantNameIdEncrypted": False,
            "requestedAuthnContext": False,
            "signatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
            "digestAlgorithm": "http://www.w3.org/2001/04/xmlenc#sha256",
        },
    }


def _prepare_request(request: Request, form: dict) -> dict:
    """Map FastAPI Request → python3-saml's expected dict."""
    return {
        "https": "on" if request.url.scheme == "https" else "off",
        "http_host": request.url.hostname or "",
        "server_port": str(request.url.port or (443 if request.url.scheme == "https" else 80)),
        "script_name": request.url.path,
        "get_data": dict(request.query_params),
        "post_data": form,
    }


@router.get("/login")
async def saml_login(request: Request):
    """SP-initiated: build AuthnRequest and redirect to IdP."""
    from onelogin.saml2.auth import OneLogin_Saml2_Auth

    settings = _saml_settings()
    req = _prepare_request(request, {})
    auth = OneLogin_Saml2_Auth(req, settings)
    redirect_url = auth.login(return_to="/", set_nameid_policy=True)
    return RedirectResponse(redirect_url, status_code=302)


@router.post("/acs")
async def saml_acs(request: Request, db: Session = Depends(get_db)):
    """
    Assertion Consumer Service. python3-saml validates:
    - Signature (on Response, Assertion, or both — per `wantAssertionsSigned`)
    - InResponseTo (when SP-initiated)
    - Conditions (NotBefore / NotOnOrAfter, AudienceRestriction)
    - Recipient (= our ACS URL)
    - Issuer
    - XSW (strict mode rejects unsigned/duplicated assertion siblings)
    """
    from onelogin.saml2.auth import OneLogin_Saml2_Auth

    settings = _saml_settings()
    form = await request.form()
    form_dict = {k: v for k, v in form.items()}
    req = _prepare_request(request, form_dict)
    auth = OneLogin_Saml2_Auth(req, settings)

    auth.process_response()
    errors = auth.get_errors()
    if errors:
        reason = auth.get_last_error_reason() or "validation failed"
        logger.warning("SAML response rejected: %s (%s)", errors, reason)
        db.add(AuditLog(
            event_type=AuditEventType.AUTH_PROVIDER_FAILED,
            event_description=f"SAML rejected: {','.join(errors)} — {reason}",
            ip_address=request.client.host if request.client else None,
            timestamp=datetime.now(),
        ))
        db.commit()
        raise HTTPException(401, "SAML response invalid")

    if not auth.is_authenticated():
        raise HTTPException(401, "SAML response not authenticated")

    # Extract identity
    strategy = SamlStrategy()
    identity = strategy.parse_attributes(
        attributes=auth.get_attributes() or {},
        name_id=auth.get_nameid() or "",
    )

    # Hand off to orchestrator (JIT + role mapping + audit)
    orchestrator = AuthOrchestrator(db)
    try:
        result = orchestrator.complete_sso_login(
            identity,
            actor_ip=request.client.host if request.client else None,
            actor_user_agent=request.headers.get("user-agent"),
        )
    except AuthError as e:
        raise HTTPException(401, e.safe_message)

    user = result.user
    if not user.is_active:
        raise HTTPException(403, "Account is deactivated")

    # MFA enforcement may demand local TOTP even for SAML users when
    # `mfa_enforce_sso_users=true` (default off). Hand over to the
    # frontend forced-setup page with a short-lived setup_token.
    if getattr(result, "mfa_setup_required", False):
        from datetime import timedelta
        setup_token = create_access_token(
            {"sub": user.username, "mfa_setup_pending": True},
            expires_delta=timedelta(minutes=10),
        )
        import os as _os
        base = _os.getenv("DASHBOARD_URL", "/").rstrip("/")
        return RedirectResponse(
            f"{base}/mfa-setup?token={setup_token}",
            status_code=302,
        )

    token_payload = {"sub": user.username, "role": user.role.value}
    access_jwt = create_access_token(token_payload)
    refresh_jwt = create_refresh_token(token_payload)

    relay_state = form_dict.get("RelayState") or "/"
    # Defend against open redirect: only allow same-origin paths.
    if relay_state.startswith("//") or "://" in relay_state:
        relay_state = "/"

    response = RedirectResponse(relay_state, status_code=302)
    response.set_cookie(
        "access_token", access_jwt, httponly=True, secure=COOKIE_SECURE,
        samesite=COOKIE_SAMESITE, max_age=JWT_ACCESS_TOKEN_EXPIRE_MINUTES * 60, path="/",
    )
    response.set_cookie(
        "refresh_token", refresh_jwt, httponly=True, secure=COOKIE_SECURE,
        samesite=COOKIE_SAMESITE, max_age=JWT_REFRESH_TOKEN_EXPIRE_DAYS * 86400, path="/",
    )
    return response


@router.get("/metadata")
async def saml_metadata():
    """Returns SP metadata XML for the IdP administrator."""
    from onelogin.saml2.settings import OneLogin_Saml2_Settings

    settings_dict = _saml_settings()
    settings = OneLogin_Saml2_Settings(settings_dict, sp_validation_only=True)
    metadata = settings.get_sp_metadata()
    errors = settings.validate_metadata(metadata)
    if errors:
        raise HTTPException(500, f"SP metadata invalid: {errors}")
    return Response(content=metadata, media_type="application/xml")
