package billing

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"slices"
	"strings"
	"time"

	"bookra/apps/backend/internal/config"
	"bookra/apps/backend/internal/db"
	"bookra/apps/backend/internal/domain"
	"bookra/apps/backend/internal/shared"

	paddle "github.com/PaddleHQ/paddle-go-sdk/v5"
	"github.com/jackc/pgx/v5"
	"github.com/stripe/stripe-go/v81"
	portalsession "github.com/stripe/stripe-go/v81/billingportal/session"
	checkoutsession "github.com/stripe/stripe-go/v81/checkout/session"
	"github.com/stripe/stripe-go/v81/customer"
	"github.com/stripe/stripe-go/v81/subscription"
	"github.com/stripe/stripe-go/v81/webhook"
)

var (
	ErrBillingMembership      = errors.New("billing membership not found")
	ErrBillingPlanUnsupported = errors.New("billing plan is not configured")
	ErrBillingCustomerMissing = errors.New("billing customer is not available")
	ErrPaddleNotConfigured    = errors.New("paddle is not configured")
	ErrPaddleSignatureMissing = errors.New("paddle signature is missing")
	ErrPaddleWebhookMissing   = errors.New("paddle webhook secret is not configured")
	ErrStripeNotConfigured    = errors.New("stripe is not configured")
	ErrStripeSignatureMissing = errors.New("stripe signature is missing")
	ErrStripeWebhookMissing   = errors.New("stripe webhook secret is not configured")
)

var allowedPaddleWebhookEvents = []string{
	"subscription.created",
	"subscription.updated",
	"subscription.activated",
	"subscription.canceled",
	"subscription.paused",
	"subscription.resumed",
	"subscription.trialing",
	"transaction.completed",
	"transaction.updated",
	"transaction.payment_failed",
	"transaction.past_due",
}

var allowedStripeWebhookEvents = []stripe.EventType{
	stripe.EventTypeCheckoutSessionCompleted,
	stripe.EventTypeCustomerSubscriptionCreated,
	stripe.EventTypeCustomerSubscriptionUpdated,
	stripe.EventTypeCustomerSubscriptionDeleted,
	stripe.EventTypeInvoicePaid,
	stripe.EventTypeInvoicePaymentFailed,
	stripe.EventTypePaymentIntentSucceeded,
	stripe.EventTypePaymentIntentPaymentFailed,
}

type Service struct {
	cfg           config.Config
	repo          db.Repository
	client        *paddle.SDK
	verifier      *paddle.WebhookVerifier
	stripeEnabled bool
}

type webhookEnvelope struct {
	EventID   string `json:"event_id"`
	EventType string `json:"event_type"`
	Data      struct {
		ID             string         `json:"id"`
		CustomerID     string         `json:"customer_id"`
		SubscriptionID string         `json:"subscription_id"`
		CustomData     map[string]any `json:"custom_data"`
	} `json:"data"`
}

func NewService(cfg config.Config, repo db.Repository) *Service {
	service := &Service{cfg: cfg, repo: repo}

	// Initialize Paddle client
	if strings.TrimSpace(cfg.PaddleAPIKey) != "" {
		var client *paddle.SDK
		var err error
		if cfg.PaddleEnvironment == "live" {
			client, err = paddle.New(cfg.PaddleAPIKey, paddle.WithBaseURL(paddle.ProductionBaseURL))
		} else {
			client, err = paddle.NewSandbox(cfg.PaddleAPIKey)
		}
		if err == nil {
			service.client = client
		}
	}

	// Initialize Stripe
	if strings.TrimSpace(cfg.StripeAPIKey) != "" {
		stripe.Key = cfg.StripeAPIKey
		service.stripeEnabled = true
	}

	return service
}

// GetEntitlements returns the plan entitlements for a tenant (used by other services for limit enforcement)
func (s *Service) GetEntitlements(ctx context.Context, tenantID string) (domain.PlanEntitlements, error) {
	tenant, err := s.repo.GetTenantByID(ctx, tenantID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			// Default to Pro entitlements for tenants without billing
			return entitlementsForPlan("pro"), nil
		}
		return domain.PlanEntitlements{}, err
	}
	return entitlementsForPlan(tenant.PlanCode), nil
}

func (s *Service) GetSubscription(ctx context.Context, principal domain.Principal) (domain.SubscriptionSnapshot, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.SubscriptionSnapshot{}, ErrBillingMembership
		}
		return domain.SubscriptionSnapshot{}, err
	}

	record, err := s.repo.GetSubscriptionSnapshot(ctx, membership.Tenant.ID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return toSnapshot(membership.Tenant, db.BillingSnapshotRecord{
				TenantID:        membership.Tenant.ID,
				BillingProvider: s.cfg.BillingProvider(),
				Status:          firstNonEmpty(membership.Tenant.SubscriptionStatus, "inactive"),
				PlanCode:        shared.NormalizePlanCode(membership.Tenant.PlanCode),
				Currency:        "czk",
			}, s.cfg), nil
		}
		return domain.SubscriptionSnapshot{}, err
	}

	return toSnapshot(membership.Tenant, record, s.cfg), nil
}

