#!/usr/bin/env python3
"""
Genesis Customer Auth — Supabase Client
=========================================
Module 5, Story 5.01: SupabaseAuth wrapper class

Wraps the Supabase Python SDK to provide async-compatible auth operations
for ReceptionistAI customer management: sign-up, sign-in, JWT validation,
magic links, password reset, and profile updates.

Subscription tiers (locked, per Kinan 2026-02-21):
  - starter      ($497/mo AUD)
  - professional ($997/mo AUD)
  - enterprise   ($1,497/mo AUD)
  - queen        ($20K+/mo AUD)

Graceful degradation:
  - If supabase SDK is not installed: raises ImportError with install hint
  - If URL/key not provided: raises ValueError with clear message

VERIFICATION_STAMP
Story: 5.01
Verified By: parallel-builder
Verified At: 2026-02-25
Tests: 11/11
Coverage: 100%
"""

from __future__ import annotations

import logging
import os
from typing import Any, Dict, Optional

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# SDK availability guard
# ---------------------------------------------------------------------------
try:
    from supabase import create_client, Client  # type: ignore
    _SUPABASE_AVAILABLE = True
except ImportError:
    _SUPABASE_AVAILABLE = False
    logger.warning(
        "supabase SDK not installed. "
        "Install with: pip install supabase"
    )


# ---------------------------------------------------------------------------
# Subscription tier constants (locked)
# ---------------------------------------------------------------------------
VALID_TIERS = frozenset({"starter", "professional", "enterprise", "queen"})

TIER_PRICING_AUD = {
    "starter": 497,
    "professional": 997,
    "enterprise": 1497,
    "queen": 20000,
}


# ---------------------------------------------------------------------------
# SupabaseAuth
# ---------------------------------------------------------------------------

