package httpapi

import (
	"context"
	"crypto/rand"
	"encoding/base64"
	"errors"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/oidcsession"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
	"github.com/google/uuid"
)

const (
	errorCodeOIDCBrowserSessionDisabled  = "OIDC_BROWSER_SESSION_DISABLED"
	errorCodeOIDCSessionInvalid          = "OIDC_SESSION_INVALID"
	errorCodeOIDCSessionExpired          = "OIDC_SESSION_EXPIRED"
	errorCodeOIDCSessionStoreUnavailable = "OIDC_SESSION_STORE_UNAVAILABLE"
	errorCodeOIDCSessionCSRF             = "OIDC_SESSION_CSRF_REJECTED"
	errorCodeOIDCLoginInvalid            = "OIDC_LOGIN_INVALID"
	errorCodeOIDCTokenExchangeFailed     = "OIDC_TOKEN_EXCHANGE_FAILED"

	oidcLoginFailureCookieMissing                = "COOKIE_MISSING"
	oidcLoginFailureTransactionInvalid           = "TRANSACTION_INVALID"
	oidcLoginFailureStateMismatch                = "STATE_MISMATCH"
	oidcLoginFailureAuthorizationResponseMissing = "AUTHORIZATION_RESPONSE_MISSING"
)

// startOIDCLogin godoc
// @Summary 开始 OIDC 公共客户端登录
// @Description Gateway 生成 state、nonce 和 PKCE S256 参数，并跳转认证中心；浏览器不接触 Token。
// @Tags auth
// @Param returnTo query string false "登录后返回的站内相对路径"
// @Param contextType query string false "兼容旧入口的显式登录上下文：platform 或 tenant；省略时由认证中心选择"
// @Param tenantHint query string false "Tenant 上下文可选的稳定 Tenant UUID"
// @Success 303
// @Failure 400 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /api/v1/auth/oidc/login [get]
func (s *Server) startOIDCLogin(w http.ResponseWriter, r *http.Request) {
	runtime := s.currentIdentityRuntime()
	if !oidcRuntimeReady(runtime) {
		writeError(w, http.StatusNotFound, "OIDC browser session is disabled", errorCodeOIDCBrowserSessionDisabled)
		return
	}
	returnTo := strings.TrimSpace(r.URL.Query().Get("returnTo"))
	if returnTo == "" {
		returnTo = "/"
	}
	contextType := strings.TrimSpace(r.URL.Query().Get("contextType"))
	tenantHint := strings.TrimSpace(r.URL.Query().Get("tenantHint"))
	validContext := contextType == "" ||
		contextType == "tenant" ||
		contextType == "platform" &&
			runtime.Revision.TenantMode == "multi_tenant"
	validTenantHint := tenantHint == "" ||
		contextType == "tenant" &&
			runtime.Revision.TenantMode == "multi_tenant" &&
			uuid.Validate(tenantHint) == nil
	if !validContext || !validTenantHint {
		writeError(w, http.StatusBadRequest, "登录上下文无效", errorCodeOIDCLoginInvalid)
		return
	}
	transaction, err := oidcsession.NewLoginTransactionWithContext(
		returnTo, contextType, tenantHint, time.Now(),
	)
	if err != nil {
		writeError(w, http.StatusBadRequest, "登录后返回地址无效", errorCodeOIDCLoginInvalid)
		return
	}
	encoded, err := runtime.SessionCipher.EncodeLoginTransaction(transaction)
	if err != nil {
		s.logger.ErrorContext(r.Context(), "encode OIDC login transaction failed", "error", err)
		writeError(w, http.StatusServiceUnavailable, "登录会话初始化失败，请稍后重试", errorCodeOIDCSessionStoreUnavailable)
		return
	}
	authorizationURL, err := runtime.PublicClient.AuthorizationURL(
		r.Context(), transaction.State, transaction.Nonce,
		transaction.PKCEVerifier, transaction.ContextType,
		transaction.TenantHint,
	)
	if err != nil {
		s.logger.ErrorContext(r.Context(), "load OIDC authorization endpoint failed", "error", err)
		writeError(w, http.StatusServiceUnavailable, "认证中心暂时不可用，请稍后重试", "OIDC_AUTHORIZATION_UNAVAILABLE")
		return
	}
	http.SetCookie(w, &http.Cookie{
		Name: oidcsession.LoginTransactionCookieName, Value: encoded,
		Path: s.oidcCallbackCookiePath(), MaxAge: 600, Expires: time.Now().Add(10 * time.Minute),
		HttpOnly: true, Secure: runtime.CookieSecure, SameSite: http.SameSiteLaxMode,
	})
	w.Header().Set("Cache-Control", "no-store")
	http.Redirect(w, r, authorizationURL, http.StatusSeeOther)
}

