// Package license defines the Vantage licence payload and its offline
// verification.
//
// A licence is a signed blob (ECDSA P-384 with SHA-256). The server checks a
// signature, an expiry, a deployment mode and an instance ID, and asks nobody's
// permission. That buys air-gapped self-hosting and means no instance depends
// on the licensing service being reachable.
//
// It costs revocation: once issued, a licence is valid until it expires
// whatever the billing system later says. Self Hosted is sold annually only so
// that window is bounded.
package license

import "time"

const (
	TierFree         = "free"
	TierProfessional = "professional"
	TierEnterprise   = "enterprise"

	// TierSelfHosted is LEGACY and no new licence carries it.
	//
	// It was a tier when self-hosting was a tier rather than a deployment. Blobs
	// already signed with it exist and cannot be rewritten, so it stays a
	// recognised value that NormaliseTier maps forward. Never put it in a plan
	// row and never offer it in a UI.
	TierSelfHosted = "self_hosted"

	DeploymentCloud      = "cloud"
	DeploymentSelfHosted = "self_hosted"

	FeatureConsole = "console" // browser SSH/RDP/VNC
	FeatureOIDC    = "oidc"    // per-instance single sign-on
	// FeatureVulnScanning gates package inventory collection as well as the
	// findings themselves. The gate is at collection, not display: an ungated
	// instance stores no inventory, and storage is the expensive half.
	FeatureVulnScanning = "vuln_scanning"
)

// Support levels. Carried for display and enforced by nothing — there is no code
// path anywhere that branches on these, and there must not be one. They are here
// so an air-gapped install can tell its operator who to call without reaching
// Vantage HQ.
const (
	SupportCommunity     = "community"
	SupportEmail24x5     = "email_24_5"
	SupportEmailCall24x7 = "email_call_24_7"
)

// Unlimited is the sentinel for "no cap" in every Limits field.
const Unlimited = -1

// Limits are the countable caps a licence grants.
//
// Every field is a plain int with Unlimited as the sentinel. AuditRetentionDays
// is the odd one out: it bounds a duration rather than a count, and Unlimited
// there means "never trim" rather than "no cap".
type Limits struct {
	MaxServers         int `json:"max_servers"`
	MaxMonitors        int `json:"max_monitors"`
	MaxSecretGroups    int `json:"max_secret_groups"`
	MaxChannels        int `json:"max_channels"`
	AuditRetentionDays int `json:"audit_retention_days"`
}

// FillUnset replaces any zero field with the same field from base.
//
// This exists for one reason: a licence signed before a field existed decodes it
// as 0, and 0 would read as the most restrictive possible value — no monitors,
// and an audit log trimmed to nothing. A blob we cannot re-sign must not be
// allowed to mean that.
//
// The cost is that 0 stops being expressible as a real allowance. No plan grants
// zero of anything, so nothing is lost today; a plan that genuinely means zero
// must use a negative-free sentinel of its own rather than reintroducing 0 here.
func (l Limits) FillUnset(base Limits) Limits {
	if l.MaxServers == 0 {
		l.MaxServers = base.MaxServers
	}
	if l.MaxMonitors == 0 {
		l.MaxMonitors = base.MaxMonitors
	}
	if l.MaxSecretGroups == 0 {
		l.MaxSecretGroups = base.MaxSecretGroups
	}
	if l.MaxChannels == 0 {
		l.MaxChannels = base.MaxChannels
	}
	if l.AuditRetentionDays == 0 {
		l.AuditRetentionDays = base.AuditRetentionDays
	}
	return l
}

// License is the signed payload.
//
// InstanceID is always populated: the self-hosted purchase flow links the
// instance UUID before the licence is signed, so there is no unbound licence
// and no claim protocol.
type License struct {
	ID           string    `json:"id"`            // uuid, for support and audit
	InstanceID   string    `json:"instance_id"`   // the instance this licence is bound to
	AccountID    string    `json:"account_id"`    // admin-side customer, informational
	InstanceName string    `json:"instance_name"` // display only
	Tier         string    `json:"tier"`
	Deployment   string    `json:"deployment"`
	SupportLevel string    `json:"support_level,omitempty"` // display only
	IssuedAt     time.Time `json:"issued_at"`
	ExpiresAt    time.Time `json:"expires_at"`
	Limits       Limits    `json:"limits"`
	Features     []string  `json:"features"`
}

// HasFeature reports whether the licence grants a named feature.
//
// Callers must use this rather than switching on Tier. Adding a tier, or
// changing what a tier includes, must never require a server release.
func (l License) HasFeature(name string) bool {
	for _, f := range l.Features {
		if f == name {
			return true
		}
	}
	return false
}

// WithinLimit reports whether one more of something is allowed.
// A max of Unlimited always allows.
func WithinLimit(current, max int) bool {
	if max == Unlimited {
		return true
	}
	return current < max
}
