package minecraft

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"syscall"

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

var (
	fabricProxyArtifactRoot = "minecraft/fabric/fabric-proxy-lite"
	fabricAPIArtifactPath   = "minecraft/fabric/fabric-api/fabric-api.jar"
	fabricProxyConfigPath   = "minecraft/fabric/FabricProxy-Lite.toml"
)

var (
	fabricProxyMapOnce sync.Once
	fabricProxyMap     map[string]string
	fabricProxyMapErr  error
)

func resolveProxyVersion(mcVersion string) (string, error) {
	mappings, err := loadFabricProxyMap()
	if err != nil {
		return "", err
	}

	mcVersion = strings.TrimSpace(mcVersion)
	if mcVersion == "" {
		return "", fmt.Errorf("empty minecraft version")
	}

	compareVersion, err := parseVersion(mcVersion)
	if err != nil {
		return "", fmt.Errorf("invalid minecraft version %q: %w", mcVersion, err)
	}

	if version, ok := mappings[mcVersion]; ok {
		return version, nil
	}

	for spec, version := range mappings {
		start, end, ok := parseVersionRange(spec)
		if !ok {
			continue
		}
		if compareVersionSlices(compareVersion, start) >= 0 && compareVersionSlices(compareVersion, end) <= 0 {
			return version, nil
		}
	}

	return "", fmt.Errorf("no fabricproxy-lite mapping for minecraft version %q", mcVersion)
}

func InjectFabricProxyLite(cfg state.Config) error {
	if !shouldInjectFabricProxyLite(cfg) {
		return nil
	}

	modsDir, targetPath, err := ensureSystemModsDir(cfg)
	if err != nil {
		log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite error=%v", cfg.VMID, err)
		return err
	}

	version, err := resolveProxyVersion(cfg.Version)
	if err != nil {
		log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite error=%v", cfg.VMID, err)
		return err
	}

	source := proxyArtifactURL(version)
	log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite source=artifact version=%s", cfg.VMID, version)
	if err := installSystemMod(source, targetPath, modsDir); err != nil {
		err = fmt.Errorf("install fabricproxy-lite: %w", err)
		log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite error=%v", cfg.VMID, err)
		return err
	}
	log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite installed=true", cfg.VMID)

	fabricAPIPath := filepath.Join(modsDir, "fabric-api.jar")
	log.Printf("[mods] vmid=%d type=system name=fabric-api source=artifact", cfg.VMID)
	if err := installSystemMod(provcommon.BuildArtifactURL(fabricAPIArtifactPath), fabricAPIPath, modsDir); err != nil {
		err = fmt.Errorf("install fabric-api: %w", err)
		log.Printf("[mods] vmid=%d type=system name=fabric-api error=%v", cfg.VMID, err)
		return err
	}
	log.Printf("[mods] vmid=%d type=system name=fabric-api installed=true", cfg.VMID)

	if err := installFabricProxyConfig(cfg); err != nil {
		log.Printf("[mods] vmid=%d type=system name=fabricproxy-lite-config error=%v", cfg.VMID, err)
		return err
	}

	return nil
}

func shouldInjectFabricProxyLite(cfg state.Config) bool {
	if !strings.EqualFold(cfg.ContainerType, "game") {
		return false
	}
	if !strings.EqualFold(cfg.Game, "minecraft") {
		return false
	}

	return strings.EqualFold(cfg.InternalProfile, "vanilla-fabric")
}

