package api

import (
	"errors"
	"fmt"
	"net/http"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/audit"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/auth"
	"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"
	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/v2/bson"
)

// entitlementBody is what a caller may set.
//
// Only Desired is writable. Granted is what a payment confirmed, and letting a
// form set it would let the portal grant itself a licence — which is the one
// thing this whole split exists to prevent. Staff promote Granted explicitly
// through a separate flag, because staff issuing a licence to somebody who has
// not paid is a real operation with a real reason, and it should be one they
// took on purpose and left an audit row for.
type entitlementBody struct {
	Tier     string   `json:"tier"`
	Term     string   `json:"term"`
	Servers  int      `json:"servers"`
	Features []string `json:"features"`
	// Grant promotes Desired into Granted in the same write. Staff only.
	Grant bool `json:"grant"`
}

// getEntitlement serves the customer's own view of one instance's configuration.
func getEntitlement(c *gin.Context) {
	inst, ok := ownedInstance(c, c.Param("id"))
	if !ok {
		return
	}
	ent, err := models.GetEntitlement(c.Request.Context(), inst.InstanceID)
	if errors.Is(err, models.ErrNoEntitlement) {
		c.JSON(http.StatusNotFound, gin.H{"error": "no entitlement"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"entitlement": ent, "pending": ent.Pending()})
}

func staffGetEntitlement(c *gin.Context) {
	var inst models.Instance
	if err := db.Admin("admin_instances").FindOne(c.Request.Context(),
		bson.M{"instance_id": c.Param("id")}).Decode(&inst); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "no such instance"})
		return
	}
	ent, err := models.GetEntitlement(c.Request.Context(), inst.InstanceID)
	if errors.Is(err, models.ErrNoEntitlement) {
		c.JSON(http.StatusNotFound, gin.H{"error": "no entitlement"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"entitlement": ent, "pending": ent.Pending()})
}

// staffSetEntitlement writes an instance's configuration.
//
// This is the endpoint that makes metering usable before Paddle exists: staff
// configure, then issue. It does NOT issue — recording what an instance is
// allowed and signing a licence for it stay separate, so a bad configuration is
// a row to correct rather than a licence to supersede.
func staffSetEntitlement(c *gin.Context) {
	ctx := c.Request.Context()
	var body entitlementBody
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid entitlement"})
		return
	}

	var inst models.Instance
	if err := db.Admin("admin_instances").FindOne(ctx,
		bson.M{"instance_id": c.Param("id")}).Decode(&inst); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "no such instance"})
		return
	}

	tier := body.Tier
	if tier == "" {
		tier = inst.Tier
	}
	plan, err := models.GetPlan(ctx, inst.Deployment, tier)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("no plan for %s/%s", inst.Deployment, tier)})
		return
	}
	if !termSold(inst.Deployment, body.Term) {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("%s does not sell %s", inst.Deployment, body.Term)})
		return
	}
	if body.Servers < plan.BaseLimits.MaxServers &&
		plan.BaseLimits.MaxServers != -1 {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": fmt.Sprintf("%s includes %d servers; cannot configure fewer",
				plan.Name, plan.BaseLimits.MaxServers)})
		return
	}

	desired := models.Config{
		Servers:  body.Servers,
		Features: models.Features(body.Features).OrEmpty(),
	}

	// Start from whatever is already granted, so writing a desired change never
	// silently alters what the instance is currently allowed.
	granted := desired
	existing, err := models.GetEntitlement(ctx, inst.InstanceID)
	switch {
	case err == nil:
		if !body.Grant {
			granted = existing.Granted
		}
	case errors.Is(err, models.ErrNoEntitlement):
		// First write. There is nothing granted to preserve, so desired becomes
		// granted — an instance with an entitlement nobody has granted would
		// fall back to the plan base at issue time and confuse everyone.
	default:
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	// Only the limits are stored. Features are NOT snapshotted onto the
	// entitlement: they live in Granted.Features, and Issue resolves them again
	// against the catalogue at signing time. Storing a second copy here would
	// give two answers to "which features does this instance have".
	limits, _, err := catalogue.Resolve(ctx, plan, granted)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	ent := models.Entitlement{
		InstanceID:     inst.InstanceID,
		AccountID:      inst.AccountID,
		Deployment:     inst.Deployment,
		Tier:           tier,
		Term:           body.Term,
		Desired:        desired,
		Granted:        granted,
		ResolvedLimits: limits,
	}
	// A reduction is a fact about the future, so it carries a date. There is no
	// billing period to read yet — plan 5 sets this from the subscription — so
	// staff-set reductions are marked as pending without one.
	if desired.Servers < granted.Servers {
		now := time.Now().UTC()
		ent.ScheduledChangeAt = &now
	}
	if existing != nil {
		ent.GrantedAt = existing.GrantedAt
	}
	if body.Grant {
		ent.GrantedAt = time.Now().UTC()
	}

	if err := models.UpsertEntitlement(ctx, ent); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	audit.Write(ctx, models.AuditEntry{
		Actor:     auth.Current(c).Email,
		Action:    "entitlement.updated",
		AccountID: inst.AccountID,
		Target:    inst.InstanceID,
		Detail: fmt.Sprintf("tier=%s term=%s desired_servers=%d granted_servers=%d granted=%t",
			tier, body.Term, desired.Servers, granted.Servers, body.Grant),
	})
	c.JSON(http.StatusOK, gin.H{"entitlement": ent, "pending": ent.Pending()})
}
