package billing

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"
)

const maxBillingResponseBodyBytes = 1 << 20

type MaaSClient struct {
	baseURL    string
	token      string
	httpClient *http.Client
}

type MaaSClientError struct {
	StatusCode int
	Body       string
}

func (err MaaSClientError) Error() string {
	return fmt.Sprintf("agentteam-maas billing API returned HTTP %d: %s", err.StatusCode, strings.TrimSpace(err.Body))
}

func NewMaaSClient(baseURL string, token string, httpClient *http.Client) *MaaSClient {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	return &MaaSClient{
		baseURL:    strings.TrimRight(strings.TrimSpace(baseURL), "/"),
		token:      strings.TrimSpace(token),
		httpClient: httpClient,
	}
}

func (client *MaaSClient) LoadSummary(ctx context.Context, organizationID string) (billingSummary, error) {
	var response billingSummary
	err := client.do(ctx, http.MethodGet, "/internal/maas/v1/billing/summary", billingQuery(organizationID, billingListOptions{}), nil, &response)
	return response, err
}

func (client *MaaSClient) ListLedger(ctx context.Context, organizationID string, options billingListOptions) (billingLedgerPage, error) {
	var response billingLedgerPage
	err := client.do(ctx, http.MethodGet, "/internal/maas/v1/billing/ledger", billingQuery(organizationID, options), nil, &response)
	return response, err
}

func (client *MaaSClient) ListModelUsage(ctx context.Context, organizationID string, options billingListOptions) (billingModelUsage, error) {
	var response billingModelUsage
	err := client.do(ctx, http.MethodGet, "/internal/maas/v1/billing/model-usage", billingQuery(organizationID, options), nil, &response)
	return response, err
}

func (client *MaaSClient) ListPayments(ctx context.Context, organizationID string, options billingListOptions) (billingPaymentOrderPage, error) {
	var response billingPaymentOrderPage
	err := client.do(ctx, http.MethodGet, "/internal/maas/v1/billing/payments", billingQuery(organizationID, options), nil, &response)
	return response, err
}

func (client *MaaSClient) LoadPricing(ctx context.Context, organizationID string) (billingPricing, error) {
	var response billingPricing
	err := client.do(ctx, http.MethodGet, "/internal/maas/v1/billing/pricing", billingQuery(organizationID, billingListOptions{}), nil, &response)
	return response, err
}

// LoadHostedPricing returns the public hosted pricing subset used by non-billing surfaces.
func (client *MaaSClient) LoadHostedPricing(ctx context.Context, organizationID string) (HostedPricing, error) {
	pricing, err := client.LoadPricing(ctx, organizationID)
	if err != nil {
		return HostedPricing{}, err
	}
	models := make([]HostedModelPricing, 0, len(pricing.HostedModels))
	for _, model := range pricing.HostedModels {
		modelReference := strings.TrimSpace(model.ModelReference)
		if modelReference == "" {
			modelReference = strings.TrimSpace(model.ID)
		}
		models = append(models, HostedModelPricing{
			ModelReference:                                modelReference,
			HostedModelID:                                 model.HostedModelID,
			PricingVersionID:                              model.PricingVersionID,
			EffectivePricingVersionID:                     model.EffectivePricingVersionID,
			PricingFormulaVersion:                         model.PricingFormulaVersion,
			BillableDimensions:                            append([]string(nil), model.BillableDimensions...),
			ContextTiers:                                  cloneHostedPricingTiers(model.ContextTiers),
			BasePricingVersionID:                          model.BasePricingVersionID,
			BasePricingFormulaVersion:                     model.BasePricingFormulaVersion,
			BaseBillableDimensions:                        append([]string(nil), model.BaseBillableDimensions...),
			BaseContextTiers:                              cloneHostedPricingTiers(model.BaseContextTiers),
			InputCreditAtomsPerMillionTokens:              model.InputCreditAtomsPerMillionTokens,
			CacheReadInputCreditAtomsPerMillionTokens:     model.CacheReadInputCreditAtomsPerMillionTokens,
			OutputCreditAtomsPerMillionTokens:             model.OutputCreditAtomsPerMillionTokens,
			BaseInputCreditAtomsPerMillionTokens:          model.BaseInputCreditAtomsPerMillionTokens,
			BaseCacheReadInputCreditAtomsPerMillionTokens: model.BaseCacheReadInputCreditAtomsPerMillionTokens,
			BaseOutputCreditAtomsPerMillionTokens:         model.BaseOutputCreditAtomsPerMillionTokens,
			PricingOverride:                               cloneHostedPricingOverride(model.PricingOverride),
		})
	}
	return HostedPricing{
		CreditAtomsPerCredit: pricing.CreditAtomsPerCredit,
		HostedModels:         models,
	}, nil
}

func cloneHostedPricingTiers(tiers []HostedPricingTier) []HostedPricingTier {
	if len(tiers) == 0 {
		return nil
	}
	cloned := make([]HostedPricingTier, 0, len(tiers))
	for _, tier := range tiers {
		cloned = append(cloned, HostedPricingTier{
			ID:               tier.ID,
			MinContextTokens: tier.MinContextTokens,
			MaxContextTokens: tier.MaxContextTokens,
			Rates:            tier.Rates,
		})
	}
	return cloned
}

func cloneHostedPricingOverride(override *HostedPricingOverride) *HostedPricingOverride {
	if override == nil {
		return nil
	}
	cloned := *override
	return &cloned
}

func (client *MaaSClient) CreateCreditRechargeOrder(ctx context.Context, params createCreditRechargeOrderParams) (billingPaymentOrder, error) {
	var response billingPaymentOrder
	err := client.do(ctx, http.MethodPost, "/internal/maas/v1/billing/credit-recharge-orders", nil, params, &response)
	return response, err
}

