package api

import (
	"errors"
	"net/http"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/auth"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/v2/mongo"
)

type authProviderView struct {
	models.AuthProvider
	ClientSecretSet bool   `json:"client_secret_set"`
	CallbackURL     string `json:"callback_url"`
}

func viewOf(c *gin.Context, p models.AuthProvider) authProviderView {
	return authProviderView{
		AuthProvider:    p,
		ClientSecretSet: p.ClientSecretEnc != "",
		CallbackURL:     auth.CallbackURL(c, p.ProviderID),
	}
}

func listAuthPresets(c *gin.Context) {
	c.JSON(http.StatusOK, auth.Presets())
}

func listAuthProviders(c *gin.Context) {
	providers, err := services.ListAuthProviders(auth.InstanceID(c))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	out := make([]authProviderView, 0, len(providers))
	for _, p := range providers {
		out = append(out, viewOf(c, p))
	}
	c.JSON(http.StatusOK, out)
}

func createAuthProvider(c *gin.Context) {
	var body struct {
		Name         string `json:"name"`
		Preset       string `json:"preset"`
		IssuerInput  string `json:"issuer_input"`
		ClientID     string `json:"client_id"`
		ClientSecret string `json:"client_secret"`
		Enabled      bool   `json:"enabled"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	issuer, err := auth.ExpandIssuer(body.Preset, body.IssuerInput)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	instanceID := auth.InstanceID(c)
	p, err := services.CreateAuthProvider(&models.AuthProvider{
		InstanceID: instanceID,
		Name:       body.Name,
		Kind:       auth.KindFor(body.Preset),
		Preset:     body.Preset,
		Issuer:     issuer,
		ClientID:   body.ClientID,
		Scopes:     auth.DefaultScopes(body.Preset),
		Enabled:    body.Enabled,
	}, body.ClientSecret)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	services.LogEvent(instanceID, "auth_provider.create", actorFromCtx(c), "", "", p.Name)
	c.JSON(http.StatusCreated, viewOf(c, *p))
}

func updateAuthProvider(c *gin.Context) {
	var body struct {
		Name         *string `json:"name"`
		IssuerInput  *string `json:"issuer_input"`
		ClientID     *string `json:"client_id"`
		ClientSecret *string `json:"client_secret"`
		Enabled      *bool   `json:"enabled"`
		Order        *int    `json:"order"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	instanceID := auth.InstanceID(c)
	providerID := c.Param("id")

	existing, err := services.GetAuthProvider(instanceID, providerID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
		return
	}

	if err := guardProviderChange(instanceID, existing, body.Enabled, false); err != nil {
		if errors.Is(err, services.ErrLockout) {
			c.JSON(http.StatusConflict, gin.H{"error": err.Error(), "code": "last_provider"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	in := services.AuthProviderUpdate{
		Name: body.Name, ClientID: body.ClientID,
		ClientSecret: body.ClientSecret, Enabled: body.Enabled, Order: body.Order,
	}
	if body.IssuerInput != nil {
		issuer, err := auth.ExpandIssuer(existing.Preset, *body.IssuerInput)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		in.Issuer = &issuer
	}
	if err := services.UpdateAuthProvider(instanceID, providerID, in); err != nil {
		if errors.Is(err, mongo.ErrNoDocuments) {
			c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
			return
		}
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	// Issuer, client ID or secret may have changed; the cached discovery
	// document was built from the old ones.
	auth.EvictProvider(providerID)
	services.LogEvent(instanceID, "auth_provider.update", actorFromCtx(c), "", "", existing.Name)
	c.JSON(http.StatusOK, gin.H{"saved": true})
}

func deleteAuthProvider(c *gin.Context) {
	instanceID := auth.InstanceID(c)
	providerID := c.Param("id")

	existing, err := services.GetAuthProvider(instanceID, providerID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
		return
	}
	if err := guardProviderChange(instanceID, existing, nil, true); err != nil {
		if errors.Is(err, services.ErrLockout) {
			c.JSON(http.StatusConflict, gin.H{"error": err.Error(), "code": "last_provider"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if err := services.DeleteAuthProvider(instanceID, providerID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	auth.EvictProvider(providerID)
	services.LogEvent(instanceID, "auth_provider.delete", actorFromCtx(c), "", "", existing.Name)
	c.JSON(http.StatusOK, gin.H{"deleted": true})
}

// guardProviderChange asks whether the instance would still have a way in.
// Deleting and disabling reach the same condition, so they share one answer.
func guardProviderChange(instanceID string, existing *models.AuthProvider, enabled *bool, deleting bool) error {
	losing := deleting || (enabled != nil && !*enabled)
	if !losing || !existing.Enabled {
		return nil
	}
	n, err := services.CountEnabledAuthProviders(instanceID)
	if err != nil {
		return err
	}
	return services.CheckLockout(services.IsLocalLoginEnabled(instanceID), n-1)
}

func ackAuthProviderNotice(c *gin.Context) {
	instanceID := auth.InstanceID(c)
	providerID := c.Param("id")
	existing, err := services.GetAuthProvider(instanceID, providerID)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
		return
	}
	if err := services.AckAuthProviderNotice(instanceID, providerID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	services.LogEvent(instanceID, "auth_provider.ack_notice", actorFromCtx(c), "", "", existing.Name)
	c.JSON(http.StatusOK, gin.H{"acknowledged": true})
}

// testAuthProvider proves the configuration is reachable. It signs nobody in.
func testAuthProvider(c *gin.Context) {
	instanceID := auth.InstanceID(c)
	p, err := services.GetAuthProvider(instanceID, c.Param("id"))
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "provider not found"})
		return
	}
	if p.Kind == models.KindOAuth2 {
		// GitHub has no discovery document. The only meaningful check without
		// a user token is that credentials are present.
		if p.ClientID == "" || p.ClientSecretEnc == "" {
			c.JSON(http.StatusOK, gin.H{"ok": false, "message": "client ID and secret are required"})
			return
		}
		c.JSON(http.StatusOK, gin.H{"ok": true, "message": "credentials are configured"})
		return
	}
	if _, err := oidc.NewProvider(c.Request.Context(), p.Issuer); err != nil {
		c.JSON(http.StatusOK, gin.H{"ok": false, "message": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"ok": true, "message": "discovery document fetched"})
}
