package license

import (
	"encoding/json"
	"errors"
	"fmt"
	"math"
	"slices"
	"strings"
	"time"
)

const (
	FeatureHTTP    = "http"
	FeatureWorker  = "worker"
	FeaturePlugins = "plugins"

	maxIssuedAtFutureSkew = 5 * time.Minute
	maxOnlineResponseAge  = 10 * time.Minute
	maxLeaseSequence      = uint64(math.MaxInt64)
)

var (
	ErrLicenseFeatureMissing = errors.New("license feature is not enabled")
	ErrLicenseSequenceStale  = errors.New("license sequence is stale")
	ErrLicenseProtocol       = errors.New("license response protocol invalid")
)

type Lease struct {
	Status      string
	ValidUntil  time.Time
	ExpiresAt   time.Time
	LicenseCode string
	LicenseNo   string
	KeyID       string
	InstanceID  string
	Sequence    uint64
	Features    []string
	IssuedAt    time.Time
}

func (l Lease) Scope() string {
	if strings.TrimSpace(l.InstanceID) != "" {
		return "instance:" + strings.TrimSpace(l.LicenseNo) + ":" + strings.TrimSpace(l.InstanceID)
	}
	return "license:" + strings.TrimSpace(l.LicenseNo)
}

func (l Lease) HasFeature(feature string) bool {
	return slices.Contains(l.Features, strings.TrimSpace(feature))
}

func decodeLeasePayload(payload []byte) (Lease, error) {
	var raw struct {
		Status      string   `json:"status"`
		ValidUntil  string   `json:"validUntil"`
		ExpiresAt   string   `json:"expiresAt"`
		LicenseCode string   `json:"license"`
		LicenseNo   string   `json:"licenseNo"`
		KeyID       string   `json:"keyId"`
		InstanceID  string   `json:"instanceId"`
		Sequence    uint64   `json:"sequence"`
		Features    []string `json:"features"`
		IssuedAt    int64    `json:"issuedAt"`
	}
	if err := json.Unmarshal(payload, &raw); err != nil {
		return Lease{}, fmt.Errorf("%w: decode payload: %v", ErrLicenseProtocol, err)
	}
	validUntil, err := time.Parse(time.RFC3339, strings.TrimSpace(raw.ValidUntil))
	if err != nil {
		return Lease{}, fmt.Errorf("%w: invalid validUntil", ErrLicenseProtocol)
	}
	expiresAt, err := time.Parse(time.RFC3339, strings.TrimSpace(raw.ExpiresAt))
	if err != nil {
		return Lease{}, fmt.Errorf("%w: invalid expiresAt", ErrLicenseProtocol)
	}
	features := make([]string, 0, len(raw.Features))
	seen := map[string]struct{}{}
	for _, feature := range raw.Features {
		feature = strings.TrimSpace(feature)
		if feature == "" {
			continue
		}
		if _, exists := seen[feature]; exists {
			continue
		}
		seen[feature] = struct{}{}
		features = append(features, feature)
	}
	return Lease{
		Status:      strings.TrimSpace(raw.Status),
		ValidUntil:  validUntil.UTC(),
		ExpiresAt:   expiresAt.UTC(),
		LicenseCode: strings.TrimSpace(raw.LicenseCode),
		LicenseNo:   strings.TrimSpace(raw.LicenseNo),
		KeyID:       strings.TrimSpace(raw.KeyID),
		InstanceID:  strings.TrimSpace(raw.InstanceID),
		Sequence:    raw.Sequence,
		Features:    features,
		IssuedAt:    time.Unix(raw.IssuedAt, 0).UTC(),
	}, nil
}

func validateLease(lease Lease, now time.Time, expectedInstanceID string, online bool) error {
	now = now.UTC()
	switch lease.Status {
	case "active", "suspended", "revoked":
	default:
		return fmt.Errorf("%w: unknown status %q", ErrLicenseProtocol, lease.Status)
	}
	if lease.LicenseNo == "" || lease.KeyID == "" || lease.Sequence == 0 || lease.IssuedAt.Unix() <= 0 {
		return fmt.Errorf("%w: required signed fields are missing", ErrLicenseProtocol)
	}
	if lease.Sequence > maxLeaseSequence {
		return fmt.Errorf("%w: sequence exceeds signed BIGINT range", ErrLicenseProtocol)
	}
	if lease.ValidUntil.IsZero() || lease.ExpiresAt.IsZero() || lease.ValidUntil.After(lease.ExpiresAt) {
		return fmt.Errorf("%w: invalid lease time range", ErrLicenseProtocol)
	}
	if expectedInstanceID = strings.TrimSpace(expectedInstanceID); expectedInstanceID != "" && lease.InstanceID != expectedInstanceID {
		return fmt.Errorf("%w: signed instanceId mismatch", ErrLicenseProtocol)
	}
	if len(lease.Features) == 0 {
		return fmt.Errorf("%w: features are missing", ErrLicenseProtocol)
	}
	if online {
		if lease.IssuedAt.After(now.Add(maxIssuedAtFutureSkew)) || lease.IssuedAt.Before(now.Add(-maxOnlineResponseAge)) {
			return fmt.Errorf("%w: issuedAt outside accepted window", ErrLicenseProtocol)
		}
		if !lease.ValidUntil.After(now) {
			return fmt.Errorf("%w: validUntil is not in the future", ErrLicenseProtocol)
		}
	}
	return nil
}

func compareLeaseSequence(previousScope string, previous uint64, next Lease) error {
	if previous == 0 || strings.TrimSpace(previousScope) == "" || previousScope != next.Scope() {
		return nil
	}
	if next.Sequence <= previous {
		return fmt.Errorf("%w: received %d after %d", ErrLicenseSequenceStale, next.Sequence, previous)
	}
	return nil
}