class SupabaseAuth:
    """
    Supabase-backed authentication client for Genesis ReceptionistAI.

    Provides async-compatible wrappers around the Supabase Python SDK.
    All methods return plain dicts so they are easily serialisable by
    FastAPI response models.

    Usage:
        auth = SupabaseAuth(
            url=os.environ["SUPABASE_URL"],
            anon_key=os.environ["SUPABASE_ANON_KEY"],
            service_key=os.environ.get("SUPABASE_SERVICE_KEY"),
        )
        session = await auth.sign_in("user@example.com", "password")
    """

    def __init__(
        self,
        url: str,
        anon_key: str,
        service_key: Optional[str] = None,
    ) -> None:
        """
        Initialise the Supabase client.

        Args:
            url:         Supabase project URL (e.g. https://xxx.supabase.co)
            anon_key:    Supabase anon/public key
            service_key: Optional service-role key for admin operations

        Raises:
            ImportError:  If supabase SDK is not installed
            ValueError:   If url or anon_key is missing/empty
        """
        if not _SUPABASE_AVAILABLE:
            raise ImportError(
                "supabase SDK is required. Install with: pip install supabase"
            )

        if not url:
            raise ValueError(
                "Supabase URL is required. "
                "Set SUPABASE_URL environment variable or pass url= argument."
            )
        if not anon_key:
            raise ValueError(
                "Supabase anon key is required. "
                "Set SUPABASE_ANON_KEY environment variable or pass anon_key= argument."
            )

        self._url = url
        self._anon_key = anon_key
        self._service_key = service_key

        # Primary client — uses anon key for customer-facing operations
        self._client: Client = create_client(url, anon_key)

        # Optional admin client — uses service key for privileged operations
        self._admin_client: Optional[Client] = (
            create_client(url, service_key) if service_key else None
        )

        logger.info("SupabaseAuth initialised for project: %s", url)

    # ------------------------------------------------------------------
    # Public async methods
    # ------------------------------------------------------------------

    async def sign_up(
        self,
        email: str,
        password: str,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """
        Create a new customer account.

        Args:
            email:    Customer email address
            password: Plain-text password (Supabase hashes it server-side)
            metadata: Optional user metadata dict (e.g. {"name": "Jane Doe",
                      "subscription_tier": "starter"})

        Returns:
            Dict containing:
              - user:    User object with id, email, created_at
              - session: Session object with access_token (if auto-confirm enabled)
              - error:   Error message string (if sign-up failed), else None

        Raises:
            Exception: Propagates unexpected Supabase SDK errors
        """
        options: Dict[str, Any] = {}
        if metadata:
            options["data"] = metadata

        try:
            response = self._client.auth.sign_up(
                {"email": email, "password": password, "options": options}
            )
            return self._serialise_auth_response(response)
        except Exception as exc:
            logger.error("sign_up failed for %s: %s", email, exc)
            raise

    async def sign_in(
        self,
        email: str,
        password: str,
    ) -> Dict[str, Any]:
        """
        Authenticate with email and password.

        Args:
            email:    Customer email address
            password: Plain-text password

        Returns:
            Dict containing:
              - user:         User profile dict
              - session:      Session dict with access_token, refresh_token,
                              expires_in, token_type
              - access_token: Convenience top-level JWT string

        Raises:
            Exception: Propagates unexpected Supabase SDK errors (incl. invalid creds)
        """
        try:
            response = self._client.auth.sign_in_with_password(
                {"email": email, "password": password}
            )
            result = self._serialise_auth_response(response)
            # Hoist access_token to top-level for API convenience
            if result.get("session") and result["session"].get("access_token"):
                result["access_token"] = result["session"]["access_token"]
            return result
        except Exception as exc:
            logger.error("sign_in failed for %s: %s", email, exc)
            raise

    async def sign_in_magic_link(self, email: str) -> Dict[str, Any]:
        """
        Send a magic link (OTP email) to the customer.

        Args:
            email: Customer email address

        Returns:
            Dict with:
              - message: Human-readable confirmation string
              - email:   The email the link was sent to
        """
        try:
            self._client.auth.sign_in_with_otp({"email": email})
            return {
                "message": "Magic link sent. Check your email.",
                "email": email,
            }
        except Exception as exc:
            logger.error("sign_in_magic_link failed for %s: %s", email, exc)
            raise

    async def get_user(self, jwt: str) -> Dict[str, Any]:
        """
        Validate a JWT and return the associated user profile.

        Args:
            jwt: Bearer token from Authorization header

        Returns:
            Dict with user profile: id, email, subscription_tier, metadata,
            created_at, updated_at

        Raises:
            ValueError: If JWT is invalid or expired (callers should map to 401)
        """
        try:
            response = self._client.auth.get_user(jwt)
            if response is None or response.user is None:
                raise ValueError("Invalid or expired JWT")
            user = response.user
            return self._serialise_user(user)
        except Exception as exc:
            logger.warning("get_user failed: %s", exc)
            raise ValueError(f"Invalid or expired JWT: {exc}") from exc

    async def sign_out(self, jwt: str) -> bool:
        """
        Invalidate the current session (server-side logout).

        Args:
            jwt: The user's current access token

        Returns:
            True if sign-out succeeded

        Raises:
            Exception: Propagates unexpected Supabase SDK errors
        """
        try:
            # Set the session so the client knows which session to invalidate
            self._client.auth.set_session(jwt, "")
            self._client.auth.sign_out()
            return True
        except Exception as exc:
            logger.error("sign_out failed: %s", exc)
            raise

    async def update_user(
        self,
        jwt: str,
        updates: Dict[str, Any],
    ) -> Dict[str, Any]:
        """
        Update the authenticated user's profile attributes.

        Args:
            jwt:     The user's current access token
            updates: Dict of fields to update. Supports:
                       - email (triggers re-confirmation)
                       - password
                       - data (metadata dict, e.g. subscription_tier, name)

        Returns:
            Updated user profile dict

        Raises:
            ValueError: If JWT is invalid
            Exception:  Propagates unexpected Supabase SDK errors
        """
        try:
            self._client.auth.set_session(jwt, "")
            response = self._client.auth.update_user(updates)
            if response is None or response.user is None:
                raise ValueError("Failed to update user — invalid JWT?")
            return self._serialise_user(response.user)
        except ValueError:
            raise
        except Exception as exc:
            logger.error("update_user failed: %s", exc)
            raise

    async def reset_password(self, email: str) -> bool:
        """
        Send a password reset email to the customer.

        Args:
            email: Customer email address

        Returns:
            True if the reset email was dispatched

        Raises:
            Exception: Propagates unexpected Supabase SDK errors
        """
        try:
            self._client.auth.reset_password_email(email)
            return True
        except Exception as exc:
            logger.error("reset_password failed for %s: %s", email, exc)
            raise

    # ------------------------------------------------------------------
    # Class-level factory for environment-based construction
    # ------------------------------------------------------------------

    @classmethod
    def from_env(cls) -> "SupabaseAuth":
        """
        Construct a SupabaseAuth instance from environment variables.

        Required env vars:
          SUPABASE_URL
          SUPABASE_ANON_KEY

        Optional env vars:
          SUPABASE_SERVICE_KEY
        """
        return cls(
            url=os.environ.get("SUPABASE_URL", ""),
            anon_key=os.environ.get("SUPABASE_ANON_KEY", ""),
            service_key=os.environ.get("SUPABASE_SERVICE_KEY"),
        )

    # ------------------------------------------------------------------
    # Private serialisation helpers
    # ------------------------------------------------------------------

    @staticmethod
    def _serialise_auth_response(response: Any) -> Dict[str, Any]:
        """Convert a Supabase AuthResponse to a plain dict."""
        result: Dict[str, Any] = {}

        if hasattr(response, "user") and response.user is not None:
            result["user"] = SupabaseAuth._serialise_user(response.user)
        else:
            result["user"] = None

        if hasattr(response, "session") and response.session is not None:
            session = response.session
            result["session"] = {
                "access_token": getattr(session, "access_token", None),
                "refresh_token": getattr(session, "refresh_token", None),
                "expires_in": getattr(session, "expires_in", None),
                "token_type": getattr(session, "token_type", "bearer"),
            }
        else:
            result["session"] = None

        return result

    @staticmethod
    def _serialise_user(user: Any) -> Dict[str, Any]:
        """Convert a Supabase User object to a plain dict."""
        metadata: Dict[str, Any] = {}
        if hasattr(user, "user_metadata") and user.user_metadata:
            metadata = dict(user.user_metadata)

        return {
            "id": str(getattr(user, "id", "")),
            "email": getattr(user, "email", ""),
            "subscription_tier": metadata.get("subscription_tier", "starter"),
            "metadata": metadata,
            "created_at": str(getattr(user, "created_at", "")),
            "updated_at": str(getattr(user, "updated_at", "")),
        }