func (s *Service) CreateCheckoutSession(ctx context.Context, principal domain.Principal, planCode string, currency string, billingInterval string) (domain.CheckoutLaunchResponse, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.CheckoutLaunchResponse{}, ErrBillingMembership
		}
		return domain.CheckoutLaunchResponse{}, err
	}

	// Default to monthly if not specified
	if billingInterval == "" {
		billingInterval = "monthly"
	}

	// Prefer Stripe if configured
	if s.cfg.StripeConfigured() {
		return s.createStripeCheckoutSession(ctx, principal, membership, planCode, currency, billingInterval)
	}

	// Fall back to Paddle
	return s.createPaddleCheckoutSession(ctx, principal, membership, planCode, currency)
}

func (s *Service) createStripeCheckoutSession(ctx context.Context, principal domain.Principal, membership db.TenantMembershipRecord, planCode string, currency string, billingInterval string) (domain.CheckoutLaunchResponse, error) {
	priceID, resolvedPlanCode, resolvedCurrency := s.stripePriceForPlan(planCode, currency, billingInterval)
	if priceID == "" {
		return domain.CheckoutLaunchResponse{}, ErrBillingPlanUnsupported
	}

	// Ensure customer exists (KV sync model: always pre-create customers)
	customerID := derefString(membership.Tenant.BillingCustomerID)
	if customerID == "" {
		cust, err := customer.New(&stripe.CustomerParams{
			Email: stripe.String(strings.TrimSpace(principal.Email)),
			Metadata: map[string]string{
				"tenantId":   membership.Tenant.ID,
				"tenantSlug": membership.Tenant.Slug,
				"userId":     principal.Subject,
			},
		})
		if err != nil {
			return domain.CheckoutLaunchResponse{}, fmt.Errorf("failed to create stripe customer: %w", err)
		}
		customerID = cust.ID
		if err := s.repo.UpdateTenantBillingCustomerID(ctx, membership.Tenant.ID, customerID); err != nil {
			return domain.CheckoutLaunchResponse{}, err
		}
	}

	// Create checkout session - 15-day free trial for Starter/Pro only (not Business)
	// Trial requires credit card to be entered
	trialDays := int64(0)
	if resolvedPlanCode == "starter" || resolvedPlanCode == "pro" {
		trialDays = 15
	}

	params := &stripe.CheckoutSessionParams{
		Customer:                stripe.String(customerID),
		SuccessURL:              stripe.String(strings.TrimRight(s.cfg.FrontendURL, "/") + "/dashboard?billing=success&session_id={CHECKOUT_SESSION_ID}"),
		CancelURL:               stripe.String(strings.TrimRight(s.cfg.FrontendURL, "/") + "/dashboard?billing=cancelled"),
		Mode:                    stripe.String(string(stripe.CheckoutSessionModeSubscription)),
		PaymentMethodCollection: stripe.String("always"), // Require credit card even for free trial
		LineItems: []*stripe.CheckoutSessionLineItemParams{
			{
				Price:    stripe.String(priceID),
				Quantity: stripe.Int64(1),
			},
		},
		SubscriptionData: &stripe.CheckoutSessionSubscriptionDataParams{
			TrialPeriodDays: stripe.Int64(trialDays),
		},
		Metadata: map[string]string{
			"tenantId":        membership.Tenant.ID,
			"tenantSlug":      membership.Tenant.Slug,
			"userId":          principal.Subject,
			"userEmail":       strings.TrimSpace(principal.Email),
			"planCode":        resolvedPlanCode,
			"currency":        resolvedCurrency,
			"billingInterval": billingInterval,
			"source":          "bookra-dashboard",
		},
	}
	sess, err := checkoutsession.New(params)
	if err != nil {
		return domain.CheckoutLaunchResponse{}, fmt.Errorf("failed to create stripe checkout session: %w", err)
	}

	return domain.CheckoutLaunchResponse{
		CheckoutURL:        sess.URL,
		SuccessRedirectURL: sess.SuccessURL,
		CancelRedirectURL:  sess.CancelURL,
	}, nil
}

func (s *Service) createPaddleCheckoutSession(ctx context.Context, principal domain.Principal, membership db.TenantMembershipRecord, planCode string, currency string) (domain.CheckoutLaunchResponse, error) {
	priceID, resolvedPlanCode, resolvedCurrency := s.paddlePriceForPlan(planCode, currency)
	if priceID == "" {
		return domain.CheckoutLaunchResponse{}, ErrBillingPlanUnsupported
	}
	if !checkoutAvailable(s.cfg, resolvedPlanCode) {
		return domain.CheckoutLaunchResponse{}, ErrPaddleNotConfigured
	}

	return domain.CheckoutLaunchResponse{
		PriceID:            priceID,
		CustomerID:         derefString(membership.Tenant.BillingCustomerID),
		CustomerEmail:      strings.TrimSpace(principal.Email),
		SuccessRedirectURL: strings.TrimRight(s.cfg.FrontendURL, "/") + "/dashboard?billing=success",
		CancelRedirectURL:  strings.TrimRight(s.cfg.FrontendURL, "/") + "/dashboard?billing=cancelled",
		CustomData: map[string]string{
			"tenantId":   membership.Tenant.ID,
			"tenantSlug": membership.Tenant.Slug,
			"userId":     principal.Subject,
			"userEmail":  strings.TrimSpace(principal.Email),
			"planCode":   resolvedPlanCode,
			"currency":   resolvedCurrency,
			"source":     "bookra-dashboard",
		},
	}, nil
}

