package stripe

import (
	"context"
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"

	"github.com/stripe/stripe-go/v83"
)

// Option configures a Client instance.
type Option func(*Client)

// Client wraps Stripe's SDK to expose the subset of functionality needed by the
// application while keeping the rest of the codebase decoupled from the SDK.
type Client struct {
	api *stripe.Client
}

// New constructs a Client using the provided secret key. The key must not be empty.
func New(secretKey string, opts ...Option) (*Client, error) {
	trimmed := strings.TrimSpace(secretKey)
	if trimmed == "" {
		return nil, errors.New("stripe secret key is required")
	}

	api := stripe.NewClient(trimmed)
	client := &Client{api: api}
	for _, opt := range opts {
		opt(client)
	}
	return client, nil
}

// Payment represents the subset of payment intent data consumed by the booking service.
type Payment struct {
	ID            string
	Amount        float64
	Currency      string
	Status        string
	PaymentMethod string
	BookingID     *uint
	Created       time.Time
}

// NormalizePaymentIntent converts a Stripe payment intent into the simplified Payment structure used
// by the application. Fields that are absent default to their zero values.
func NormalizePaymentIntent(pi *stripe.PaymentIntent) Payment {
	if pi == nil {
		return Payment{}
	}

	amount := float64(pi.AmountReceived) / 100.0
	if amount == 0 {
		amount = float64(pi.Amount) / 100.0
	}

	return Payment{
		ID:            pi.ID,
		Amount:        amount,
		Currency:      strings.ToUpper(string(pi.Currency)),
		Status:        string(pi.Status),
		PaymentMethod: deriveMethod(pi),
		BookingID:     extractBookingID(pi.Metadata),
		Created:       time.Unix(int64(pi.Created), 0),
	}
}

// ListPaymentsParams defines the time boundaries used when fetching Stripe payments.
type ListPaymentsParams struct {
	From time.Time
	To   time.Time
}

// ListPayments fetches payment intents created within the provided time range. The
// results are normalised into Payment structs suitable for downstream processing.
func (c *Client) ListPayments(ctx context.Context, params ListPaymentsParams) ([]Payment, error) {
	listParams := &stripe.PaymentIntentListParams{}
	listParams.Context = ctx
	listParams.AddExpand("data.latest_charge")
	listParams.AddExpand("data.payment_method")

	if !params.From.IsZero() {
		listParams.Filters.AddFilter("created", "gte", strconv.FormatInt(params.From.Unix(), 10))
	}
	if !params.To.IsZero() {
		listParams.Filters.AddFilter("created", "lte", strconv.FormatInt(params.To.Unix(), 10))
	}

	seq := c.api.V1PaymentIntents.List(ctx, listParams)
	payments := make([]Payment, 0)
	var listErr error

	seq(func(pi *stripe.PaymentIntent, err error) bool {
		if err != nil {
			listErr = err
			return false
		}
		if pi == nil {
			return true
		}
		payments = append(payments, NormalizePaymentIntent(pi))
		return true
	})

	if listErr != nil {
		return nil, fmt.Errorf("stripe payment intents iteration failed: %w", listErr)
	}

	return payments, nil
}

// CreatePaymentLinkParams encapsulates the data required to generate a Stripe payment link.
type CreatePaymentLinkParams struct {
	Amount             float64
	Currency           string
	BookingID          uint
	Description        string
	PaymentMethodTypes []string
}

// CreatePaymentLink creates a payment link for the provided booking metadata and amount.
func (c *Client) CreatePaymentLink(ctx context.Context, params CreatePaymentLinkParams) (string, error) {
	if params.Amount <= 0 {
		return "", errors.New("amount must be greater than zero")
	}
	currency := strings.ToLower(strings.TrimSpace(params.Currency))
	if currency == "" {
		return "", errors.New("currency is required")
	}
	if params.BookingID == 0 {
		return "", errors.New("booking id is required")
	}

	amountCents := int64(math.Round(params.Amount * 100))

	linkParams := &stripe.PaymentLinkCreateParams{
		LineItems: []*stripe.PaymentLinkCreateLineItemParams{
			{
				PriceData: &stripe.PaymentLinkCreateLineItemPriceDataParams{
					Currency: stripe.String(currency),
					ProductData: &stripe.PaymentLinkCreateLineItemPriceDataProductDataParams{
						Name: stripe.String(strings.TrimSpace(params.Description)),
					},
					UnitAmount: stripe.Int64(amountCents),
				},
				Quantity: stripe.Int64(1),
			},
		},
		Metadata: map[string]string{
			"booking_id": strconv.FormatUint(uint64(params.BookingID), 10),
		},
	}

	if linkParams.LineItems[0].PriceData.ProductData.Name == nil || *linkParams.LineItems[0].PriceData.ProductData.Name == "" {
		linkParams.LineItems[0].PriceData.ProductData.Name = stripe.String(fmt.Sprintf("Booking %d", params.BookingID))
	}

	if len(params.PaymentMethodTypes) > 0 {
		linkParams.PaymentMethodTypes = stripe.StringSlice(params.PaymentMethodTypes)
	}

	linkParams.Context = ctx

	pl, err := c.api.V1PaymentLinks.Create(ctx, linkParams)
	if err != nil {
		return "", fmt.Errorf("failed to create payment link: %w", err)
	}

	return pl.URL, nil
}

func deriveMethod(pi *stripe.PaymentIntent) string {
	if pi == nil {
		return ""
	}

	if pi.LatestCharge != nil && pi.LatestCharge.PaymentMethodDetails != nil {
		typ := pi.LatestCharge.PaymentMethodDetails.Type
		if typ != "" {
			return string(typ)
		}
	}

	if pi.PaymentMethod != nil && pi.PaymentMethod.Type != "" {
		return string(pi.PaymentMethod.Type)
	}

	if len(pi.PaymentMethodTypes) > 0 {
		return pi.PaymentMethodTypes[0]
	}

	return ""
}

func extractBookingID(metadata map[string]string) *uint {
	if len(metadata) == 0 {
		return nil
	}

	keys := []string{"booking_id", "bookingId", "bookingID"}
	for _, key := range keys {
		if raw, ok := metadata[key]; ok {
			if raw == "" {
				continue
			}
			if id, err := strconv.ParseUint(raw, 10, 32); err == nil {
				value := uint(id)
				return &value
			}
		}
	}
	return nil
}
