"""User authentication system with OAuth Social Login (GitHub + Google).

Handles OAuth 2.0 authorization flows, token exchange, user info fetching,
session creation (JWT cookies), and registration rate limiting.

This module is for user-facing authentication (Track E.2). The existing
admin authentication (core/auth.py, core/dashboard/auth.py) is unchanged.

Usage:
    auth = initialize_user_auth(
        github_client_id="...",
        github_client_secret="...",
        public_url="https://mcp.example.com",
    )
    url, state = auth.get_authorization_url("github")
    # ... user redirected, callback received ...
    user_info = await auth.exchange_code("github", code)
    token = auth.create_user_session(user_id="...", email="...", name="...", role="user")
"""

import logging
import os
import secrets
import time
from urllib.parse import urlencode

import httpx
import jwt

logger = logging.getLogger(__name__)

# OAuth provider endpoints
_GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize"
_GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"
_GITHUB_USER_URL = "https://api.github.com/user"
_GITHUB_EMAILS_URL = "https://api.github.com/user/emails"

# api.github.com rejects requests without a User-Agent header and treats
# requests that omit Accept/X-GitHub-Api-Version as forward-compatibility
# violations on some endpoints (observed as 403 on /user and /user/emails
# after httpx upgraded its default UA). Keep these explicit.
_GITHUB_API_HEADERS = {
    "Accept": "application/vnd.github+json",
    "X-GitHub-Api-Version": "2022-11-28",
    "User-Agent": "mcphub-oauth-client",
}

_GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
_GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
_GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"

# State expiry: 10 minutes
_STATE_EXPIRY_SECONDS = 600

# Registration rate limit: 3 per IP per hour
_REG_RATE_LIMIT = 3
_REG_RATE_WINDOW = 3600  # 1 hour


class OAuthProvider:
    """OAuth provider constants (for type hints / constants)."""

    GITHUB = "github"
    GOOGLE = "google"


