import re
import secrets
from datetime import datetime

from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from fastapi.security import HTTPBearer
from motor.motor_asyncio import AsyncIOMotorDatabase

from ...core.config import settings
from ...core.database import get_database
from ...core.logging import get_logger
from ...core.security import (
    create_access_token,
    create_refresh_token,
    decode_token,
    verify_password,
)
from ...models.audit_log import AuditAction, AuditLogSeverity
from ...models.user import AuthProvider, User, UserRole
from ...schemas.auth import (
    LoginRequest,
    LoginResponse,
    LogoutResponse,
    MicrosoftLoginRequest,
    MicrosoftLoginResponse,
    RefreshResponse,
)
from ...services.audit_logger import audit_logger, log_auth_failure, log_auth_success
from ...services.microsoft_auth import (
    MicrosoftAuthError,
    MicrosoftTokenValidationError,
    get_microsoft_auth_service,
)

logger = get_logger(__name__)
router = APIRouter(prefix="/auth", tags=["auth"])
security = HTTPBearer()


async def _get_user_org_ids(user_id: str, db: AsyncIOMotorDatabase) -> list[str]:
    """Return list of org IDs the user belongs to — used as a JWT hint only."""
    cursor = db.memberships.find({"user_id": user_id}, {"organization_id": 1})
    memberships = await cursor.to_list(length=200)
    return [str(m["organization_id"]) for m in memberships if m.get("organization_id")]


def _set_auth_cookies(response: Response, refresh_token: str) -> str:
    """Set httponly refresh_token cookie and readable csrf_token cookie. Returns the csrf token."""
    csrf_token = secrets.token_hex(32)
    ttl = settings.jwt_refresh_ttl_days * 24 * 60 * 60
    domain = settings.cookie_domain if settings.app_env == "prod" else None

    response.set_cookie(
        key="refresh_token",
        value=refresh_token,
        httponly=True,
        secure=settings.cookie_secure,
        samesite=settings.cookie_samesite,
        domain=domain,
        max_age=ttl,
    )
    response.set_cookie(
        key="csrf_token",
        value=csrf_token,
        httponly=False,  # JS-readable for Double Submit Cookie pattern
        secure=settings.cookie_secure,
        samesite=settings.cookie_samesite,
        domain=domain,
        max_age=ttl,
    )
    return csrf_token


@router.post("/login", response_model=LoginResponse)
async def login(
    login_data: LoginRequest,
    request: Request,
    response: Response,
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    user_doc = await db.users.find_one({"email": login_data.email})
    if not user_doc:
        await log_auth_failure(login_data.email, request, "User not found")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
        )

    user = User(**user_doc)

    if user.auth_provider == AuthProvider.MICROSOFT:
        await log_auth_failure(login_data.email, request, "Account uses Microsoft SSO")
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="This account uses Microsoft authentication. Please sign in with Microsoft.",
        )

    if not user.hashed_password or not verify_password(login_data.password, user.hashed_password):
        await log_auth_failure(login_data.email, request, "Invalid password")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect email or password",
        )

    if not user.is_active:
        await log_auth_failure(login_data.email, request, "Account disabled")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="User account is disabled",
        )

    org_ids = await _get_user_org_ids(str(user.id), db)
    access_token = create_access_token(subject=str(user.id), org_ids=org_ids)
    refresh_token = create_refresh_token(subject=str(user.id))

    _set_auth_cookies(response, refresh_token)

    await log_auth_success(user, request)
    return LoginResponse(
        access_token=access_token,
        user_id=str(user.id),
        role=user.role,
    )


