# ABOUTME: OAuth provider integration using Authlib.
# ABOUTME: Handles provider registration, builtin configs, and auth flow handlers.
from typing import TYPE_CHECKING, Any, Optional

from authlib.integrations.base_client.errors import OAuthError
from authlib.integrations.starlette_client import OAuth
from fastapi import Request
from fastapi.responses import RedirectResponse
from pydantic import BaseModel, Field
from pydantic_extra_types.language_code import LanguageAlpha2
from starlette.authentication import BaseUser

from vibetuner.frontend.routes import get_homepage_url
from vibetuner.logging import logger
from vibetuner.models.oauth import OAuthAccountModel, OauthProviderModel
from vibetuner.models.user import UserModel


if TYPE_CHECKING:
    from vibetuner.models.oauth_app import OAuthProviderAppModel


DEFAULT_AVATAR_IMAGE = "/statics/img/user-avatar.png"

_PROVIDERS: dict[str, OauthProviderModel] = {}


def register_oauth_provider(name: str, provider: OauthProviderModel) -> None:
    _PROVIDERS[name] = provider
    PROVIDER_IDENTIFIERS[name] = provider.identifier
    _oauth_config.update(**provider.config)
    client_kwargs = dict(provider.client_kwargs)
    if provider.scopes:
        client_kwargs["scope"] = " ".join(provider.scopes)
    register_kwargs: dict = {"client_kwargs": client_kwargs, **provider.params}
    if provider.compliance_fix is not None:
        register_kwargs["compliance_fix"] = provider.compliance_fix
    oauth.register(name, overwrite=True, **register_kwargs)


class WebUser(BaseUser, BaseModel):
    id: str
    name: str
    email: str
    picture: Optional[str] = Field(
        default=DEFAULT_AVATAR_IMAGE,
        description="URL to the user's avatar image",
    )
    language: Optional[LanguageAlpha2] = Field(
        default=None,
        description="Preferred language for the user",
    )

    @property
    def is_authenticated(self) -> bool:
        return True

    @property
    def display_name(self) -> str:
        return self.name


class Config:
    def __init__(self, **kwargs):
        self._data = kwargs

    def get(self, key, default=None):
        return self._data.get(key, default)

    def update(self, **kwargs):
        self._data.update(kwargs)


_oauth_config = Config()
oauth = OAuth(_oauth_config)

PROVIDER_IDENTIFIERS: dict[str, str] = {}


def get_oauth_providers() -> list[str]:
    return list(_PROVIDERS.keys())


async def detect_capabilities(provider_name: str, token: dict[str, Any]) -> list[str]:
    """Detect capabilities for a provider using its registered detector.

    Returns an empty list when the provider is unknown or has no detector.
    """
    provider = _PROVIDERS.get(provider_name)
    if not provider or not provider.capability_detector:
        return []
    return await provider.capability_detector.detect(token)


def get_registered_providers() -> dict[str, OauthProviderModel]:
    """Return a copy of the registered provider configs (for admin UI dropdowns)."""
    return dict(_PROVIDERS)


def _authlib_name_for_app(provider: str, app_id: str) -> str:
    """Build a deterministic Authlib client name for a DB-backed OAuth app."""
    return f"{provider}__app_{app_id}"


def _register_app_client(app: "OAuthProviderAppModel") -> str:
    """Register an Authlib client for a DB-backed OAuth app. Returns the client name."""
    provider_config = _PROVIDERS.get(app.provider)
    if not provider_config:
        raise ValueError(f"No registered provider '{app.provider}'")

    client_name = _authlib_name_for_app(app.provider, str(app.id))
    env_prefix = client_name.upper()

    _oauth_config.update(
        **{
            f"{env_prefix}_CLIENT_ID": app.client_id,
            f"{env_prefix}_CLIENT_SECRET": app.client_secret,
        }
    )

    client_kwargs = dict(provider_config.client_kwargs)
    scopes = app.scopes or provider_config.scopes
    if scopes:
        client_kwargs["scope"] = " ".join(scopes)

    register_kwargs: dict = {"client_kwargs": client_kwargs, **provider_config.params}
    if provider_config.compliance_fix is not None:
        register_kwargs["compliance_fix"] = provider_config.compliance_fix
    oauth.register(client_name, overwrite=True, **register_kwargs)

    return client_name


