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

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

	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/audit"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/catalogue"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/db"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
	"github.com/google/uuid"
	"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 of that deployment type")
	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
	}

	// The plan is looked up by the INSTANCE's deployment, not by a caller's
	// guess. That is what makes the deployment comparison below a consistency
	// check rather than the thing that decides which plan applies.
	plan, err := models.GetPlan(ctx, inst.Deployment, 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.Deployment, inst.InstanceID); err != nil {
			return nil, err
		}
	}

	// What this licence grants comes from the instance's entitlement, not from
	// the plan. The plan is only the base.
	//
	// An instance with no entitlement gets the plan's base, which covers staff
	// manual issuance and anything predating the backfill. Falling back is
	// deliberate: refusing here would make a missing row an outage rather than a
	// default.
	limits, features := plan.BaseLimits, []string(plan.BaseFeatures.OrEmpty())
	ent, entErr := models.GetEntitlement(ctx, inst.InstanceID)
	switch {
	case entErr == nil:
		// Granted, never Desired. A configuration nobody has paid for must not
		// reach a signed payload.
		limits, features, err = catalogue.Resolve(ctx, plan, ent.Granted)
		if err != nil {
			return nil, fmt.Errorf("resolve entitlement: %w", err)
		}
	case errors.Is(entErr, models.ErrNoEntitlement):
		log.Printf("licensing: instance %s has no entitlement; issuing plan base",
			inst.InstanceID)
	default:
		return nil, fmt.Errorf("read entitlement: %w", entErr)
	}

	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 or an entitlement tomorrow
		// must not change what this licence grants.
		Limits:       limits,
		Features:     features,
		SupportLevel: plan.SupportLevel,
	}

	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:     limits,
		Features:   models.Features(features).OrEmpty(),
		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 PER DEPLOYMENT.
//
// It used to be one per account, which was sufficient while Free existed only on
// cloud. With a self-hosted Free plan, an account-wide count would refuse a
// self-hosted Free instance to anyone holding a cloud one, and tell them about a
// limit they have not reached.
//
// Cancelled instances do not count: a customer who cancelled their Free instance
// is allowed another one.
func checkFreeLimit(ctx context.Context, accountID, deployment, exceptInstanceID string) error {
	n, err := db.Admin("admin_instances").CountDocuments(ctx, bson.M{
		"account_id":  accountID,
		"deployment":  deployment,
		"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
}