@router.post("/microsoft", response_model=MicrosoftLoginResponse)
async def microsoft_login(
    login_data: MicrosoftLoginRequest,
    request: Request,
    response: Response,
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    """Authenticate user with Microsoft ID token.

    This endpoint validates the Microsoft ID token, finds or creates the user,
    and returns JWT tokens for API access.
    """
    microsoft_auth = get_microsoft_auth_service()
    try:
        user_info = await microsoft_auth.validate_token(login_data.id_token)
    except MicrosoftTokenValidationError as e:
        await log_auth_failure(login_data.id_token[:20] + "…", request, f"MS token invalid: {e}")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail=f"Microsoft authentication failed: {str(e)}",
        ) from None
    except MicrosoftAuthError as e:
        await log_auth_failure("microsoft-sso", request, f"MS auth service error: {e}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Microsoft authentication service error",
        ) from None

    # Look up by Microsoft-derived ID first — handles email casing changes across logins
    ms_user_id = f"ms-{user_info.sub[:20]}"
    user_doc = await db.users.find_one({"_id": ms_user_id})
    if not user_doc:
        # Fall back to case-insensitive email lookup (handles local-to-Microsoft migration)
        user_doc = await db.users.find_one(
            {"email": {"$regex": f"^{re.escape(user_info.email)}$", "$options": "i"}}
        )

    if user_doc:
        user = User(**user_doc)
        if user.auth_provider == AuthProvider.LOCAL:
            await db.users.update_one(
                {"_id": user_doc["_id"]},
                {"$set": {"auth_provider": AuthProvider.MICROSOFT.value, "updated_at": datetime.utcnow()}},
            )
            user.auth_provider = AuthProvider.MICROSOFT
    else:
        new_user = {
            "_id": ms_user_id,
            "email": user_info.email,
            "full_name": user_info.name,
            "hashed_password": None,
            "role": UserRole.CLIENT.value,
            "auth_provider": AuthProvider.MICROSOFT.value,
            "is_active": True,
            "pm_client_ids": [],
            "created_at": datetime.utcnow(),
            "updated_at": datetime.utcnow(),
        }
        await db.users.insert_one(new_user)
        user = User(**new_user)

    if not user.is_active:
        await log_auth_failure(user.email, request, "Account disabled")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="User account is disabled",
        )

    org_ids = await _get_user_org_ids(str(user.id), db)
    access_token = create_access_token(subject=str(user.id), org_ids=org_ids)
    refresh_token = create_refresh_token(subject=str(user.id))

    _set_auth_cookies(response, refresh_token)

    await log_auth_success(user, request)
    return MicrosoftLoginResponse(
        access_token=access_token,
        user_id=str(user.id),
        role=user.role if isinstance(user.role, str) else user.role.value,
        email=user.email,
        full_name=user.full_name,
        auth_provider=user.auth_provider,
    )


@router.post("/refresh", response_model=RefreshResponse)
async def refresh_token(
    request: Request,
    response: Response,
    db: AsyncIOMotorDatabase = Depends(get_database),
):
    refresh_token = request.cookies.get("refresh_token")

    if not refresh_token:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Refresh token not found",
        )

    # CSRF protection: Double Submit Cookie pattern
    csrf_cookie = request.cookies.get("csrf_token")
    csrf_header = request.headers.get("X-CSRF-Token")
    if csrf_cookie and (not csrf_header or csrf_header != csrf_cookie):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="CSRF token mismatch",
        )

    try:
        payload = decode_token(refresh_token)

        if payload.get("type") != "refresh":
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token type",
            )

        user_id = payload.get("sub")
        if not user_id:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid token",
            )

        # Verify user still exists and is active
        user_doc = await db.users.find_one({"_id": user_id})
        if not user_doc:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="User not found",
            )

        user = User(**user_doc)
        if not user.is_active:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="User account is disabled",
            )

        # Create new tokens (include org_ids claim for prefilter hint)
        _org_ids = await _get_user_org_ids(user_id, db)
        new_access_token = create_access_token(subject=user_id, org_ids=_org_ids)
        new_refresh_token = create_refresh_token(subject=user_id)

        # Rotate both refresh and CSRF cookies
        _set_auth_cookies(response, new_refresh_token)

        logger.info("Token refresh successful for user %s", user_id)
        return RefreshResponse(
            access_token=new_access_token,
            user_id=user_id,
            role=user.role if isinstance(user.role, str) else user.role.value,
            email=user.email,
            full_name=user.full_name
        )

    except HTTPException:
        raise
    except Exception as e:
        import traceback
        logger.exception("Refresh token error: %s\n%s", type(e).__name__, traceback.format_exc())
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid refresh token",
        ) from None


@router.post("/logout", response_model=LogoutResponse)
async def logout(request: Request, response: Response):
    # Clear refresh token cookie
    response.delete_cookie(
        key="refresh_token",
        httponly=True,
        secure=settings.cookie_secure,
        samesite=settings.cookie_samesite,
        domain=settings.cookie_domain if settings.app_env == "prod" else None,
    )

    await audit_logger.log_action(
        action=AuditAction.LOGOUT,
        description="User logged out",
        request=request,
        severity=AuditLogSeverity.INFO,
    )
    return LogoutResponse()