async def get_db_oauth_apps() -> list["OAuthProviderAppModel"]:
    """Return active DB-backed OAuth apps for providers with login routes."""
    from vibetuner.models.oauth_app import OAuthProviderAppModel

    providers_with_routes = {
        name for name, config in _PROVIDERS.items() if config.login_routes
    }
    if not providers_with_routes:
        return []

    apps = await OAuthProviderAppModel.get_all_active()
    return [app for app in apps if app.provider in providers_with_routes]


async def resolve_oauth_client(provider_name: str, app_id: str | None) -> str:
    """Resolve the Authlib client name, optionally loading a DB-backed app.

    Returns the bare provider name when no app_id is given (env-var fallback),
    or registers and returns a namespaced client name for a DB-backed app.
    """
    if not app_id:
        return provider_name

    from vibetuner.models.oauth_app import OAuthProviderAppModel

    app = await OAuthProviderAppModel.get(app_id)
    if not app or not app.is_active:
        raise ValueError(f"OAuth app '{app_id}' not found or inactive")
    if app.provider != provider_name:
        raise ValueError(
            f"OAuth app '{app_id}' is for provider '{app.provider}', not '{provider_name}'"
        )
    return _register_app_client(app)


_BUILTIN_PROVIDERS: dict[str, OauthProviderModel] = {
    "google": OauthProviderModel(
        identifier="sub",
        params={
            "server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
        },
        scopes=["openid", "email", "profile"],
    ),
    "github": OauthProviderModel(
        identifier="id",
        params={
            "authorize_url": "https://github.com/login/oauth/authorize",
            "access_token_url": "https://github.com/login/oauth/access_token",
            "api_base_url": "https://api.github.com/",
            "userinfo_endpoint": "https://api.github.com/user",
        },
        scopes=["user:email"],
    ),
}


def auto_register_providers(
    provider_names: list[str],
    custom_providers: dict[str, OauthProviderModel] | None = None,
) -> None:
    """Register OAuth providers from builtin configs, settings credentials, and custom configs."""
    from vibetuner.config import settings

    known = sorted(_BUILTIN_PROVIDERS.keys())
    for name in provider_names:
        if name not in _BUILTIN_PROVIDERS:
            logger.warning(f"Unknown OAuth provider '{name}', known providers: {known}")
            continue

        client_id = getattr(settings, f"{name}_client_id", None)
        client_secret = getattr(settings, f"{name}_client_secret", None)

        if not client_id or not client_secret:
            logger.warning(
                f"Skipping OAuth provider '{name}': "
                f"set {name}_client_id and {name}_client_secret in env or .env"
            )
            continue

        env_prefix = name.upper()
        builtin = _BUILTIN_PROVIDERS[name]
        provider = OauthProviderModel(
            identifier=builtin.identifier,
            params=builtin.params,
            scopes=builtin.scopes,
            client_kwargs=builtin.client_kwargs,
            config={
                **builtin.config,
                f"{env_prefix}_CLIENT_ID": client_id.get_secret_value(),
                f"{env_prefix}_CLIENT_SECRET": client_secret.get_secret_value(),
            },
        )
        register_oauth_provider(name, provider)
        logger.info(f"Auto-registered OAuth provider: {name}")

    # Register custom providers (fully configured by the user)
    for name, provider in (custom_providers or {}).items():
        if not isinstance(name, str):
            logger.warning(
                f"Skipping custom OAuth provider with non-string key: {name!r}"
            )
            continue
        if not isinstance(provider, OauthProviderModel):
            logger.warning(
                f"Skipping custom OAuth provider '{name}': expected OauthProviderModel, "
                f"got {type(provider).__name__}"
            )
            continue
        if name in _PROVIDERS:
            logger.warning(
                f"Custom OAuth provider '{name}' conflicts with already-registered provider, skipping"
            )
            continue
        register_oauth_provider(name, provider)
        logger.info(f"Registered custom OAuth provider: {name}")


async def _handle_user_account(
    provider: str, identifier: str, email: str, name: str, picture: str
) -> UserModel:
    """Handle user account creation or OAuth linking."""
    # Check if OAuth account already exists
    oauth_account = await OAuthAccountModel.get_by_provider_and_id(
        provider=provider,
        provider_user_id=identifier,
    )

    if oauth_account:
        # OAuth account exists, resolve user through the link (survives email changes)
        account = await UserModel.find_one({"oauth_accounts.$id": oauth_account.id})
        if not account:
            raise OAuthError("No account linked to this OAuth account")
        return account

    # OAuth account doesn't exist, check if user exists

    if account := (await UserModel.get_by_email(email)):
        # User exists, link OAuth account
        await _link_oauth_account(account, provider, identifier, email, name, picture)
    else:
        # New user, create account and OAuth link
        account = await _create_new_user_with_oauth(
            provider, identifier, email, name, picture
        )

    return account


