package http

import (
	"net/http"
	"strconv"
	"strings"
)

// CORSConfig 是 gateway transport 层的浏览器跨域策略；生产可用白名单，联调环境可用 "*" 放开所有 Origin。
type CORSConfig struct {
	Enabled          bool
	AllowedOrigins   []string
	AllowedMethods   []string
	AllowedHeaders   []string
	ExposeHeaders    []string
	AllowCredentials bool
	MaxAgeSec        int64
}

// WithCORS 在业务路由之前处理浏览器 preflight，避免 OPTIONS 被登录/上传等 handler 当成业务请求。
func WithCORS(config CORSConfig, next http.Handler) http.Handler {
	config = normalizeCORSConfig(config)
	if !config.Enabled {
		return next
	}
	allowedOrigins := allowedOriginSet(config.AllowedOrigins)
	allowAnyOrigin := allowedOrigins["*"]
	allowedMethods := strings.Join(config.AllowedMethods, ", ")
	allowedHeaders := strings.Join(config.AllowedHeaders, ", ")
	exposeHeaders := strings.Join(config.ExposeHeaders, ", ")
	maxAge := strconv.FormatInt(config.MaxAgeSec, 10)

	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		origin := strings.TrimSpace(request.Header.Get("Origin"))
		if origin == "" {
			next.ServeHTTP(writer, request)
			return
		}

		if !allowAnyOrigin && !allowedOrigins[origin] {
			if isCORSPreflight(request) {
				http.Error(writer, "cors origin denied", http.StatusForbidden)
				return
			}
			next.ServeHTTP(writer, request)
			return
		}

		header := writer.Header()
		header.Add("Vary", "Origin")
		if isCORSPreflight(request) {
			header.Add("Vary", "Access-Control-Request-Method")
			header.Add("Vary", "Access-Control-Request-Headers")
		}
		if allowAnyOrigin && !config.AllowCredentials {
			header.Set("Access-Control-Allow-Origin", "*")
		} else {
			// 浏览器规范不允许 "*" 和 Allow-Credentials 同时使用；全放开且允许凭证时必须回显当前 Origin。
			header.Set("Access-Control-Allow-Origin", origin)
		}
		if config.AllowCredentials {
			header.Set("Access-Control-Allow-Credentials", "true")
		}
		if exposeHeaders != "" {
			header.Set("Access-Control-Expose-Headers", exposeHeaders)
		}

		if isCORSPreflight(request) {
			header.Set("Access-Control-Allow-Methods", allowedMethods)
			header.Set("Access-Control-Allow-Headers", allowedHeaders)
			header.Set("Access-Control-Max-Age", maxAge)
			writer.WriteHeader(http.StatusNoContent)
			return
		}

		next.ServeHTTP(writer, request)
	})
}

func isCORSPreflight(request *http.Request) bool {
	return request.Method == http.MethodOptions && strings.TrimSpace(request.Header.Get("Access-Control-Request-Method")) != ""
}

func allowedOriginSet(origins []string) map[string]bool {
	set := make(map[string]bool, len(origins))
	for _, origin := range origins {
		origin = strings.TrimRight(strings.TrimSpace(origin), "/")
		if origin != "" {
			set[origin] = true
		}
	}
	return set
}

func normalizeCORSConfig(config CORSConfig) CORSConfig {
	config.AllowedOrigins = trimStrings(config.AllowedOrigins)
	config.AllowedMethods = upperStrings(config.AllowedMethods)
	config.AllowedHeaders = trimStrings(config.AllowedHeaders)
	config.ExposeHeaders = trimStrings(config.ExposeHeaders)
	if len(config.AllowedMethods) == 0 {
		config.AllowedMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}
	}
	if len(config.AllowedHeaders) == 0 {
		// 设备资料头进入允许集，H5 客户端才能与原生共用同一套设备上报协议。
		config.AllowedHeaders = []string{"Authorization", "Content-Type", "X-Request-ID", "X-App-Code", "X-HY-App-Code", "X-App-Package", "X-Package-Name", "X-App-Bundle-ID", "X-Bundle-ID", "X-App-Platform", "X-Platform", "X-App-Version", "X-App-Build-Number", "X-Device-Model", "X-Device-Manufacturer", "X-OS-Version", "X-Device-IMEI"}
	}
	if len(config.ExposeHeaders) == 0 {
		config.ExposeHeaders = []string{"X-Request-ID", "X-Hyapp-Access-Token", "X-Hyapp-Token-Type", "X-Hyapp-Expires-In"}
	}
	if config.MaxAgeSec <= 0 {
		config.MaxAgeSec = 600
	}
	return config
}

func trimStrings(values []string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value != "" {
			out = append(out, value)
		}
	}
	return out
}

func upperStrings(values []string) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		value = strings.ToUpper(strings.TrimSpace(value))
		if value != "" {
			out = append(out, value)
		}
	}
	return out
}
