// Package catalogue turns an entitlement into the two things derived from it:
// the limits and features a licence grants, and the Paddle line items a
// subscription is made of.
//
// Both folds live here so the arithmetic exists once. The temptation is to
// compute limits in the issuer and quantities in the checkout, and then the two
// disagree about whether the base allowance is included in the number — which is
// a bug that bills a customer for three servers they were given.
package catalogue

import (
	"context"
	"errors"
	"fmt"

	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
)

var (
	// ErrUnknownPrice means an item named a price ID no catalogue row claims.
	//
	// This is always a configuration error and never a customer error: someone
	// bought something at a price we cannot map to a plan. It must fail loudly
	// rather than guess a tier — a guessed tier is a wrong licence with no
	// record of why.
	ErrUnknownPrice = errors.New("no catalogue row claims that price ID")

	// ErrNoBaseItem means no item matched a base row, so the subscription names
	// no plan. Quantities are meaningless without one.
	ErrNoBaseItem = errors.New("no item matches a base price; the subscription names no plan")

	// ErrTermNotSold means a price resolved to a term its deployment does not
	// sell — in practice a self-hosted monthly price.
	ErrTermNotSold = errors.New("that deployment does not sell that term")

	// ErrUnpriced means a component needed for this configuration has no price
	// ID in this environment. Refusing is correct: a checkout that silently
	// drops a paid line item gives away the thing it was meant to charge for.
	ErrUnpriced = errors.New("component has no price in this environment")
)

// Resolve folds a configuration into the limits and features a licence grants.
//
// Limits start at the plan's base and each metered component adds its configured
// amount. Features are the plan's base features plus the configured ones,
// deduplicated and filtered to keys the catalogue actually offers — a stale
// feature key in a stored entitlement must not survive into a signed payload.
func Resolve(ctx context.Context, plan *models.Plan, cfg models.Config) (license.Limits, []string, error) {
	rows, err := models.CatalogueFor(ctx, plan.Deployment, plan.Tier)
	if err != nil {
		return license.Limits{}, nil, err
	}

	limits := plan.BaseLimits
	offered := map[string]bool{}
	for _, r := range rows {
		switch r.Kind {
		case models.KindLimit:
			if err := addLimit(&limits, r.LimitKey, configured(cfg, r.LimitKey), plan.BaseLimits); err != nil {
				return license.Limits{}, nil, err
			}
		case models.KindFeature:
			offered[r.FeatureKey] = true
		}
	}

	seen := map[string]bool{}
	features := []string{}
	for _, f := range plan.BaseFeatures {
		if !seen[f] {
			seen[f] = true
			features = append(features, f)
		}
	}
	for _, f := range cfg.Features {
		if seen[f] || !offered[f] {
			continue
		}
		seen[f] = true
		features = append(features, f)
	}
	return limits, features, nil
}

// configured reads the configured total for one metered limit key.
//
// A switch rather than reflection, so every metered dimension is greppable and
// adding one is a visible edit here as well as a catalogue row.
func configured(cfg models.Config, limitKey string) int {
	switch limitKey {
	case models.LimitKeyServers:
		return cfg.Servers
	default:
		return 0
	}
}

// addLimit sets a metered limit to its configured total.
//
// The configured value is a TOTAL, not an increment, so this assigns rather than
// adds. A base of Unlimited is left alone: nothing can be added to no cap, and a
// plan that meters an already-unlimited dimension is a configuration mistake
// rather than something to compute around.
func addLimit(l *license.Limits, limitKey string, total int, base license.Limits) error {
	switch limitKey {
	case models.LimitKeyServers:
		if base.MaxServers == license.Unlimited {
			return nil
		}
		if total > base.MaxServers {
			l.MaxServers = total
		}
		return nil
	case "":
		return fmt.Errorf("catalogue limit row has no limit_key")
	default:
		return fmt.Errorf("%w: limit_key %q", ErrUnknownPrice, limitKey)
	}
}
