# Helix Admin SDK
# For Helix internal use to manage customers and platform

# =============================================================================
# DEPRECATION NOTICE
# =============================================================================
# This module is moving to a separate package 'helix-admin' in v2.0.0.
# Install the new package:
#   pip install git+https://github.com/helix-tools/helix-admin-sdk-python.git
# See MIGRATION_v2.md for migration instructions.
# =============================================================================

import warnings as _warnings

_warnings.warn(
    "The helix_connect.admin module is moving to a separate package 'helix-admin' in v2.0.0. "
    "Install: pip install git+https://github.com/helix-tools/helix-admin-sdk-python.git "
    "See MIGRATION_v2.md for details.",
    FutureWarning,
    stacklevel=2,
)

import logging
import os
import time
from datetime import datetime, timezone
from typing import Dict, List, Literal, Optional

import jwt
import requests

from .consumer import API_TIMEOUT, HelixConsumer
from .exceptions import HelixError, TokenGenerationError, ValidationError

logger = logging.getLogger(__name__)

# Valid enum values for JWT claims
VALID_CUSTOMER_TYPES = ("producer", "consumer", "both")
VALID_ROLES = ("admin", "user")
VALID_TIERS = ("free", "basic", "starter", "professional", "enterprise")
VALID_LOGIN_METHODS = ("password", "oauth", "sso", "magic_link", "api_key")

# Token defaults
DEFAULT_TOKEN_EXPIRY_MINUTES = 60
DEFAULT_ISSUER = "helix-admin-sdk"

# Secret caching
_secret_cache: Dict[str, tuple] = {}
_secret_cache_ttl = 300  # 5 minutes


