package billing

import (
	"context"
	"encoding/json"
	"errors"
	"strconv"
	"time"

	"github.com/stripe/stripe-go/v82"
	"github.com/stripe/stripe-go/v82/client"
	"github.com/stripe/stripe-go/v82/webhook"
)

type StripeProvider struct{ api *client.API }

func NewStripeProvider(secretKey string) *StripeProvider {
	return &StripeProvider{api: client.New(secretKey, nil)}
}

func (p *StripeProvider) CreateCheckout(ctx context.Context, input CheckoutParams) (*ProviderCheckout, error) {
	uidText := stripe.String(formatUID(input.UID))
	params := &stripe.CheckoutSessionParams{
		Params:     stripe.Params{Context: ctx},
		Mode:       stripe.String(string(stripe.CheckoutSessionModeSubscription)),
		SuccessURL: stripe.String(input.SuccessURL), CancelURL: stripe.String(input.CancelURL),
		ClientReferenceID: uidText,
		Metadata:          map[string]string{"uid": formatUID(input.UID), "attempt_id": input.AttemptID, "plan_id": input.PlanID},
		SubscriptionData:  &stripe.CheckoutSessionSubscriptionDataParams{Metadata: map[string]string{"uid": formatUID(input.UID), "attempt_id": input.AttemptID}},
		LineItems:         []*stripe.CheckoutSessionLineItemParams{{Price: stripe.String(input.PriceID), Quantity: stripe.Int64(1)}},
	}
	if input.CustomerID != "" {
		params.Customer = stripe.String(input.CustomerID)
	}
	params.SetIdempotencyKey(input.IdempotencyKey)
	session, err := p.api.CheckoutSessions.New(params)
	if err != nil {
		return nil, err
	}
	return checkoutFromStripe(session), nil
}

func (p *StripeProvider) GetCheckout(ctx context.Context, id string) (*ProviderCheckout, error) {
	params := &stripe.CheckoutSessionParams{Params: stripe.Params{Context: ctx}}
	params.AddExpand("line_items.data.price")
	session, err := p.api.CheckoutSessions.Get(id, params)
	if err != nil {
		return nil, err
	}
	return checkoutFromStripe(session), nil
}

func checkoutFromStripe(s *stripe.CheckoutSession) *ProviderCheckout {
	co := &ProviderCheckout{SessionID: s.ID, URL: s.URL, ExpiresAt: s.ExpiresAt * int64(time.Second), Status: string(s.Status), PaymentStatus: string(s.PaymentStatus)}
	if s.Customer != nil {
		co.CustomerID = s.Customer.ID
	}
	if s.Subscription != nil {
		co.SubscriptionID = s.Subscription.ID
	}
	if s.LineItems != nil {
		for _, item := range s.LineItems.Data {
			if item != nil && item.Price != nil {
				co.PriceID = item.Price.ID
				break
			}
		}
	}
	return co
}

func (p *StripeProvider) GetSubscription(ctx context.Context, id string) (*ProviderSubscription, error) {
	params := &stripe.SubscriptionParams{Params: stripe.Params{Context: ctx}}
	params.AddExpand("items.data.price")
	s, err := p.api.Subscriptions.Get(id, params)
	if err != nil {
		return nil, err
	}
	out := &ProviderSubscription{ID: s.ID, Status: string(s.Status), CancelAtPeriodEnd: s.CancelAtPeriodEnd}
	if s.Customer != nil {
		out.CustomerID = s.Customer.ID
	}
	if s.Items != nil {
		for _, item := range s.Items.Data {
			if item == nil {
				continue
			}
			if item.Price != nil && out.PriceID == "" {
				out.PriceID = item.Price.ID
			}
			if item.CurrentPeriodStart > out.CurrentPeriodStart {
				out.CurrentPeriodStart = item.CurrentPeriodStart
			}
			if item.CurrentPeriodEnd > out.CurrentPeriodEnd {
				out.CurrentPeriodEnd = item.CurrentPeriodEnd
			}
		}
	}
	return out, nil
}

