package server

import (
	"log/slog"
	"mime"
	"net/http"
	"os"
	"path"
	"strings"
	"time"

	"kra/internal/conf"
	websocket "kra/internal/integration/websocket"
	"kra/internal/server/handler"
	"kra/internal/server/httpx"
	"kra/internal/server/middleware"
	"kra/internal/server/router"
	"kra/internal/service"
	platformmodule "kra/pkg/module"

	"github.com/gin-gonic/gin"
	kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
)

func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine {
	return NewGinEngineWithRuntime(runtime, access, auth, security, audit, logger, version, platformmodule.NewRuntime(router.NewRoutes(handlers)), nil)
}

func NewGinEngineWithRuntime(runtime *conf.Runtime, access *service.AccessControlService, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string, routes *platformmodule.Runtime, ws *websocket.Server) *gin.Engine {
	gin.SetMode(gin.ReleaseMode)
	engine := gin.New()
	if err := engine.SetTrustedProxies(nil); err != nil && logger != nil {
		logger.Error("disable trusted proxies failed", "error", err)
	}
	engine.Use(middleware.RequestMeta(), middleware.Recovery(logger), middleware.AccessLog(runtime, logger, version), middleware.CORS(runtime), middleware.ErrorAudit(logger), middleware.SecurityRateLimit(security))

	prefix := ""
	config := runtime.Admin()
	if config != nil {
		prefix = strings.TrimSuffix(config.RouterPrefix, "/")
	}
	public := engine.Group(prefix)
	public.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, "ok") })

	private := engine.Group(prefix)
	// The reference administration behavior installs operation recording after JWT,
	// password and Casbin/data-scope middleware. Keeping the recorder after the
	// equivalent Kra guards avoids persisting rejected unauthenticated or
	// unauthorized requests as successful business operations.
	private.Use(middleware.Auth(auth), middleware.MustChangePassword(), middleware.AccessControl(runtime, access), middleware.OperationAudit(runtime, audit))
	if routes != nil {
		routes.RegisterRoutes(public, private, engine)
	}
	if ws != nil && ws.Enabled() {
		path := ws.Path()
		engine.GET(path, func(c *gin.Context) {
			if !ws.Enabled() || c.Request.URL.Path != ws.Path() {
				c.Status(http.StatusNotFound)
				return
			}
			if err := ws.HandleRequest(c.Writer, c.Request); err != nil && logger != nil {
				logger.Warn("websocket request failed", "mod", "websocket", "error", err)
			}
		})
	}
	registerSwagger(engine, prefix, version, logger)
	registerLocalStorage(engine, runtime)
	if logger != nil {
		for _, route := range engine.Routes() {
			logger.Info("router registered", "method", route.Method, "path", route.Path)
		}
		logger.Info("router register success", "route_count", len(engine.Routes()))
	}

	engine.NoRoute(func(c *gin.Context) {
		if ws != nil && ws.Enabled() && c.Request.Method == http.MethodGet && c.Request.URL.Path == ws.Path() {
			if err := ws.HandleRequest(c.Writer, c.Request); err != nil && logger != nil {
				logger.Warn("websocket request failed", "mod", "websocket", "error", err)
			}
			return
		}
		if serveLocalStorage(c, runtime) {
			return
		}
		httpx.Fail(c, "请求的接口不存在")
	})
	return engine
}

func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server {
	network, address := "tcp", ":8000"
	if c != nil && c.Http != nil {
		if c.Http.Network != "" {
			network = c.Http.Network
		}
		if c.Http.Addr != "" {
			address = c.Http.Addr
		}
	}
	timeout := 10 * time.Minute
	if c != nil && c.Http != nil && c.Http.Timeout != nil && c.Http.Timeout.AsDuration() > 0 {
		timeout = c.Http.Timeout.AsDuration()
	}
	server := kratoshttp.NewServer(kratoshttp.Network(network), kratoshttp.Address(address), kratoshttp.Timeout(timeout))
	server.HandlePrefix("/", engine)
	server.Server.ReadHeaderTimeout = 10 * time.Second
	server.Server.ReadTimeout = timeout
	server.Server.WriteTimeout = timeout
	server.Server.IdleTimeout = 2 * time.Minute
	server.Server.MaxHeaderBytes = 1 << 20
	return server
}

// serveLocalStorage resolves the local path for every request so a config
// reload takes effect without rebuilding the Gin engine.
func serveLocalStorage(c *gin.Context, runtime *conf.Runtime) bool {
	config := runtime.Admin()
	if config == nil || config.Local == nil || config.Local.StorePath == "" {
		return false
	}
	prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
	return serveLocalStorageAt(c, runtime, prefix)
}

func registerLocalStorage(engine *gin.Engine, runtime *conf.Runtime) {
	config := runtime.Admin()
	if config == nil || config.Local == nil || config.Local.StorePath == "" || strings.Trim(config.Local.PathPrefix, "/") == "" {
		return
	}
	if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
		return
	}
	prefix := "/" + strings.Trim(config.Local.PathPrefix, "/")
	if localStorageRouteConflicts(engine.Routes(), prefix) {
		return
	}
	handler := func(c *gin.Context) {
		if !serveLocalStorageAt(c, runtime, prefix) {
			c.Status(http.StatusNotFound)
		}
	}
	engine.GET(prefix+"/*filepath", handler)
	engine.HEAD(prefix+"/*filepath", handler)
}

func localStorageRouteConflicts(routes []gin.RouteInfo, prefix string) bool {
	staticRoot := strings.Split(strings.TrimPrefix(prefix, "/"), "/")[0]
	for _, route := range routes {
		if route.Method != http.MethodGet && route.Method != http.MethodHead {
			continue
		}
		routeRoot := strings.Split(strings.TrimPrefix(route.Path, "/"), "/")[0]
		if routeRoot == staticRoot {
			return true
		}
	}
	return false
}

func serveLocalStorageAt(c *gin.Context, runtime *conf.Runtime, prefix string) bool {
	config := runtime.Admin()
	if config == nil || config.Local == nil || config.Local.StorePath == "" {
		return false
	}
	if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
		return false
	}
	if currentPrefix := "/" + strings.Trim(config.Local.PathPrefix, "/"); currentPrefix != prefix {
		return false
	}
	if prefix == "/" || (c.Request.URL.Path != prefix && !strings.HasPrefix(c.Request.URL.Path, prefix+"/")) {
		return false
	}
	c.Header("X-Content-Type-Options", "nosniff")
	filename := path.Base(c.Request.URL.Path)
	if !canServeUploadInline(filename) {
		c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
	}
	http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.Local.StorePath)})).ServeHTTP(c.Writer, c.Request)
	return true
}

func canServeUploadInline(filename string) bool {
	switch strings.ToLower(path.Ext(filename)) {
	case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".ico", ".avif",
		".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac",
		".mp4", ".webm", ".mov", ".avi", ".mkv":
		return true
	default:
		return false
	}
}

type filesOnly struct{ http.FileSystem }

func (f filesOnly) Open(name string) (http.File, error) {
	file, err := f.FileSystem.Open(name)
	if err != nil {
		return nil, err
	}
	info, statErr := file.Stat()
	if statErr != nil {
		_ = file.Close()
		return nil, statErr
	}
	if info.IsDir() {
		_ = file.Close()
		return nil, os.ErrPermission
	}
	return file, nil
}
