// Package api mounts admin's HTTP surface.
//
// The route table is the single place scoping is guaranteed. Customer routes
// live behind RequireCustomer and every handler that names an instance calls
// ownedInstance. A new customer route that skips that helper is a scoping bug,
// so keep them together and review them together.
package api

import (
	"net/http"
	"slices"

	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/auth"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/config"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/models"
	"github.com/gin-gonic/gin"
)

func Routes(cfg config.Config) http.Handler {
	r := gin.New()
	r.Use(gin.Logger(), gin.Recovery())
	r.Use(cors(cfg.AllowedOrigins))

	if cfg.TrustProxy {
		_ = r.SetTrustedProxies(nil)
	}

	r.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"ok": true}) })

	r.POST("/auth/staff/login", auth.HandleStaffLogin)
	// Every customer authenticates against admin's own customer_users. There is
	// deliberately no path that looks a customer up in the control plane by
	// email alone: HQ sign-in names no instance, so such a lookup could not be
	// scoped, and users.email is no longer globally unique.
	r.POST("/auth/login", auth.HandleCustomerLogin)
	r.POST("/auth/logout", auth.HandleLogout)
	r.GET("/auth/verify", auth.HandleVerify)
	r.GET("/auth/me", getMe)
	r.POST("/auth/signup", auth.HandleSignup)
	r.POST("/auth/accept-invite", auth.HandleAcceptInvite)

	// Public: Paddle carries no session cookie; its signature is its auth. Must
	// NOT sit under the cust group's session middleware.
	r.POST("/api/paddle/webhook", paddleWebhook(cfg))

	cust := r.Group("/api")
	cust.Use(auth.RequireCustomer())
	{
		cust.GET("/account", getAccount)

		// People. Reading is open to any member; changing anything is
		// owner-or-admin, enforced per route rather than by splitting the group,
		// so the guard is visible next to the route it guards.
		cust.GET("/account/users", listAccountUsers)
		cust.POST("/account/users",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			inviteAccountUser)
		cust.PUT("/account/users/:id/role",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			updateAccountUserRole)
		cust.DELETE("/account/users/:id",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			deleteAccountUser)

		// Any member may change their own password — it is theirs. There is no
		// endpoint for changing anyone else's.
		cust.PUT("/account/password", changeAccountPassword)

		cust.POST("/instances",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			createInstance)
		cust.POST("/instances/link", linkInstance)
		cust.POST("/instances/:id/relink", relinkInstance)
		cust.POST("/instances/:id/renew", renewInstance)
		cust.POST("/instances/:id/claim-free",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			claimFree)
		cust.GET("/instances/:id/entitlement", getEntitlement)
		cust.GET("/checkout/options", checkoutOptions)
		cust.POST("/instances/self-hosted",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			createSelfHostedPlaceholder)
		// Paid cloud: provisions a real instance the paid webhook then licenses.
		cust.POST("/instances/cloud",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			createCloudCheckout)
		cust.PUT("/instances/:id/entitlement",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			updateEntitlement)
		cust.POST("/billing/portal", billingPortal)
		cust.POST("/instances/:id/claim-link",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			claimPlaceholderLink)
		cust.GET("/instances/:id/license", getInstanceLicense)
		cust.GET("/instances/:id/license/download", downloadInstanceLicense)
		cust.GET("/instances/:id/members", listInstanceMembers)
		cust.POST("/instances/:id/members",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			grantInstanceMember)
		cust.PUT("/instances/:id/members/:uid/role",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			updateInstanceMemberRole)
		cust.DELETE("/instances/:id/members/:uid",
			auth.RequireAccountRole(models.AccountRoleOwner, models.AccountRoleAdmin),
			revokeInstanceMember)
		cust.GET("/subscriptions", listSubscriptions)
	}

	staff := r.Group("/api/staff")
	staff.Use(auth.RequireStaff())
	{
		staff.GET("/accounts", staffListAccounts)
		staff.POST("/accounts", staffCreateAccount)
		staff.GET("/accounts/:id", staffGetAccount)
		staff.POST("/accounts/:id/users", staffCreateAccountUser)
		staff.GET("/instances", staffListInstances)
		staff.POST("/instances", staffCreateInstance)
		staff.GET("/instances/:id", staffGetInstance)
		staff.GET("/subscriptions", staffListSubscriptions)
		staff.POST("/instances/:id/issue", staffIssue)
		staff.POST("/instances/:id/relink", staffRelink)
		staff.GET("/licenses", staffListLicenses)
		staff.GET("/plans", staffListPlans)
		// Plans are keyed on the pair now, so the path is too. A single :tier
		// segment could name three rows.
		staff.PUT("/plans/:deployment/:tier", staffUpdatePlan)

		staff.GET("/catalogue", staffListCatalogue)
		staff.PUT("/catalogue", staffUpdateCatalogue)

		staff.GET("/instances/:id/entitlement", staffGetEntitlement)
		staff.PUT("/instances/:id/entitlement", staffSetEntitlement)
		staff.GET("/audit", staffAudit)
		staff.GET("/health/injection", staffInjectionHealth)
		staff.GET("/health/billing", staffBillingHealth)
	}

	return r
}

func cors(allowed []string) gin.HandlerFunc {
	return func(c *gin.Context) {
		origin := c.GetHeader("Origin")
		if origin != "" && slices.Contains(allowed, origin) {
			c.Header("Access-Control-Allow-Origin", origin)
			c.Header("Access-Control-Allow-Credentials", "true")
			c.Header("Access-Control-Allow-Headers", "Content-Type")
			c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
		}
		if c.Request.Method == http.MethodOptions {
			c.AbortWithStatus(http.StatusNoContent)
			return
		}
		c.Next()
	}
}
