# app/modules/billing/services/signup_service.py
"""
Core platform signup service.

Handles all database operations for the platform signup flow:
- Session management
- Account creation (User + Merchant)
- Store creation (separate step)
- Stripe customer & subscription setup
- Payment method collection

Platform-specific signup extensions (e.g., OMS Letzshop claiming)
live in their respective modules and call into this core service.
"""

from __future__ import annotations

import logging
import secrets
from dataclasses import dataclass, field
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING

from sqlalchemy.orm import Session

from app.core.config import settings
from app.exceptions import (
    ConflictException,
    ResourceNotFoundException,
    ValidationException,
)
from app.modules.billing.services.stripe_service import stripe_service
from app.modules.billing.services.subscription_service import (
    subscription_service as sub_service,
)
from app.modules.messaging.services.email_service import EmailService
from middleware.auth import AuthManager

if TYPE_CHECKING:
    from app.modules.tenancy.models import Store, User

logger = logging.getLogger(__name__)


# =============================================================================
# In-memory signup session storage
# In production, use Redis or database table
# =============================================================================

_signup_sessions: dict[str, dict] = {}


def _create_session_id() -> str:
    """Generate a secure session ID."""
    return secrets.token_urlsafe(32)


# =============================================================================
# Data Classes
# =============================================================================


@dataclass
class SignupSessionData:
    """Data stored in a signup session."""

    session_id: str
    step: str
    tier_code: str
    is_annual: bool
    platform_code: str = ""
    created_at: str = ""
    updated_at: str | None = None
    store_name: str | None = None
    user_id: int | None = None
    merchant_id: int | None = None
    store_id: int | None = None
    store_code: str | None = None
    platform_id: int | None = None
    stripe_customer_id: str | None = None
    setup_intent_id: str | None = None
    extra: dict = field(default_factory=dict)


@dataclass
class AccountCreationResult:
    """Result of account creation (includes auto-created store)."""

    user_id: int
    merchant_id: int
    stripe_customer_id: str
    store_id: int
    store_code: str


@dataclass
class StoreCreationResult:
    """Result of store creation."""

    store_id: int
    store_code: str


@dataclass
class SignupCompletionResult:
    """Result of signup completion."""

    success: bool
    store_code: str
    store_id: int
    redirect_url: str
    trial_ends_at: str
    access_token: str | None = None  # JWT token for automatic login


# =============================================================================
# Platform Signup Service
# =============================================================================