class HelixAdmin(HelixConsumer):
    """
    SDK for Helix administrators to manage the platform.
    Inherits consumer capabilities plus admin operations.

    Args:
        aws_access_key_id: Admin AWS access key ID
        aws_secret_access_key: Admin AWS secret access key
        customer_id: Admin customer ID
        api_endpoint: API endpoint (default: HELIX_API_ENDPOINT or https://api-go.helix.tools)
        region: AWS region (default: us-east-1)
    """

    @staticmethod
    def _normalize_address(address: Optional[Dict]) -> Optional[Dict]:
        """
        Normalize address to use postal_code (supports both postal_code and legacy zip).

        Args:
            address: Address dict (may contain 'zip' or 'postal_code')

        Returns:
            Normalized address dict with 'postal_code' field, or None
        """
        if not address:
            return None

        if not isinstance(address, dict):
            raise ValueError(
                "Address must be a dict with keys: street, city, state, postal_code, country. "
                "String addresses are no longer supported."
            )

        # Normalize zip to postal_code
        normalized = {
            "street": address.get("street"),
            "city": address.get("city"),
            "state": address.get("state"),
            "postal_code": address.get("postal_code") or address.get("zip"),
            "country": address.get("country"),
        }

        return normalized

    def create_customer(
        self,
        company_name: str,
        business_email: str,
        customer_type: str = "consumer",
        tier: str = "starter",
        billing_email: Optional[str] = None,
        phone: Optional[str] = None,
        address: Optional[Dict] = None,
    ) -> Dict:
        """
        Create a new customer account.

        Args:
            company_name: Company name
            business_email: Business contact email
            customer_type: 'consumer' or 'producer'
            tier: Customer tier - 'starter', 'professional', or 'enterprise' (default: 'starter')
            billing_email: Billing contact email (defaults to business_email if not set)
            phone: Optional phone number
            address: Optional address dict with keys: street, city, state, postal_code, country.
                    Note: 'zip' is also accepted for backward compatibility but 'postal_code' is preferred.

        Returns:
            Customer object with customer_id, status, and credential_delivery_url

        Raises:
            ValueError: If address is not a dict (string addresses are no longer supported)
        """
        payload = {
            "company_name": company_name,
            "business_email": business_email,
            "customer_type": customer_type,
            "tier": tier,
        }

        if billing_email:
            payload["billing_email"] = billing_email
        if phone:
            payload["phone"] = phone
        if address:
            payload["address"] = self._normalize_address(address)

        return self._make_api_request("POST", "/v1/customers/onboard", json=payload)

    def list_all_customers(self) -> List[Dict]:
        """
        List all customers in the system.

        Returns:
            List of customer objects
        """
        response = self._make_api_request("GET", "/v1/customers")
        return response.get("customers", [])

    def get_customer(self, customer_id: str) -> Dict:
        """
        Get details about a specific customer.

        Args:
            customer_id: Customer ID

        Returns:
            Customer object
        """
        return self._make_api_request("GET", f"/v1/customers/{customer_id}")

    def update_customer(
        self,
        customer_id: str,
        company_name: Optional[str] = None,
        business_email: Optional[str] = None,
        billing_email: Optional[str] = None,
        phone: Optional[str] = None,
        address: Optional[Dict] = None,
        customer_type: Optional[str] = None,
        status: Optional[str] = None,
    ) -> Dict:
        """
        Update a customer's information.

        Args:
            customer_id: Customer ID
            company_name: Optional company name update
            business_email: Optional business email update
            billing_email: Optional billing email update
            phone: Optional phone number update
            address: Optional address dict with keys: street, city, state, postal_code, country
            customer_type: Optional customer type update ('consumer', 'producer', or 'both')
            status: Optional status update ('active', 'suspended', etc.)

        Returns:
            Updated customer object
        """
        payload = {}
        if company_name is not None:
            payload["company_name"] = company_name
        if business_email is not None:
            payload["business_email"] = business_email
        if billing_email is not None:
            payload["billing_email"] = billing_email
        if phone is not None:
            payload["phone"] = phone
        if address is not None:
            payload["address"] = self._normalize_address(address)
        if customer_type is not None:
            payload["customer_type"] = customer_type
        if status is not None:
            payload["status"] = status

        return self._make_api_request(
            "PATCH", f"/v1/customers/{customer_id}", json=payload
        )

    def upgrade_customer(self, customer_id: str, new_type: str) -> Dict:
        """
        Upgrade a customer (e.g., consumer -> producer).

        Args:
            customer_id: Customer ID
            new_type: New customer type ('producer' or 'both')

        Returns:
            Updated customer object
        """
        return self.update_customer(customer_id, customer_type=new_type)

    def list_all_datasets(self) -> List[Dict]:
        """
        List all datasets in the system (admin view).

        Returns:
            List of all dataset objects
        """
        return self.list_datasets()  # No producer_id filter = all datasets

    def _ssm_param_candidates(self, customer_id: str, param_name: str) -> List[str]:
        env = (
            os.environ.get("HELIX_ENVIRONMENT")
            or os.environ.get("ENVIRONMENT")
            or "production"
        )
        prefixes = [
            os.environ.get("HELIX_SSM_CUSTOMER_PREFIX"),
            f"/helix-tools/{env}/customers",
            f"/helix/{env}/customers",
            "/helix/customers",
        ]
        candidates = []
        seen = set()
        for prefix in prefixes:
            if not prefix:
                continue
            prefix = prefix.rstrip("/")
            if prefix in seen:
                continue
            seen.add(prefix)
            candidates.append(f"{prefix}/{customer_id}/{param_name}")
        return candidates

    def _get_ssm_parameter_value(
        self, customer_id: str, param_name: str, decrypt: bool = False
    ) -> str:
        last_error = None
        for name in self._ssm_param_candidates(customer_id, param_name):
            try:
                response = self.ssm.get_parameter(Name=name, WithDecryption=decrypt)
                return response["Parameter"]["Value"]
            except Exception as e:
                last_error = e
        if last_error:
            raise last_error
        raise HelixError(
            f"SSM parameter not found for customer {customer_id}: {param_name}"
        )

    def get_customer_credentials(self, customer_id: str) -> Dict:
        """
        Retrieve AWS credentials for a customer from SSM Parameter Store.
        Admin-only operation to retrieve credentials after customer creation.

        Args:
            customer_id: Customer ID (format: customer-{uuid} or company-{timestamp}-{name})

        Returns:
            Dictionary with keys:
                - aws_access_key_id: AWS access key ID
                - aws_secret_access_key: AWS secret access key
                - customer_id: Customer ID
                - metadata: Customer metadata (JSON parsed)

        Raises:
            HelixError: If SSM parameters cannot be retrieved
        """
        try:
            # Retrieve AWS credentials from SSM Parameter Store
            aws_access_key_id = self._get_ssm_parameter_value(
                customer_id, "aws_access_key_id", decrypt=True
            )
            aws_secret_access_key = self._get_ssm_parameter_value(
                customer_id, "aws_secret_access_key", decrypt=True
            )

            # Get metadata (optional, may not exist for all customers)
            metadata = None
            try:
                import json

                metadata_value = self._get_ssm_parameter_value(
                    customer_id, "metadata", decrypt=True
                )
                metadata = json.loads(metadata_value)
            except self.ssm.exceptions.ParameterNotFound:
                # Metadata parameter doesn't exist, that's okay
                pass
            except Exception as e:
                # Log warning but don't fail the whole operation
                print(f"Warning: Could not parse metadata: {e}")

            return {
                "customer_id": customer_id,
                "aws_access_key_id": aws_access_key_id,
                "aws_secret_access_key": aws_secret_access_key,
                "metadata": metadata,
            }

        except self.ssm.exceptions.ParameterNotFound as e:
            raise HelixError(
                f"Credentials not found for customer {customer_id}. "
                f"Customer may not be fully provisioned yet. Error: {e}"
            )
        except Exception as e:
            raise HelixError(
                f"Failed to retrieve credentials for customer {customer_id}: {e}"
            )

    # Credential Management Methods (Admin Operations)

    def create_credential(self, customer_id: Optional[str] = None) -> Dict:
        """
        Create initial IAM credentials for a customer (admin-only operation).
        This endpoint is protected and requires admin role in JWT token.

        IMPORTANT: This returns a portal URL to view the new credentials.
        The actual credentials are NOT returned in the API response for security.
        Use this when a customer needs their first set of credentials created by an admin.

        Args:
            customer_id: Optional customer ID. If not provided, creates credentials for admin's account.

        Returns:
            Dict containing:
                - success: True if successful
                - message: Success message
                - credential_portal_url: Secure one-time portal link to view credentials
                - portal_expires_at: When the portal link expires (24 hours)
                - credential: New credential info (without secret key)

        Raises:
            HelixError: If customer already has credentials or if not admin

        Example:
            >>> admin = HelixAdmin(...)
            >>> # Create initial credentials for a new customer
            >>> response = admin.create_credential(customer_id="customer-123")
            >>> print(f"Portal URL: {response['credential_portal_url']}")
            >>> # Send this portal URL to the customer to retrieve their credentials
        """
        if customer_id:
            # Admin creating credentials for another customer
            return self._make_api_request(
                "POST", f"/v1/customers/{customer_id}/credentials"
            )
        else:
            # Admin creating their own credentials
            return self._make_api_request("POST", "/v1/credentials")

    def list_credentials(self, customer_id: Optional[str] = None) -> Dict:
        """
        List credentials for a specific customer or all customers (admin only).

        Args:
            customer_id: Optional customer ID to filter credentials.
                        If not provided, returns credentials for the admin's own account.

        Returns:
            Dict with 'credentials' list and 'count'. Each credential contains:
                - id: Credential ID
                - customer_id: Customer ID
                - access_key_id: AWS Access Key ID
                - status: active, rotating, inactive, or deleted
                - created_at: Creation timestamp
                - rotation_count: Number of times rotated
                - expires_at: Expiration timestamp (if rotating)
                - days_until_expiry: Days until expiration (if applicable)

        Example:
            >>> admin = HelixAdmin(...)
            >>> # List all credentials for a specific customer
            >>> creds = admin.list_credentials(customer_id="customer-123")
            >>> print(f"Found {creds['count']} credentials")
        """
        if customer_id:
            # Admin viewing another customer's credentials
            return self._make_api_request(
                "GET", f"/v1/customers/{customer_id}/credentials"
            )
        else:
            # Admin viewing their own credentials
            return self._make_api_request("GET", "/v1/credentials")

    def regenerate_credential(
        self,
        customer_id: Optional[str] = None,
        reason: Optional[str] = None,
        old_key_expiration_days: int = 7,
    ) -> Dict:
        """
        Rotate credentials for a customer (admin operation).
        Old key remains active during transition period (7-30 days).

        IMPORTANT: This returns a portal URL to view the new credentials.
        The actual credentials are NOT returned in the API response for security.

        Args:
            customer_id: Optional customer ID. If not provided, rotates admin's own credentials.
            reason: Optional reason for rotation (e.g., "Security audit", "Compromised key")
            old_key_expiration_days: Days until old key expires (default: 7, max: 30)

        Returns:
            Dict containing:
                - success: True if successful
                - message: Success message
                - credential_portal_url: Secure one-time portal link to view new credentials
                - portal_expires_at: When the portal link expires (24 hours)
                - new_credential: New credential info (without secret)
                - old_credential: Old credential info
                - expires_old_key_at: When old key will be disabled

        Example:
            >>> admin = HelixAdmin(...)
            >>> # Rotate credentials for a specific customer
            >>> response = admin.regenerate_credential(
            ...     customer_id="customer-123",
            ...     reason="Security incident - key potentially compromised",
            ...     old_key_expiration_days=7
            ... )
            >>> print(f"Portal URL: {response['credential_portal_url']}")
            >>> print(f"Old key expires: {response['expires_old_key_at']}")
        """
        payload = {}
        if reason:
            payload["reason"] = reason
        if old_key_expiration_days:
            payload["old_key_expiration_days"] = old_key_expiration_days

        if customer_id:
            # Admin rotating another customer's credentials
            return self._make_api_request(
                "POST",
                f"/v1/customers/{customer_id}/credentials/regenerate",
                json=payload,
            )
        else:
            # Admin rotating their own credentials
            return self._make_api_request(
                "POST", "/v1/credentials/regenerate", json=payload
            )

    def delete_credential(
        self, access_key_id: str, customer_id: Optional[str] = None
    ) -> Dict:
        """
        Delete a specific credential (admin operation).

        Note: Cannot delete the last active credential for a customer.

        Args:
            access_key_id: The AWS Access Key ID to delete (e.g., "AKIAIOSFODNN7EXAMPLE")
            customer_id: Optional customer ID. If not provided, deletes from admin's account.

        Returns:
            Dict with success status and message

        Raises:
            HelixError: If credential not found or cannot be deleted

        Example:
            >>> admin = HelixAdmin(...)
            >>> # Delete a credential for a specific customer
            >>> result = admin.delete_credential(
            ...     access_key_id="AKIAIOSFODNN7OLDKEY",
            ...     customer_id="customer-123"
            ... )
            >>> print(result['message'])
        """
        if customer_id:
            # Admin deleting another customer's credential
            return self._make_api_request(
                "DELETE", f"/v1/customers/{customer_id}/credentials/{access_key_id}"
            )
        else:
            # Admin deleting their own credential
            return self._make_api_request("DELETE", f"/v1/credentials/{access_key_id}")

    def offboard_customer(
        self,
        customer_id: str,
        cancel_stripe: bool = True,
        dry_run: bool = False,
    ) -> Dict:
        """
        Offboard a customer by revoking subscriptions, deleting credentials,
        cancelling Stripe, triggering infrastructure teardown via
        POST /v1/customers/{id}/decommission, and then soft-deleting the
        customer record (only after successful decommission).

        Args:
            customer_id: Customer ID to offboard
            cancel_stripe: Whether to cancel via DELETE /v1/companies/{id} (default: True)
            dry_run: If True, report what would be done without making changes

        Returns:
            Dict summarizing actions taken:
                - customer_id: Customer ID
                - dry_run: Whether this was a dry run
                - customer: Customer record before offboarding
                - subscriptions_revoked: List of revoked subscription IDs
                - credentials_deleted: List of deleted access key IDs
                - stripe_cancelled: Whether Stripe was cancelled
                - customer_deleted: Whether soft-delete was performed
                - infrastructure_note: Reminder about Terraform destroy

        Raises:
            HelixError: If customer not found or API calls fail
        """
        result: Dict = {
            "customer_id": customer_id,
            "dry_run": dry_run,
            "subscriptions_revoked": [],
            "credentials_deleted": [],
            "stripe_cancelled": False,
            "stripe_skipped": False,
            "customer_deleted": False,
            "errors": [],
            "infrastructure_teardown": None,
            "infrastructure_skipped": False,
            "infrastructure_note": "Pending — will trigger Terraform destroy via API.",
        }

        # 1. Fetch customer record
        customer = self.get_customer(customer_id)
        result["customer"] = customer

        # Determine customer type to skip irrelevant steps
        customer_type = customer.get("customer_type", "unknown")
        is_consumer_only = customer_type == "consumer"
        has_infrastructure = bool(customer.get("infrastructure"))

        # Consumers don't have Stripe subscriptions (only producers do)
        if is_consumer_only and cancel_stripe:
            cancel_stripe = False
            result["stripe_skipped"] = True

        # Consumers without infrastructure don't need Terraform teardown
        skip_decommission = is_consumer_only and not has_infrastructure

        if dry_run:
            # Gather what would happen without acting
            try:
                subs = self._make_api_request(
                    "GET", f"/v1/subscriptions?customer_id={customer_id}"
                )
                result["subscriptions_revoked"] = [
                    s.get("_id") or s.get("id")
                    for s in subs.get("subscriptions", [])
                    if s.get("status") == "active"
                ]
            except HelixError:
                result["subscriptions_revoked"] = ["(unable to list)"]

            try:
                creds = self.list_credentials(customer_id)
                result["credentials_deleted"] = [
                    c.get("access_key_id")
                    for c in creds.get("credentials", [])
                    if c.get("status") == "active"
                ]
            except HelixError:
                result["credentials_deleted"] = ["(unable to list)"]

            result["stripe_cancelled"] = cancel_stripe
            result["customer_deleted"] = True
            if skip_decommission:
                result["infrastructure_skipped"] = True
                result["infrastructure_note"] = (
                    "Skipped — consumer has no provisioned infrastructure to tear down."
                )
            else:
                result["infrastructure_note"] = (
                    "Would trigger Terraform destroy via POST /v1/customers/{}/decommission".format(
                        customer_id
                    )
                )
            return result

        # 2. Revoke all active subscriptions
        try:
            subs = self._make_api_request(
                "GET", f"/v1/subscriptions?customer_id={customer_id}"
            )
            for sub in subs.get("subscriptions", []):
                sub_id = sub.get("_id") or sub.get("id")
                if sub.get("status") == "active" and sub_id:
                    try:
                        self._make_api_request(
                            "PUT", f"/v1/subscriptions/{sub_id}/revoke"
                        )
                        result["subscriptions_revoked"].append(sub_id)
                    except HelixError as e:
                        result["errors"].append(
                            f"Failed to revoke subscription {sub_id}: {e}"
                        )
        except HelixError as e:
            result["errors"].append(f"Failed to list subscriptions: {e}")

        # 3. Delete all credentials
        try:
            creds = self.list_credentials(customer_id)
            for cred in creds.get("credentials", []):
                key_id = cred.get("access_key_id")
                if key_id and cred.get("status") in ("active", "rotating"):
                    try:
                        self.delete_credential(key_id, customer_id)
                        result["credentials_deleted"].append(key_id)
                    except HelixError as e:
                        result["errors"].append(
                            f"Failed to delete credential {key_id}: {e}"
                        )
        except HelixError as e:
            result["errors"].append(f"Failed to list credentials: {e}")

        # 4. Cancel Stripe subscription via company endpoint (producers only)
        if cancel_stripe:
            try:
                self._make_api_request("DELETE", f"/v1/companies/{customer_id}")
                result["stripe_cancelled"] = True
            except HelixError as e:
                result["errors"].append(f"Failed to cancel Stripe/company: {e}")
        elif result["stripe_skipped"]:
            pass  # Already marked as skipped

        # 5. Trigger infrastructure teardown via Terraform destroy (skip for consumers with no infra)
        #    MUST happen BEFORE soft-delete — the decommission API needs the
        #    customer record to exist (queries with deleted_at=nil).
        if skip_decommission:
            result["infrastructure_skipped"] = True
            result["infrastructure_note"] = (
                "Skipped — consumer has no provisioned infrastructure to tear down."
            )
        else:
            try:
                decommission = self._make_api_request(
                    "POST", f"/v1/customers/{customer_id}/decommission"
                )
                result["infrastructure_teardown"] = decommission
                result["infrastructure_note"] = (
                    "Infrastructure teardown triggered (task_arn: {}). "
                    "The provision-complete callback will set status to 'inactive' when done.".format(
                        decommission.get("task_arn", "unknown")
                    )
                )
            except HelixError as e:
                result["errors"].append(
                    f"Failed to trigger infrastructure teardown: {e}"
                )
                result["decommission_failed"] = True
                logger.warning(
                    "Offboard %s: decommission failed, skipping soft-delete "
                    "to preserve customer record for retry. Error: %s",
                    customer_id,
                    e,
                )
                return result

        # 6. Soft-delete customer record (only after successful decommission)
        try:
            self._make_api_request("DELETE", f"/v1/customers/{customer_id}")
            result["customer_deleted"] = True
        except HelixError as e:
            result["errors"].append(f"Failed to soft-delete customer: {e}")

        # Warn if any non-fatal errors accumulated
        if result["errors"]:
            logger.warning(
                "Offboard %s completed with errors: %s",
                customer_id,
                result["errors"],
            )

        return result

    # JWT Token Generation Methods

    def _get_jwt_secret(self, secret: Optional[str] = None) -> str:
        """
        Resolve JWT secret with priority: explicit arg -> env var -> SSM.

        Args:
            secret: Explicit secret (highest priority)

        Returns:
            JWT secret string

        Raises:
            TokenGenerationError: If no secret can be resolved
        """
        # 1. Explicit argument
        if secret:
            return secret

        # 2. Environment variable
        env_secret = os.environ.get("HELIX_JWT_SECRET")
        if env_secret:
            return env_secret

        # 3. SSM Parameter Store (with caching)
        cache_key = f"jwt_secret:{self.customer_id}"
        now = time.time()

        if cache_key in _secret_cache:
            cached_secret, cached_at = _secret_cache[cache_key]
            if now - cached_at < _secret_cache_ttl:
                return cached_secret

        try:
            ssm_secret = self._get_ssm_parameter_value(
                self.customer_id, "jwt_secret", decrypt=True
            )
            _secret_cache[cache_key] = (ssm_secret, now)
            return ssm_secret
        except Exception as e:
            raise TokenGenerationError(
                f"Failed to resolve JWT secret. Provide 'secret' argument, "
                f"set HELIX_JWT_SECRET env var, or ensure SSM parameter exists. "
                f"Error: {e}"
            )

    @staticmethod
    def _validate_enum(value: str, valid_values: tuple, field_name: str) -> str:
        """
        Validate that a value is in the allowed enum set.

        Args:
            value: Value to validate
            valid_values: Tuple of valid values
            field_name: Field name for error message

        Returns:
            Validated value (lowercase normalized)

        Raises:
            ValidationError: If value is not valid
        """
        normalized = value.lower().strip()
        if normalized not in valid_values:
            raise ValidationError(
                f"Invalid {field_name}: '{value}'. "
                f"Must be one of: {', '.join(valid_values)}"
            )
        return normalized

    def generate_token(
        self,
        *,
        sub: str,
        customer_id: str,
        email: str,
        customer_type: Literal["producer", "consumer", "both"],
        role: Literal["admin", "user"] = "user",
        tier: Optional[
            Literal["free", "basic", "starter", "professional", "enterprise"]
        ] = None,
        authenticated_at: Optional[datetime] = None,
        login_method: Optional[
            Literal["password", "oauth", "sso", "magic_link", "api_key"]
        ] = None,
        nbf: Optional[datetime] = None,
        issuer: Optional[str] = None,
        expiry_minutes: int = DEFAULT_TOKEN_EXPIRY_MINUTES,
        secret: Optional[str] = None,
    ) -> str:
        """
        Generate a schema-compliant JWT token for Helix Connect API authentication.

        This method creates JWT tokens for testing, development, service-to-service
        communication, or admin operations. All tokens use HS256 (symmetric) signing.

        Args:
            sub: Subject identifier (typically user_id or email)
            customer_id: Customer/company ID (e.g., "company-1234567890-acme")
            email: User's email address
            customer_type: Customer type - "producer", "consumer", or "both"
                           NOTE: "admin" is NOT a valid customer_type
            role: User role - "admin" or "user" (default: "user")
            tier: Subscription tier (optional) - "free", "basic", "starter",
                  "professional", or "enterprise"
            authenticated_at: When authentication occurred (default: now)
            login_method: How user authenticated (optional) - "password", "oauth",
                          "sso", "magic_link", or "api_key"
            nbf: Not-before time (optional) - token is invalid before this time
            issuer: Token issuer (default: "helix-admin-sdk")
            expiry_minutes: Token validity in minutes (default: 60)
            secret: JWT signing secret. Resolution order:
                    1. This argument (if provided)
                    2. HELIX_JWT_SECRET environment variable
                    3. SSM Parameter Store (/{env}/customers/{customer_id}/jwt_secret)

        Returns:
            Signed JWT token string

        Raises:
            ValidationError: If enum values are invalid
            TokenGenerationError: If secret cannot be resolved or signing fails

        Example:
            >>> admin = HelixAdmin(...)
            >>> token = admin.generate_token(
            ...     sub="user@example.com",
            ...     customer_id="company-1234567890-acme",
            ...     email="user@example.com",
            ...     customer_type="consumer",
            ...     role="user",
            ...     tier="starter",
            ... )
            >>> # Use token for API authentication
            >>> headers = {"Authorization": f"Bearer {token}"}
        """
        # Validate required enum fields
        validated_customer_type = self._validate_enum(
            customer_type, VALID_CUSTOMER_TYPES, "customer_type"
        )
        validated_role = self._validate_enum(role, VALID_ROLES, "role")

        # Validate optional enum fields
        validated_tier = None
        if tier:
            validated_tier = self._validate_enum(tier, VALID_TIERS, "tier")

        validated_login_method = None
        if login_method:
            validated_login_method = self._validate_enum(
                login_method, VALID_LOGIN_METHODS, "login_method"
            )

        # Resolve secret
        jwt_secret = self._get_jwt_secret(secret)

        # Build claims
        now = datetime.now(timezone.utc)
        iat = int(now.timestamp())
        exp = int(now.timestamp() + (expiry_minutes * 60))

        claims = {
            # Required claims
            "sub": sub,
            "customer_id": customer_id,
            "email": email,
            "customer_type": validated_customer_type,
            "role": validated_role,
            "iss": issuer or DEFAULT_ISSUER,
            "iat": iat,
            "exp": exp,
        }

        # Optional claims
        if validated_tier:
            claims["tier"] = validated_tier

        if authenticated_at:
            claims["authenticated_at"] = int(authenticated_at.timestamp())
        else:
            claims["authenticated_at"] = iat  # Default to issued-at time

        if validated_login_method:
            claims["login_method"] = validated_login_method

        if nbf:
            claims["nbf"] = int(nbf.timestamp())

        # Sign and return token
        try:
            token = jwt.encode(claims, jwt_secret, algorithm="HS256")
            return token
        except Exception as e:
            raise TokenGenerationError(f"Failed to sign JWT token: {e}")

    def generate_admin_token(
        self,
        *,
        sub: str,
        customer_id: str,
        email: str,
        customer_type: Literal["producer", "consumer", "both"],
        tier: Optional[
            Literal["free", "basic", "starter", "professional", "enterprise"]
        ] = None,
        authenticated_at: Optional[datetime] = None,
        login_method: Optional[
            Literal["password", "oauth", "sso", "magic_link", "api_key"]
        ] = None,
        nbf: Optional[datetime] = None,
        issuer: Optional[str] = None,
        expiry_minutes: int = DEFAULT_TOKEN_EXPIRY_MINUTES,
        secret: Optional[str] = None,
    ) -> str:
        """
        Generate a JWT token with role="admin".

        Convenience wrapper around generate_token() that sets role="admin".
        See generate_token() for full documentation.

        Args:
            sub: Subject identifier (typically user_id or email)
            customer_id: Customer/company ID
            email: User's email address
            customer_type: Customer type - "producer", "consumer", or "both"
            tier: Subscription tier (optional)
            authenticated_at: When authentication occurred (default: now)
            login_method: How user authenticated (optional)
            nbf: Not-before time (optional)
            issuer: Token issuer (default: "helix-admin-sdk")
            expiry_minutes: Token validity in minutes (default: 60)
            secret: JWT signing secret

        Returns:
            Signed JWT token string with role="admin"

        Example:
            >>> admin = HelixAdmin(...)
            >>> token = admin.generate_admin_token(
            ...     sub="admin@helix.tools",
            ...     customer_id="company-admin",
            ...     email="admin@helix.tools",
            ...     customer_type="both",
            ... )
        """
        return self.generate_token(
            sub=sub,
            customer_id=customer_id,
            email=email,
            customer_type=customer_type,
            role="admin",
            tier=tier,
            authenticated_at=authenticated_at,
            login_method=login_method,
            nbf=nbf,
            issuer=issuer,
            expiry_minutes=expiry_minutes,
            secret=secret,
        )

    @staticmethod
    def forgot_credentials(
        email: str,
        api_endpoint: Optional[str] = None,
    ) -> Dict:
        """
        Request credentials recovery for a customer with forgotten credentials.
        This is a public endpoint that doesn't require authentication.

        SECURITY NOTE: This endpoint always returns success to prevent email enumeration.
        If the email exists in the system, a secure portal link will be sent to that email.

        Args:
            email: Business email address associated with the customer account
            api_endpoint: API endpoint (default: HELIX_API_ENDPOINT or https://api-go.helix.tools)

        Returns:
            Dict with success message (always returns success for security)

        Example:
            >>> # Customer or admin can call this to recover credentials
            >>> HelixAdmin.forgot_credentials("customer@example.com")
            {
                'success': True,
                'message': 'If the email address is associated with an account, '
                           'you will receive instructions to retrieve your credentials.'
            }
        """
        resolved_endpoint = api_endpoint or os.environ.get(
            "HELIX_API_ENDPOINT", "https://api-go.helix.tools"
        )
        url = f"{resolved_endpoint.rstrip('/')}/v1/credentials/forgot"
        response = requests.post(
            url,
            json={"email": email},
            timeout=API_TIMEOUT,
        )
        response.raise_for_status()
        return response.json()
