// Package staticfiles serves files from the configured local upload store.
// It owns the Gin/storage adapter so the server root only has to compose it.
package staticfiles

import (
	"mime"
	"net/http"
	"os"
	"path"
	"strings"

	"kra/internal/conf"

	"github.com/gin-gonic/gin"
)

// Register adds local upload routes when local storage is enabled. A route is
// skipped when its root conflicts with an existing GET/HEAD route.
func Register(engine *gin.Engine, runtime *conf.Runtime) {
	config := localConfig(runtime)
	if config == nil || strings.Trim(config.PathPrefix, "/") == "" {
		return
	}
	prefix := "/" + strings.Trim(config.PathPrefix, "/")
	if localStorageRouteConflicts(engine.Routes(), prefix) {
		return
	}
	handler := func(c *gin.Context) {
		if !serveAt(c, runtime, prefix) {
			c.Status(http.StatusNotFound)
		}
	}
	engine.GET(prefix+"/*filepath", handler)
	engine.HEAD(prefix+"/*filepath", handler)
}

// Serve resolves the current runtime configuration on every request so a
// configuration reload takes effect without rebuilding the Gin engine.
func Serve(c *gin.Context, runtime *conf.Runtime) bool {
	config := localConfig(runtime)
	if config == nil {
		return false
	}
	prefix := "/" + strings.Trim(config.PathPrefix, "/")
	return serveAt(c, runtime, prefix)
}

func localConfig(runtime *conf.Runtime) *conf.AdminBackend_Local {
	if runtime == nil {
		return nil
	}
	config := runtime.Admin()
	if config == nil || config.Local == nil || config.Local.StorePath == "" {
		return nil
	}
	if config.Storage != nil && config.Storage.Type != "" && config.Storage.Type != "local" {
		return nil
	}
	return config.Local
}

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 serveAt(c *gin.Context, runtime *conf.Runtime, prefix string) bool {
	config := localConfig(runtime)
	if config == nil || "/"+strings.Trim(config.PathPrefix, "/") != 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 !canServeInline(filename) {
		c.Header("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}))
	}
	http.StripPrefix(prefix, http.FileServer(filesOnly{FileSystem: http.Dir(config.StorePath)})).ServeHTTP(c.Writer, c.Request)
	return true
}

func canServeInline(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
}