class SignupService:
    """Core service for handling platform signup operations."""

    def __init__(self):
        self.auth_manager = AuthManager()

    # =========================================================================
    # Session Management
    # =========================================================================

    def create_session(
        self,
        tier_code: str,
        is_annual: bool,
        platform_code: str,
        language: str = "fr",
    ) -> str:
        """
        Create a new signup session.

        Args:
            tier_code: The subscription tier code
            is_annual: Whether annual billing is selected
            platform_code: Platform code (e.g., 'loyalty', 'oms')
            language: User's browsing language (from lang cookie)

        Returns:
            The session ID

        Raises:
            ValidationException: If tier code or platform code is invalid
        """
        if not platform_code:
            raise ValidationException(
                message="Platform code is required for signup.",
                field="platform_code",
            )

        # Validate tier code
        from app.modules.billing.models import TierCode

        try:
            tier = TierCode(tier_code)
        except ValueError:
            raise ValidationException(
                message=f"Invalid tier code: {tier_code}",
                field="tier_code",
            )

        session_id = _create_session_id()
        now = datetime.now(UTC).isoformat()

        _signup_sessions[session_id] = {
            "step": "tier_selected",
            "tier_code": tier.value,
            "is_annual": is_annual,
            "platform_code": platform_code,
            "language": language,
            "created_at": now,
            "updated_at": now,
        }

        logger.info(
            f"Created signup session {session_id} for tier {tier.value}"
            f" on platform {platform_code}"
        )
        return session_id

    def get_session(self, session_id: str) -> dict | None:
        """Get a signup session by ID."""
        return _signup_sessions.get(session_id)

    def get_session_or_raise(self, session_id: str) -> dict:
        """
        Get a signup session or raise an exception.

        Raises:
            ResourceNotFoundException: If session not found
        """
        session = self.get_session(session_id)
        if not session:
            raise ResourceNotFoundException(
                resource_type="SignupSession",
                identifier=session_id,
            )
        return session

    def update_session(self, session_id: str, data: dict) -> None:
        """Update signup session data."""
        session = self.get_session_or_raise(session_id)
        session.update(data)
        session["updated_at"] = datetime.now(UTC).isoformat()
        _signup_sessions[session_id] = session

    def delete_session(self, session_id: str) -> None:
        """Delete a signup session."""
        _signup_sessions.pop(session_id, None)

    # =========================================================================
    # Platform Resolution
    # =========================================================================

    def _resolve_platform_id(self, db: Session, session: dict) -> int:
        """
        Resolve platform_id from session data.

        The platform_code is always required in the session (set during
        create_session). Raises if the platform cannot be resolved.

        Raises:
            ValidationException: If platform_code is missing or unknown
        """
        from app.modules.tenancy.services.platform_service import platform_service

        platform_code = session.get("platform_code")
        if not platform_code:
            raise ValidationException(
                message="Platform code is missing from signup session.",
                field="platform_code",
            )

        platform = platform_service.get_platform_by_code_optional(
            db, platform_code
        )
        if not platform:
            raise ValidationException(
                message=f"Unknown platform: {platform_code}",
                field="platform_code",
            )

        return platform.id

    # =========================================================================
    # Account Creation (User + Merchant only)
    # =========================================================================

    def check_email_exists(self, db: Session, email: str) -> bool:
        """Check if an email already exists."""
        from app.modules.tenancy.services.admin_service import admin_service

        return admin_service.get_user_by_email(db, email) is not None

    def generate_unique_username(self, db: Session, email: str) -> str:
        """Generate a unique username from email."""
        from app.modules.tenancy.services.admin_service import admin_service

        username = email.split("@")[0]
        base_username = username
        counter = 1
        while admin_service.get_user_by_username(db, username):
            username = f"{base_username}_{counter}"
            counter += 1
        return username

    def generate_unique_store_code(self, db: Session, name: str) -> str:
        """Generate a unique store code from a name."""
        from app.modules.tenancy.services.store_service import store_service

        store_code = name.upper().replace(" ", "_")[:20]
        base_code = store_code
        counter = 1
        while store_service.is_store_code_taken(db, store_code):
            store_code = f"{base_code}_{counter}"
            counter += 1
        return store_code

    def generate_unique_subdomain(self, db: Session, name: str) -> str:
        """Generate a unique subdomain from a name."""
        from app.modules.tenancy.services.store_service import store_service

        subdomain = name.lower().replace(" ", "-")
        subdomain = "".join(c for c in subdomain if c.isalnum() or c == "-")[:50]
        base_subdomain = subdomain
        counter = 1
        while store_service.is_subdomain_taken(db, subdomain):
            subdomain = f"{base_subdomain}-{counter}"
            counter += 1
        return subdomain

    def create_account(
        self,
        db: Session,
        session_id: str,
        email: str,
        password: str,
        first_name: str,
        last_name: str,
        merchant_name: str,
        phone: str | None = None,
    ) -> AccountCreationResult:
        """
        Create user, merchant, store, and subscription in a single atomic step.

        Creates User + Merchant + Store + Stripe Customer + MerchantSubscription.
        Store name defaults to merchant_name, language from signup session.

        Args:
            db: Database session
            session_id: Signup session ID
            email: User email
            password: User password
            first_name: User first name
            last_name: User last name
            merchant_name: Merchant/business name
            phone: Optional phone number

        Returns:
            AccountCreationResult with user, merchant, and store IDs

        Raises:
            ResourceNotFoundException: If session not found
            ConflictException: If email already exists
        """
        session = self.get_session_or_raise(session_id)

        # Check if email already exists
        if self.check_email_exists(db, email):
            raise ConflictException(
                message="An account with this email already exists",
            )

        # Generate unique username
        username = self.generate_unique_username(db, email)

        # Create User
        from app.modules.tenancy.models import Merchant, Store, User

        user = User(
            email=email,
            username=username,
            hashed_password=self.auth_manager.hash_password(password),
            first_name=first_name,
            last_name=last_name,
            role="merchant_owner",
            is_active=True,
        )
        db.add(user)
        db.flush()

        # Create Merchant
        merchant = Merchant(
            name=merchant_name,
            owner_user_id=user.id,
            contact_email=email,
            contact_phone=phone,
        )
        db.add(merchant)
        db.flush()

        # Create Stripe Customer
        stripe_customer_id = stripe_service.create_customer_for_merchant(
            merchant=merchant,
            email=email,
            name=f"{first_name} {last_name}",
            metadata={
                "merchant_name": merchant_name,
                "tier": session.get("tier_code"),
                "platform": session.get("platform_code", ""),
            },
        )

        # Create Store (name = merchant_name, language from browsing session)
        store_code = self.generate_unique_store_code(db, merchant_name)
        subdomain = self.generate_unique_subdomain(db, merchant_name)
        language = session.get("language", "fr")

        store = Store(
            merchant_id=merchant.id,
            store_code=store_code,
            subdomain=subdomain,
            name=merchant_name,
            contact_email=email,
            is_active=True,
        )
        if language:
            store.default_language = language
        db.add(store)
        db.flush()

        # Resolve platform and create subscription
        platform_id = self._resolve_platform_id(db, session)

        subscription = sub_service.create_merchant_subscription(
            db=db,
            merchant_id=merchant.id,
            platform_id=platform_id,
            tier_code=session.get("tier_code", "essential"),
            trial_days=settings.stripe_trial_days,
            is_annual=session.get("is_annual", False),
        )
        subscription.stripe_customer_id = stripe_customer_id

        db.commit()  # SVC-006 - Atomic account + store creation

        # Update session
        self.update_session(session_id, {
            "user_id": user.id,
            "merchant_id": merchant.id,
            "merchant_name": merchant_name,
            "email": email,
            "stripe_customer_id": stripe_customer_id,
            "store_id": store.id,
            "store_code": store_code,
            "platform_id": platform_id,
            "step": "account_created",
        })

        logger.info(
            f"Created account + store for {email}: "
            f"user_id={user.id}, merchant_id={merchant.id}, "
            f"store_code={store_code}"
        )

        return AccountCreationResult(
            user_id=user.id,
            merchant_id=merchant.id,
            stripe_customer_id=stripe_customer_id,
            store_id=store.id,
            store_code=store_code,
        )

    # =========================================================================
    # Store Creation (separate step)
    # =========================================================================

    def create_store(
        self,
        db: Session,
        session_id: str,
        store_name: str | None = None,
        language: str | None = None,
    ) -> StoreCreationResult:
        """
        Create the first store for the merchant.

        Store name defaults to the merchant name if not provided.
        The merchant can modify store details later in the merchant panel.

        Args:
            db: Database session
            session_id: Signup session ID
            store_name: Store name (defaults to merchant name)
            language: Store language code (e.g., 'fr', 'en', 'de')

        Returns:
            StoreCreationResult with store ID and code

        Raises:
            ResourceNotFoundException: If session not found
            ValidationException: If account not created yet
        """
        session = self.get_session_or_raise(session_id)

        merchant_id = session.get("merchant_id")
        if not merchant_id:
            raise ValidationException(
                message="Account not created. Please complete the account step first.",
                field="session_id",
            )

        from app.modules.tenancy.models import Store

        # Use merchant name as default store name
        effective_name = store_name or session.get("merchant_name", "My Store")
        email = session.get("email")

        # Generate unique store code and subdomain
        store_code = self.generate_unique_store_code(db, effective_name)
        subdomain = self.generate_unique_subdomain(db, effective_name)

        # Create Store
        store = Store(
            merchant_id=merchant_id,
            store_code=store_code,
            subdomain=subdomain,
            name=effective_name,
            contact_email=email,
            is_active=True,
        )
        if language:
            store.default_language = language
        db.add(store)
        db.flush()

        # Resolve platform and create subscription
        platform_id = self._resolve_platform_id(db, session)

        # Create MerchantSubscription (trial status)
        stripe_customer_id = session.get("stripe_customer_id")
        subscription = sub_service.create_merchant_subscription(
            db=db,
            merchant_id=merchant_id,
            platform_id=platform_id,
            tier_code=session.get("tier_code", "essential"),
            trial_days=settings.stripe_trial_days,
            is_annual=session.get("is_annual", False),
        )
        subscription.stripe_customer_id = stripe_customer_id

        db.commit()  # SVC-006 - Atomic store creation needs commit

        # Update session
        self.update_session(session_id, {
            "store_id": store.id,
            "store_code": store_code,
            "platform_id": platform_id,
            "step": "store_created",
        })

        logger.info(
            f"Created store {store_code} for merchant {merchant_id} "
            f"on platform {session.get('platform_code')}"
        )

        return StoreCreationResult(
            store_id=store.id,
            store_code=store_code,
        )

    # =========================================================================
    # Payment Setup
    # =========================================================================

    def setup_payment(self, session_id: str) -> tuple[str, str]:
        """
        Create Stripe SetupIntent for card collection.

        Args:
            session_id: Signup session ID

        Returns:
            Tuple of (client_secret, stripe_customer_id)

        Raises:
            ResourceNotFoundException: If session not found
            ValidationException: If account not created yet
        """
        session = self.get_session_or_raise(session_id)

        stripe_customer_id = session.get("stripe_customer_id")
        if not stripe_customer_id:
            raise ValidationException(
                message="Account not created. Please complete earlier steps first.",
                field="session_id",
            )

        # Create SetupIntent
        setup_intent = stripe_service.create_setup_intent(
            customer_id=stripe_customer_id,
            metadata={
                "session_id": session_id,
                "store_id": str(session.get("store_id")),
                "tier": session.get("tier_code"),
            },
        )

        # Update session
        self.update_session(session_id, {
            "setup_intent_id": setup_intent.id,
            "step": "payment_pending",
        })

        logger.info(
            f"Created SetupIntent {setup_intent.id} for session {session_id}"
        )

        return setup_intent.client_secret, stripe_customer_id

    # =========================================================================
    # Welcome Email
    # =========================================================================

    def send_welcome_email(
        self,
        db: Session,
        user: User,
        store: Store,
        tier_code: str,
        language: str = "fr",
    ) -> None:
        """
        Send welcome email to new store.

        Args:
            db: Database session
            user: User who signed up
            store: Store that was created
            tier_code: Selected tier code
            language: Language for email (default: French)
        """
        try:
            # Get tier name
            from app.modules.billing.services.billing_service import billing_service

            tier = billing_service.get_tier_by_code(db, tier_code)
            tier_name = tier.name if tier else tier_code.title()

            # Build login URL
            login_url = (
                f"{settings.app_base_url.rstrip('/')}"
                f"/store/{store.store_code}/dashboard"
            )

            email_service = EmailService(db)
            email_service.send_template(
                template_code="signup_welcome",
                language=language,
                to_email=user.email,
                to_name=f"{user.first_name} {user.last_name}",
                variables={
                    "first_name": user.first_name,
                    "merchant_name": store.name,
                    "email": user.email,
                    "store_code": store.store_code,
                    "login_url": login_url,
                    "trial_days": settings.stripe_trial_days,
                    "tier_name": tier_name,
                },
                store_id=store.id,
                user_id=user.id,
                related_type="signup",
            )

            logger.info(f"Welcome email sent to {user.email}")

        except Exception as e:  # noqa: EXC003
            # Log error but don't fail signup
            logger.error(f"Failed to send welcome email to {user.email}: {e}")

    # =========================================================================
    # Signup Completion
    # =========================================================================

    def complete_signup(
        self,
        db: Session,
        session_id: str,
        setup_intent_id: str,
    ) -> SignupCompletionResult:
        """
        Complete signup after card collection.

        Verifies the SetupIntent, attaches the payment method to the Stripe
        customer, creates the Stripe Subscription with trial, and generates
        a JWT token for automatic login.

        Args:
            db: Database session
            session_id: Signup session ID
            setup_intent_id: Stripe SetupIntent ID

        Returns:
            SignupCompletionResult

        Raises:
            ResourceNotFoundException: If session not found
            ValidationException: If signup incomplete or payment failed
        """
        from app.modules.billing.models import SubscriptionTier, TierCode

        session = self.get_session_or_raise(session_id)

        # Guard against completing signup more than once
        if session.get("step") == "completed":
            raise ValidationException(
                message="Signup already completed.",
                field="session_id",
            )

        store_id = session.get("store_id")
        stripe_customer_id = session.get("stripe_customer_id")

        if not store_id or not stripe_customer_id:
            raise ValidationException(
                message="Incomplete signup. Please start again.",
                field="session_id",
            )

        # Retrieve SetupIntent to get payment method
        setup_intent = stripe_service.get_setup_intent(setup_intent_id)

        if setup_intent.status != "succeeded":
            raise ValidationException(
                message="Card setup not completed. Please try again.",
                field="setup_intent_id",
            )

        payment_method_id = setup_intent.payment_method

        # Attach payment method to customer
        stripe_service.attach_payment_method_to_customer(
            customer_id=stripe_customer_id,
            payment_method_id=payment_method_id,
            set_as_default=True,
        )

        # Update subscription record
        subscription = sub_service.get_subscription_for_store(db, store_id)

        if subscription:
            subscription.stripe_payment_method_id = payment_method_id

            # Create the actual Stripe Subscription with trial period
            # This is what enables automatic charging after trial ends
            if subscription.tier_id:
                tier = (
                    db.query(SubscriptionTier)
                    .filter(SubscriptionTier.id == subscription.tier_id)
                    .first()
                )
                if tier:
                    price_id = (
                        tier.stripe_price_annual_id
                        if subscription.is_annual and tier.stripe_price_annual_id
                        else tier.stripe_price_monthly_id
                    )
                    if price_id:
                        stripe_sub = stripe_service.create_subscription_with_trial(
                            customer_id=stripe_customer_id,
                            price_id=price_id,
                            trial_days=settings.stripe_trial_days,
                            metadata={
                                "merchant_id": str(subscription.merchant_id),
                                "platform_id": str(subscription.platform_id),
                                "tier_code": tier.code,
                            },
                        )
                        subscription.stripe_subscription_id = stripe_sub.id
                        logger.info(
                            f"Created Stripe subscription {stripe_sub.id} "
                            f"for merchant {subscription.merchant_id}"
                        )

            db.commit()  # SVC-006 - Finalize signup needs commit

        # Get store info
        from app.modules.tenancy.models import Store, User

        store = db.query(Store).filter(Store.id == store_id).first()
        store_code = store.store_code if store else session.get("store_code")

        trial_ends_at = (
            subscription.trial_ends_at
            if subscription
            else datetime.now(UTC) + timedelta(days=30)
        )

        # Get user for welcome email and token generation
        user_id = session.get("user_id")
        user = (
            db.query(User).filter(User.id == user_id).first() if user_id else None
        )

        # Generate access token for automatic login after signup
        access_token = None
        if user and store:
            # Create store-scoped JWT token (user is owner since they just signed up)
            token_data = self.auth_manager.create_access_token(
                user=user,
                store_id=store.id,
                store_code=store.store_code,
                store_role="Owner",  # New signup is always the owner
            )
            access_token = token_data["access_token"]
            logger.info(f"Generated access token for new store user {user.email}")

        # Send welcome email
        if user and store:
            tier_code = session.get("tier_code", TierCode.ESSENTIAL.value)
            self.send_welcome_email(db, user, store, tier_code)

        # Determine redirect based on platform
        redirect_url = self._get_post_signup_redirect(db, session, store_code)

        # Clean up session
        self.delete_session(session_id)

        logger.info(f"Completed signup for store {store_id}")

        return SignupCompletionResult(
            success=True,
            store_code=store_code,
            store_id=store_id,
            redirect_url=redirect_url,
            trial_ends_at=trial_ends_at.isoformat(),
            access_token=access_token,
        )

    def _get_post_signup_redirect(
        self, db: Session, session: dict, store_code: str
    ) -> str:
        """
        Determine redirect URL after signup.

        Always redirects to the store dashboard. Platform-specific onboarding
        is handled by the dashboard's onboarding banner (module-driven).
        """
        return f"/store/{store_code}/dashboard"


# Singleton instance
signup_service = SignupService()
