//go:generate ${GOBIN}/mockgen -destination client_mock.go -package=billingcli ${GO_MODULE}/pkg/infra/clients/billingcli Client
package billingcli

import (
	"context"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/go-resty/resty/v2"
)

type Client interface {
	GetTenantPlanInfo(ctx context.Context, tenantId uint64) (*TenantPlanInfo, error)
	CreateCredits(ctx context.Context, tenantId uint64, req *CreateCreditsPackageReq) error
	ListBillingConfigs(ctx context.Context, tenantId uint64, operatorEmail string) ([]*BillingConfig, error)
	ListStripePaymentMethods(ctx context.Context, tenantId uint64, billingConfigID uint64, operatorEmail string) ([]*StripePaymentMethod, error)
	ListCredits(ctx context.Context, tenantId uint64, billingConfigID uint64, operatorEmail string) ([]*CreditsPackage, error)
	CreateCreditsWithBillingConfig(ctx context.Context, tenantId uint64, billingConfigID uint64, req *CreateCreditsWithBillingConfigReq, operatorEmail string) error
	RecalculateBills(ctx context.Context, tenantId uint64, billingConfigID uint64, req *RecalculateBillsReq, operatorEmail string) error
}

type TenantPlanInfo struct {
	TenantID      uint   `json:"tenant_id"`
	EffectivePlan string `json:"effective_plan"`
}

type CreateCreditsPackageReq struct {
	StartDate       string            `json:"start_date"`
	ExpiredDate     string            `json:"expired_date"`
	TotalCredits    string            `json:"total_credits"`
	Usage           string            `json:"usage"`
	Note            string            `json:"note"`
	UnitFilters     []*UnitFilterSlim `json:"unit_filters"`
	DiscountEnabled bool              `json:"discount_enabled"`
	ForeignID       string            `json:"foreign_id"`
	UserEmail       string            `json:"user_email"`
}

type UnitFilterSlim struct {
	FilterID uint   `json:"filter_id,omitempty"`
	Name     string `json:"name,omitempty"`
	TypeID   uint64 `json:"type_id,omitempty"`
}

type BillingConfig struct {
	BillingConfigID uint64 `json:"billing_config_id"`
	Status          string `json:"status"`
}

type StripePaymentMethod struct {
	ID             string                `json:"id"`
	Card           *StripeCard           `json:"card"`
	BillingDetails *StripeBillingDetails `json:"billing_details"`
}

type StripeCard struct {
	Brand       string `json:"brand"`
	Country     string `json:"country"`
	ExpMonth    string `json:"exp_month"`
	ExpYear     string `json:"exp_year"`
	Fingerprint string `json:"fingerprint"`
	Funding     string `json:"funding"`
	Last4       string `json:"last4"`
}

type StripeBillingDetails struct {
	Address *StripeBillingAddress `json:"address"`
	Email   string                `json:"email"`
	Name    string                `json:"name"`
	Phone   string                `json:"phone"`
}

type StripeBillingAddress struct {
	City       string `json:"city"`
	Country    string `json:"country"`
	Line1      string `json:"line1"`
	Line2      string `json:"line2"`
	PostalCode string `json:"postal_code"`
	State      string `json:"state"`
}

type CreditsPackage struct {
	CreditsPackageID uint64 `json:"credits_package_id"`
	Note             string `json:"note"`
}

type CreateCreditsWithBillingConfigReq struct {
	BindTypes       []*UnitFilterSlim `json:"bind_types,omitempty"`
	DiscountEnabled bool              `json:"discount_enabled"`
	ExpiredDate     string            `json:"expired_date"`
	Note            string            `json:"note"`
	Provider        string            `json:"provider,omitempty"`
	StartDate       string            `json:"start_date"`
	TotalCredits    string            `json:"total_credits"`
	Usage           string            `json:"usage"`
}

type RecalculateBillsReq struct {
	SourceRunID          string `json:"source_run_id,omitempty"`
	OverwriteCurrentCost bool   `json:"overwrite_current_cost"`
}

type client struct {
	httpCli *resty.Client
	svcAddr string
}

func NewClient(addr string) (Client, error) {
	httpCli := resty.New().
		SetTransport(http.DefaultTransport).
		SetTimeout(60 * time.Second).
		SetBaseURL(fmt.Sprintf("http://%s", addr)).
		OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
			if !r.IsSuccess() {
				return fmt.Errorf("call billing service failed, url: %s, status: %s, body: %s", r.Request.URL, r.Status(), string(r.Body()))
			}
			return nil
		})

	return &client{httpCli: httpCli, svcAddr: addr}, nil
}

