// Package db holds admin's two MongoDB connections.
//
// Admin() is its own database and it owns every collection there. Control() is
// the control plane's database. Admin's access to it is narrow and lives in
// exactly two packages: inject writes three licence fields on `instances`, and
// cloudprov creates and rolls back `instances` and `users` when a customer
// provisions a cloud instance. Nothing else may write there, and a third write
// path is a design change rather than a refactor.
package db

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

	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/config"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

var (
	adminDB   *mongo.Database
	controlDB *mongo.Database
)

func Connect(ctx context.Context, cfg config.Config) error {
	ac, err := mongo.Connect(options.Client().ApplyURI(cfg.AdminMongoURI))
	if err != nil {
		return fmt.Errorf("connect admin mongo: %w", err)
	}
	if err := ac.Ping(ctx, nil); err != nil {
		return fmt.Errorf("ping admin mongo: %w", err)
	}
	adminDB = ac.Database(cfg.AdminDBName)

	cc, err := mongo.Connect(options.Client().ApplyURI(cfg.ControlMongoURI))
	if err != nil {
		return fmt.Errorf("connect control mongo: %w", err)
	}
	if err := cc.Ping(ctx, nil); err != nil {
		return fmt.Errorf("ping control mongo: %w", err)
	}
	controlDB = cc.Database(cfg.ControlDBName)

	// The control plane must already be deployed and migrated. Without the
	// instances collection, injection would silently create it and write
	// licence fields into a collection nothing reads.
	names, err := controlDB.ListCollectionNames(ctx, map[string]any{"name": "instances"})
	if err != nil {
		return fmt.Errorf("inspect control database: %w", err)
	}
	if len(names) == 0 {
		return fmt.Errorf("control database %q has no instances collection; deploy and migrate the control plane first", cfg.ControlDBName)
	}
	return nil
}

func Admin(name string) *mongo.Collection   { return adminDB.Collection(name) }
func Control(name string) *mongo.Collection { return controlDB.Collection(name) }

// ControlDB exposes the control-plane database itself, because shared/provision
// takes a database rather than a collection.
//
// It is used by cloudprov and nothing else. Reach for Control(name) unless you
// are calling into shared/provision.
func ControlDB() *mongo.Database { return controlDB }

func Ctx() (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), 10*time.Second)
}

// EnsureIndexes creates admin's unique indexes.
//
// These are a correctness property, not an optimisation. In particular
// admin_instances.instance_id unique is what stops the same self-hosted UUID
// being linked to two accounts — without it, two customers could both claim one
// instance and both be issued licences for it.
func EnsureIndexes(ctx context.Context) error {
	unique := []struct {
		coll  string
		field string
	}{
		{"accounts", "account_id"},
		{"admin_instances", "instance_id"},
		{"licenses", "license_id"},
		{"paddle_events", "event_id"},
		{"staff_users", "email"},
		{"customer_users", "email"},
	}
	for _, u := range unique {
		if _, err := Admin(u.coll).Indexes().CreateOne(ctx, mongo.IndexModel{
			Keys:    bson.D{{Key: u.field, Value: 1}},
			Options: options.Index().SetUnique(true).SetName(u.field + "_unique"),
		}); err != nil {
			return fmt.Errorf("index %s.%s: %w", u.coll, u.field, err)
		}
	}

	// Sparse: a subscription exists before Paddle assigns an ID, so empty must
	// not collide with empty.
	if _, err := Admin("subscriptions").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys:    bson.D{{Key: "paddle_subscription_id", Value: 1}},
		Options: options.Index().SetUnique(true).SetSparse(true).SetName("paddle_subscription_id_unique"),
	}); err != nil {
		return fmt.Errorf("index subscriptions.paddle_subscription_id: %w", err)
	}

	// plans was unique on tier alone until spec 7. Mongo will not replace an
	// index implicitly, and the old one would refuse the second row of every
	// tier, so it is dropped by name here. Dropping a missing index is not an
	// error worth failing boot over — a fresh database has never had it.
	if err := Admin("plans").Indexes().DropOne(ctx, "tier_unique"); err != nil {
		log.Printf("index plans.tier_unique: not dropped (%v); expected on a fresh database", err)
	}

	for _, u := range []struct {
		coll string
		keys bson.D
		name string
	}{
		{"plans", bson.D{{Key: "deployment", Value: 1}, {Key: "tier", Value: 1}}, "deployment_tier_unique"},
		{"catalogue", bson.D{
			{Key: "deployment", Value: 1}, {Key: "tier", Value: 1}, {Key: "kind", Value: 1},
			{Key: "limit_key", Value: 1}, {Key: "feature_key", Value: 1},
		}, "component_unique"},
		{"entitlements", bson.D{{Key: "instance_id", Value: 1}}, "instance_id_unique"},
	} {
		if _, err := Admin(u.coll).Indexes().CreateOne(ctx, mongo.IndexModel{
			Keys:    u.keys,
			Options: options.Index().SetUnique(true).SetName(u.name),
		}); err != nil {
			return fmt.Errorf("index %s.%s: %w", u.coll, u.name, err)
		}
	}

	for _, idx := range []struct {
		coll string
		keys bson.D
	}{
		{"licenses", bson.D{{Key: "instance_id", Value: 1}, {Key: "issued_at", Value: -1}}},
		{"admin_instances", bson.D{{Key: "account_id", Value: 1}}},
		{"admin_audit", bson.D{{Key: "created_at", Value: -1}}},
	} {
		if _, err := Admin(idx.coll).Indexes().CreateOne(ctx, mongo.IndexModel{Keys: idx.keys}); err != nil {
			return fmt.Errorf("index %s: %w", idx.coll, err)
		}
	}

	// One person holds at most one user in one instance. This is the property
	// that makes a grant idempotent-by-refusal rather than silently doubling a
	// projection, and it mirrors users' own (instance_id, email) uniqueness.
	if _, err := Admin("instance_members").Indexes().CreateOne(ctx, mongo.IndexModel{
		Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "customer_user_id", Value: 1}},
		Options: options.Index().SetUnique(true).
			SetName("instance_customer_user_unique"),
	}); err != nil {
		return fmt.Errorf("index instance_members.(instance_id,customer_user_id): %w", err)
	}
	for _, keys := range []bson.D{
		{{Key: "account_id", Value: 1}},
		{{Key: "customer_user_id", Value: 1}},
	} {
		if _, err := Admin("instance_members").Indexes().CreateOne(ctx,
			mongo.IndexModel{Keys: keys}); err != nil {
			return fmt.Errorf("index instance_members: %w", err)
		}
	}
	return nil
}