class UserAuth:
    """OAuth Social Login and user session management.

    Manages OAuth authorization URL generation, code-for-token exchange,
    user info fetching, JWT session creation/validation, and registration
    rate limiting.
    """

    def __init__(
        self,
        github_client_id: str | None = None,
        github_client_secret: str | None = None,
        google_client_id: str | None = None,
        google_client_secret: str | None = None,
        public_url: str | None = None,
        session_secret: str | None = None,
        session_expiry_hours: int = 168,  # 7 days
    ) -> None:
        """Initialize user authentication.

        Args:
            github_client_id: GitHub OAuth App client ID.
            github_client_secret: GitHub OAuth App client secret.
            google_client_id: Google OAuth client ID.
            google_client_secret: Google OAuth client secret.
            public_url: Public-facing URL of the server (for callbacks).
            session_secret: Secret key for JWT session signing.
            session_expiry_hours: Session token lifetime in hours.
        """
        self._github_client_id = github_client_id
        self._github_client_secret = github_client_secret
        self._google_client_id = google_client_id
        self._google_client_secret = google_client_secret
        self._public_url = (public_url or "").rstrip("/")

        self._session_secret = session_secret or os.getenv(
            "DASHBOARD_SESSION_SECRET",
            os.getenv("OAUTH_JWT_SECRET_KEY", secrets.token_hex(32)),
        )
        self._session_expiry_hours = int(
            os.getenv("SESSION_EXPIRY_HOURS", str(session_expiry_hours))
        )

        # CSRF state tokens: state -> timestamp
        self._pending_states: dict[str, float] = {}

        # Registration rate limiting: IP -> [timestamps]
        self._registration_records: dict[str, list[float]] = {}

        providers = self.available_providers()
        if providers and not self._public_url:
            logger.warning(
                "OAuth providers configured (%s) but PUBLIC_URL is not set. "
                "OAuth login will fail until PUBLIC_URL is configured.",
                providers,
            )
        logger.info(
            "UserAuth initialized: providers=%s, session_expiry=%dh",
            providers,
            self._session_expiry_hours,
        )

    # ── Provider availability ─────────────────────────────────

    def available_providers(self) -> list[str]:
        """Return list of configured OAuth providers.

        Returns:
            List of provider name strings (e.g. ["github", "google"]).
        """
        providers: list[str] = []
        if self._github_client_id and self._github_client_secret:
            providers.append("github")
        if self._google_client_id and self._google_client_secret:
            providers.append("google")
        return providers

    # ── OAuth URL generation ──────────────────────────────────

    def get_authorization_url(self, provider: str) -> tuple[str, str]:
        """Generate OAuth authorization URL with CSRF state.

        Args:
            provider: "github" or "google".

        Returns:
            Tuple of (authorization_url, state_token).

        Raises:
            ValueError: If provider is unsupported or not configured.
        """
        if not self._public_url:
            raise ValueError(
                "PUBLIC_URL environment variable must be set for OAuth login to work. "
                "Example: PUBLIC_URL=https://mcp.example.com"
            )

        state = secrets.token_hex(32)
        self._pending_states[state] = time.time()
        self._cleanup_expired_states()

        callback_url = f"{self._public_url}/auth/callback/{provider}"

        if provider == "github":
            if not self._github_client_id:
                raise ValueError("GitHub OAuth not configured")
            params = {
                "client_id": self._github_client_id,
                "redirect_uri": callback_url,
                "scope": "read:user user:email",
                "state": state,
            }
            return f"{_GITHUB_AUTH_URL}?{urlencode(params)}", state

        if provider == "google":
            if not self._google_client_id:
                raise ValueError("Google OAuth not configured")
            params = {
                "client_id": self._google_client_id,
                "redirect_uri": callback_url,
                "scope": "openid email profile",
                "response_type": "code",
                "state": state,
                "access_type": "online",
            }
            return f"{_GOOGLE_AUTH_URL}?{urlencode(params)}", state

        raise ValueError(f"Unsupported provider: {provider}")

    # ── State validation ──────────────────────────────────────

    def validate_state(self, state: str) -> bool:
        """Validate and consume an OAuth state token (one-time use).

        Args:
            state: The state parameter from the callback.

        Returns:
            True if valid and not expired, False otherwise.
        """
        timestamp = self._pending_states.pop(state, None)
        if timestamp is None:
            return False
        return not (time.time() - timestamp > _STATE_EXPIRY_SECONDS)

    def _cleanup_expired_states(self) -> None:
        """Remove expired state tokens."""
        now = time.time()
        expired = [s for s, t in self._pending_states.items() if now - t > _STATE_EXPIRY_SECONDS]
        for s in expired:
            del self._pending_states[s]

    # ── Token exchange ────────────────────────────────────────

    async def exchange_code(self, provider: str, code: str) -> dict:
        """Exchange authorization code for user info.

        Args:
            provider: "github" or "google".
            code: Authorization code from callback.

        Returns:
            Dict with keys: provider, provider_id, email, name, avatar_url.

        Raises:
            ValueError: If token exchange or user info fetch fails.
        """
        if provider == "github":
            return await self._exchange_github(code)
        if provider == "google":
            return await self._exchange_google(code)
        raise ValueError(f"Unsupported provider: {provider}")

    async def _exchange_github(self, code: str) -> dict:
        """Exchange GitHub authorization code for user info.

        Args:
            code: GitHub authorization code.

        Returns:
            Dict with provider, provider_id, email, name, avatar_url.
        """
        callback_url = f"{self._public_url}/auth/callback/github"

        async with httpx.AsyncClient() as client:
            # Exchange code for access token
            token_resp = await client.post(
                _GITHUB_TOKEN_URL,
                data={
                    "client_id": self._github_client_id,
                    "client_secret": self._github_client_secret,
                    "code": code,
                    "redirect_uri": callback_url,
                },
                headers={"Accept": "application/json"},
            )
            if token_resp.status_code != 200:
                raise ValueError(f"Failed to exchange GitHub code: {token_resp.text}")

            token_data = token_resp.json()
            access_token = token_data.get("access_token")
            if not access_token:
                raise ValueError(f"Failed to exchange GitHub code: {token_data}")

            auth_headers = {
                "Authorization": f"Bearer {access_token}",
                **_GITHUB_API_HEADERS,
            }

            # Fetch user info
            user_resp = await client.get(_GITHUB_USER_URL, headers=auth_headers)
            if user_resp.status_code != 200:
                raise ValueError(f"GitHub /user returned {user_resp.status_code}: {user_resp.text}")
            user_data = user_resp.json()
            if "id" not in user_data:
                raise ValueError(f"GitHub /user response missing 'id' field: {user_data}")

            email = user_data.get("email")
            if not email:
                # Fetch from /user/emails endpoint (private email fallback)
                emails_resp = await client.get(
                    _GITHUB_EMAILS_URL,
                    headers=auth_headers,
                )
                if emails_resp.status_code == 200:
                    emails = emails_resp.json()
                    primary = next(
                        (e for e in emails if e.get("primary") and e.get("verified")),
                        None,
                    )
                    if primary:
                        email = primary["email"]
                    elif emails:
                        email = emails[0]["email"]

        return {
            "provider": "github",
            "provider_id": str(user_data["id"]),
            "email": email,
            "name": user_data.get("name") or user_data.get("login"),
            "avatar_url": user_data.get("avatar_url"),
        }

    async def _exchange_google(self, code: str) -> dict:
        """Exchange Google authorization code for user info.

        Args:
            code: Google authorization code.

        Returns:
            Dict with provider, provider_id, email, name, avatar_url.
        """
        callback_url = f"{self._public_url}/auth/callback/google"

        async with httpx.AsyncClient() as client:
            # Exchange code for access token
            token_resp = await client.post(
                _GOOGLE_TOKEN_URL,
                data={
                    "client_id": self._google_client_id,
                    "client_secret": self._google_client_secret,
                    "code": code,
                    "redirect_uri": callback_url,
                    "grant_type": "authorization_code",
                },
            )
            if token_resp.status_code != 200:
                raise ValueError(f"Failed to exchange Google code: {token_resp.text}")

            token_data = token_resp.json()
            access_token = token_data.get("access_token")
            if not access_token:
                raise ValueError(f"Failed to exchange Google code: {token_data}")

            # Fetch user info
            user_resp = await client.get(
                _GOOGLE_USERINFO_URL,
                headers={"Authorization": f"Bearer {access_token}"},
            )
            user_data = user_resp.json()

        return {
            "provider": "google",
            "provider_id": str(user_data["sub"]),
            "email": user_data.get("email"),
            "name": user_data.get("name"),
            "avatar_url": user_data.get("picture"),
        }

    # ── Registration rate limiting ────────────────────────────

    def check_registration_rate(self, client_ip: str) -> bool:
        """Check if IP is within registration rate limit.

        Allows up to 3 registrations per IP per hour. Expired records
        are cleaned up automatically.

        Args:
            client_ip: Client IP address.

        Returns:
            True if registration is allowed, False if rate limited.
        """
        now = time.time()
        records = self._registration_records.get(client_ip, [])
        # Keep only records within the window
        records = [t for t in records if now - t < _REG_RATE_WINDOW]
        self._registration_records[client_ip] = records
        return len(records) < _REG_RATE_LIMIT

    def record_registration(self, client_ip: str) -> None:
        """Record a registration event for rate limiting.

        Args:
            client_ip: Client IP address.
        """
        if client_ip not in self._registration_records:
            self._registration_records[client_ip] = []
        self._registration_records[client_ip].append(time.time())

    # ── Session management ────────────────────────────────────

    def create_user_session(
        self,
        user_id: str,
        email: str,
        name: str | None,
        role: str,
    ) -> str:
        """Create a JWT session token for an OAuth user.

        Args:
            user_id: User UUID from database.
            email: User email.
            name: User display name.
            role: User role ('user' or 'admin').

        Returns:
            JWT token string.
        """
        now = time.time()
        payload = {
            "uid": user_id,
            "email": email,
            "name": name or "",
            "role": role,
            "type": "oauth_user",
            "iat": now,
            "exp": now + self._session_expiry_hours * 3600,
        }
        return jwt.encode(payload, self._session_secret, algorithm="HS256")

    def validate_user_session(self, token: str) -> dict | None:
        """Validate a user session JWT token.

        Args:
            token: JWT token string.

        Returns:
            Dict with user_id, email, name, role, type -- or None if invalid.
        """
        try:
            payload = jwt.decode(token, self._session_secret, algorithms=["HS256"])
            return {
                "user_id": payload["uid"],
                "email": payload["email"],
                "name": payload.get("name", ""),
                "role": payload.get("role", "user"),
                "type": payload.get("type", "oauth_user"),
            }
        except jwt.ExpiredSignatureError:
            logger.debug("User session expired")
            return None
        except jwt.InvalidTokenError as e:
            logger.debug("Invalid user session token: %s", e)
            return None


