package auth

import (
	"fmt"
	"net/http"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/services"
	"github.com/gin-gonic/gin"
)

func SetSessionCookie(c *gin.Context, sessionID string) {
	secure := c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https"
	http.SetCookie(c.Writer, &http.Cookie{
		Name:     sessionCookieName,
		Value:    sessionID,
		Path:     "/",
		HttpOnly: true,
		Secure:   secure,
		SameSite: http.SameSiteLaxMode,
		MaxAge:   int(sessionTTL.Seconds()),
	})
}

// resolveLoginInstance decides which instance a sign-in attempt belongs to.
//
// Cloud always answers from the host: every instance has its own subdomain, and
// APP_LOGIN_URL fills the slug in, so every sign-in link already points at one.
// Self-hosted has no subdomain and exactly one instance, because a licence
// binds one instance UUID.
//
// Anything else is refused rather than guessed. Picking an instance on someone's
// behalf is how you sign them into the wrong tenant.
func resolveLoginInstance(c *gin.Context) (string, error) {
	if inst, ok := InstanceFromHost(c); ok {
		return inst.InstanceID, nil
	}
	n, err := services.CountInstances()
	if err != nil {
		return "", err
	}
	if n != 1 {
		return "", fmt.Errorf(
			"cannot tell which instance this sign-in is for: %d instances exist and the host %q names none of them; sign in at your instance's own address",
			n, c.Request.Host)
	}
	inst, err := services.FirstInstance()
	if err != nil {
		return "", err
	}
	return inst.InstanceID, nil
}

func HandleLocalLogin(c *gin.Context) {
	var body struct {
		Email    string `json:"email"`
		Password string `json:"password"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "email and password required"})
		return
	}
	instanceID, err := resolveLoginInstance(c)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	// The login page hides the form, but the page is a courtesy and the API is
	// the boundary.
	if !services.LocalLoginPermitted(instanceID) {
		c.JSON(http.StatusForbidden, gin.H{"error": "password sign-in is disabled for this instance"})
		return
	}
	u, err := services.GetUserInInstanceByEmail(instanceID, body.Email)
	if err != nil || !services.VerifyPassword(u, body.Password) {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
		return
	}
	sessionID, err := SaveSession(c.Request.Context(), &Session{
		UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"})
		return
	}
	_ = services.TouchLastLogin(u.UserID)
	SetSessionCookie(c, sessionID)
	c.JSON(http.StatusOK, gin.H{"ok": true})
}

// HandleListPublicProviders is unauthenticated: it is what the login page reads
// to decide what to draw. It carries no issuer, no client ID and no secret —
// only what a button needs, because anyone who can reach the login page can
// read this.
func HandleListPublicProviders(c *gin.Context) {
	type publicProvider struct {
		ID     string `json:"id"`
		Name   string `json:"name"`
		Preset string `json:"preset"`
	}
	out := []publicProvider{}

	instanceID, err := resolveLoginInstance(c)
	if err != nil {
		// An unresolvable instance is not an error the login page can act on:
		// it still has to render a password form. Answer the safe shape.
		c.JSON(http.StatusOK, gin.H{"local_enabled": true, "providers": out})
		return
	}

	// A lapsed licence stops SSO, so a button that cannot work is not offered.
	if services.GetLicenseState(instanceID).Feature("oidc") {
		providers, err := services.ListEnabledAuthProviders(instanceID)
		if err == nil {
			for _, p := range providers {
				out = append(out, publicProvider{ID: p.ProviderID, Name: p.Name, Preset: p.Preset})
			}
		}
	}

	c.JSON(http.StatusOK, gin.H{"local_enabled": services.LocalLoginPermitted(instanceID), "providers": out})
}

func HandleBootstrapStatus(c *gin.Context) {
	var (
		n        int64
		err      error
		instName string
	)
	if inst, ok := InstanceFromHost(c); ok {
		n, err = services.CountInstanceUsers(inst.InstanceID)
		instName = inst.Name
	} else {
		n, err = services.CountUsers()
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, gin.H{"needs_setup": n == 0, "instance_name": instName})
}

func HandleBootstrap(c *gin.Context) {
	n, err := services.CountUsers()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if n > 0 {
		c.JSON(http.StatusConflict, gin.H{"error": "setup already complete"})
		return
	}
	var body struct {
		InstanceName string `json:"instance_name"`
		Email        string `json:"email"`
		Password     string `json:"password"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || body.InstanceName == "" || body.Email == "" || len(body.Password) < 8 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "instance_name, email, and password (>=8 chars) required"})
		return
	}

	instanceCount, err := services.CountInstances()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	var inst *models.Instance
	switch instanceCount {
	case 0:
		inst, err = services.CreateInstance(body.InstanceName)
	case 1:
		var existing *models.Instance
		existing, err = services.FirstInstance()
		if err == nil {
			inst, err = services.AdoptInstance(existing.InstanceID, body.InstanceName)
		}
	default:
		c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf(
			"cannot bootstrap: %d organizations already exist but no users do; "+
				"create the owner against the intended inst rather than through setup, "+
				"or remove the unintended orgs and retry", instanceCount)})
		return
	}
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	u, err := services.CreateUser(inst.InstanceID, body.Email, body.Password, "owner", "local")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	sessionID, err := SaveSession(c.Request.Context(), &Session{
		UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"})
		return
	}
	SetSessionCookie(c, sessionID)
	c.JSON(http.StatusCreated, gin.H{
		"instance":    inst,
		"slug":        inst.Slug,
		"instance_id": inst.InstanceID,
	})
}

func HandleMe(c *gin.Context) {
	cookie, err := c.Request.Cookie(sessionCookieName)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "not authenticated"})
		return
	}
	sess, err := GetSession(c.Request.Context(), cookie.Value)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "session expired"})
		return
	}

	if hostInstance, ok := InstanceFromHost(c); ok && hostInstance.InstanceID != sess.InstanceID {
		c.JSON(http.StatusForbidden, gin.H{"error": "instance host mismatch"})
		return
	}

	inst, _ := services.GetInstance(sess.InstanceID)
	c.JSON(http.StatusOK, gin.H{"user": sess, "instance": inst})
}
