// Package licensing issues licences. It is the only place that signs.
package licensing

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/google/uuid"
	"github.com/mrhid6/vantage/admin/internal/audit"
	"github.com/mrhid6/vantage/admin/internal/db"
	"github.com/mrhid6/vantage/admin/internal/models"
	"github.com/mrhid6/vantage/shared/license"
	"go.mongodb.org/mongo-driver/v2/bson"
)

var (
	ErrUnknownTier        = errors.New("unknown tier")
	ErrDeploymentMismatch = errors.New("that plan is not available for this deployment type")
	ErrFreeLimit          = errors.New("this account already has a Free instance")
	ErrUnknownInstance    = errors.New("instance not found")
)

type IssueInput struct {
	InstanceID string
	Tier       string
	Term       string    // "monthly" or "annual"; ignored when ExpiresAt is set
	ExpiresAt  time.Time // explicit expiry, used by relink to preserve the remaining term
	Reason     string
	IssuedBy   string // staff email, "system", or "paddle:<event id>"
}

// signingKey is set once at boot from LICENSE_SIGNING_KEY.
var signingKey string

func SetSigningKey(k string) { signingKey = k }

// Issue signs a licence, records it, supersedes its predecessor and updates the
// instance.
//
// It does NOT deliver. Recording and delivery are deliberately separate and
// ordered: a licence recorded but not delivered is recoverable, because the
// customer can download it. A licence delivered but not recorded is a support
// mystery with no paper trail. Callers deliver after this returns.
func Issue(ctx context.Context, in IssueInput) (*models.License, error) {
	if signingKey == "" {
		return nil, errors.New("no signing key configured")
	}

	var inst models.Instance
	if err := db.Admin("admin_instances").FindOne(ctx,
		bson.M{"instance_id": in.InstanceID}).Decode(&inst); err != nil {
		return nil, ErrUnknownInstance
	}

	plan, err := models.GetPlan(ctx, in.Tier)
	if err != nil {
		return nil, ErrUnknownTier
	}

	// This single comparison is what makes Free cloud-only. Free's plan is
	// deployment "cloud", so it can never be issued against a self-hosted
	// instance, and verification on the instance would reject it anyway.
	if plan.Deployment != inst.Deployment {
		return nil, fmt.Errorf("%w: %s is %s only", ErrDeploymentMismatch, plan.Name, plan.Deployment)
	}

	if plan.Tier == license.TierFree {
		if err := checkFreeLimit(ctx, inst.AccountID, inst.InstanceID); err != nil {
			return nil, err
		}
	}

	now := time.Now().UTC()
	expires := in.ExpiresAt
	if expires.IsZero() {
		switch in.Term {
		case "monthly":
			expires = now.AddDate(0, 1, 0).Add(models.GracePeriod)
		case "annual", "":
			expires = now.AddDate(1, 0, 0).Add(models.GracePeriod)
		default:
			return nil, fmt.Errorf("unknown term %q", in.Term)
		}
	}

	payload := license.License{
		ID:           uuid.NewString(),
		InstanceID:   inst.InstanceID,
		AccountID:    inst.AccountID,
		InstanceName: inst.Name,
		Tier:         plan.Tier,
		Deployment:   plan.Deployment,
		IssuedAt:     now,
		ExpiresAt:    expires,
		// Snapshotted, not referenced: editing a plan tomorrow must not change
		// what this licence grants.
		Limits:   plan.Limits,
		Features: plan.Features,
	}

	blob, err := license.Sign(payload, signingKey)
	if err != nil {
		return nil, fmt.Errorf("sign: %w", err)
	}

	rec := models.License{
		LicenseID:  payload.ID,
		InstanceID: inst.InstanceID,
		AccountID:  inst.AccountID,
		Tier:       plan.Tier,
		Deployment: plan.Deployment,
		Limits:     plan.Limits,
		Features:   plan.Features,
		IssuedAt:   now,
		ExpiresAt:  expires,
		Blob:       blob,
		IssuedBy:   in.IssuedBy,
		Reason:     in.Reason,
	}
	if _, err := db.Admin("licenses").InsertOne(ctx, rec); err != nil {
		return nil, fmt.Errorf("record licence: %w", err)
	}

	// Supersede rather than delete. The history is the support tool.
	if inst.CurrentLicense != "" {
		if _, err := db.Admin("licenses").UpdateOne(ctx,
			bson.M{"license_id": inst.CurrentLicense},
			bson.M{"$set": bson.M{"superseded_by": rec.LicenseID}}); err != nil {
			return nil, fmt.Errorf("supersede previous licence: %w", err)
		}
	}

	set := bson.M{
		"current_license": rec.LicenseID,
		"tier":            plan.Tier,
		"status":          models.StatusActive,
	}
	if in.Reason == models.ReasonRenewal {
		set["relink_count"] = 0 // the cap is per term
	}
	if _, err := db.Admin("admin_instances").UpdateOne(ctx,
		bson.M{"instance_id": inst.InstanceID}, bson.M{"$set": set}); err != nil {
		return nil, fmt.Errorf("update instance: %w", err)
	}

	audit.Write(ctx, models.AuditEntry{
		Actor:     in.IssuedBy,
		Action:    "license.issued",
		AccountID: inst.AccountID,
		Target:    inst.InstanceID,
		Detail: fmt.Sprintf("tier=%s reason=%s expires=%s licence=%s",
			plan.Tier, in.Reason, expires.Format(time.RFC3339), rec.LicenseID),
	})

	return &rec, nil
}

// checkFreeLimit enforces one Free instance per account.
//
// Cancelled instances do not count: a customer who cancelled their Free instance
// is allowed another one.
func checkFreeLimit(ctx context.Context, accountID, exceptInstanceID string) error {
	n, err := db.Admin("admin_instances").CountDocuments(ctx, bson.M{
		"account_id":  accountID,
		"tier":        license.TierFree,
		"status":      bson.M{"$ne": models.StatusCancelled},
		"instance_id": bson.M{"$ne": exceptInstanceID},
	})
	if err != nil {
		return err
	}
	if n > 0 {
		return ErrFreeLimit
	}
	return nil
}