// completeOIDCLogin godoc
// @Summary 完成 OIDC 公共客户端登录
// @Description Gateway 使用 client_id、授权码和 PKCE verifier 换取 Token，不发送 Client Secret；Token 加密存入服务端 Session。
// @Tags auth
// @Success 303
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Router /api/v1/auth/oidc/callback [get]
func (s *Server) completeOIDCLogin(w http.ResponseWriter, r *http.Request) {
	runtime := s.currentIdentityRuntime()
	if !oidcRuntimeReady(runtime) {
		writeError(w, http.StatusNotFound, "OIDC browser session is disabled", errorCodeOIDCBrowserSessionDisabled)
		return
	}
	s.clearOIDCLoginCookie(w)
	cookie, err := r.Cookie(oidcsession.LoginTransactionCookieName)
	if err != nil {
		s.writeOIDCLoginTransactionError(w, r, "登录事务无效或已过期", oidcLoginFailureCookieMissing)
		return
	}
	transaction, err := runtime.SessionCipher.DecodeLoginTransaction(cookie.Value, time.Now())
	if err != nil {
		s.writeOIDCLoginTransactionError(w, r, "登录事务校验失败，请重新登录", oidcLoginFailureTransactionInvalid)
		return
	}
	if r.URL.Query().Get("state") == "" || r.URL.Query().Get("state") != transaction.State {
		s.writeOIDCLoginTransactionError(w, r, "登录事务校验失败，请重新登录", oidcLoginFailureStateMismatch)
		return
	}
	if r.URL.Query().Get("code") == "" {
		s.writeOIDCLoginTransactionError(w, r, "认证中心回调缺少必要参数，请重新登录", oidcLoginFailureAuthorizationResponseMissing)
		return
	}
	tokens, err := runtime.PublicClient.ExchangeCode(r.Context(), r.URL.Query().Get("code"), transaction.PKCEVerifier)
	if stage, category, failed := classifyOIDCTokenExchangeFailure(tokens, err); failed {
		s.writeOIDCTokenFailure(w, r, stage, category, "认证中心登录结果无效，请重新登录")
		return
	}
	identity, err := runtime.Verifier.Verify(r.Context(), tokens.AccessToken)
	if err != nil || identity == nil {
		s.writeOIDCTokenFailure(w, r, "ACCESS_TOKEN_INVALID", auth.OIDCValidationCategory(err), "认证中心访问令牌校验失败")
		return
	}
	if !oidcLoginContextMatches(transaction, identity) {
		s.writeOIDCTokenFailure(
			w, r, "ACCESS_TOKEN_CONTEXT_MISMATCH",
			"STABLE_IDENTITY_CLAIMS_INVALID",
			"认证中心访问令牌上下文与登录入口不匹配",
		)
		return
	}
	idSubject, err := runtime.PublicClient.VerifyIDToken(r.Context(), tokens.IDToken, transaction.Nonce)
	if err != nil || idSubject != identity.ID {
		s.writeOIDCTokenFailure(w, r, "ID_TOKEN_INVALID", auth.OIDCValidationCategory(err), "认证中心身份令牌校验失败")
		return
	}
	projection, err := s.resolveOIDCUserProjectionForRuntime(r.Context(), r, identity, runtime)
	if err != nil {
		s.writeOIDCCallbackProjectionError(w, r, err)
		return
	}
	if projection.User == nil || projection.User.GatewayUserID == "" {
		s.writeOIDCUserResolutionError(w, r, errors.New("OIDC user resolver returned no local user"))
		return
	}
	rawSession, err := runtime.Sessions.Create(r.Context(), oidcsession.TokenBundle{
		AccessToken: tokens.AccessToken, RefreshToken: tokens.RefreshToken, IDToken: tokens.IDToken,
	}, projection.User)
	if err != nil {
		s.writeOIDCSessionFailure(w, r, err)
		return
	}
	if err := s.replaceOIDCBrowserSession(r.Context(), r, runtime, rawSession); err != nil {
		if _, cleanupErr := runtime.Sessions.Delete(r.Context(), rawSession); cleanupErr != nil {
			s.logger.WarnContext(r.Context(), "cleanup replacement OIDC session failed", "error", cleanupErr)
		}
		s.writeOIDCCallbackError(w, r, http.StatusServiceUnavailable, "旧登录会话清理失败，请稍后重试", errorCodeOIDCSessionStoreUnavailable)
		return
	}
	now := time.Now()
	http.SetCookie(w, &http.Cookie{
		Name: auth.OIDCSessionCookieName, Value: rawSession, Path: "/",
		MaxAge: runtime.Revision.SessionAbsoluteSeconds, Expires: now.Add(time.Duration(runtime.Revision.SessionAbsoluteSeconds) * time.Second),
		HttpOnly: true, Secure: runtime.CookieSecure, SameSite: http.SameSiteStrictMode,
	})
	w.Header().Set("Cache-Control", "no-store")
	s.recordOIDCSessionAudit(r, projection.User)
	http.Redirect(w, r, oidcReturnLocation(runtime.Revision.WebBaseURL, transaction.ReturnTo), http.StatusSeeOther)
}