func (s *Service) Refresh(ctx context.Context, principal domain.Principal) (domain.SubscriptionSnapshot, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.SubscriptionSnapshot{}, ErrBillingMembership
		}
		return domain.SubscriptionSnapshot{}, err
	}

	customerID := derefString(membership.Tenant.BillingCustomerID)
	if customerID == "" {
		return toSnapshot(membership.Tenant, db.BillingSnapshotRecord{
			TenantID:        membership.Tenant.ID,
			BillingProvider: s.cfg.BillingProvider(),
			Status:          "inactive",
			PlanCode:        shared.NormalizePlanCode(membership.Tenant.PlanCode),
			Currency:        "czk",
		}, s.cfg), nil
	}

	// Prefer Stripe if configured
	if s.cfg.StripeConfigured() {
		record, err := s.syncStripeDataToKV(ctx, membership.Tenant, customerID)
		if err != nil {
			return domain.SubscriptionSnapshot{}, err
		}
		return toSnapshot(membership.Tenant, record, s.cfg), nil
	}

	// Fall back to Paddle
	if s.client == nil {
		return domain.SubscriptionSnapshot{}, ErrPaddleNotConfigured
	}
	record, err := s.syncPaddleData(ctx, membership.Tenant, customerID)
	if err != nil {
		return domain.SubscriptionSnapshot{}, err
	}

	return toSnapshot(membership.Tenant, record, s.cfg), nil
}

func (s *Service) CreatePortalSession(ctx context.Context, principal domain.Principal) (domain.PortalSessionResponse, error) {
	membership, err := s.repo.GetTenantMembershipByUserID(ctx, principal.Subject)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return domain.PortalSessionResponse{}, ErrBillingMembership
		}
		return domain.PortalSessionResponse{}, err
	}

	customerID := derefString(membership.Tenant.BillingCustomerID)
	if customerID == "" {
		return domain.PortalSessionResponse{}, ErrBillingCustomerMissing
	}

	// Prefer Stripe if configured
	if s.cfg.StripeConfigured() {
		return s.createStripePortalSession(customerID)
	}

	// Fall back to Paddle
	return s.createPaddlePortalSession(ctx, membership, customerID)
}

func (s *Service) createStripePortalSession(customerID string) (domain.PortalSessionResponse, error) {
	params := &stripe.BillingPortalSessionParams{
		Customer:  stripe.String(customerID),
		ReturnURL: stripe.String(s.cfg.FrontendURL + "/dashboard?billing=refresh"),
	}
	sess, err := portalsession.New(params)
	if err != nil {
		return domain.PortalSessionResponse{}, fmt.Errorf("failed to create stripe portal session: %w", err)
	}
	return domain.PortalSessionResponse{URL: sess.URL}, nil
}

func (s *Service) createPaddlePortalSession(ctx context.Context, membership db.TenantMembershipRecord, customerID string) (domain.PortalSessionResponse, error) {
	if s.client == nil {
		return domain.PortalSessionResponse{}, ErrPaddleNotConfigured
	}

	request := &paddle.CreateCustomerPortalSessionRequest{CustomerID: customerID}
	if subscriptionID := derefString(membership.Tenant.BillingSubscription); subscriptionID != "" {
		request.SubscriptionIDs = []string{subscriptionID}
	}

	sess, err := s.client.CreateCustomerPortalSession(ctx, request)
	if err != nil {
		return domain.PortalSessionResponse{}, err
	}

	url := strings.TrimSpace(sess.URLs.General.Overview)
	if url == "" && len(sess.URLs.Subscriptions) > 0 {
		url = firstNonEmpty(
			sess.URLs.Subscriptions[0].UpdateSubscriptionPaymentMethod,
			sess.URLs.Subscriptions[0].CancelSubscription,
		)
	}
	if url == "" {
		return domain.PortalSessionResponse{}, ErrBillingCustomerMissing
	}

	return domain.PortalSessionResponse{URL: url}, nil
}

func (s *Service) HandleWebhook(ctx context.Context, req *http.Request) error {
	// Detect provider based on signature header
	stripeSig := req.Header.Get("Stripe-Signature")
	paddleSig := req.Header.Get("Paddle-Signature")

	if stripeSig != "" {
		return s.handleStripeWebhook(ctx, req)
	}

	if paddleSig != "" {
		return s.handlePaddleWebhook(ctx, req)
	}

	return errors.New("missing webhook signature header")
}

func (s *Service) HandleStripeWebhook(ctx context.Context, req *http.Request) error {
	return s.handleStripeWebhook(ctx, req)
}

