package mods

import (
	"errors"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"sync"
	"time"

	"zlh-agent/internal/provision"
	"zlh-agent/internal/state"
)

var (
	modIDPattern    = regexp.MustCompile(`^[a-zA-Z0-9._-]{1,64}$`)
	filenamePattern = regexp.MustCompile(`^[a-zA-Z0-9._+-]{1,128}$`)

	cacheMu   sync.Mutex
	scanCache = map[string]cacheEntry{}
)

const (
	cacheTTL          = 5 * time.Minute
	defaultServerRoot = "/opt/zlh/minecraft/vanilla/world"
)

func ResolveServerRoot(cfg *state.Config) string {
	if v := strings.TrimSpace(os.Getenv("ZLH_SERVER_ROOT")); v != "" {
		return filepath.Clean(v)
	}
	if cfg != nil {
		return filepath.Clean(provision.ServerDir(*cfg))
	}
	return defaultServerRoot
}

func ScanMods(serverRoot string) (ScanResponse, error) {
	serverRoot = filepath.Clean(serverRoot)

	cacheMu.Lock()
	if c, ok := scanCache[serverRoot]; ok && time.Now().Before(c.ExpiresAt) {
		resp := c.Resp
		cacheMu.Unlock()
		return resp, nil
	}
	cacheMu.Unlock()

	resp, err := scanModsUncached(serverRoot)
	if err != nil {
		return ScanResponse{}, err
	}

	cacheMu.Lock()
	scanCache[serverRoot] = cacheEntry{ExpiresAt: time.Now().Add(cacheTTL), Resp: resp}
	cacheMu.Unlock()
	return resp, nil
}

func InvalidateCache(serverRoot string) {
	cacheMu.Lock()
	defer cacheMu.Unlock()
	if serverRoot == "" {
		scanCache = map[string]cacheEntry{}
		return
	}
	delete(scanCache, filepath.Clean(serverRoot))
}

func scanModsUncached(serverRoot string) (ScanResponse, error) {
	modsDir := filepath.Join(serverRoot, "mods")
	entries, err := os.ReadDir(modsDir)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return ScanResponse{
				Variant:       detectVariant(serverRoot, nil),
				Mods:          []ModInfo{},
				TotalCount:    0,
				ScanTimestamp: time.Now().UTC().Format(time.RFC3339),
			}, nil
		}
		return ScanResponse{}, err
	}

	mods := make([]ModInfo, 0, len(entries))
	var evidence []jarMetadata

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		filename := entry.Name()
		enabled, ok := modEnabledState(filename)
		if !ok || !isSafeFilename(filename) {
			continue
		}

		fullPath := filepath.Join(modsDir, filename)
		meta, err := parseJarMetadata(fullPath)
		if err == nil {
			evidence = append(evidence, meta)
		}

		id, name := fallbackNameFromFilename(filename)
		version := "unknown"
		if err == nil {
			if v := strings.TrimSpace(meta.ID); v != "" {
				id = sanitizeID(v)
			}
			if v := strings.TrimSpace(meta.Name); v != "" {
				name = v
			}
			if v := strings.TrimSpace(meta.Version); v != "" {
				version = v
			}
		}
		if !isValidModID(id) {
			id = sanitizeID(strings.TrimSuffix(strings.TrimSuffix(filename, ".jar"), ".disabled"))
		}

		mods = append(mods, ModInfo{
			ID:       id,
			Name:     name,
			Version:  version,
			Filename: filename,
			Enabled:  enabled,
			Source:   "manual",
		})
	}

	sort.Slice(mods, func(i, j int) bool { return mods[i].Filename < mods[j].Filename })

	variant, mcVersion, variantVersion := detectVariantWithVersion(serverRoot, evidence)
	return ScanResponse{
		Variant:          variant,
		MinecraftVersion: mcVersion,
		VariantVersion:   variantVersion,
		Mods:             mods,
		TotalCount:       len(mods),
		ScanTimestamp:    time.Now().UTC().Format(time.RFC3339),
	}, nil
}

func detectVariant(serverRoot string, evidence []jarMetadata) string {
	v, _, _ := detectVariantWithVersion(serverRoot, evidence)
	return v
}

func detectVariantWithVersion(serverRoot string, evidence []jarMetadata) (variant string, minecraftVersion string, variantVersion string) {
	for _, m := range evidence {
		if m.HasFabricMeta {
			return "fabric", m.MinecraftVersion, m.Version
		}
	}
	for _, m := range evidence {
		if m.HasForgeMeta {
			return "forge", m.MinecraftVersion, m.Version
		}
	}
	for _, m := range evidence {
		if m.HasPluginMeta {
			return "paper", m.MinecraftVersion, m.Version
		}
	}

	pluginsDir := filepath.Join(serverRoot, "plugins")
	if entries, err := os.ReadDir(pluginsDir); err == nil {
		for _, e := range entries {
			if !e.IsDir() && strings.HasSuffix(strings.ToLower(e.Name()), ".jar") {
				return "paper", "", ""
			}
		}
	}

	forgeChecks := []string{
		filepath.Join(serverRoot, "forge-server.toml"),
		filepath.Join(serverRoot, "forge-client.toml"),
		filepath.Join(serverRoot, "config", "forge-common.toml"),
	}
	for _, p := range forgeChecks {
		if _, err := os.Stat(p); err == nil {
			return "forge", "", ""
		}
	}

	return "unknown", "", ""
}

func modEnabledState(filename string) (enabled bool, ok bool) {
	lower := strings.ToLower(filename)
	if strings.HasSuffix(lower, ".jar") {
		return true, true
	}
	if strings.HasSuffix(lower, ".jar.disabled") {
		return false, true
	}
	return false, false
}

func isSafeFilename(name string) bool {
	if strings.Contains(name, "..") || strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.ContainsRune(name, 0) || strings.Contains(name, "~") {
		return false
	}
	for _, r := range name {
		if r <= 31 || r == 127 || r == ' ' || r == '\t' || r == '\n' || r == '\r' {
			return false
		}
	}
	return filenamePattern.MatchString(name)
}

func sanitizeID(id string) string {
	id = strings.TrimSpace(id)
	id = strings.ReplaceAll(id, " ", "_")
	id = strings.Map(func(r rune) rune {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '.' || r == '_' || r == '-' {
			return r
		}
		return '_'
	}, id)
	if len(id) > 64 {
		id = id[:64]
	}
	if id == "" {
		id = "unknown"
	}
	return id
}

func isValidModID(id string) bool {
	return modIDPattern.MatchString(id)
}

func IsValidModID(id string) bool {
	return isValidModID(id)
}

func ResolveByModID(serverRoot, modID string) (enabledName string, disabledName string, err error) {
	if !isValidModID(modID) {
		return "", "", errors.New("invalid mod_id")
	}
	resp, err := ScanMods(serverRoot)
	if err != nil {
		return "", "", err
	}
	for _, mod := range resp.Mods {
		if mod.ID != modID {
			continue
		}
		if mod.Enabled {
			enabledName = mod.Filename
		} else {
			disabledName = mod.Filename
		}
	}
	if enabledName != "" && disabledName != "" {
		return "", "", errors.New("mod has both enabled and disabled files")
	}
	if enabledName == "" && disabledName == "" {
		return "", "", os.ErrNotExist
	}
	return enabledName, disabledName, nil
}