func loadFabricProxyMap() (map[string]string, error) {
	fabricProxyMapOnce.Do(func() {
		raw, err := fetchArtifact(proxyMapURL())
		if err != nil {
			fabricProxyMapErr = fmt.Errorf("read fabricproxy-lite map: %w", err)
			return
		}

		var mappings map[string]string
		if err := json.Unmarshal(raw, &mappings); err != nil {
			fabricProxyMapErr = fmt.Errorf("parse fabricproxy-lite map: %w", err)
			return
		}
		if len(mappings) == 0 {
			fabricProxyMapErr = fmt.Errorf("fabricproxy-lite map is empty")
			return
		}

		for spec, version := range mappings {
			spec = strings.TrimSpace(spec)
			version = strings.TrimSpace(version)
			if spec == "" || version == "" {
				fabricProxyMapErr = fmt.Errorf("fabricproxy-lite map contains empty key or value")
				return
			}
			if start, end, ok := parseVersionRange(spec); ok {
				if compareVersionSlices(start, end) > 0 {
					fabricProxyMapErr = fmt.Errorf("invalid fabricproxy-lite range %q", spec)
					return
				}
				continue
			}
			if _, err := parseVersion(spec); err != nil {
				fabricProxyMapErr = fmt.Errorf("invalid fabricproxy-lite version key %q: %w", spec, err)
				return
			}
		}

		fabricProxyMap = mappings
	})

	if fabricProxyMapErr != nil {
		return nil, fabricProxyMapErr
	}
	return fabricProxyMap, nil
}

func proxyMapURL() string {
	return provcommon.BuildArtifactURL(filepath.ToSlash(filepath.Join(fabricProxyArtifactRoot, "map.json")))
}

func proxyArtifactURL(version string) string {
	rel := filepath.ToSlash(filepath.Join(
		fabricProxyArtifactRoot,
		version,
		fmt.Sprintf("FabricProxy-Lite-%s.jar", version),
	))
	return provcommon.BuildArtifactURL(rel)
}

func ensureSystemModsDir(cfg state.Config) (string, string, error) {
	serverDir := provcommon.ServerDir(cfg)
	info, err := os.Stat(serverDir)
	if err != nil {
		return "", "", fmt.Errorf("stat server dir: %w", err)
	}

	modsDir := filepath.Join(serverDir, "mods")
	created := false
	if _, err := os.Stat(modsDir); err != nil {
		if !os.IsNotExist(err) {
			return "", "", fmt.Errorf("stat mods dir: %w", err)
		}
		created = true
	}
	if err := os.MkdirAll(modsDir, info.Mode().Perm()); err != nil {
		return "", "", fmt.Errorf("create mods dir: %w", err)
	}
	if err := syncOwnershipAndPerms(modsDir, info); err != nil {
		return "", "", fmt.Errorf("sync mods dir ownership: %w", err)
	}

	log.Printf("[provision] vmid=%d profile=vanilla-fabric step=mods_dir created=%t", cfg.VMID, created)
	return modsDir, filepath.Join(modsDir, "fabricproxy-lite.jar"), nil
}

func installFabricProxyConfig(cfg state.Config) error {
	serverDir := provcommon.ServerDir(cfg)
	info, err := os.Stat(serverDir)
	if err != nil {
		return fmt.Errorf("stat server dir: %w", err)
	}

	configDir := filepath.Join(serverDir, "config")
	created := false
	if _, err := os.Stat(configDir); err != nil {
		if !os.IsNotExist(err) {
			return fmt.Errorf("stat config dir: %w", err)
		}
		created = true
	}
	if err := os.MkdirAll(configDir, info.Mode().Perm()); err != nil {
		return fmt.Errorf("create config dir: %w", err)
	}
	if err := syncOwnershipAndPerms(configDir, info); err != nil {
		return fmt.Errorf("sync config dir ownership: %w", err)
	}

	targetPath := filepath.Join(configDir, "FabricProxy-Lite.toml")
	if err := installSystemFile(provcommon.BuildArtifactURL(fabricProxyConfigPath), targetPath, configDir, 0o644); err != nil {
		return fmt.Errorf("install FabricProxy-Lite.toml: %w", err)
	}
	log.Printf("[provision] vmid=%d profile=vanilla-fabric step=config_dir created=%t", cfg.VMID, created)
	log.Printf("[mods] vmid=%d type=system name=FabricProxy-Lite.toml installed=true", cfg.VMID)
	return nil
}