func (s *Service) handleStripeWebhook(ctx context.Context, req *http.Request) error {
	if s.cfg.StripeWebhookKey == "" {
		return ErrStripeWebhookMissing
	}

	body, err := io.ReadAll(req.Body)
	if err != nil {
		return err
	}

	event, err := webhook.ConstructEvent(body, req.Header.Get("Stripe-Signature"), s.cfg.StripeWebhookKey)
	if err != nil {
		return fmt.Errorf("invalid stripe webhook signature: %w", err)
	}

	if !slices.Contains(allowedStripeWebhookEvents, event.Type) {
		return nil
	}

	// Extract customer ID from event data
	var customerID string
	var eventID = event.ID

	switch event.Type {
	case "checkout.session.completed":
		var sess stripe.CheckoutSession
		if err := json.Unmarshal(event.Data.Raw, &sess); err != nil {
			return err
		}
		customerID = sess.Customer.ID
		if sess.Metadata != nil {
			if tenantID := sess.Metadata["tenantId"]; tenantID != "" {
				tenant, err := s.repo.GetTenantByID(ctx, tenantID)
				if err != nil {
					if errors.Is(err, pgx.ErrNoRows) {
						return nil
					}
					return err
				}
				if customerID != "" && derefString(tenant.BillingCustomerID) == "" {
					if err := s.repo.UpdateTenantBillingCustomerID(ctx, tenant.ID, customerID); err != nil {
						return err
					}
					tenant.BillingCustomerID = &customerID
				}
				_, err = s.syncStripeDataToKV(ctx, tenant, customerID)
				return err
			}
		}

	default:
		var data struct {
			Customer struct {
				ID string `json:"id"`
			} `json:"customer"`
		}
		if err := json.Unmarshal(event.Data.Raw, &data); err != nil {
			return err
		}
		customerID = data.Customer.ID
	}

	if customerID == "" {
		return nil
	}

	tenant, err := s.repo.GetTenantByBillingCustomerID(ctx, customerID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil
		}
		return err
	}

	inserted, err := s.repo.RecordBillingEvent(ctx, tenant.ID, "stripe", eventID, string(event.Type), event.Data.Raw)
	if err != nil || !inserted {
		return err
	}

	_, err = s.syncStripeDataToKV(ctx, tenant, customerID)
	return err
}

func (s *Service) handlePaddleWebhook(ctx context.Context, req *http.Request) error {
	if s.verifier == nil {
		return ErrPaddleWebhookMissing
	}
	if strings.TrimSpace(req.Header.Get("Paddle-Signature")) == "" {
		return ErrPaddleSignatureMissing
	}

	ok, err := s.verifier.Verify(req)
	if err != nil {
		return err
	}
	if !ok {
		return errors.New("invalid paddle webhook signature")
	}

	payload, err := io.ReadAll(req.Body)
	if err != nil {
		return err
	}

	var event webhookEnvelope
	if err := json.Unmarshal(payload, &event); err != nil {
		return err
	}
	if !slices.Contains(allowedPaddleWebhookEvents, event.EventType) {
		return nil
	}

	tenant, customerID, err := s.resolveWebhookTenant(ctx, event)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return nil
		}
		return err
	}
	if tenant.ID == "" {
		return nil
	}

	inserted, err := s.repo.RecordBillingEvent(ctx, tenant.ID, "paddle", event.EventID, event.EventType, payload)
	if err != nil || !inserted {
		return err
	}

	if customerID != "" && derefString(tenant.BillingCustomerID) == "" {
		if err := s.repo.UpdateTenantBillingCustomerID(ctx, tenant.ID, customerID); err != nil {
			return err
		}
		tenant.BillingCustomerID = &customerID
	}

	customerID = firstNonEmpty(customerID, derefString(tenant.BillingCustomerID))
	if customerID == "" || s.client == nil {
		return nil
	}

	_, err = s.syncPaddleData(ctx, tenant, customerID)
	return err
}

func (s *Service) resolveWebhookTenant(ctx context.Context, event webhookEnvelope) (db.TenantRecord, string, error) {
	customerID := strings.TrimSpace(event.Data.CustomerID)
	if tenantID := customDataString(event.Data.CustomData, "tenantId"); tenantID != "" {
		tenant, err := s.repo.GetTenantByID(ctx, tenantID)
		return tenant, customerID, err
	}
	if customerID != "" {
		tenant, err := s.repo.GetTenantByBillingCustomerID(ctx, customerID)
		return tenant, customerID, err
	}
	return db.TenantRecord{}, customerID, pgx.ErrNoRows
}

