"""OAuth Authorization Server Provider for single-owner Bybit MCP."""

import json
import secrets
import time

from mcp.server.auth.provider import (
    AccessToken,
    AuthorizationCode,
    AuthorizationParams,
    RefreshToken,
    construct_redirect_uri,
)
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken

from bybit_mcp_server.auth.db import (
    delete_auth_code,
    delete_token,
    load_auth_code,
    load_oauth_client,
    load_token,
    save_auth_code,
    save_oauth_client,
    save_token,
)
from bybit_mcp_server.config import get_admin_password, get_admin_username, get_issuer_url

# In-memory store for pending authorization requests (authorize → login → redirect)
_pending_authorizations: dict[str, dict] = {}
PENDING_AUTH_TTL = 600  # 10 minutes
ACCESS_TOKEN_TTL = 3600  # 1 hour


def store_pending_authorization(
    request_id: str, client: OAuthClientInformationFull, params: AuthorizationParams
) -> None:
    _cleanup_expired_authorizations()
    _pending_authorizations[request_id] = {
        "client": client,
        "params": params,
        "timestamp": time.time(),
    }


def get_pending_authorization(request_id: str) -> dict | None:
    _cleanup_expired_authorizations()
    return _pending_authorizations.pop(request_id, None)


def _cleanup_expired_authorizations() -> None:
    now = time.time()
    expired = [k for k, v in _pending_authorizations.items() if now - v["timestamp"] > PENDING_AUTH_TTL]
    for k in expired:
        del _pending_authorizations[k]


def verify_admin_credentials(username: str, password: str) -> bool:
    expected_user = get_admin_username()
    expected_pass = get_admin_password()
    if not expected_user or not expected_pass:
        return False
    return secrets.compare_digest(username, expected_user) and secrets.compare_digest(password, expected_pass)


class BybitMCPAuthProvider:
    """Single-owner OAuth authorization server.

    Admin credentials are set via BYBIT_MCP_ADMIN_USERNAME / BYBIT_MCP_ADMIN_PASSWORD env vars.
    OAuth clients, codes, and tokens are persisted in SQLite.
    """

    def __init__(self, db_path: str) -> None:
        self.db_path = db_path

    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
        row = load_oauth_client(self.db_path, client_id)
        if row is None:
            return None
        return OAuthClientInformationFull.model_validate_json(row["client_info_json"])

    async def register_client(self, client_info: OAuthClientInformationFull) -> None:
        save_oauth_client(self.db_path, client_info.client_id, client_info.model_dump_json())

    async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
        request_id = secrets.token_urlsafe(32)
        store_pending_authorization(request_id, client, params)
        issuer = get_issuer_url().rstrip("/")
        return f"{issuer}/login?request_id={request_id}"

    async def load_authorization_code(
        self, client: OAuthClientInformationFull, authorization_code: str
    ) -> AuthorizationCode | None:
        row = load_auth_code(self.db_path, authorization_code)
        if row is None:
            return None
        if row["client_id"] != client.client_id:
            return None
        return AuthorizationCode.model_validate_json(row["code_data_json"])

    async def exchange_authorization_code(
        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
    ) -> OAuthToken:
        delete_auth_code(self.db_path, authorization_code.code)

        access_token = secrets.token_urlsafe(32)
        refresh_token = secrets.token_urlsafe(32)
        expires_at = int(time.time()) + ACCESS_TOKEN_TTL

        save_token(
            self.db_path,
            token=access_token,
            token_type="access",
            client_id=client.client_id,
            scopes=authorization_code.scopes,
            expires_at=expires_at,
            resource=authorization_code.resource,
            related_token=refresh_token,
        )
        save_token(
            self.db_path,
            token=refresh_token,
            token_type="refresh",
            client_id=client.client_id,
            scopes=authorization_code.scopes,
            resource=authorization_code.resource,
            related_token=access_token,
        )

        return OAuthToken(
            access_token=access_token,
            token_type="Bearer",
            expires_in=ACCESS_TOKEN_TTL,
            scope=" ".join(authorization_code.scopes) if authorization_code.scopes else None,
            refresh_token=refresh_token,
        )

    async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:
        row = load_token(self.db_path, refresh_token, "refresh")
        if row is None:
            return None
        if row["client_id"] != client.client_id:
            return None
        return RefreshToken(
            token=row["token"],
            client_id=row["client_id"],
            scopes=json.loads(row["scopes_json"]),
            expires_at=row["expires_at"],
        )

    async def exchange_refresh_token(
        self,
        client: OAuthClientInformationFull,
        refresh_token: RefreshToken,
        scopes: list[str],
    ) -> OAuthToken:
        # Revoke old token pair
        delete_token(self.db_path, refresh_token.token)

        new_scopes = scopes if scopes else refresh_token.scopes
        access_token = secrets.token_urlsafe(32)
        new_refresh_token = secrets.token_urlsafe(32)
        expires_at = int(time.time()) + ACCESS_TOKEN_TTL

        save_token(
            self.db_path,
            token=access_token,
            token_type="access",
            client_id=client.client_id,
            scopes=new_scopes,
            expires_at=expires_at,
            related_token=new_refresh_token,
        )
        save_token(
            self.db_path,
            token=new_refresh_token,
            token_type="refresh",
            client_id=client.client_id,
            scopes=new_scopes,
            related_token=access_token,
        )

        return OAuthToken(
            access_token=access_token,
            token_type="Bearer",
            expires_in=ACCESS_TOKEN_TTL,
            scope=" ".join(new_scopes) if new_scopes else None,
            refresh_token=new_refresh_token,
        )

    async def load_access_token(self, token: str) -> AccessToken | None:
        row = load_token(self.db_path, token, "access")
        if row is None:
            return None
        return AccessToken(
            token=row["token"],
            client_id=row["client_id"],
            scopes=json.loads(row["scopes_json"]),
            expires_at=row["expires_at"],
            resource=row["resource"],
        )

    async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
        delete_token(self.db_path, token.token)


def complete_authorization(
    db_path: str,
    request_id: str,
) -> str | None:
    """Complete the OAuth authorize flow after successful login.

    Returns the redirect URL to send the user back to Claude, or None if request_id is invalid.
    """
    pending = get_pending_authorization(request_id)
    if pending is None:
        return None

    client: OAuthClientInformationFull = pending["client"]
    params: AuthorizationParams = pending["params"]

    code = secrets.token_urlsafe(32)
    expires_at = time.time() + 300  # 5 min

    auth_code = AuthorizationCode(
        code=code,
        scopes=params.scopes or [],
        expires_at=expires_at,
        client_id=client.client_id,
        code_challenge=params.code_challenge,
        redirect_uri=params.redirect_uri,
        redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
        resource=params.resource,
    )

    save_auth_code(db_path, code, client.client_id, auth_code.model_dump_json(), expires_at)

    return construct_redirect_uri(str(params.redirect_uri), code=code, state=params.state)