func parseVersionRange(spec string) ([]int, []int, bool) {
	parts := strings.Split(spec, "-")
	if len(parts) != 2 {
		return nil, nil, false
	}

	start, err := parseVersion(parts[0])
	if err != nil {
		return nil, nil, false
	}
	end, err := parseVersion(parts[1])
	if err != nil {
		return nil, nil, false
	}
	return start, end, true
}

func parseVersion(raw string) ([]int, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil, fmt.Errorf("empty version")
	}

	parts := strings.Split(raw, ".")
	out := make([]int, 0, len(parts))
	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part == "" {
			return nil, fmt.Errorf("invalid version %q", raw)
		}
		n, err := strconv.Atoi(part)
		if err != nil {
			return nil, fmt.Errorf("invalid version segment %q", part)
		}
		out = append(out, n)
	}
	return out, nil
}

func compareVersionSlices(a, b []int) int {
	maxLen := len(a)
	if len(b) > maxLen {
		maxLen = len(b)
	}

	for i := 0; i < maxLen; i++ {
		ai := 0
		if i < len(a) {
			ai = a[i]
		}
		bi := 0
		if i < len(b) {
			bi = b[i]
		}
		switch {
		case ai < bi:
			return -1
		case ai > bi:
			return 1
		}
	}
	return 0
}

func fetchArtifact(url string) ([]byte, error) {
	client := &http.Client{}
	resp, err := client.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("status %d", resp.StatusCode)
	}
	return io.ReadAll(resp.Body)
}

func installSystemMod(url, dst, modsDir string) error {
	payload, err := fetchArtifact(url)
	if err != nil {
		return err
	}

	if same, err := sameFileChecksum(dst, payload); err == nil && same {
		return syncFileOwnership(dst, modsDir)
	}

	tmp, err := os.CreateTemp(filepath.Dir(dst), "fabricproxy-lite-*.jar")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	defer func() {
		_ = os.Remove(tmpPath)
	}()

	if _, err := tmp.Write(payload); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Rename(tmpPath, dst); err != nil {
		return err
	}
	return syncFileOwnership(dst, modsDir)
}

func installSystemFile(url, dst, referenceDir string, mode os.FileMode) error {
	payload, err := fetchArtifact(url)
	if err != nil {
		return err
	}

	if same, err := sameFileChecksum(dst, payload); err == nil && same {
		return syncFileOwnershipWithMode(dst, referenceDir, mode)
	}

	tmp, err := os.CreateTemp(filepath.Dir(dst), "zlh-system-file-*")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	defer func() {
		_ = os.Remove(tmpPath)
	}()

	if _, err := tmp.Write(payload); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Rename(tmpPath, dst); err != nil {
		return err
	}
	return syncFileOwnershipWithMode(dst, referenceDir, mode)
}

func sameFileChecksum(path string, payload []byte) (bool, error) {
	current, err := os.ReadFile(path)
	if err != nil {
		return false, err
	}
	currentSum := sha256.Sum256(current)
	newSum := sha256.Sum256(payload)
	return currentSum == newSum, nil
}

func syncOwnershipAndPerms(path string, info os.FileInfo) error {
	if err := os.Chmod(path, info.Mode().Perm()); err != nil {
		return err
	}
	stat, ok := info.Sys().(*syscall.Stat_t)
	if !ok {
		return nil
	}
	return os.Chown(path, int(stat.Uid), int(stat.Gid))
}

func syncFileOwnership(path, referenceDir string) error {
	return syncFileOwnershipWithMode(path, referenceDir, 0o644)
}

func syncFileOwnershipWithMode(path, referenceDir string, mode os.FileMode) error {
	info, err := os.Stat(referenceDir)
	if err != nil {
		return err
	}
	if err := os.Chmod(path, mode); err != nil {
		return err
	}
	stat, ok := info.Sys().(*syscall.Stat_t)
	if !ok {
		return nil
	}
	return os.Chown(path, int(stat.Uid), int(stat.Gid))
}