func (s *Service) syncPaddleData(ctx context.Context, tenant db.TenantRecord, customerID string) (db.BillingSnapshotRecord, error) {
	if s.client == nil {
		return db.BillingSnapshotRecord{}, ErrPaddleNotConfigured
	}

	subscriptions, err := s.client.ListSubscriptions(ctx, &paddle.ListSubscriptionsRequest{
		CustomerID: []string{customerID},
	})
	if err != nil {
		return db.BillingSnapshotRecord{}, err
	}

	var selected *paddle.Subscription
	if err := subscriptions.Iter(ctx, func(subscription *paddle.Subscription) (bool, error) {
		if subscription == nil {
			return true, nil
		}
		if selected == nil || subscriptionRank(subscription) > subscriptionRank(selected) {
			selected = subscription
		}
		return true, nil
	}); err != nil {
		return db.BillingSnapshotRecord{}, err
	}

	now := time.Now().UTC()
	record := db.BillingSnapshotRecord{
		TenantID:              tenant.ID,
		BillingProvider:       "paddle",
		BillingCustomerID:     customerID,
		BillingSubscriptionID: "",
		Status:                "inactive",
		PlanCode:              shared.NormalizePlanCode(tenant.PlanCode),
		Currency:              "czk",
		PriceID:               "",
		LastSyncedAt:          &now,
	}

	if selected != nil {
		record.BillingSubscriptionID = selected.ID
		record.Status = normalizeSubscriptionStatus(string(selected.Status))
		record.Currency = normalizeCurrency(string(selected.CurrencyCode))
		record.CancelAtPeriodEnd = selected.ScheduledChange != nil && string(selected.ScheduledChange.Action) == "cancel"
		record.CurrentPeriodStart = parseRFC3339Ptr(timePeriodStart(selected.CurrentBillingPeriod))
		record.CurrentPeriodEnd = parseRFC3339Ptr(timePeriodEnd(selected.CurrentBillingPeriod))
		if len(selected.Items) > 0 {
			record.PriceID = selected.Items[0].Price.ID
			record.PlanCode = s.paddlePlanCodeForPrice(record.PriceID, tenant.PlanCode)
		}
	}

	if err := s.repo.UpsertSubscriptionSnapshot(ctx, record); err != nil {
		return db.BillingSnapshotRecord{}, err
	}
	if err := s.repo.UpdateTenantBillingState(ctx, tenant.ID, record.PlanCode, record.Status, record.BillingSubscriptionID); err != nil {
		return db.BillingSnapshotRecord{}, err
	}

	return record, nil
}

// syncStripeDataToKV is the core sync function following the KV sync model.
// It fetches full subscription state from Stripe and stores it in the database.
// This function is called after checkout success and on every relevant webhook event.
func (s *Service) syncStripeDataToKV(ctx context.Context, tenant db.TenantRecord, customerID string) (db.BillingSnapshotRecord, error) {
	// Fetch all subscriptions for this customer from Stripe
	iter := subscription.List(&stripe.SubscriptionListParams{
		Customer: stripe.String(customerID),
	})

	var selected *stripe.Subscription
	for iter.Next() {
		sub := iter.Subscription()
		if selected == nil || stripeSubscriptionRank(sub) > stripeSubscriptionRank(selected) {
			selected = sub
		}
	}
	if iter.Err() != nil {
		return db.BillingSnapshotRecord{}, fmt.Errorf("failed to list stripe subscriptions: %w", iter.Err())
	}

	now := time.Now().UTC()
	record := db.BillingSnapshotRecord{
		TenantID:              tenant.ID,
		BillingProvider:       "stripe",
		BillingCustomerID:     customerID,
		BillingSubscriptionID: "",
		Status:                "inactive",
		PlanCode:              shared.NormalizePlanCode(tenant.PlanCode),
		Currency:              "czk",
		PriceID:               "",
		LastSyncedAt:          &now,
	}

	if selected != nil {
		record.BillingSubscriptionID = selected.ID
		record.Status = normalizeStripeSubscriptionStatus(selected.Status)
		record.Currency = strings.ToLower(string(selected.Currency))
		record.CancelAtPeriodEnd = selected.CancelAtPeriodEnd
		record.CurrentPeriodStart = stripeTimeToPtr(selected.CurrentPeriodStart)
		record.CurrentPeriodEnd = stripeTimeToPtr(selected.CurrentPeriodEnd)

		// Extract price ID from subscription items
		if len(selected.Items.Data) > 0 {
			record.PriceID = selected.Items.Data[0].Price.ID
			record.PlanCode = s.stripePlanCodeForPrice(record.PriceID, tenant.PlanCode)
		}

		// Get payment method info if available
		if selected.DefaultPaymentMethod != nil && selected.DefaultPaymentMethod.Card != nil {
			record.PaymentMethodBrand = string(selected.DefaultPaymentMethod.Card.Brand)
			record.PaymentMethodLast4 = selected.DefaultPaymentMethod.Card.Last4
		}
	}

	// Store normalized snapshot in DB (KV cache)
	if err := s.repo.UpsertSubscriptionSnapshot(ctx, record); err != nil {
		return db.BillingSnapshotRecord{}, err
	}
	if err := s.repo.UpdateTenantBillingState(ctx, tenant.ID, record.PlanCode, record.Status, record.BillingSubscriptionID); err != nil {
		return db.BillingSnapshotRecord{}, err
	}

	return record, nil
}

