package http

import (
	"context"
	"errors"
	"log/slog"
	"net/http"
	"strings"
	"time"

	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/logx"
	"hyapp/pkg/xerr"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
)

// apiHandler 包装需要 app 解析和 access token 的业务 API。
func (h *Handler) apiHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
	return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, next))))
}

// profileAPIHandler 包装需要 app 解析、access token 和完整资料门禁的业务 API。
func (h *Handler) profileAPIHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
	return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withAuth(jwtVerifier, httpkit.RequireCompletedProfile(next)))))
}

// publicAPIHandler 包装只需要 app 解析的公开 API；登录/注册会用解析结果创建对应 App 的用户。
func (h *Handler) publicAPIHandler(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
	return httpkit.WithRequestID(withAccessLog(h.withResolvedApp(h.withOptionalAuth(jwtVerifier, next))))
}

// withAccessLog 统一记录 gateway 业务 API 的请求体、响应体、状态码和耗时。
// healthcheck 不经过该 middleware，避免探活日志淹没业务调用。
func withAccessLog(next http.Handler) http.Handler {
	return logx.HTTPMiddleware("gateway-service", httpkit.RequestIDFromContext, next)
}

// withAuth 在 transport 层完成鉴权失败响应写入。
// auth 包只返回身份校验结果，避免底层鉴权模块绑定 gateway 的 JSON envelope。
func (h *Handler) withAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		claims, err := jwtVerifier.Verify(request.Header.Get("Authorization"))
		if err != nil {
			if errors.Is(err, auth.ErrAccessTokenExpired) {
				// 只有明确 JWT exp 过期才给客户端刷新信号；签名非法、缺 token 仍保持普通 UNAUTHORIZED。
				httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeAccessTokenExpired, "access token expired")
				return
			}
			httpkit.WriteError(writer, request, http.StatusUnauthorized, httpkit.CodeUnauthorized, "unauthorized")
			return
		}
		if strings.TrimSpace(claims.SessionID) == "" {
			httpkit.WriteError(writer, request, http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized")
			return
		}

		ctx := auth.WithClaims(request.Context(), claims)
		revoked, err := h.revokedSession(ctx, claims.AppCode, claims.SessionID)
		if err != nil {
			logx.Warn(ctx, "session_denylist_read_failed", slog.String("component", "gateway_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error()))
			httpkit.WriteError(writer, request.WithContext(ctx), http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
			return
		}
		if revoked {
			httpkit.WriteError(writer, request.WithContext(ctx), http.StatusUnauthorized, string(xerr.SessionRevoked), "unauthorized")
			return
		}
		next.ServeHTTP(writer, request.WithContext(ctx))
	})
}

func (h *Handler) withOptionalAuth(jwtVerifier *auth.Verifier, next http.HandlerFunc) http.Handler {
	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		header := strings.TrimSpace(request.Header.Get("Authorization"))
		if header == "" || jwtVerifier == nil {
			next.ServeHTTP(writer, request)
			return
		}
		claims, err := jwtVerifier.Verify(header)
		if err != nil || strings.TrimSpace(claims.SessionID) == "" {
			// 公开接口不能因为旧客户端带了过期或残缺 token 就失败；无效 token 只是不参与用户态配置覆盖。
			next.ServeHTTP(writer, request)
			return
		}
		ctx := auth.WithClaims(request.Context(), claims)
		revoked, err := h.revokedSession(ctx, claims.AppCode, claims.SessionID)
		if err != nil {
			logx.Warn(ctx, "optional_session_denylist_read_failed", slog.String("component", "gateway_public_auth"), slog.String("session_id", claims.SessionID), slog.String("error", err.Error()))
			next.ServeHTTP(writer, request)
			return
		}
		if revoked {
			next.ServeHTTP(writer, request)
			return
		}
		next.ServeHTTP(writer, request.WithContext(ctx))
	})
}

func (h *Handler) revokedSession(ctx context.Context, appCode string, sessionID string) (bool, error) {
	if h.loginRiskCache == nil {
		// Handler 允许被纯 transport 单测和轻量工具独立构造；生产 app 的 configureLoginRisk 已把
		// Redis 缺失/连接失败设为启动失败，因此线上进入此分支只可能是非生产装配。
		return false, nil
	}
	return h.loginRiskCache.RevokedSessionExists(ctx, appCode, sessionID)
}

// withResolvedApp 在入口处把包名或显式 app_code 解析成内部租户键并写入 context。
// 公开接口依赖请求头解析；鉴权接口稍后会由 token claims 再覆盖一次，防止跨 App 串 token。
func (h *Handler) withResolvedApp(next http.Handler) http.Handler {
	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		appCode, err := h.resolveRequestAppCode(request.Context(), request)
		if err != nil {
			httpkit.WriteRPCError(writer, request, err)
			return
		}

		ctx := appcode.WithContext(request.Context(), appCode)
		next.ServeHTTP(writer, request.WithContext(ctx))
	})
}

func (h *Handler) resolveRequestAppCode(ctx context.Context, request *http.Request) (string, error) {
	explicitAppCode := firstHeader(request, "X-App-Code", "X-HY-App-Code", "App-Code")
	packageName := firstHeader(request, "X-App-Package", "X-Package-Name", "X-App-Bundle-ID", "X-Bundle-ID")
	platform := firstHeader(request, "X-App-Platform", "X-Platform")
	if h.appRegistryClient != nil && (explicitAppCode != "" || packageName != "") {
		resp, err := h.appRegistryClient.ResolveApp(ctx, &userv1.ResolveAppRequest{
			Meta: &userv1.RequestMeta{
				RequestId: httpkit.RequestIDFromContext(ctx),
				Caller:    "gateway-service",
				SentAtMs:  time.Now().UnixMilli(),
				AppCode:   explicitAppCode,
			},
			PackageName: packageName,
			Platform:    platform,
			AppCode:     explicitAppCode,
		})
		if err != nil {
			return "", err
		}
		if resp.GetApp() != nil {
			return appcode.Normalize(resp.GetApp().GetAppCode()), nil
		}
	}

	if appCode := fallbackAppCodeByPackage(packageName); appCode != "" {
		return appCode, nil
	}

	return appcode.Normalize(explicitAppCode), nil
}

func fallbackAppCodeByPackage(packageName string) string {
	// 注册表 client 不可用时只兜底内置 App；生产主路径仍以 user-service apps 表校验为准。
	switch strings.ToLower(strings.TrimSpace(packageName)) {
	case "com.org.laluparty":
		return "lalu"
	case "com.chat.fami":
		return "fami"
	default:
		return ""
	}
}

func firstHeader(request *http.Request, names ...string) string {
	for _, name := range names {
		value := strings.TrimSpace(request.Header.Get(name))
		if value != "" {
			return value
		}
	}

	return ""
}
