package billing

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

	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/catalogue"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/cloudprov"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/db"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/paddle"
	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
	sharedmodels "gitea.hostxtra.co.uk/mrhid6/vantage/shared/models"
	"github.com/google/uuid"
	"go.mongodb.org/mongo-driver/v2/bson"
)

// placeholderReconcileInterval is how often placeholders are swept: paid cloud
// ones a failed webhook left unprovisioned are completed, and stale unpaid ones
// of either deployment are reaped.
const placeholderReconcileInterval = 5 * time.Minute

// abandonedPlaceholderAfter is how long an unpaid placeholder may sit before it
// is treated as an abandoned checkout and deleted. Comfortably longer than a
// webhook's delivery lag, so a just-paid placeholder awaiting its subscription
// event is never mistaken for an abandoned one.
const abandonedPlaceholderAfter = 24 * time.Hour

// StartPlaceholderReconciler owns the after-checkout lifecycle of placeholders.
//
// It recovers the one failure the webhook cannot on its own — a confirmed payment
// whose provisioning handler errored, which is not retried once its event is
// claimed and which the inject reconciler (licences only) does not repair — by
// completing paid cloud placeholders here. And it reaps abandoned ones: a
// placeholder with no subscription past abandonedPlaceholderAfter is a checkout
// nobody finished, and deleting it loses nothing, because a placeholder has no
// control-plane footprint until it is paid for and provisioned.
func StartPlaceholderReconciler(ctx context.Context) {
	go func() {
		t := time.NewTicker(placeholderReconcileInterval)
		defer t.Stop()
		reconcilePlaceholders(ctx)
		for {
			select {
			case <-ctx.Done():
				return
			case <-t.C:
				reconcilePlaceholders(ctx)
			}
		}
	}()
}

func reconcilePlaceholders(ctx context.Context) {
	cur, err := db.Admin("admin_instances").Find(ctx, bson.M{"placeholder": true})
	if err != nil {
		log.Printf("placeholder reconcile: query: %v", err)
		return
	}
	var placeholders []models.Instance
	if err := cur.All(ctx, &placeholders); err != nil {
		log.Printf("placeholder reconcile: decode: %v", err)
		return
	}

	now := time.Now().UTC()
	for _, inst := range placeholders {
		var sub models.Subscription
		paid := db.Admin("subscriptions").FindOne(ctx,
			bson.M{"instance_id": inst.InstanceID, "status": models.SubActive}).Decode(&sub) == nil

		if !paid {
			// Never paid for. Reap once it is old enough to be an abandoned
			// checkout rather than one still awaiting its subscription webhook.
			if now.Sub(inst.CreatedAt) > abandonedPlaceholderAfter {
				if _, err := db.Admin("admin_instances").DeleteOne(ctx,
					bson.M{"instance_id": inst.InstanceID, "placeholder": true}); err != nil {
					log.Printf("placeholder reconcile: reap abandoned %s: %v", inst.InstanceID, err)
				} else {
					log.Printf("placeholder reconcile: reaped abandoned placeholder %s", inst.InstanceID)
				}
			}
			continue
		}

		// Paid, self-hosted: nothing to provision — the customer installs and
		// links, and lifecycle chases them. Only cloud is completed here.
		if inst.Deployment != license.DeploymentCloud {
			continue
		}

		items := make([]catalogue.Item, 0, len(sub.Items))
		for _, it := range sub.Items {
			items = append(items, catalogue.Item{PriceID: it.PriceID, Quantity: it.Quantity})
		}
		match, err := catalogue.ResolveItems(ctx, paddle.Get().Env(), items)
		if err != nil {
			log.Printf("placeholder reconcile: resolve items for %s: %v", inst.InstanceID, err)
			continue
		}
		provisioned, err := completeCloudPlaceholder(ctx, &inst)
		if err != nil {
			log.Printf("placeholder reconcile: complete %s: %v", inst.InstanceID, err)
			continue
		}
		if err := promoteAndIssue(ctx, provisioned, match, models.ReasonNew); err != nil {
			log.Printf("placeholder reconcile: issue %s: %v", inst.InstanceID, err)
			continue
		}
		log.Printf("placeholder reconcile: completed paid cloud instance %s", inst.InstanceID)
	}
}

