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"
	"go.mongodb.org/mongo-driver/v2/mongo"
)

var (
	ErrBadUUID       = errors.New("that does not look like an instance ID")
	ErrAlreadyLinked = errors.New("that instance ID is already linked to an account")
	ErrRelinkLimit   = errors.New("relink limit reached for this term; contact support")
)

// LinkInstance attaches a self-hosted instance UUID to an account.
//
// The duplicate error deliberately does not say WHICH account holds it. It is a
// small enumeration surface, but there is no reason to leave it open.
func LinkInstance(ctx context.Context, accountID, instanceID, name string) (*models.Instance, error) {
	if _, err := uuid.Parse(instanceID); err != nil {
		return nil, ErrBadUUID
	}

	// A self-hosted UUID must not collide with a cloud instance either.
	if n, err := db.Control("instances").CountDocuments(ctx, bson.M{"instance_id": instanceID}); err == nil && n > 0 {
		return nil, ErrAlreadyLinked
	}

	inst := models.Instance{
		InstanceID: instanceID,
		AccountID:  accountID,
		Name:       name,
		Deployment: license.DeploymentSelfHosted,
		Status:     models.StatusActive,
		CreatedAt:  time.Now().UTC(),
	}
	if _, err := db.Admin("admin_instances").InsertOne(ctx, inst); err != nil {
		if mongo.IsDuplicateKeyError(err) {
			// The unique index is what actually prevents two accounts owning
			// one instance. The check above is a nicety; this is the guarantee.
			return nil, ErrAlreadyLinked
		}
		return nil, err
	}

	audit.Write(ctx, models.AuditEntry{
		Actor: accountID, Action: "instance.linked", AccountID: accountID, Target: instanceID})
	return &inst, nil
}

// Relink moves a licence to a rebuilt server's new UUID.
//
// The replacement covers the REMAINING term, not a fresh one — relinking is not
// a way to extend a subscription.
//
// The old licence is not revoked, because offline verification has no
// revocation. It simply no longer matches any UUID the customer controls, and
// its binding stops it working on another machine anyway.
func Relink(ctx context.Context, accountID, oldID, newID string, staff bool) (*models.License, error) {
	if _, err := uuid.Parse(newID); err != nil {
		return nil, ErrBadUUID
	}

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

	// The cap is a signal, not a defence. Its job is to put a human in front of
	// the fourth attempt, so staff bypass it.
	if !staff && inst.RelinkCount >= models.MaxRelinksPerTerm {
		return nil, ErrRelinkLimit
	}

	if n, err := db.Admin("admin_instances").CountDocuments(ctx, bson.M{"instance_id": newID}); err == nil && n > 0 {
		return nil, ErrAlreadyLinked
	}

	// Preserve the remaining term from the current licence.
	remaining := time.Now().UTC().Add(models.GracePeriod)
	var current models.License
	if err := db.Admin("licenses").FindOne(ctx,
		bson.M{"license_id": inst.CurrentLicense}).Decode(&current); err == nil {
		remaining = current.ExpiresAt
	}

	if _, err := db.Admin("admin_instances").UpdateOne(ctx,
		bson.M{"instance_id": oldID},
		bson.M{"$set": bson.M{"instance_id": newID}, "$inc": bson.M{"relink_count": 1}}); err != nil {
		if mongo.IsDuplicateKeyError(err) {
			return nil, ErrAlreadyLinked
		}
		return nil, fmt.Errorf("relink: %w", err)
	}

	actor := accountID
	if staff {
		actor = "staff"
	}
	audit.Write(ctx, models.AuditEntry{
		Actor: actor, Action: "instance.relinked", AccountID: accountID,
		Target: newID, Detail: "was " + oldID})

	return Issue(ctx, IssueInput{
		InstanceID: newID,
		Tier:       inst.Tier,
		ExpiresAt:  remaining,
		Reason:     models.ReasonRelink,
		IssuedBy:   actor,
	})
}