# ── Singleton ─────────────────────────────────────────────────

_user_auth: UserAuth | None = None


def initialize_user_auth(
    github_client_id: str | None = None,
    github_client_secret: str | None = None,
    google_client_id: str | None = None,
    google_client_secret: str | None = None,
    public_url: str | None = None,
    **kwargs,
) -> UserAuth:
    """Initialize the global UserAuth singleton.

    Reads from env vars if arguments are not provided:
    GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET,
    GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET,
    PUBLIC_URL.

    Args:
        github_client_id: GitHub OAuth App client ID.
        github_client_secret: GitHub OAuth App client secret.
        google_client_id: Google OAuth client ID.
        google_client_secret: Google OAuth client secret.
        public_url: Public-facing URL of the server.
        **kwargs: Additional keyword args passed to UserAuth.

    Returns:
        The initialized UserAuth instance.
    """
    global _user_auth
    _user_auth = UserAuth(
        github_client_id=github_client_id or os.getenv("GITHUB_CLIENT_ID"),
        github_client_secret=github_client_secret or os.getenv("GITHUB_CLIENT_SECRET"),
        google_client_id=google_client_id or os.getenv("GOOGLE_CLIENT_ID"),
        google_client_secret=google_client_secret or os.getenv("GOOGLE_CLIENT_SECRET"),
        public_url=public_url or os.getenv("PUBLIC_URL", ""),
        **kwargs,
    )
    return _user_auth


def get_user_auth() -> UserAuth:
    """Get the global UserAuth singleton.

    Returns:
        The UserAuth singleton instance.

    Raises:
        RuntimeError: If initialize_user_auth() has not been called.
    """
    if _user_auth is None:
        raise RuntimeError("UserAuth not initialized. Call initialize_user_auth() first.")
    return _user_auth
