# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Routes for Google authentication — split Auth app vs Data app.

See wiki: concepts/OAuth-Auth-vs-Data-Connection-Konzept.md
"""

from fastapi import APIRouter, HTTPException, Request, Response, status, Depends, Query
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
import logging
import json
import time
from typing import Dict, Any, Optional
from requests_oauthlib import OAuth2Session
import httpx
from jose import jwt as jose_jwt
from jose import JWTError

from modules.shared.configuration import APP_CONFIG
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatus, UserConnection
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
from modules.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.auth import (
    createAccessToken,
    setAccessTokenCookie,
    createRefreshToken,
    setRefreshTokenCookie,
    clearAccessTokenCookie,
    clearRefreshTokenCookie,
)
from modules.auth.tokenManager import TokenManager
from modules.auth.oauthProviderConfig import googleAuthScopes, googleDataScopes
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp

logger = logging.getLogger(__name__)

_FLOW_LOGIN = "google_login"
_FLOW_CONNECT = "google_connect"


async def verify_google_token(access_token: str) -> Dict[str, Any]:
    """Verify Google access token and return token info including scopes."""
    try:
        headers = {
            "Authorization": f"Bearer {access_token}",
            "Content-Type": "application/json",
        }
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://www.googleapis.com/oauth2/v1/tokeninfo",
                headers=headers,
                params={"access_token": access_token},
            )
            if response.status_code == 200:
                token_info = response.json()
                return {
                    "valid": True,
                    "token_info": token_info,
                    "scopes": token_info.get("scope", "").split(" ")
                    if token_info.get("scope")
                    else [],
                    "expires_in": int(token_info.get("expires_in", 0)),
                    "user_id": token_info.get("user_id"),
                    "email": token_info.get("email"),
                }
            return {
                "valid": False,
                "error": f"HTTP {response.status_code}",
                "details": response.text,
            }
    except Exception as e:
        logger.error(f"Error verifying Google token: {str(e)}")
        return {"valid": False, "error": str(e)}


def _issue_oauth_state(claims: Dict[str, Any]) -> str:
    body = {**claims, "exp": int(time.time()) + 600}
    return jose_jwt.encode(body, SECRET_KEY, algorithm=ALGORITHM)


def _parse_oauth_state(state: str) -> Dict[str, Any]:
    try:
        return jose_jwt.decode(state, SECRET_KEY, algorithms=[ALGORITHM])
    except JWTError as e:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail=f"Invalid OAuth state: {e}"
        ) from e


router = APIRouter(
    prefix="/api/google",
    tags=["Security Google"],
    responses={
        404: {"description": "Not found"},
        400: {"description": "Bad request"},
        401: {"description": "Unauthorized"},
        403: {"description": "Forbidden"},
        500: {"description": "Internal server error"},
    },
)

AUTH_CLIENT_ID = APP_CONFIG.get("Service_GOOGLE_AUTH_CLIENT_ID")
AUTH_CLIENT_SECRET = APP_CONFIG.get("Service_GOOGLE_AUTH_CLIENT_SECRET")
AUTH_REDIRECT_URI = APP_CONFIG.get("Service_GOOGLE_AUTH_REDIRECT_URI")
DATA_CLIENT_ID = APP_CONFIG.get("Service_GOOGLE_DATA_CLIENT_ID")
DATA_CLIENT_SECRET = APP_CONFIG.get("Service_GOOGLE_DATA_CLIENT_SECRET")
DATA_REDIRECT_URI = APP_CONFIG.get("Service_GOOGLE_DATA_REDIRECT_URI")


@router.get("/config")
def get_config():
    """Debug: OAuth configuration (Auth vs Data apps)."""
    return {
        "auth_client_id": AUTH_CLIENT_ID,
        "auth_client_secret": "***" if AUTH_CLIENT_SECRET else None,
        "auth_redirect_uri": AUTH_REDIRECT_URI,
        "auth_scopes": googleAuthScopes,
        "data_client_id": DATA_CLIENT_ID,
        "data_client_secret": "***" if DATA_CLIENT_SECRET else None,
        "data_redirect_uri": DATA_REDIRECT_URI,
        "data_scopes": googleDataScopes,
        "config_loaded": bool(
            AUTH_CLIENT_ID and AUTH_CLIENT_SECRET and AUTH_REDIRECT_URI and DATA_CLIENT_ID and DATA_CLIENT_SECRET and DATA_REDIRECT_URI
        ),
    }


def _require_google_auth_config():
    if not AUTH_CLIENT_ID or not AUTH_CLIENT_SECRET or not AUTH_REDIRECT_URI:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Google Auth OAuth is not configured (Service_GOOGLE_AUTH_*)",
        )


def _require_google_data_config():
    if not DATA_CLIENT_ID or not DATA_CLIENT_SECRET or not DATA_REDIRECT_URI:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail="Google Data OAuth is not configured (Service_GOOGLE_DATA_*)",
        )


@router.get("/auth/login")
@limiter.limit("5/minute")
def auth_login(request: Request) -> RedirectResponse:
    """Start Google login (Auth app — minimal scopes)."""
    try:
        _require_google_auth_config()
        state_jwt = _issue_oauth_state({"flow": _FLOW_LOGIN})
        oauth = OAuth2Session(
            client_id=AUTH_CLIENT_ID,
            redirect_uri=AUTH_REDIRECT_URI,
            scope=googleAuthScopes,
        )
        auth_url, _ = oauth.authorization_url(
            "https://accounts.google.com/o/oauth2/auth",
            state=state_jwt,
            access_type="online",
            prompt="consent select_account",
        )
        return RedirectResponse(auth_url)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error initiating Google auth login: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to initiate Google login: {str(e)}",
        )


@router.get("/auth/login/callback")
async def auth_login_callback(
    code: str, state: str, request: Request, response: Response
) -> HTMLResponse:
    """OAuth callback for Google Auth app (login only)."""
    state_data = _parse_oauth_state(state)
    if state_data.get("flow") != _FLOW_LOGIN:
        raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")

    _require_google_auth_config()
    oauth = OAuth2Session(client_id=AUTH_CLIENT_ID, redirect_uri=AUTH_REDIRECT_URI)
    token_data = oauth.fetch_token(
        "https://oauth2.googleapis.com/token",
        client_secret=AUTH_CLIENT_SECRET,
        code=code,
        include_client_id=True,
    )
    access_token = token_data.get("access_token")
    if not access_token:
        return HTMLResponse(
            content="<html><body><h1>Authentication Failed</h1><p>No access token.</p></body></html>",
            status_code=400,
        )

    token_verification = await verify_google_token(access_token)
    if not token_verification.get("valid"):
        return HTMLResponse(
            content=f"<html><body><h1>Authentication Failed</h1><p>{token_verification.get('error')}</p></body></html>",
            status_code=400,
        )

    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json",
    }
    async with httpx.AsyncClient() as client:
        user_info_response = await client.get(
            "https://www.googleapis.com/oauth2/v2/userinfo", headers=headers
        )
        if user_info_response.status_code != 200:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Failed to get user info from Google",
            )
        user_info = user_info_response.json()

    rootInterface = getRootInterface()
    isNewUser = False
    user = rootInterface.getUserByUsername(user_info.get("email"))
    if not user:
        user = rootInterface.createUser(
            username=user_info.get("email"),
            email=user_info.get("email"),
            fullName=user_info.get("name"),
            authenticationAuthority=AuthAuthority.GOOGLE,
            externalId=user_info.get("id"),
            externalUsername=user_info.get("email"),
            externalEmail=user_info.get("email"),
            addExternalIdentityConnection=False,
        )
        isNewUser = True

    jwt_token_data = {
        "sub": user.username,
        "userId": str(user.id),
        "authenticationAuthority": AuthAuthority.GOOGLE.value,
    }
    jwt_token, jwt_expires_at = createAccessToken(jwt_token_data)
    refresh_token, _refresh_expires = createRefreshToken(jwt_token_data)
    from jose import jwt

    payload = jwt.decode(jwt_token, SECRET_KEY, algorithms=[ALGORITHM])
    jti = payload.get("jti")

    token = Token(
        id=jti,
        userId=user.id,
        authority=AuthAuthority.GOOGLE,
        tokenPurpose=TokenPurpose.AUTH_SESSION,
        tokenAccess=jwt_token,
        tokenRefresh="",
        tokenType="bearer",
        expiresAt=jwt_expires_at.timestamp(),
        createdAt=getUtcTimestamp(),
    )
    appInterface = getInterface(user)
    appInterface.saveAccessToken(token)

    # Activate PENDING subscriptions on first login
    try:
        rootInterface._activatePendingSubscriptions(str(user.id))
    except Exception as subErr:
        logger.error(f"Error activating subscriptions on Google login: {subErr}")

    token_dict = token.model_dump()

    html_response = HTMLResponse(
        content=f"""
        <html>
            <head><title>Authentication Successful</title></head>
            <body>
                <script>
                    if (window.opener) {{
                        window.opener.postMessage({{
                            type: 'google_auth_success',
                            token_data: {json.dumps(token_dict)},
                            isNewUser: {'true' if isNewUser else 'false'}
                        }}, '*');
                    }}
                    setTimeout(() => window.close(), 1000);
                </script>
            </body>
        </html>
        """
    )
    setAccessTokenCookie(html_response, jwt_token, expiresDelta=None)
    setRefreshTokenCookie(html_response, refresh_token)
    return html_response


@router.get("/auth/connect")
@limiter.limit("5/minute")
def auth_connect(
    request: Request,
    connectionId: str = Query(..., description="UserConnection id"),
    currentUser: User = Depends(getCurrentUser),
) -> RedirectResponse:
    """Start Google Data OAuth for an existing connection (requires gateway session)."""
    try:
        _require_google_data_config()
        interface = getInterface(currentUser)
        connections = interface.getUserConnections(currentUser.id)
        connection = None
        for conn in connections:
            if conn.id == connectionId and conn.authority == AuthAuthority.GOOGLE:
                connection = conn
                break
        if not connection:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Google connection not found")

        state_jwt = _issue_oauth_state(
            {
                "flow": _FLOW_CONNECT,
                "connectionId": connectionId,
                "userId": str(currentUser.id),
            }
        )
        oauth = OAuth2Session(
            client_id=DATA_CLIENT_ID,
            redirect_uri=DATA_REDIRECT_URI,
            scope=googleDataScopes,
        )
        extra_params: Dict[str, Any] = {
            "access_type": "offline",
            "include_granted_scopes": "true",
            "state": state_jwt,
        }
        login_hint = connection.externalEmail or connection.externalUsername
        if login_hint:
            extra_params["login_hint"] = login_hint
            if "@" in login_hint:
                extra_params["hd"] = login_hint.split("@", 1)[1]
            extra_params["prompt"] = "consent"
        else:
            extra_params["prompt"] = "consent select_account"

        auth_url, _ = oauth.authorization_url(
            "https://accounts.google.com/o/oauth2/auth", **extra_params
        )
        return RedirectResponse(auth_url)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error initiating Google connect: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to initiate Google connect: {str(e)}",
        )


@router.get("/auth/connect/callback")
async def auth_connect_callback(
    code: str, state: str, request: Request, response: Response
) -> HTMLResponse:
    """OAuth callback for Google Data app (UserConnection)."""
    state_data = _parse_oauth_state(state)
    if state_data.get("flow") != _FLOW_CONNECT:
        raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
    connection_id = state_data.get("connectionId")
    user_id = state_data.get("userId")
    if not connection_id or not user_id:
        raise HTTPException(status_code=400, detail="Missing connection or user in OAuth state")

    _require_google_data_config()
    oauth = OAuth2Session(client_id=DATA_CLIENT_ID, redirect_uri=DATA_REDIRECT_URI)
    token_data = oauth.fetch_token(
        "https://oauth2.googleapis.com/token",
        client_secret=DATA_CLIENT_SECRET,
        code=code,
        include_client_id=True,
    )
    granted_scopes = token_data.get("scope", "")
    token_response = {
        "access_token": token_data.get("access_token"),
        "refresh_token": token_data.get("refresh_token", ""),
        "token_type": token_data.get("token_type", "bearer"),
        "expires_in": token_data.get("expires_in", 0),
    }

    if not token_response.get("refresh_token"):
        try:
            rootInterface = getRootInterface()
            existing_tokens = rootInterface.getTokensByConnectionIdAndAuthority(
                connection_id, AuthAuthority.GOOGLE
            )
            if existing_tokens:
                existing_tokens.sort(
                    key=lambda x: parseTimestamp(x.createdAt, default=0), reverse=True
                )
                token_response["refresh_token"] = existing_tokens[0].tokenRefresh or ""
        except Exception:
            pass

    if not token_response.get("access_token"):
        return HTMLResponse(
            content="<html><body><h1>Connection Failed</h1><p>No access token.</p></body></html>",
            status_code=400,
        )

    token_verification = await verify_google_token(token_response["access_token"])
    if not token_verification.get("valid"):
        return HTMLResponse(
            content=f"<html><body><h1>Connection Failed</h1><p>{token_verification.get('error')}</p></body></html>",
            status_code=400,
        )

    headers = {
        "Authorization": f"Bearer {token_response['access_token']}",
        "Content-Type": "application/json",
    }
    async with httpx.AsyncClient() as client:
        user_info_response = await client.get(
            "https://www.googleapis.com/oauth2/v2/userinfo", headers=headers
        )
        if user_info_response.status_code != 200:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Failed to get user info from Google",
            )
        user_info = user_info_response.json()

    rootInterface = getRootInterface()
    user = rootInterface.getUser(user_id)
    if not user:
        return HTMLResponse(
            content=f"""
            <html><body><script>
            if (window.opener) {{
                window.opener.postMessage({{ type: 'google_connection_error', error: 'User not found' }}, '*');
                setTimeout(() => window.close(), 1000);
            }} else window.close();
            </script></body></html>
            """,
            status_code=404,
        )

    interface = getInterface(user)
    connections = interface.getUserConnections(user_id)
    connection = None
    for conn in connections:
        if conn.id == connection_id:
            connection = conn
            break
    if not connection:
        return HTMLResponse(
            content=f"""
            <html><body><script>
            if (window.opener) {{
                window.opener.postMessage({{ type: 'google_connection_error', error: 'Connection not found' }}, '*');
                setTimeout(() => window.close(), 1000);
            }} else window.close();
            </script></body></html>
            """,
            status_code=404,
        )

    try:
        connection.status = ConnectionStatus.ACTIVE
        connection.lastChecked = getUtcTimestamp()
        connection.expiresAt = getUtcTimestamp() + token_response.get("expires_in", 0)
        connection.externalId = user_info.get("id")
        connection.externalUsername = user_info.get("email")
        connection.externalEmail = user_info.get("email")
        granted_scopes_list = (
            granted_scopes
            if isinstance(granted_scopes, list)
            else (granted_scopes.split(" ") if granted_scopes else googleDataScopes)
        )
        connection.grantedScopes = granted_scopes_list
        rootInterface.db.recordModify(UserConnection, connection_id, connection.model_dump())

        token = Token(
            userId=user.id,
            authority=AuthAuthority.GOOGLE,
            connectionId=connection_id,
            tokenPurpose=TokenPurpose.DATA_CONNECTION,
            tokenAccess=token_response["access_token"],
            tokenRefresh=token_response.get("refresh_token", ""),
            tokenType=token_response.get("token_type", "bearer"),
            expiresAt=createExpirationTimestamp(token_response.get("expires_in", 0)),
            createdAt=getUtcTimestamp(),
        )
        interface.saveConnectionToken(token)

        return HTMLResponse(
            content=f"""
            <html>
                <head><title>Connection Successful</title></head>
                <body>
                    <script>
                        if (window.opener) {{
                            window.opener.postMessage({{
                                type: 'google_connection_success',
                                connection: {{
                                    id: '{connection.id}',
                                    status: 'connected',
                                    type: 'google',
                                    lastChecked: {getUtcTimestamp()},
                                    expiresAt: {createExpirationTimestamp(token_response.get("expires_in", 0))}
                                }}
                            }}, '*');
                            setTimeout(() => window.close(), 1000);
                        }} else {{
                            window.close();
                        }}
                    </script>
                </body>
            </html>
            """
        )
    except Exception as e:
        logger.error(f"Error updating Google connection: {str(e)}", exc_info=True)
        return HTMLResponse(
            content=f"""
            <html><body><script>
            if (window.opener) {{
                window.opener.postMessage({{ type: 'google_connection_error', error: {json.dumps(str(e))} }}, '*');
                setTimeout(() => window.close(), 1000);
            }} else window.close();
            </script></body></html>
            """,
            status_code=500,
        )


@router.get("/me", response_model=User)
@limiter.limit("30/minute")
def get_current_user(
    request: Request,
    currentUser: User = Depends(getCurrentUser),
) -> User:
    return currentUser


@router.post("/logout")
@limiter.limit("10/minute")
def logout(
    request: Request,
    currentUser: User = Depends(getCurrentUser),
) -> JSONResponse:
    """
    End only the PowerOn gateway session. Does not revoke the Google account session in the browser.
    """
    try:
        appInterface = getInterface(currentUser)

        token = request.cookies.get("auth_token")
        if not token:
            auth_header = request.headers.get("Authorization")
            if auth_header and auth_header.lower().startswith("bearer "):
                token = auth_header.split(" ", 1)[1].strip()

        if not token:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="No token found",
            )

        try:
            payload = jose_jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
            session_id = payload.get("sid") or payload.get("sessionId")
            jti = payload.get("jti")
        except Exception as e:
            logger.error(f"Failed to decode JWT on Google logout: {str(e)}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Invalid token",
            )

        revoked = 0
        if session_id:
            revoked = appInterface.revokeTokensBySessionId(
                session_id,
                currentUser.id,
                AuthAuthority.GOOGLE,
                revokedBy=currentUser.id,
                reason="logout",
            )
        elif jti:
            appInterface.revokeTokenById(jti, revokedBy=currentUser.id, reason="logout")
            revoked = 1

        try:
            from modules.shared.auditLogger import audit_logger

            audit_logger.logUserAccess(
                userId=str(currentUser.id),
                mandateId="system",
                action="logout",
                successInfo=f"google_gateway_logout revoked={revoked}",
                ipAddress=request.client.host if request.client else None,
                userAgent=request.headers.get("user-agent"),
                success=True,
            )
        except Exception:
            pass

        json_response = JSONResponse(
            {
                "message": "Successfully logged out from application (Google account stays signed in elsewhere)",
                "revokedTokens": revoked,
            }
        )
        clearAccessTokenCookie(json_response)
        clearRefreshTokenCookie(json_response)
        return json_response
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error during logout: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to logout: {str(e)}",
        )


@router.post("/verify")
@limiter.limit("30/minute")
async def verify_token(
    request: Request,
    currentUser: User = Depends(getCurrentUser),
) -> Dict[str, Any]:
    try:
        appInterface = getInterface(currentUser)
        connections = appInterface.getUserConnections(currentUser.id)
        google_connection = None
        for conn in connections:
            if conn.authority == AuthAuthority.GOOGLE:
                google_connection = conn
                break
        if not google_connection:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="No Google connection found for current user",
            )
        current_token = TokenManager().getFreshToken(google_connection.id)
        if not current_token:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="No Google token found for this connection",
            )
        token_verification = await verify_google_token(current_token.tokenAccess)
        return {
            "valid": token_verification.get("valid", False),
            "scopes": token_verification.get("scopes", []),
            "expires_in": token_verification.get("expires_in", 0),
            "email": token_verification.get("email"),
            "user_id": token_verification.get("user_id"),
            "error": token_verification.get("error")
            if not token_verification.get("valid")
            else None,
        }
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error verifying Google token: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to verify token: {str(e)}",
        )


@router.post("/refresh")
@limiter.limit("10/minute")
async def refresh_token(
    request: Request,
    currentUser: User = Depends(getCurrentUser),
) -> Dict[str, Any]:
    try:
        appInterface = getInterface(currentUser)
        payload = {}
        try:
            payload = await request.json()
        except Exception:
            payload = {}
        requested_connection_id = (
            payload.get("connectionId") if isinstance(payload, dict) else None
        )
        connections = appInterface.getUserConnections(currentUser.id)
        google_connection = None
        if requested_connection_id:
            for conn in connections:
                if conn.id == requested_connection_id and conn.authority == AuthAuthority.GOOGLE:
                    google_connection = conn
                    break
            if not google_connection:
                raise HTTPException(
                    status_code=status.HTTP_404_NOT_FOUND,
                    detail="Requested Google connection not found for current user",
                )
        else:
            for conn in connections:
                if conn.authority == AuthAuthority.GOOGLE:
                    google_connection = conn
                    break
        if not google_connection:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="No Google connection found for current user",
            )
        current_token = TokenManager().getFreshToken(google_connection.id)
        if not current_token:
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="No Google token found for this connection",
            )
        expiresAtValue = parseTimestamp(current_token.expiresAt)
        google_connection.expiresAt = (
            expiresAtValue if expiresAtValue else google_connection.expiresAt
        )
        google_connection.lastChecked = getUtcTimestamp()
        google_connection.status = ConnectionStatus.ACTIVE
        appInterface.db.recordModify(
            UserConnection, google_connection.id, google_connection.model_dump()
        )
        currentTime = getUtcTimestamp()
        expiresAt = parseTimestamp(current_token.expiresAt)
        expiresIn = int(expiresAt - currentTime) if expiresAt else 0
        return {
            "message": "Token refreshed successfully",
            "expires_at": expiresAt,
            "expires_in_seconds": expiresIn,
        }
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error refreshing Google token: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to refresh token: {str(e)}",
        )