func oidcLoginContextMatches(
	transaction oidcsession.LoginTransaction,
	identity *auth.User,
) bool {
	return identity != nil &&
		(transaction.ContextType == "" ||
			identity.ContextType == transaction.ContextType) &&
		(transaction.TenantHint == "" ||
			identity.TenantID == transaction.TenantHint)
}

func (s *Server) replaceOIDCBrowserSession(
	ctx context.Context,
	r *http.Request,
	runtime *identityRequestRuntime,
	newRawSession string,
) error {
	previous, err := r.Cookie(auth.OIDCSessionCookieName)
	if errors.Is(err, http.ErrNoCookie) {
		return nil
	}
	if err != nil {
		return err
	}
	if previous.Value == "" || previous.Value == newRawSession {
		return nil
	}
	bundle, err := runtime.Sessions.Delete(ctx, previous.Value)
	if err != nil {
		return err
	}
	if bundle.RefreshToken != "" {
		if err := runtime.PublicClient.RevokeRefreshToken(ctx, bundle.RefreshToken); err != nil {
			s.logger.WarnContext(ctx, "revoke replaced OIDC refresh token failed", "error", err)
		}
	}
	return nil
}

// logoutOIDCSession godoc
// @Summary 注销 OIDC 登录会话
// @Description 删除 Gateway Session、撤销公共 Client Refresh Token，并跳转认证中心退出地址。
// @Tags auth
// @Success 303
// @Failure 403 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Router /api/v1/auth/oidc/logout [post]
func (s *Server) logoutOIDCSession(w http.ResponseWriter, r *http.Request) {
	runtime := s.currentIdentityRuntime()
	if !oidcRuntimeReady(runtime) {
		writeError(w, http.StatusNotFound, "OIDC browser session is disabled", errorCodeOIDCBrowserSessionDisabled)
		return
	}
	var bundle oidcsession.TokenBundle
	if cookie, err := r.Cookie(auth.OIDCSessionCookieName); err == nil {
		bundle, err = runtime.Sessions.Delete(r.Context(), cookie.Value)
		if err != nil {
			writeError(w, http.StatusServiceUnavailable, "登录会话存储暂时不可用", errorCodeOIDCSessionStoreUnavailable)
			return
		}
	}
	s.clearOIDCSessionCookie(w)
	if bundle.RefreshToken != "" {
		if err := runtime.PublicClient.RevokeRefreshToken(r.Context(), bundle.RefreshToken); err != nil {
			s.logger.WarnContext(r.Context(), "revoke OIDC refresh token failed", "error", err)
		}
	}
	// Do not put the encrypted-at-rest ID Token into a browser-visible redirect URL.
	location, err := runtime.PublicClient.EndSessionURL(r.Context(), "")
	if err != nil {
		location = runtime.Revision.WebBaseURL + "/"
	}
	w.Header().Set("Cache-Control", "no-store")
	http.Redirect(w, r, location, http.StatusSeeOther)
}