func (p *StripeProvider) CreatePortal(ctx context.Context, customerID, returnURL string) (string, error) {
	s, err := p.api.BillingPortalSessions.New(&stripe.BillingPortalSessionParams{Params: stripe.Params{Context: ctx}, Customer: stripe.String(customerID), ReturnURL: stripe.String(returnURL)})
	if err != nil {
		return "", err
	}
	return s.URL, nil
}

func (p *StripeProvider) VerifyEvent(payload []byte, signature, secret string) (*Event, error) {
	e, err := webhook.ConstructEventWithOptions(payload, signature, secret, webhook.ConstructEventOptions{Tolerance: 5 * time.Minute})
	if err != nil {
		return nil, errors.Join(ErrInvalidSignature, err)
	}
	out := &Event{ID: e.ID, Type: string(e.Type), Created: e.Created}
	var obj stripeObject
	if err := json.Unmarshal(e.Data.Raw, &obj); err != nil {
		return nil, err
	}
	out.ObjectID = obj.ID
	out.CustomerID = objectID(obj.Customer)
	out.Status = obj.Status
	out.PeriodStart, out.PeriodEnd = obj.PeriodStart, obj.PeriodEnd
	out.CancelAtPeriodEnd = obj.CancelAtPeriodEnd
	out.SubscriptionID = objectID(obj.Subscription)
	if out.SubscriptionID == "" {
		out.SubscriptionID = objectID(obj.Parent.SubscriptionDetails.Subscription)
	}
	if len(obj.Items.Data) > 0 {
		out.PriceID = objectID(obj.Items.Data[0].Price)
		out.PeriodStart, out.PeriodEnd = obj.Items.Data[0].CurrentPeriodStart, obj.Items.Data[0].CurrentPeriodEnd
	}
	if len(obj.Lines.Data) > 0 {
		line := obj.Lines.Data[0]
		out.PriceID = objectID(line.Price)
		if out.PriceID == "" {
			out.PriceID = objectID(line.Pricing.PriceDetails.Price)
		}
		if line.Period.Start != 0 {
			out.PeriodStart, out.PeriodEnd = line.Period.Start, line.Period.End
		}
	}
	return out, nil
}

type stripeObject struct {
	ID                string          `json:"id"`
	Customer          json.RawMessage `json:"customer"`
	Subscription      json.RawMessage `json:"subscription"`
	Status            string          `json:"status"`
	PeriodStart       int64           `json:"period_start"`
	PeriodEnd         int64           `json:"period_end"`
	CancelAtPeriodEnd bool            `json:"cancel_at_period_end"`
	Parent            struct {
		SubscriptionDetails struct {
			Subscription json.RawMessage `json:"subscription"`
		} `json:"subscription_details"`
	} `json:"parent"`
	Items struct {
		Data []struct {
			Price              json.RawMessage `json:"price"`
			CurrentPeriodStart int64           `json:"current_period_start"`
			CurrentPeriodEnd   int64           `json:"current_period_end"`
		} `json:"data"`
	} `json:"items"`
	Lines struct {
		Data []struct {
			Price  json.RawMessage `json:"price"`
			Period struct {
				Start int64 `json:"start"`
				End   int64 `json:"end"`
			} `json:"period"`
			Pricing struct {
				PriceDetails struct {
					Price json.RawMessage `json:"price"`
				} `json:"price_details"`
			} `json:"pricing"`
		} `json:"data"`
	} `json:"lines"`
}

func objectID(raw json.RawMessage) string {
	if len(raw) == 0 || string(raw) == "null" {
		return ""
	}
	var id string
	if json.Unmarshal(raw, &id) == nil {
		return id
	}
	var obj struct {
		ID string `json:"id"`
	}
	_ = json.Unmarshal(raw, &obj)
	return obj.ID
}

func formatUID(uid int64) string { return strconv.FormatInt(uid, 10) }

var _ Provider = (*StripeProvider)(nil)