func stripeSubscriptionRank(sub *stripe.Subscription) int {
	switch sub.Status {
	case stripe.SubscriptionStatusActive:
		return 6
	case stripe.SubscriptionStatusTrialing:
		return 5
	case stripe.SubscriptionStatusPastDue:
		return 4
	case stripe.SubscriptionStatusPaused:
		return 3
	case stripe.SubscriptionStatusCanceled:
		return 2
	default:
		return 1
	}
}

func normalizeStripeSubscriptionStatus(status stripe.SubscriptionStatus) string {
	switch status {
	case stripe.SubscriptionStatusActive:
		return "active"
	case stripe.SubscriptionStatusTrialing:
		return "trialing"
	case stripe.SubscriptionStatusPastDue:
		return "past_due"
	case stripe.SubscriptionStatusPaused:
		return "paused"
	case stripe.SubscriptionStatusCanceled:
		return "canceled"
	case stripe.SubscriptionStatusUnpaid:
		return "canceled"
	default:
		return "inactive"
	}
}

func stripeTimeToPtr(t int64) *time.Time {
	if t == 0 {
		return nil
	}
	ts := time.Unix(t, 0).UTC()
	return &ts
}

func toSnapshot(tenant db.TenantRecord, record db.BillingSnapshotRecord, cfg config.Config) domain.SubscriptionSnapshot {
	if record.PlanCode == "" {
		record.PlanCode = shared.NormalizePlanCode(tenant.PlanCode)
	} else {
		record.PlanCode = shared.NormalizePlanCode(record.PlanCode)
	}
	record.Currency = normalizeCurrency(record.Currency)
	if record.Status == "" {
		record.Status = firstNonEmpty(tenant.SubscriptionStatus, "inactive")
	}

	customerID := firstNonEmpty(record.BillingCustomerID, derefString(tenant.BillingCustomerID))
	provider := firstNonEmpty(record.BillingProvider, tenant.BillingProvider, cfg.BillingProvider())

	syncAvailable := cfg.BillingConfigured()
	portalAvailable := cfg.BillingConfigured() && customerID != ""
	checkoutAvailable := billingCheckoutAvailable(cfg, record.PlanCode)

	return domain.SubscriptionSnapshot{
		TenantID:           tenant.ID,
		Provider:           provider,
		CustomerID:         customerID,
		SubscriptionID:     firstNonEmpty(record.BillingSubscriptionID, derefString(tenant.BillingSubscription)),
		Status:             record.Status,
		PlanCode:           record.PlanCode,
		Currency:           record.Currency,
		PriceID:            record.PriceID,
		CancelAtPeriodEnd:  record.CancelAtPeriodEnd,
		CurrentPeriodStart: record.CurrentPeriodStart,
		CurrentPeriodEnd:   record.CurrentPeriodEnd,
		PaymentMethodBrand: record.PaymentMethodBrand,
		PaymentMethodLast4: record.PaymentMethodLast4,
		Entitlements:       entitlementsForPlan(record.PlanCode),
		DisplayPrices:      displayPricesForPlan(record.PlanCode),
		TrialDays: func() int {
			if record.PlanCode == "starter" || record.PlanCode == "pro" {
				return 15
			}
			return 0
		}(),
		LastSyncedAt:         record.LastSyncedAt,
		CheckoutURLAvailable: checkoutAvailable,
		SyncAvailable:        syncAvailable,
		PortalAvailable:      portalAvailable,
	}
}

func entitlementsForPlan(planCode string) domain.PlanEntitlements {
	switch shared.NormalizePlanCode(planCode) {
	case "starter":
		// Starter: 1 location, 1 staff, 50 bookings/month
		return domain.PlanEntitlements{
			MaxLocations:      1,
			MaxStaff:          1,
			MaxBookingsMonth:  50,
			EmailReminders:    false,
			AdvancedReporting: false,
			WidgetEmbedding:   true,
			UmamiTracking:     false,
			APIAccess:         false,
			SMSAvailable:      false,
		}
	case "business":
		// Business: Unlimited everything, API access, dedicated manager
		return domain.PlanEntitlements{
			MaxLocations:      -1, // Unlimited
			MaxStaff:          -1, // Unlimited
			MaxBookingsMonth:  -1, // Unlimited
			EmailReminders:    true,
			AdvancedReporting: true,
			WidgetEmbedding:   true,
			UmamiTracking:     true,
			APIAccess:         true,
			DedicatedManager:  true,
			SMSAvailable:      true,
		}
	default:
		// Pro: 3 locations, 10 staff, unlimited bookings, email reminders, analytics
		return domain.PlanEntitlements{
			MaxLocations:      3,
			MaxStaff:          10,
			MaxBookingsMonth:  -1, // Unlimited
			EmailReminders:    true,
			AdvancedReporting: true,
			WidgetEmbedding:   true,
			UmamiTracking:     true,
			APIAccess:         false,
			SMSAvailable:      true,
		}
	}
}