// deleteOIDCBrowserSession godoc
// @Summary 删除本地 OIDC 浏览器会话
// @Tags auth
// @Success 204
// @Failure 503 {object} ErrorEnvelope
// @Router /api/v1/auth/oidc/session [delete]
func (s *Server) deleteOIDCBrowserSession(w http.ResponseWriter, r *http.Request) {
	runtime := s.currentIdentityRuntime()
	if runtime != nil && runtime.Sessions != nil {
		if cookie, err := r.Cookie(auth.OIDCSessionCookieName); err == nil {
			if _, err := runtime.Sessions.Delete(r.Context(), cookie.Value); err != nil {
				writeError(w, http.StatusServiceUnavailable, "登录会话存储暂时不可用", errorCodeOIDCSessionStoreUnavailable)
				return
			}
		}
	}
	s.clearOIDCSessionCookie(w)
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(http.StatusNoContent)
}

func (s *Server) oidcBrowserSessionReady() bool {
	return oidcRuntimeReady(s.currentIdentityRuntime())
}

func oidcRuntimeReady(runtime *identityRequestRuntime) bool {
	return runtime != nil && runtime.BrowserEnabled && runtime.Verifier != nil && runtime.PublicClient != nil && runtime.Sessions != nil && runtime.SessionCipher != nil
}

func (s *Server) clearOIDCLoginCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name: oidcsession.LoginTransactionCookieName, Value: "", Path: s.oidcCallbackCookiePath(),
		Expires: time.Unix(1, 0), MaxAge: -1, HttpOnly: true, Secure: s.oidcCookieSecure(), SameSite: http.SameSiteLaxMode,
	})
}

func (s *Server) oidcCallbackCookiePath() string {
	return "/api/v1/auth/oidc/callback"
}

func (s *Server) clearOIDCSessionCookie(w http.ResponseWriter) {
	http.SetCookie(w, &http.Cookie{
		Name: auth.OIDCSessionCookieName, Value: "", Path: "/",
		Expires: time.Unix(1, 0), MaxAge: -1, HttpOnly: true, Secure: s.oidcCookieSecure(), SameSite: http.SameSiteStrictMode,
	})
}

func (s *Server) oidcReturnLocation(returnTo string) string {
	if runtime := s.currentIdentityRuntime(); runtime != nil {
		return oidcReturnLocation(runtime.Revision.WebBaseURL, returnTo)
	}
	return returnTo
}

func oidcReturnLocation(webBaseURL, returnTo string) string {
	if base := strings.TrimRight(strings.TrimSpace(webBaseURL), "/"); base != "" {
		return base + returnTo
	}
	return returnTo
}

func (s *Server) writeOIDCCallbackError(w http.ResponseWriter, r *http.Request, status int, message, code string) {
	s.writeOIDCCallbackErrorWithDiagnostics(w, r, status, message, code, "", "")
}

func (s *Server) writeOIDCTokenFailure(w http.ResponseWriter, r *http.Request, stage, validationCategory, message string) {
	diagnosticID := newOIDCDiagnosticID()
	if s.logger != nil {
		s.logger.WarnContext(r.Context(), "OIDC token processing rejected",
			"event", "oidc_token_processing_rejected",
			"stage", stage,
			"validationCategory", validationCategory,
			"diagnosticId", diagnosticID,
		)
	}
	s.writeOIDCCallbackErrorWithDiagnostics(
		w,
		r,
		http.StatusUnauthorized,
		message,
		errorCodeOIDCTokenExchangeFailed,
		validationCategory,
		diagnosticID,
	)
}

