package api

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

	"github.com/gin-gonic/gin"
	"github.com/mrhid6/vantage/admin/internal/auth"
	"github.com/mrhid6/vantage/admin/internal/db"
	"github.com/mrhid6/vantage/admin/internal/inject"
	"github.com/mrhid6/vantage/admin/internal/licensing"
	"github.com/mrhid6/vantage/admin/internal/mail"
	"github.com/mrhid6/vantage/admin/internal/models"
	"github.com/mrhid6/vantage/shared/license"
	"go.mongodb.org/mongo-driver/v2/bson"
)

// ownedInstance resolves an instance and confirms the session's account owns it.
//
// EVERY customer handler that names an instance must go through this. It returns
// 404 for another account's instance rather than 403: a 403 confirms the
// instance exists, which is an existence oracle over customer data.
func ownedInstance(c *gin.Context, instanceID string) (*models.Instance, bool) {
	s := auth.Current(c)
	if s == nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "sign in required"})
		return nil, false
	}
	var inst models.Instance
	err := db.Admin("admin_instances").FindOne(c.Request.Context(),
		bson.M{"instance_id": instanceID, "account_id": s.AccountID}).Decode(&inst)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return nil, false
	}
	return &inst, true
}

// getMe reports who the caller is, for route guards in the UI.
//
// It is deliberately outside RequireCustomer/RequireStaff: the UI needs a
// truthful 401 to redirect on, not an error page. It reveals nothing a caller
// does not already possess, because it only ever describes their own cookie.
func getMe(c *gin.Context) {
	s := auth.Load(c)
	if s == nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "not signed in"})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"kind":       s.Kind,
		"email":      s.Email,
		"account_id": s.AccountID,
	})
}

func getAccount(c *gin.Context) {
	s := auth.Current(c)
	ctx := c.Request.Context()

	var acct models.Account
	if err := db.Admin("accounts").FindOne(ctx, bson.M{"account_id": s.AccountID}).Decode(&acct); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return
	}

	cur, err := db.Admin("admin_instances").Find(ctx, bson.M{"account_id": s.AccountID})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	instances := []models.Instance{}
	if err := cur.All(ctx, &instances); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"account":   acct,
		"instances": instances,
		// Sent rather than mirrored in the UI: a hardcoded 3 in TypeScript is a
		// second source of truth for a rule the backend enforces.
		"max_relinks": models.MaxRelinksPerTerm,
	})
}

func linkInstance(c *gin.Context) {
	var body struct {
		InstanceID string `json:"instance_id"`
		Name       string `json:"name"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "instance_id is required"})
		return
	}

	s := auth.Current(c)
	inst, err := licensing.LinkInstance(c.Request.Context(), s.AccountID, body.InstanceID, body.Name)
	if err != nil {
		status := http.StatusBadRequest
		if errors.Is(err, licensing.ErrAlreadyLinked) {
			status = http.StatusConflict
		}
		c.JSON(status, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusCreated, inst)
}

func relinkInstance(c *gin.Context) {
	inst, ok := ownedInstance(c, c.Param("id"))
	if !ok {
		return
	}
	var body struct {
		InstanceID string `json:"instance_id"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "instance_id is required"})
		return
	}

	s := auth.Current(c)
	lic, err := licensing.Relink(c.Request.Context(), s.AccountID, inst.InstanceID, body.InstanceID, false)
	if err != nil {
		status := http.StatusBadRequest
		if errors.Is(err, licensing.ErrRelinkLimit) {
			status = http.StatusForbidden
		}
		c.JSON(status, gin.H{"error": err.Error()})
		return
	}
	deliver(c, inst, lic)
	c.JSON(http.StatusOK, lic)
}

func getInstanceLicense(c *gin.Context) {
	inst, ok := ownedInstance(c, c.Param("id"))
	if !ok {
		return
	}
	var lic models.License
	if err := db.Admin("licenses").FindOne(c.Request.Context(),
		bson.M{"license_id": inst.CurrentLicense}).Decode(&lic); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "no licence issued yet"})
		return
	}
	// The owner gets the blob itself: it is signed public data bound to their
	// own instance, and the download endpoint hands over the same bytes. The
	// struct tag hides it, so the fields are listed explicitly.
	c.JSON(http.StatusOK, gin.H{
		"license_id": lic.LicenseID, "instance_id": lic.InstanceID, "tier": lic.Tier,
		"deployment": lic.Deployment, "limits": lic.Limits, "features": lic.Features,
		"issued_at": lic.IssuedAt, "expires_at": lic.ExpiresAt, "reason": lic.Reason,
		"issued_by": lic.IssuedBy, "blob": lic.Blob,
	})
}

func downloadInstanceLicense(c *gin.Context) {
	inst, ok := ownedInstance(c, c.Param("id"))
	if !ok {
		return
	}
	var lic models.License
	if err := db.Admin("licenses").FindOne(c.Request.Context(),
		bson.M{"license_id": inst.CurrentLicense}).Decode(&lic); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "no licence issued yet"})
		return
	}
	c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="vantage-%s.lic"`, inst.InstanceID))
	c.Data(http.StatusOK, "application/octet-stream", []byte(lic.Blob+"\n"))
}

func listSubscriptions(c *gin.Context) {
	s := auth.Current(c)
	cur, err := db.Admin("subscriptions").Find(c.Request.Context(), bson.M{"account_id": s.AccountID})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	subs := []models.Subscription{}
	if err := cur.All(c.Request.Context(), &subs); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, subs)
}

// deliver sends a freshly issued licence where it needs to go. Cloud instances
// are injected; self-hosted customers are emailed and can download.
//
// Delivery failures are logged, never returned: the licence is already recorded,
// which is the part that must not be lost.
func deliver(c *gin.Context, inst *models.Instance, lic *models.License) {
	if inst.Deployment == license.DeploymentCloud {
		inject.Deliver(c.Request.Context(), lic)
		return
	}
	s := auth.Current(c)
	if s != nil && mail.Enabled() {
		_ = mail.SendLicense(s.Email, inst.Name, lic.Blob)
	}
}