// completeCloudPlaceholder provisions the cloud instance a paid placeholder stands
// for, once payment is confirmed, and returns the row promoted to a real instance.
//
// It is the payment-first half of the paid-cloud flow: createCloudCheckout made
// the placeholder before payment, this provisions it after. The control-plane
// instance is created with the placeholder's OWN id (cloudprov.CreateInstanceWithID),
// so nothing is rewritten and the subscription's custom_data still resolves this
// row on every later webhook.
//
// Every step is idempotent, because a webhook can be retried after this partly
// ran: provisioning converges rather than duplicates, and the row flip and
// membership insert are guarded on what they write. The caller then issues.
func completeCloudPlaceholder(ctx context.Context, inst *models.Instance) (*models.Instance, error) {
	cu, err := placeholderOwner(ctx, inst)
	if err != nil {
		return nil, err
	}

	prov, err := cloudprov.CreateInstanceWithID(ctx, inst.InstanceID, inst.Name,
		cu.Email, cu.PasswordHash, cu.UserID)
	if err != nil {
		return nil, fmt.Errorf("provision cloud instance %s: %w", inst.InstanceID, err)
	}

	if _, err := db.Admin("admin_instances").UpdateOne(ctx,
		bson.M{"instance_id": inst.InstanceID},
		bson.M{
			"$set":   bson.M{"slug": prov.Slug, "status": models.StatusActive, "placeholder": false},
			"$unset": bson.M{"pending_owner_user_id": ""},
		}); err != nil {
		return nil, fmt.Errorf("promote placeholder %s: %w", inst.InstanceID, err)
	}

	// Record the owner's membership. Best-effort and guarded on absence: the
	// projected user is what grants access, so a missing row costs a line in the
	// members panel, not access — and the boot backfill rebuilds it.
	if ownerID, err := cloudprov.OwnerUserID(ctx, inst.InstanceID); err == nil {
		if n, _ := db.Admin("instance_members").CountDocuments(ctx,
			bson.M{"instance_id": inst.InstanceID, "customer_user_id": cu.UserID}); n == 0 {
			if _, err := db.Admin("instance_members").InsertOne(ctx, models.InstanceMember{
				MemberID:       uuid.NewString(),
				AccountID:      inst.AccountID,
				InstanceID:     inst.InstanceID,
				CustomerUserID: cu.UserID,
				ControlUserID:  ownerID,
				Role:           sharedmodels.RoleOwner,
				Email:          cu.Email,
				CreatedAt:      time.Now().UTC(),
			}); err != nil {
				log.Printf("completeCloudPlaceholder: record owner membership for %s: %v",
					inst.InstanceID, err)
			}
		}
	}

	next := *inst
	next.Slug = prov.Slug
	next.Status = models.StatusActive
	next.Placeholder = false
	next.PendingOwnerUserID = ""
	return &next, nil
}

// placeholderOwner resolves the customer_user who should own a provisioned cloud
// placeholder: the buyer recorded at checkout, or the account owner if that
// pointer is somehow missing.
func placeholderOwner(ctx context.Context, inst *models.Instance) (*models.CustomerUser, error) {
	var cu models.CustomerUser
	if inst.PendingOwnerUserID != "" {
		if err := db.Admin("customer_users").FindOne(ctx,
			bson.M{"user_id": inst.PendingOwnerUserID}).Decode(&cu); err == nil {
			return &cu, nil
		}
	}
	if err := db.Admin("customer_users").FindOne(ctx,
		bson.M{"account_id": inst.AccountID, "account_role": models.AccountRoleOwner}).Decode(&cu); err != nil {
		return nil, fmt.Errorf("no owner for account %s to provision %s: %w",
			inst.AccountID, inst.InstanceID, err)
	}
	return &cu, nil
}