func (s *Server) writeOIDCSessionFailure(w http.ResponseWriter, r *http.Request, err error) {
	category := oidcsession.SessionCreationCategory(err)
	diagnosticID := newOIDCDiagnosticID()
	if s.logger != nil {
		s.logger.WarnContext(r.Context(), "OIDC session creation rejected",
			"event", "oidc_session_creation_rejected",
			"category", category,
			"diagnosticId", diagnosticID,
		)
	}
	s.writeOIDCCallbackErrorWithDiagnostics(
		w,
		r,
		http.StatusServiceUnavailable,
		"登录会话保存失败，请稍后重试",
		errorCodeOIDCSessionStoreUnavailable,
		category,
		diagnosticID,
	)
}

func classifyOIDCTokenExchangeFailure(tokens auth.OIDCTokenResponse, err error) (stage, category string, failed bool) {
	switch {
	case err != nil:
		return "TOKEN_ENDPOINT_REJECTED", "TOKEN_ENDPOINT_REQUEST_FAILED", true
	case strings.TrimSpace(tokens.AccessToken) == "":
		return "TOKEN_RESPONSE_INVALID", "ACCESS_TOKEN_MISSING", true
	case strings.TrimSpace(tokens.RefreshToken) == "":
		return "TOKEN_RESPONSE_INVALID", "REFRESH_TOKEN_MISSING", true
	case strings.TrimSpace(tokens.IDToken) == "":
		return "TOKEN_RESPONSE_INVALID", "ID_TOKEN_MISSING", true
	default:
		return "", "", false
	}
}

func (s *Server) writeOIDCLoginTransactionError(w http.ResponseWriter, r *http.Request, message, reason string) {
	diagnosticID := newOIDCDiagnosticID()
	if s.logger != nil {
		s.logger.WarnContext(r.Context(), "OIDC login transaction rejected",
			"event", "oidc_login_transaction_rejected",
			"reason", reason,
			"diagnosticId", diagnosticID,
		)
	}
	s.writeOIDCCallbackErrorWithDiagnostics(w, r, http.StatusBadRequest, message, errorCodeOIDCLoginInvalid, reason, diagnosticID)
}

func (s *Server) writeOIDCCallbackErrorWithDiagnostics(w http.ResponseWriter, r *http.Request, status int, message, code, reason, diagnosticID string) {
	base := ""
	if runtime := s.currentIdentityRuntime(); runtime != nil {
		base = strings.TrimRight(strings.TrimSpace(runtime.Revision.WebBaseURL), "/")
	}
	if parsed, err := url.Parse(base); base != "" && err == nil && parsed.Host != "" && (parsed.Scheme == "https" || parsed.Scheme == "http" && (parsed.Hostname() == "localhost" || parsed.Hostname() == "127.0.0.1")) {
		query := parsed.Query()
		query.Set("oidcError", code)
		if reason != "" {
			query.Set("oidcErrorReason", reason)
		}
		if diagnosticID != "" {
			query.Set("oidcDiagnosticId", diagnosticID)
		}
		parsed.RawQuery = query.Encode()
		http.Redirect(w, r, parsed.String(), http.StatusSeeOther)
		return
	}
	if reason != "" || diagnosticID != "" {
		writeErrorWithDetails(w, status, message, map[string]any{
			"reason": reason, "diagnosticId": diagnosticID,
		}, code)
		return
	}
	writeError(w, status, message, code)
}

func newOIDCDiagnosticID() string {
	var value [12]byte
	if _, err := rand.Read(value[:]); err == nil {
		return base64.RawURLEncoding.EncodeToString(value[:])
	}
	return base64.RawURLEncoding.EncodeToString([]byte(time.Now().UTC().Format(time.RFC3339Nano)))
}

