# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""ClickUp OAuth for data connections (UserConnection + Token)."""

from fastapi import APIRouter, HTTPException, Request, status, Depends, Query
from fastapi.responses import HTMLResponse, RedirectResponse
import logging
import json
import time
from typing import Dict, Any
from urllib.parse import urlencode
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.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp
from modules.shared.i18nRegistry import apiRouteContext
routeApiMsg = apiRouteContext("routeSecurityClickup")

logger = logging.getLogger(__name__)

_FLOW_CONNECT = "clickup_connect"

CLICKUP_AUTH_BASE = "https://app.clickup.com/api"
CLICKUP_API_BASE = "https://api.clickup.com/api/v2"

CLIENT_ID = APP_CONFIG.get("Service_CLICKUP_CLIENT_ID")
CLIENT_SECRET = APP_CONFIG.get("Service_CLICKUP_CLIENT_SECRET")
REDIRECT_URI = APP_CONFIG.get("Service_CLICKUP_OAUTH_REDIRECT_URI")

# ClickUp states OAuth access tokens do not expire today; store a long horizon for DB status.
_CLICKUP_TOKEN_EXPIRES_IN_SEC = 10 * 365 * 24 * 3600


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


def _require_clickup_config():
    if not CLIENT_ID or not CLIENT_SECRET or not REDIRECT_URI:
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=routeApiMsg("ClickUp OAuth is not configured (Service_CLICKUP_CLIENT_ID, Service_CLICKUP_CLIENT_SECRET, Service_CLICKUP_OAUTH_REDIRECT_URI)"),
        )


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


@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 ClickUp OAuth for an existing connection (requires gateway session)."""
    try:
        _require_clickup_config()
        interface = getInterface(currentUser)
        connections = interface.getUserConnections(currentUser.id)
        connection = None
        for conn in connections:
            if conn.id == connectionId and conn.authority == AuthAuthority.CLICKUP:
                connection = conn
                break
        if not connection:
            raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("ClickUp connection not found"))

        state_jwt = _issue_oauth_state(
            {
                "flow": _FLOW_CONNECT,
                "connectionId": connectionId,
                "userId": str(currentUser.id),
            }
        )
        query = urlencode(
            {
                "client_id": CLIENT_ID,
                "redirect_uri": REDIRECT_URI,
                "state": state_jwt,
            }
        )
        auth_url = f"{CLICKUP_AUTH_BASE}?{query}"
        return RedirectResponse(auth_url)
    except HTTPException:
        raise
    except Exception as e:
        logger.error(f"Error initiating ClickUp connect: {str(e)}")
        raise HTTPException(
            status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
            detail=f"Failed to initiate ClickUp connect: {str(e)}",
        )


@router.get("/auth/connect/callback")
async def auth_connect_callback(
    code: str = Query(...),
    state: str = Query(...),
) -> HTMLResponse:
    """OAuth callback for ClickUp data connection."""
    state_data = _parse_oauth_state(state)
    if state_data.get("flow") != _FLOW_CONNECT:
        raise HTTPException(status_code=400, detail=routeApiMsg("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=routeApiMsg("Missing connection or user in OAuth state"))

    _require_clickup_config()

    async with httpx.AsyncClient() as client:
        token_resp = await client.post(
            f"{CLICKUP_API_BASE}/oauth/token",
            json={
                "client_id": CLIENT_ID,
                "client_secret": CLIENT_SECRET,
                "code": code,
            },
            headers={"Content-Type": "application/json"},
            timeout=30.0,
        )
    if token_resp.status_code != 200:
        logger.error(f"ClickUp token exchange failed: {token_resp.status_code} {token_resp.text}")
        return HTMLResponse(
            content=f"<html><body><h1>Connection Failed</h1><p>{token_resp.text}</p></body></html>",
            status_code=400,
        )
    token_json = token_resp.json()
    access_token = token_json.get("access_token")
    if not access_token:
        return HTMLResponse(
            content="<html><body><h1>Connection Failed</h1><p>No access token.</p></body></html>",
            status_code=400,
        )

    async with httpx.AsyncClient() as client:
        user_resp = await client.get(
            f"{CLICKUP_API_BASE}/user",
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            },
            timeout=30.0,
        )
    if user_resp.status_code != 200:
        logger.error(f"ClickUp user failed: {user_resp.status_code} {user_resp.text}")
        return HTMLResponse(
            content="<html><body><h1>Connection Failed</h1><p>Could not load ClickUp user.</p></body></html>",
            status_code=400,
        )
    user_payload = user_resp.json()
    cu_user = user_payload.get("user") or {}

    rootInterface = getRootInterface()
    user = rootInterface.getUser(user_id)
    if not user:
        return HTMLResponse(
            content="""
            <html><body><script>
            if (window.opener) {
                window.opener.postMessage({ type: 'clickup_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="""
            <html><body><script>
            if (window.opener) {
                window.opener.postMessage({ type: 'clickup_connection_error', error: 'Connection not found' }, '*');
                setTimeout(() => window.close(), 1000);
            } else window.close();
            </script></body></html>
            """,
            status_code=404,
        )

    ext_id = str(cu_user.get("id", "")) if cu_user.get("id") is not None else ""
    username = cu_user.get("username") or cu_user.get("email") or ext_id
    email = cu_user.get("email")

    expires_at = createExpirationTimestamp(_CLICKUP_TOKEN_EXPIRES_IN_SEC)

    try:
        connection.status = ConnectionStatus.ACTIVE
        connection.lastChecked = getUtcTimestamp()
        connection.expiresAt = expires_at
        connection.externalId = ext_id
        connection.externalUsername = username
        if email:
            connection.externalEmail = email
        connection.grantedScopes = None
        rootInterface.db.recordModify(UserConnection, connection_id, connection.model_dump())

        token = Token(
            userId=user.id,
            authority=AuthAuthority.CLICKUP,
            connectionId=connection_id,
            tokenPurpose=TokenPurpose.DATA_CONNECTION,
            tokenAccess=access_token,
            tokenRefresh=None,
            tokenType="bearer",
            expiresAt=expires_at,
            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: 'clickup_connection_success',
                                connection: {{
                                    id: '{connection.id}',
                                    status: 'connected',
                                    type: 'clickup',
                                    lastChecked: {getUtcTimestamp()},
                                    expiresAt: {expires_at}
                                }}
                            }}, '*');
                            setTimeout(() => window.close(), 1000);
                        }} else {{
                            window.close();
                        }}
                    </script>
                </body>
            </html>
            """
        )
    except Exception as e:
        logger.error(f"Error updating ClickUp connection: {str(e)}", exc_info=True)
        return HTMLResponse(
            content=f"""
            <html><body><script>
            if (window.opener) {{
                window.opener.postMessage({{ type: 'clickup_connection_error', error: {json.dumps(str(e))} }}, '*');
                setTimeout(() => window.close(), 1000);
            }} else window.close();
            </script></body></html>
            """,
            status_code=500,
        )
