package auth

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"sync"

	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/gin-gonic/gin"
	"github.com/mrhid6/vantage/server/internal/services"
	"golang.org/x/oauth2"
)

var (
	provMu    sync.Mutex
	provCache = map[string]*oidc.Provider{}
)

func EvictOIDCProvider(instanceID string) {
	provMu.Lock()
	delete(provCache, instanceID)
	provMu.Unlock()
}

func redirectURL(c *gin.Context) string {
	scheme := "https"
	if c.Request.TLS == nil && c.GetHeader("X-Forwarded-Proto") != "https" {
		scheme = "http"
	}
	return fmt.Sprintf("%s://%s/auth/oidc/callback", scheme, c.Request.Host)
}

func providerForInstance(ctx context.Context, c *gin.Context, instanceID string) (*oidc.Provider, *oauth2.Config, error) {
	cfg, err := services.GetInstanceOIDC(instanceID)
	if err != nil || !cfg.Enabled {
		return nil, nil, fmt.Errorf("inst SSO not configured")
	}
	secret, err := services.GetInstanceOIDCSecret(instanceID)
	if err != nil {
		return nil, nil, err
	}
	provMu.Lock()
	p := provCache[instanceID]
	provMu.Unlock()
	if p == nil {
		p, err = oidc.NewProvider(ctx, cfg.Issuer)
		if err != nil {
			return nil, nil, err
		}
		provMu.Lock()
		provCache[instanceID] = p
		provMu.Unlock()
	}
	return p, &oauth2.Config{
		ClientID: cfg.ClientID, ClientSecret: secret,
		RedirectURL: redirectURL(c), Endpoint: p.Endpoint(),
		Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
	}, nil
}

func HandleOIDCStart(c *gin.Context) {
	inst, ok := InstanceFromHost(c)
	if !ok {
		c.JSON(http.StatusBadRequest, gin.H{"error": "unknown instance host"})
		return
	}
	// Losing the feature stops new SSO logins. It deliberately does not touch
	// session validation, so nobody is evicted mid-session.
	if !services.GetLicenseState(inst.InstanceID).Feature("oidc") {
		c.Redirect(http.StatusFound, "/login?error=oidc_unavailable")
		return
	}
	ctx := c.Request.Context()
	_, oauthCfg, err := providerForInstance(ctx, c, inst.InstanceID)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	state, err := randomHex(16)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "state gen failed"})
		return
	}
	if err := SaveStateInstance(ctx, state, inst.InstanceID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "state save failed"})
		return
	}
	c.Redirect(http.StatusFound, oauthCfg.AuthCodeURL(state))
}

func HandleOIDCCallback(c *gin.Context) {
	ctx := c.Request.Context()
	instanceID, ok := ConsumeStateInstance(ctx, c.Query("state"))
	if !ok {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid state"})
		return
	}
	provider, oauthCfg, err := providerForInstance(ctx, c, instanceID)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	token, err := oauthCfg.Exchange(ctx, c.Query("code"))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "token exchange failed"})
		return
	}
	rawIDToken, ok := token.Extra("id_token").(string)
	if !ok {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "missing id_token"})
		return
	}
	idToken, err := provider.Verifier(&oidc.Config{ClientID: oauthCfg.ClientID}).Verify(ctx, rawIDToken)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "token verification failed"})
		return
	}
	var claims struct {
		Email string `json:"email"`
		Name  string `json:"name"`
	}
	if err := idToken.Claims(&claims); err != nil || claims.Email == "" {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "claims extraction failed"})
		return
	}

	email := strings.ToLower(claims.Email)
	u, err := services.GetUserByEmail(email)
	if err != nil {

		u, err = services.CreateUser(instanceID, email, "", "member", "oidc")
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "provisioning failed"})
			return
		}
	} else if u.InstanceID != instanceID {
		c.JSON(http.StatusForbidden, gin.H{"error": "email belongs to a different organization"})
		return
	}

	sessionID, err := SaveSession(ctx, &Session{
		UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email, Name: claims.Name,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"})
		return
	}
	_ = services.TouchLastLogin(u.UserID)
	SetSessionCookie(c, sessionID)
	c.Redirect(http.StatusFound, "/")
}

func HandleLogout(c *gin.Context) {
	if cookie, err := c.Request.Cookie(sessionCookieName); err == nil {
		_ = DeleteSession(c.Request.Context(), cookie.Value)
	}
	http.SetCookie(c.Writer, &http.Cookie{
		Name:     sessionCookieName,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		MaxAge:   -1,
	})
	c.Redirect(http.StatusFound, "/")
}