func (s *Server) writeOIDCCallbackProjectionError(w http.ResponseWriter, r *http.Request, err error) {
	switch {
	case errors.Is(err, store.ErrOIDCUserNotProvisioned):
		s.writeOIDCCallbackError(w, r, http.StatusForbidden, "该账号尚未开通 EasyAI Gateway", errorCodeGatewayUserNotProvisioned)
	case errors.Is(err, store.ErrOIDCUserDisabled):
		s.writeOIDCCallbackError(w, r, http.StatusForbidden, "该 Gateway 账号已停用，请联系管理员", errorCodeGatewayUserDisabled)
	case errors.Is(err, store.ErrOIDCTenantUnavailable):
		s.writeOIDCCallbackError(w, r, http.StatusServiceUnavailable, "Gateway 租户尚未就绪，请联系管理员", errorCodeGatewayTenantUnavailable)
	default:
		s.logger.ErrorContext(r.Context(), "resolve OIDC gateway user during callback failed", "error", err)
		s.writeOIDCCallbackError(w, r, http.StatusServiceUnavailable, "Gateway 账号初始化失败，请稍后重试", errorCodeGatewayProvisioningFailed)
	}
}

func (s *Server) recordOIDCSessionAudit(r *http.Request, user *auth.User) {
	if s.store == nil || user == nil {
		return
	}
	audit, err := s.store.RecordAuditLog(r.Context(), store.AuditLogInput{
		Category: "identity", Action: "identity.oidc_session.created",
		ActorGatewayUserID: user.GatewayUserID, ActorUsername: user.Username, ActorSource: "oidc", ActorRoles: user.Roles,
		TargetType: "gateway_user", TargetID: user.GatewayUserID,
		TargetGatewayUserID: user.GatewayUserID, TargetGatewayTenantID: user.GatewayTenantID,
		RequestIP: limitAuditText(requestIP(r), 128), UserAgent: limitAuditText(r.UserAgent(), 512),
		Metadata: map[string]any{"sessionMode": "public-client-pkce-bff"},
	})
	if err != nil {
		s.logger.WarnContext(r.Context(), "record OIDC session audit failed", "error", err)
		return
	}
	s.logger.InfoContext(r.Context(), "OIDC server session created", "gatewayUserId", user.GatewayUserID, "auditId", audit.ID)
}

func (s *Server) startOIDCSessionCleanup(ctx context.Context) {
	go func() {
		ticker := time.NewTicker(15 * time.Minute)
		defer ticker.Stop()
		for {
			select {
			case <-ctx.Done():
				return
			case <-ticker.C:
				runtime := s.currentIdentityRuntime()
				if runtime == nil || runtime.Sessions == nil {
					continue
				}
				if _, err := runtime.Sessions.Cleanup(ctx); err != nil {
					s.logger.WarnContext(ctx, "cleanup expired OIDC sessions failed", "error", err)
				}
			}
		}
	}()
}

func (s *Server) protectOIDCSessionCookie(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		runtime := s.currentIdentityRuntime()
		if runtime == nil || !runtime.BrowserEnabled || hasExplicitCredential(r) {
			next.ServeHTTP(w, r)
			return
		}
		if _, err := r.Cookie(auth.OIDCSessionCookieName); err != nil {
			next.ServeHTTP(w, r)
			return
		}
		origin := strings.TrimSpace(r.Header.Get("Origin"))
		if origin != "" && !originMatchesBaseURL(origin, runtime.Revision.WebBaseURL) || origin == "" && !isSafeHTTPMethod(r.Method) {
			writeError(w, http.StatusForbidden, "browser session request origin was rejected", errorCodeOIDCSessionCSRF)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func isSafeHTTPMethod(method string) bool {
	switch method {
	case http.MethodGet, http.MethodHead, http.MethodOptions:
		return true
	default:
		return false
	}
}

func hasExplicitCredential(r *http.Request) bool {
	return extractBearerCredential(r.Header.Get("Authorization")) != "" ||
		strings.TrimSpace(r.Header.Get("x-comfy-api-key")) != "" ||
		strings.TrimSpace(r.Header.Get("x-goog-api-key")) != "" ||
		strings.HasPrefix(strings.TrimSpace(r.URL.Query().Get("key")), "sk-")
}

func extractBearerCredential(value string) string {
	fields := strings.Fields(value)
	if len(fields) == 2 && strings.EqualFold(fields[0], "bearer") {
		return fields[1]
	}
	return ""
}
