// Copyright (c) 2021-2024, Ludvig Lundgren and the autobrr contributors.
// SPDX-License-Identifier: GPL-2.0-or-later

package auth

import (
	"context"
	"crypto/rand"
	"fmt"
	"net/http"
	"strings"

	"github.com/autobrr/autobrr/internal/domain"
	"github.com/autobrr/autobrr/pkg/argon2id"
	"github.com/autobrr/autobrr/pkg/errors"

	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/gorilla/sessions"
	"github.com/rs/zerolog"
	"golang.org/x/oauth2"
)

type OIDCConfig struct {
	Enabled      bool
	Issuer       string
	ClientID     string
	ClientSecret string
	RedirectURL  string
	Scopes       []string
}

type OIDCHandler struct {
	config      *OIDCConfig
	provider    *oidc.Provider
	verifier    *oidc.IDTokenVerifier
	oauthConfig *oauth2.Config
	log         zerolog.Logger
	cookieStore *sessions.CookieStore
}

func NewOIDCHandler(cfg *domain.Config, log zerolog.Logger) (*OIDCHandler, error) {
	log.Debug().
		Bool("oidc_enabled", cfg.OIDCEnabled).
		Str("oidc_issuer", cfg.OIDCIssuer).
		Str("oidc_client_id", cfg.OIDCClientID).
		Str("oidc_redirect_url", cfg.OIDCRedirectURL).
		Str("oidc_scopes", cfg.OIDCScopes).
		Msg("initializing OIDC handler with config")

	//if !cfg.OIDCEnabled {
	//	log.Debug().Msg("OIDC is not enabled, returning nil handler")
	//	return nil, nil
	//}

	if cfg.OIDCIssuer == "" {
		log.Error().Msg("OIDC issuer is empty")
		return nil, errors.New("OIDC issuer is required")
	}

	if cfg.OIDCClientID == "" {
		log.Error().Msg("OIDC client ID is empty")
		return nil, errors.New("OIDC client ID is required")
	}

	if cfg.OIDCClientSecret == "" {
		log.Error().Msg("OIDC client secret is empty")
		return nil, errors.New("OIDC client secret is required")
	}

	if cfg.OIDCRedirectURL == "" {
		log.Error().Msg("OIDC redirect URL is empty")
		return nil, errors.New("OIDC redirect URL is required")
	}

	scopes := []string{"openid", "profile", "email"}

	issuer := cfg.OIDCIssuer
	ctx := context.Background()

	// First try with original issuer
	provider, err := oidc.NewProvider(ctx, issuer)
	if err != nil {
		// If failed and issuer ends with slash, try without
		if strings.HasSuffix(issuer, "/") {
			withoutSlash := strings.TrimRight(issuer, "/")
			log.Debug().
				Str("original_issuer", issuer).
				Str("retry_issuer", withoutSlash).
				Msg("retrying OIDC provider initialization without trailing slash")

			provider, err = oidc.NewProvider(ctx, withoutSlash)
		} else {
			// If failed and issuer doesn't end with slash, try with
			withSlash := issuer + "/"
			log.Debug().Str("original_issuer", issuer).Str("retry_issuer", withSlash).Msg("retrying OIDC provider initialization with trailing slash")

			provider, err = oidc.NewProvider(ctx, withSlash)
		}

		if err != nil {
			log.Error().Err(err).Msg("failed to initialize OIDC provider")
			return nil, errors.Wrap(err, "failed to initialize OIDC provider")
		}
	}

	var claims struct {
		AuthURL  string `json:"authorization_endpoint"`
		TokenURL string `json:"token_endpoint"`
		JWKSURL  string `json:"jwks_uri"`
		UserURL  string `json:"userinfo_endpoint"`
	}
	if err := provider.Claims(&claims); err != nil {
		log.Warn().Err(err).Msg("failed to parse provider claims for endpoints")
	} else {
		log.Debug().Str("authorization_endpoint", claims.AuthURL).Str("token_endpoint", claims.TokenURL).Str("jwks_uri", claims.JWKSURL).Str("userinfo_endpoint", claims.UserURL).Msg("discovered OIDC provider endpoints")
	}

	oidcConfig := &oidc.Config{
		ClientID: cfg.OIDCClientID,
	}

	stateSecret := generateRandomState()

	handler := &OIDCHandler{
		log: log,
		config: &OIDCConfig{
			Enabled:      cfg.OIDCEnabled,
			Issuer:       cfg.OIDCIssuer,
			ClientID:     cfg.OIDCClientID,
			ClientSecret: cfg.OIDCClientSecret,
			RedirectURL:  cfg.OIDCRedirectURL,
			Scopes:       scopes,
		},
		provider: provider,
		verifier: provider.Verifier(oidcConfig),
		oauthConfig: &oauth2.Config{
			ClientID:     cfg.OIDCClientID,
			ClientSecret: cfg.OIDCClientSecret,
			RedirectURL:  cfg.OIDCRedirectURL,
			Endpoint:     provider.Endpoint(),
			Scopes:       scopes,
		},
		cookieStore: sessions.NewCookieStore([]byte(stateSecret)),
	}

	log.Debug().Msg("OIDC handler initialized successfully")
	return handler, nil
}

func (h *OIDCHandler) GetConfig() *OIDCConfig {
	if h == nil {
		return &OIDCConfig{
			Enabled: false,
		}
	}
	h.log.Debug().Bool("enabled", h.config.Enabled).Str("issuer", h.config.Issuer).Msg("returning OIDC config")
	return h.config
}