func (s *Service) paddlePlanCodeForPrice(priceID string, fallback string) string {
	for planCode, currencies := range s.cfg.PaddlePriceMatrix {
		for _, configuredPriceID := range currencies {
			if configuredPriceID != "" && configuredPriceID == priceID {
				return shared.NormalizePlanCode(planCode)
			}
		}
	}
	return shared.NormalizePlanCode(fallback)
}

func (s *Service) stripePlanCodeForPrice(priceID string, fallback string) string {
	for planCode, currencies := range s.cfg.StripePriceMatrix {
		for _, configuredPriceID := range currencies {
			if configuredPriceID != "" && configuredPriceID == priceID {
				return shared.NormalizePlanCode(planCode)
			}
		}
	}
	return shared.NormalizePlanCode(fallback)
}

func (s *Service) paddlePriceForPlan(planCode string, currency string) (string, string, string) {
	resolvedPlan := shared.NormalizePlanCode(strings.TrimSpace(planCode))
	if resolvedPlan == "" {
		resolvedPlan = "pro"
	}
	resolvedCurrency := normalizeCurrency(currency)
	if priceID := s.cfg.PaddlePriceMatrix[resolvedPlan][resolvedCurrency]; priceID != "" {
		return priceID, resolvedPlan, resolvedCurrency
	}
	if resolvedCurrency != "czk" {
		if priceID := s.cfg.PaddlePriceMatrix[resolvedPlan]["czk"]; priceID != "" {
			return priceID, resolvedPlan, "czk"
		}
	}
	return s.cfg.PaddlePriceMatrix[resolvedPlan]["usd"], resolvedPlan, "usd"
}

func (s *Service) stripePriceForPlan(planCode string, currency string, billingInterval string) (string, string, string) {
	resolvedPlan := shared.NormalizePlanCode(strings.TrimSpace(planCode))
	if resolvedPlan == "" {
		resolvedPlan = "pro"
	}
	resolvedCurrency := normalizeCurrency(currency)
	resolvedInterval := billingInterval
	if resolvedInterval == "" {
		resolvedInterval = "monthly"
	}

	// Build the price key: plan:currency:interval (e.g., "pro:usd:monthly", "pro:usd:yearly")
	priceKey := resolvedPlan + ":" + resolvedCurrency + ":" + resolvedInterval
	if priceID := s.cfg.StripePriceMatrix[resolvedPlan][priceKey]; priceID != "" {
		return priceID, resolvedPlan, resolvedCurrency
	}

	// Fall back to plan:currency format (for backwards compatibility)
	priceKey = resolvedPlan + ":" + resolvedCurrency
	if priceID := s.cfg.StripePriceMatrix[resolvedPlan][priceKey]; priceID != "" {
		return priceID, resolvedPlan, resolvedCurrency
	}

	// Try just plan code with interval
	if resolvedInterval != "monthly" {
		priceKey = resolvedPlan + ":" + resolvedInterval
		if priceID := s.cfg.StripePriceMatrix[resolvedPlan][priceKey]; priceID != "" {
			return priceID, resolvedPlan, resolvedCurrency
		}
	}

	// Default currency fallback
	if resolvedCurrency != "usd" {
		priceKey = resolvedPlan + ":usd:" + resolvedInterval
		if priceID := s.cfg.StripePriceMatrix[resolvedPlan][priceKey]; priceID != "" {
			return priceID, resolvedPlan, "usd"
		}
	}

	return s.cfg.StripePriceMatrix[resolvedPlan][resolvedPlan+":czk"], resolvedPlan, "czk"
}

func subscriptionRank(subscription *paddle.Subscription) int {
	switch subscription.Status {
	case paddle.SubscriptionStatusActive:
		return 6
	case paddle.SubscriptionStatusTrialing:
		return 5
	case paddle.SubscriptionStatusPastDue:
		return 4
	case paddle.SubscriptionStatusPaused:
		return 3
	case paddle.SubscriptionStatusCanceled:
		return 2
	default:
		return 1
	}
}

func displayPricesForPlan(planCode string) []domain.PlanDisplayPrice {
	switch shared.NormalizePlanCode(planCode) {
	case "starter":
		// Starter: $5/month, $50/year (save $10 = ~17%)
		return []domain.PlanDisplayPrice{
			{Currency: "czk", AmountCents: 11900, Formatted: "119 Kč/mo", YearlyAmountCents: 119000, YearlyFormatted: "1 190 Kč/yr", YearlySavings: "Save 199 Kč", YearlySavingsPercent: 17},
			{Currency: "usd", AmountCents: 500, Formatted: "$5/mo", YearlyAmountCents: 5000, YearlyFormatted: "$50/yr", YearlySavings: "Save $10", YearlySavingsPercent: 17},
		}
	case "business":
		// Business: $50/month, $500/year (save $100 = ~17%)
		return []domain.PlanDisplayPrice{
			{Currency: "czk", AmountCents: 119900, Formatted: "1 199 Kč/mo", YearlyAmountCents: 1199000, YearlyFormatted: "11 990 Kč/yr", YearlySavings: "Save 1 999 Kč", YearlySavingsPercent: 17},
			{Currency: "usd", AmountCents: 5000, Formatted: "$50/mo", YearlyAmountCents: 50000, YearlyFormatted: "$500/yr", YearlySavings: "Save $100", YearlySavingsPercent: 17},
		}
	default:
		// Pro: $20/month, $200/year (save $40 = ~17%)
		return []domain.PlanDisplayPrice{
			{Currency: "czk", AmountCents: 49900, Formatted: "499 Kč/mo", YearlyAmountCents: 499000, YearlyFormatted: "4 990 Kč/yr", YearlySavings: "Save 999 Kč", YearlySavingsPercent: 17},
			{Currency: "usd", AmountCents: 2000, Formatted: "$20/mo", YearlyAmountCents: 20000, YearlyFormatted: "$200/yr", YearlySavings: "Save $40", YearlySavingsPercent: 17},
		}
	}
}