func (c *client) GetTenantPlanInfo(ctx context.Context, tenantId uint64) (*TenantPlanInfo, error) {
	var tenantPlanInfo TenantPlanInfo
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"tenant_id": strconv.FormatUint(tenantId, 10),
		}).
		SetResult(&tenantPlanInfo).
		Get("/api/v1/billing/tenants/{tenant_id}/plans")
	return &tenantPlanInfo, err
}

func (c *client) CreateCredits(ctx context.Context, tenantId uint64, req *CreateCreditsPackageReq) error {
	_, err := c.httpCli.R().
		SetContext(ctx).
		SetPathParams(map[string]string{
			"tenant_id": strconv.FormatUint(tenantId, 10),
		}).
		SetBody(req).
		Post("/api/v1/billing/tenants/{tenant_id}/credits")
	return err
}

func (c *client) ListBillingConfigs(ctx context.Context, tenantId uint64, operatorEmail string) ([]*BillingConfig, error) {
	var billingConfigs []*BillingConfig
	_, err := c.withOpsHeaders(c.httpCli.R(), operatorEmail).
		SetContext(ctx).
		SetPathParams(map[string]string{
			"tenant_id": strconv.FormatUint(tenantId, 10),
		}).
		SetResult(&billingConfigs).
		Get("/api/v1/billing/tenants/{tenant_id}/billing_configs")
	return billingConfigs, err
}

func (c *client) ListStripePaymentMethods(ctx context.Context, tenantId uint64, billingConfigID uint64, operatorEmail string) ([]*StripePaymentMethod, error) {
	var paymentMethods []*StripePaymentMethod
	_, err := c.withOpsHeaders(c.httpCli.R(), operatorEmail).
		SetContext(ctx).
		SetPathParams(map[string]string{
			"tenant_id":         strconv.FormatUint(tenantId, 10),
			"billing_config_id": strconv.FormatUint(billingConfigID, 10),
		}).
		SetResult(&paymentMethods).
		Get("/api/v1/billing/tenants/{tenant_id}/billing_config_id/{billing_config_id}/payments/stripe/payment_methods")
	return paymentMethods, err
}

func (c *client) ListCredits(ctx context.Context, tenantId uint64, billingConfigID uint64, operatorEmail string) ([]*CreditsPackage, error) {
	var credits []*CreditsPackage
	_, err := c.withOpsHeaders(c.httpCli.R(), operatorEmail).
		SetContext(ctx).
		SetPathParams(map[string]string{
			"tenant_id":         strconv.FormatUint(tenantId, 10),
			"billing_config_id": strconv.FormatUint(billingConfigID, 10),
		}).
		SetResult(&credits).
		Get("/api/v1/billing/tenants/{tenant_id}/billing_config_id/{billing_config_id}/credits")
	return credits, err
}

func (c *client) CreateCreditsWithBillingConfig(ctx context.Context, tenantId uint64, billingConfigID uint64, req *CreateCreditsWithBillingConfigReq, operatorEmail string) error {
	_, err := c.withOpsHeaders(c.httpCli.R(), operatorEmail).
		SetContext(ctx).
		SetPathParams(map[string]string{
			"tenant_id":         strconv.FormatUint(tenantId, 10),
			"billing_config_id": strconv.FormatUint(billingConfigID, 10),
		}).
		SetBody(req).
		Post("/api/v1/billing/tenants/{tenant_id}/billing_config_id/{billing_config_id}/credits")
	return err
}

func (c *client) RecalculateBills(ctx context.Context, tenantId uint64, billingConfigID uint64, req *RecalculateBillsReq, operatorEmail string) error {
	_, err := c.withOpsHeaders(c.httpCli.R(), operatorEmail).
		SetContext(ctx).
		SetPathParams(map[string]string{
			"tenant_id":         strconv.FormatUint(tenantId, 10),
			"billing_config_id": strconv.FormatUint(billingConfigID, 10),
		}).
		SetBody(req).
		Post("/api/v1/billing/tenants/{tenant_id}/billing_config_id/{billing_config_id}/internal/recalculate")
	return err
}

func (c *client) withOpsHeaders(req *resty.Request, operatorEmail string) *resty.Request {
	req.SetHeader("X-BILLING-REQUEST-SOURCE", "ops")
	if operatorEmail != "" {
		req.SetHeader("X-ACCOUNT-USER-EMAIL", operatorEmail)
	}
	return req
}

func IsNonExpiredStripeCard(card *StripeCard, now time.Time) bool {
	if card == nil {
		return false
	}

	expYear, err := strconv.Atoi(strings.TrimSpace(card.ExpYear))
	if err != nil {
		return false
	}
	expMonth, err := strconv.Atoi(strings.TrimSpace(card.ExpMonth))
	if err != nil || expMonth < 1 || expMonth > 12 {
		return false
	}

	now = now.UTC()
	if expYear < now.Year() {
		return false
	}
	if expYear == now.Year() && expMonth < int(now.Month()) {
		return false
	}
	return true
}