func (h *OIDCHandler) HandleLogin(w http.ResponseWriter, r *http.Request) {
	session, err := h.cookieStore.Get(r, "user_session")
	if err != nil {
		h.log.Error().Err(err).Msg("failed to get user session")
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if session.Values["authenticated"] == true {
		h.log.Debug().Msg("user already has valid session, skipping OIDC login")
		http.Redirect(w, r, "/", http.StatusFound)
		return
	}

	state := generateRandomState()
	h.SetStateCookie(w, r, state)

	authURL := h.oauthConfig.AuthCodeURL(state)
	http.Redirect(w, r, authURL, http.StatusFound)
}

func (h *OIDCHandler) HandleCallback(w http.ResponseWriter, r *http.Request) (string, error) {
	h.log.Debug().Msg("handling OIDC callback")

	// get state from session
	session, err := h.cookieStore.Get(r, "oidc_state")
	if err != nil {
		h.log.Error().Err(err).Msg("state session not found")
		return "", errors.New("state session not found")
	}

	expectedState, ok := session.Values["state"].(string)
	if !ok {
		h.log.Error().Msg("state not found in session")
		return "", errors.New("state not found in session")
	}

	if r.URL.Query().Get("state") != expectedState {
		h.log.Error().Str("expected", expectedState).Str("got", r.URL.Query().Get("state")).Msg("state did not match")
		return "", errors.New("state did not match")
	}

	// clear the state session after use
	session.Options.MaxAge = -1
	if err := session.Save(r, w); err != nil {
		h.log.Error().Err(err).Msg("failed to clear state session")
	}

	code := r.URL.Query().Get("code")
	if code == "" {
		h.log.Error().Msg("authorization code is missing from callback request")
		return "", errors.New("authorization code is missing from callback request")
	}

	oauth2Token, err := h.oauthConfig.Exchange(r.Context(), code)
	if err != nil {
		h.log.Error().Err(err).Msg("failed to exchange token")
		return "", errors.Wrap(err, "failed to exchange token")
	}

	rawIDToken, ok := oauth2Token.Extra("id_token").(string)
	if !ok {
		h.log.Error().Msg("no id_token found in oauth2 token")
		return "", errors.New("no id_token found in oauth2 token")
	}

	idToken, err := h.verifier.Verify(r.Context(), rawIDToken)
	if err != nil {
		h.log.Error().Err(err).Msg("failed to verify ID Token")
		return "", errors.Wrap(err, "failed to verify ID Token")
	}

	var claims struct {
		Email     string `json:"email"`
		Username  string `json:"preferred_username"`
		Name      string `json:"name"`
		GivenName string `json:"given_name"`
		Nickname  string `json:"nickname"`
		Sub       string `json:"sub"`
	}
	if err := idToken.Claims(&claims); err != nil {
		h.log.Error().Err(err).Msg("failed to parse claims")
		return "", errors.Wrap(err, "failed to parse claims")
	}

	// Try different claims in order of preference for username
	// This is solely used for frontend display
	username := claims.Username
	if username == "" {
		if claims.Nickname != "" {
			username = claims.Nickname
		} else if claims.Name != "" {
			username = claims.Name
		} else if claims.Email != "" {
			username = claims.Email
		} else if claims.Sub != "" {
			username = claims.Sub
		} else {
			username = "oidc_user"
		}
	}

	h.log.Debug().Str("username", username).Str("email", claims.Email).Str("nickname", claims.Nickname).Str("name", claims.Name).Str("sub", claims.Sub).Msg("successfully processed OIDC claims")

	return username, nil
}

func generateRandomState() string {
	b, err := argon2id.GenerateRandomBytes(32)
	if err != nil {
		b = make([]byte, 32)
		rand.Read(b)
	}
	return fmt.Sprintf("%x", b)
}

func (h *OIDCHandler) GetAuthorizationURL() string {
	if h == nil {
		return ""
	}
	state := generateRandomState()
	return h.oauthConfig.AuthCodeURL(state)
}

type GetConfigResponse struct {
	Enabled          bool   `json:"enabled"`
	AuthorizationURL string `json:"authorizationUrl"`
	State            string `json:"state"`
}

func (h *OIDCHandler) GetConfigResponse() GetConfigResponse {
	if h == nil {
		return GetConfigResponse{
			Enabled: false,
		}
	}

	state := generateRandomState()
	authURL := h.oauthConfig.AuthCodeURL(state)

	h.log.Debug().Bool("enabled", h.config.Enabled).Str("authorization_url", authURL).Str("state", state).Msg("returning OIDC config response")

	return GetConfigResponse{
		Enabled:          h.config.Enabled,
		AuthorizationURL: authURL,
		State:            state,
	}
}

// SetStateCookie sets a secure cookie containing the OIDC state parameter.
// The state parameter is verified when the OAuth provider redirects back to our callback.
// Short expiration ensures the authentication flow must be completed in a reasonable timeframe.
func (h *OIDCHandler) SetStateCookie(w http.ResponseWriter, r *http.Request, state string) {
	session, _ := h.cookieStore.New(r, "oidc_state")
	session.Values["state"] = state
	session.Options.MaxAge = 300
	session.Options.HttpOnly = true
	session.Options.Secure = r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https"
	session.Options.SameSite = http.SameSiteLaxMode
	session.Options.Path = "/"

	if err := session.Save(r, w); err != nil {
		h.log.Error().Err(err).Msg("failed to save state session")
	}
}