func normalizeCurrency(currency string) string {
	switch strings.ToLower(strings.TrimSpace(currency)) {
	case "usd":
		return "usd"
	case "eur":
		return "eur"
	default:
		return "czk"
	}
}

func normalizeSubscriptionStatus(status string) string {
	switch strings.TrimSpace(strings.ToLower(status)) {
	case "active", "trialing", "past_due", "paused", "canceled":
		return strings.TrimSpace(strings.ToLower(status))
	default:
		return "inactive"
	}
}

func checkoutAvailable(cfg config.Config, planCode string) bool {
	if !cfg.PaddleConfigured() || !cfg.PaddleWebhookConfigured() {
		return false
	}
	planCode = shared.NormalizePlanCode(planCode)
	for _, priceID := range cfg.PaddlePriceMatrix[planCode] {
		if strings.TrimSpace(priceID) != "" {
			return true
		}
	}
	return false
}

func billingCheckoutAvailable(cfg config.Config, planCode string) bool {
	planCode = shared.NormalizePlanCode(planCode)

	// Prefer Stripe
	if cfg.StripeConfigured() && cfg.StripeWebhookConfigured() {
		for _, priceID := range cfg.StripePriceMatrix[planCode] {
			if strings.TrimSpace(priceID) != "" {
				return true
			}
		}
	}

	// Fall back to Paddle
	if cfg.PaddleConfigured() && cfg.PaddleWebhookConfigured() {
		for _, priceID := range cfg.PaddlePriceMatrix[planCode] {
			if strings.TrimSpace(priceID) != "" {
				return true
			}
		}
	}

	return false
}

func customDataString(data map[string]any, key string) string {
	if data == nil {
		return ""
	}
	value, ok := data[key]
	if !ok {
		return ""
	}
	switch typed := value.(type) {
	case string:
		return strings.TrimSpace(typed)
	default:
		return ""
	}
}

func parseRFC3339Ptr(value string) *time.Time {
	if strings.TrimSpace(value) == "" {
		return nil
	}
	parsed, err := time.Parse(time.RFC3339, value)
	if err != nil {
		return nil
	}
	utc := parsed.UTC()
	return &utc
}

func timePeriodStart(period *paddle.TimePeriod) string {
	if period == nil {
		return ""
	}
	return period.StartsAt
}

func timePeriodEnd(period *paddle.TimePeriod) string {
	if period == nil {
		return ""
	}
	return period.EndsAt
}

func derefString(value *string) string {
	if value == nil {
		return ""
	}
	return *value
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if strings.TrimSpace(value) != "" {
			return strings.TrimSpace(value)
		}
	}
	return ""
}

// CheckAndSendTrialEndingEmails checks all tenants with trials and sends emails for those ending soon
func (s *Service) CheckAndSendTrialEndingEmails(ctx context.Context, notificationService interface {
	SendTrialEndingEmail(ctx context.Context, tenantID string, daysRemaining int) error
}) error {
	// Get all tenants with trial status
	tenants, _, err := s.repo.ListAllTenants(ctx, 1000, 0)
	if err != nil {
		return err
	}

	now := time.Now().UTC()
	for _, tenant := range tenants {
		if tenant.SubscriptionStatus != "trialing" && tenant.SubscriptionStatus != "trial" {
			continue
		}

		// Get subscription to check trial end date
		snapshot, err := s.repo.GetSubscriptionSnapshot(ctx, tenant.ID)
		if err != nil {
			continue
		}

		// Calculate trial end: assume 15-day trial from period start
		var trialEnd time.Time
		if snapshot.CurrentPeriodStart != nil {
			trialEnd = snapshot.CurrentPeriodStart.Add(15 * 24 * time.Hour)
		} else {
			// Default to 15 days from now if no start date
			trialEnd = now.Add(15 * 24 * time.Hour)
		}

		daysRemaining := int(trialEnd.Sub(now).Hours() / 24)

		// Send email if trial ends in 1-3 days
		if daysRemaining >= 1 && daysRemaining <= 3 {
			if err := notificationService.SendTrialEndingEmail(ctx, tenant.ID, daysRemaining); err != nil {
				// Log but don't fail
				fmt.Printf("Failed to send trial ending email for tenant %s: %v\n", tenant.ID, err)
			}
		}
	}

	return nil
}