func (client *MaaSClient) LoadCreditRechargeOrder(ctx context.Context, organizationID string, orderID string) (billingPaymentOrder, error) {
	var response billingPaymentOrder
	path := "/internal/maas/v1/billing/credit-recharge-orders/" + url.PathEscape(strings.TrimSpace(orderID))
	err := client.do(ctx, http.MethodGet, path, billingQuery(organizationID, billingListOptions{}), nil, &response)
	return response, err
}

func (client *MaaSClient) LoadCreditRechargeOrderByOutTradeNo(ctx context.Context, outTradeNo string) (billingPaymentOrder, error) {
	var response billingPaymentOrder
	path := "/internal/maas/v1/billing/credit-recharge-orders/by-out-trade-no/" + url.PathEscape(strings.TrimSpace(outTradeNo))
	err := client.do(ctx, http.MethodGet, path, nil, nil, &response)
	return response, err
}

func (client *MaaSClient) ConfirmCreditRechargeOrderPaid(ctx context.Context, orderID string, params confirmCreditRechargeOrderPaidParams) (billingPaymentOrder, error) {
	var response billingPaymentOrder
	path := "/internal/maas/v1/billing/credit-recharge-orders/" + url.PathEscape(strings.TrimSpace(orderID)) + "/confirm-paid"
	err := client.do(ctx, http.MethodPost, path, nil, params, &response)
	return response, err
}

func (client *MaaSClient) CloseCreditRechargeOrder(ctx context.Context, orderID string, params closeCreditRechargeOrderParams) (billingPaymentOrder, error) {
	var response billingPaymentOrder
	path := "/internal/maas/v1/billing/credit-recharge-orders/" + url.PathEscape(strings.TrimSpace(orderID)) + "/close"
	err := client.do(ctx, http.MethodPost, path, nil, params, &response)
	return response, err
}

func (client *MaaSClient) GrantHostedCredit(ctx context.Context, params HostedCreditGrantParams) (HostedCreditGrant, error) {
	var response HostedCreditGrant
	err := client.do(ctx, http.MethodPost, "/internal/maas/v1/billing/credit-grants", nil, params, &response)
	return response, err
}

func (client *MaaSClient) do(ctx context.Context, method string, path string, query url.Values, payload any, out any) error {
	if client == nil {
		return fmt.Errorf("agentteam-maas billing client is not configured")
	}
	if strings.TrimSpace(client.baseURL) == "" {
		return fmt.Errorf("AGENTTEAM_MAAS_BASE_URL is required")
	}
	if strings.TrimSpace(client.token) == "" {
		return fmt.Errorf("AGENTTEAM_MAAS_INTERNAL_BEARER_TOKEN is required")
	}
	endpoint, err := client.endpoint(path, query)
	if err != nil {
		return err
	}

	var body io.Reader
	if payload != nil {
		var buffer bytes.Buffer
		if err := json.NewEncoder(&buffer).Encode(payload); err != nil {
			return fmt.Errorf("encode agentteam-maas billing request: %w", err)
		}
		body = &buffer
	}
	request, err := http.NewRequestWithContext(ctx, method, endpoint, body)
	if err != nil {
		return fmt.Errorf("build agentteam-maas billing request: %w", err)
	}
	request.Header.Set("Authorization", "Bearer "+client.token)
	if payload != nil {
		request.Header.Set("Content-Type", "application/json")
	}
	request.Header.Set("Accept", "application/json")

	httpClient := client.httpClient
	if httpClient == nil {
		httpClient = http.DefaultClient
	}
	response, err := httpClient.Do(request)
	if err != nil {
		return fmt.Errorf("call agentteam-maas billing API: %w", err)
	}
	defer response.Body.Close()

	responseBody, err := io.ReadAll(io.LimitReader(response.Body, maxBillingResponseBodyBytes))
	if err != nil {
		return fmt.Errorf("read agentteam-maas billing response: %w", err)
	}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return MaaSClientError{StatusCode: response.StatusCode, Body: string(responseBody)}
	}
	if out == nil {
		return nil
	}
	if err := json.Unmarshal(responseBody, out); err != nil {
		return fmt.Errorf("decode agentteam-maas billing response: %w", err)
	}
	return nil
}

func (client *MaaSClient) endpoint(path string, query url.Values) (string, error) {
	base, err := url.Parse(strings.TrimSpace(client.baseURL))
	if err != nil {
		return "", fmt.Errorf("parse AGENTTEAM_MAAS_BASE_URL: %w", err)
	}
	if base.Scheme == "" || base.Host == "" {
		return "", fmt.Errorf("AGENTTEAM_MAAS_BASE_URL must be an absolute URL")
	}
	base.Path = strings.TrimRight(base.Path, "/") + "/" + strings.TrimLeft(path, "/")
	base.RawQuery = query.Encode()
	return base.String(), nil
}

func billingQuery(organizationID string, options billingListOptions) url.Values {
	query := url.Values{}
	if trimmed := strings.TrimSpace(organizationID); trimmed != "" {
		query.Set("organization_id", trimmed)
	}
	if trimmed := strings.TrimSpace(options.Month); trimmed != "" {
		query.Set("month", trimmed)
	}
	if options.Limit > 0 {
		query.Set("limit", strconv.Itoa(options.Limit))
	}
	if trimmed := strings.TrimSpace(options.Cursor); trimmed != "" {
		query.Set("cursor", trimmed)
	}
	if trimmed := strings.TrimSpace(options.Type); trimmed != "" {
		query.Set("type", trimmed)
	}
	return query
}