async def _link_oauth_account(
    account: UserModel,
    provider: str,
    identifier: str,
    email: str,
    name: str,
    picture: str,
) -> None:
    """Link OAuth account to existing user."""
    oauth_account = OAuthAccountModel(
        provider=provider,
        provider_user_id=identifier,
        email=email,
        name=name,
        picture=picture,
    )
    await oauth_account.insert()
    account.oauth_accounts.append(oauth_account)
    await account.save()


async def _create_new_user_with_oauth(
    provider: str, identifier: str, email: str, name: str, picture: str
) -> UserModel:
    """Create new user account with OAuth linking."""
    # Create user account
    oauth_account = OAuthAccountModel(
        provider=provider,
        provider_user_id=identifier,
        email=email,
        name=name,
        picture=picture,
    )
    await oauth_account.insert()

    account = UserModel(
        email=email,
        name=name,
        picture=picture,
        oauth_accounts=[oauth_account],
    )
    await account.insert()

    return account


def _create_auth_login_handler(provider_name: str):
    async def auth_login(
        request: Request, next: str | None = None, app_id: str | None = None
    ):
        from vibetuner.config import settings

        request.session["next_url"] = next or get_homepage_url(request)

        try:
            client_name = await resolve_oauth_client(provider_name, app_id)
        except ValueError:
            logger.warning(f"Invalid app_id '{app_id}' for provider '{provider_name}'")
            return RedirectResponse(url=get_homepage_url(request))

        if app_id:
            request.session["oauth_app_id"] = app_id

        client = oauth.create_client(client_name)
        if not client:
            return RedirectResponse(url=get_homepage_url(request))

        if settings.oauth_relay_url and settings.environment == "dev":
            relay_url = str(settings.oauth_relay_url).rstrip("/")
            redirect_uri = f"{relay_url}/auth/provider/{provider_name}"
            response = await client.authorize_redirect(
                request, redirect_uri, hl=request.state.language
            )
            # Wrap the state param with the app URL so the relay
            # can redirect back: {scheme}://{host}:{port}|{original_state}
            from urllib.parse import parse_qs, urlencode, urlparse, urlunparse

            location = response.headers.get("location", "")
            parsed = urlparse(location)
            params = parse_qs(parsed.query, keep_blank_values=True)
            if "state" in params:
                original_state = params["state"][0]
                params["state"] = [f"{settings.expose_url}|{original_state}"]
                new_query = urlencode(params, doseq=True)
                new_location = urlunparse(parsed._replace(query=new_query))
                response.headers["location"] = new_location
            return response

        redirect_uri = request.url_for(f"auth_with_{provider_name}")
        return await client.authorize_redirect(
            request, redirect_uri, hl=request.state.language
        )

    return auth_login


def _create_auth_handler(provider_name: str):
    async def auth_handler(request: Request):
        """Handle OAuth authentication flow."""
        try:
            # Resolve the Authlib client (DB-backed app or env-var default)
            app_id = request.session.pop("oauth_app_id", None)
            try:
                client_name = await resolve_oauth_client(provider_name, app_id)
            except ValueError:
                logger.warning(
                    f"Failed to resolve OAuth app '{app_id}' for provider '{provider_name}'"
                )
                return get_homepage_url(request)

            client = oauth.create_client(client_name)
            if not client:
                return get_homepage_url(request)

            # Get user info from OAuth provider
            token = await client.authorize_access_token(request)
            userinfo = token.get("userinfo")
            if not userinfo:
                raise OAuthError("No userinfo found in token")

            # Extract user data
            identifier = userinfo.get(PROVIDER_IDENTIFIERS[provider_name])
            email = userinfo.get("email")
            name = userinfo.get("name")
            picture = userinfo.get("picture")

            # Handle user account creation/linking
            account = await _handle_user_account(
                provider_name, identifier, email, name, picture
            )

            # Set session and redirect
            request.session["user"] = account.session_dict
            return request.session.pop("next_url", get_homepage_url(request))
        except OAuthError:
            return get_homepage_url(request)

    return auth_handler
