package mods

import (
	"crypto/sha1"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

const (
	maxDownloadSize = 200 * 1024 * 1024
	defaultTimeout  = 120 * time.Second
	maxRedirects    = 3
	tempModsDir     = "/tmp/zlh-agent/mods"
)

var allowedHosts = []string{
	"artifacts.zerolaghub.com",
	"cdn.modrinth.com",
}

func InstallCurated(serverRoot string, req InstallRequest) (ActionResponse, error) {
	source := strings.TrimSpace(strings.ToLower(req.Source))
	if source == "" {
		source = "curated"
	}
	if source != "curated" && source != "modrinth" {
		return ActionResponse{}, errors.New("unsupported source")
	}

	var downloadURL string
	var filename string
	verifyFunc := func(string) error { return nil }

	if source == "modrinth" {
		downloadURL = strings.TrimSpace(req.DownloadURL)
		filename = strings.TrimSpace(req.Filename)
		if downloadURL == "" {
			return ActionResponse{}, errors.New("download_url required for modrinth source")
		}
		if filename == "" || !isSafeFilename(filename) || !strings.HasSuffix(strings.ToLower(filename), ".jar") {
			return ActionResponse{}, errors.New("invalid filename")
		}
		if err := validateArtifactURL(downloadURL); err != nil {
			return ActionResponse{}, err
		}
		if strings.TrimSpace(req.SHA512) != "" {
			normalized, err := normalizeExpectedHash(req.SHA512, 128, "sha512")
			if err != nil {
				return ActionResponse{}, err
			}
			verifyFunc = func(path string) error { return VerifyHashWithAlgorithm(path, normalized, "sha512") }
		} else if strings.TrimSpace(req.SHA1) != "" {
			normalized, err := normalizeExpectedHash(req.SHA1, 40, "sha1")
			if err != nil {
				return ActionResponse{}, err
			}
			verifyFunc = func(path string) error { return VerifyHashWithAlgorithm(path, normalized, "sha1") }
		} else {
			return ActionResponse{}, errors.New("sha512 or sha1 required for modrinth source")
		}
	} else {
		if !isValidModID(req.ModID) {
			return ActionResponse{}, errors.New("invalid mod_id")
		}
		if err := validateArtifactURL(req.ArtifactURL); err != nil {
			return ActionResponse{}, err
		}
		if err := validateExpectedHash(req.ArtifactHash); err != nil {
			return ActionResponse{}, err
		}
		downloadURL = req.ArtifactURL
		filename = safeInstallFilename(req)
		if !isSafeFilename(filename) || !strings.HasSuffix(strings.ToLower(filename), ".jar") {
			return ActionResponse{}, errors.New("invalid artifact filename")
		}
		verifyFunc = func(path string) error { return VerifyHash(path, req.ArtifactHash) }
	}

	modsDir := filepath.Join(serverRoot, "mods")
	if err := os.MkdirAll(modsDir, 0o755); err != nil {
		return ActionResponse{}, fmt.Errorf("create mods dir: %w", err)
	}
	finalPath := filepath.Join(modsDir, filename)
	if _, err := os.Stat(finalPath); err == nil {
		return ActionResponse{}, fmt.Errorf("mod already exists: %s", filename)
	}

	if err := os.MkdirAll(tempModsDir, 0o755); err != nil {
		return ActionResponse{}, fmt.Errorf("create temp dir: %w", err)
	}

	tmpFile, err := os.CreateTemp(tempModsDir, "zlh-mod-*.jar")
	if err != nil {
		return ActionResponse{}, fmt.Errorf("create temp file: %w", err)
	}
	tmpPath := tmpFile.Name()
	defer func() {
		_ = os.Remove(tmpPath)
	}()
	if err := tmpFile.Close(); err != nil {
		return ActionResponse{}, fmt.Errorf("close temp file: %w", err)
	}

	if err := downloadArtifact(downloadURL, tmpPath); err != nil {
		return ActionResponse{}, err
	}

	if err := verifyFunc(tmpPath); err != nil {
		return ActionResponse{}, err
	}

	if err := os.Chmod(tmpPath, 0o644); err != nil {
		return ActionResponse{}, fmt.Errorf("chmod temp file: %w", err)
	}

	if err := os.Rename(tmpPath, finalPath); err != nil {
		return ActionResponse{}, fmt.Errorf("install mod: %w", err)
	}
	if err := os.Chmod(finalPath, 0o644); err != nil {
		return ActionResponse{}, fmt.Errorf("set permissions: %w", err)
	}

	InvalidateCache(serverRoot)
	return ActionResponse{Success: true, Action: "installed", RestartRequired: true}, nil
}

func SetEnabled(serverRoot, modID string, enabled bool) (ActionResponse, error) {
	modsDir := filepath.Join(serverRoot, "mods")
	enabledName, disabledName, err := ResolveByModID(serverRoot, modID)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return ActionResponse{}, os.ErrNotExist
		}
		return ActionResponse{}, err
	}

	if enabled {
		if enabledName != "" {
			return ActionResponse{Success: true, Action: "enabled", RestartRequired: true}, nil
		}
		src := filepath.Join(modsDir, disabledName)
		dstName := strings.TrimSuffix(disabledName, ".disabled")
		dst := filepath.Join(modsDir, dstName)
		if _, err := os.Stat(dst); err == nil {
			return ActionResponse{}, errors.New("cannot enable: target file already exists")
		}
		if err := os.Rename(src, dst); err != nil {
			return ActionResponse{}, fmt.Errorf("enable mod: %w", err)
		}
		if err := os.Chmod(dst, 0o644); err != nil {
			return ActionResponse{}, err
		}
		InvalidateCache(serverRoot)
		return ActionResponse{Success: true, Action: "enabled", RestartRequired: true}, nil
	}

	if disabledName != "" {
		return ActionResponse{Success: true, Action: "disabled", RestartRequired: true}, nil
	}
	src := filepath.Join(modsDir, enabledName)
	dstName := enabledName + ".disabled"
	dst := filepath.Join(modsDir, dstName)
	if _, err := os.Stat(dst); err == nil {
		return ActionResponse{}, errors.New("cannot disable: target file already exists")
	}
	if err := os.Rename(src, dst); err != nil {
		return ActionResponse{}, fmt.Errorf("disable mod: %w", err)
	}
	if err := os.Chmod(dst, 0o644); err != nil {
		return ActionResponse{}, err
	}
	InvalidateCache(serverRoot)
	return ActionResponse{Success: true, Action: "disabled", RestartRequired: true}, nil
}

func DeleteMod(serverRoot, modID string) (ActionResponse, error) {
	modsDir := filepath.Join(serverRoot, "mods")
	enabledName, disabledName, err := ResolveByModID(serverRoot, modID)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return ActionResponse{}, os.ErrNotExist
		}
		return ActionResponse{}, err
	}

	sourceName := enabledName
	if sourceName == "" {
		sourceName = disabledName
	}
	src := filepath.Join(modsDir, sourceName)

	removedDir := filepath.Join(serverRoot, "mods-removed")
	if err := os.MkdirAll(removedDir, 0o755); err != nil {
		return ActionResponse{}, fmt.Errorf("create removed dir: %w", err)
	}
	targetPath := uniqueRemovedPath(removedDir, sourceName)
	if err := os.Rename(src, targetPath); err != nil {
		return ActionResponse{}, fmt.Errorf("remove mod: %w", err)
	}
	if err := os.Chmod(targetPath, 0o644); err != nil {
		return ActionResponse{}, err
	}

	InvalidateCache(serverRoot)
	return ActionResponse{Success: true, Action: "deleted", RestartRequired: true}, nil
}

func VerifyHash(path string, expected string) error {
	if err := validateExpectedHash(expected); err != nil {
		return err
	}
	want := strings.TrimPrefix(expected, "sha256:")

	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return err
	}
	have := hex.EncodeToString(h.Sum(nil))
	if !strings.EqualFold(have, want) {
		return errors.New("sha256 mismatch")
	}
	return nil
}

func VerifyHashWithAlgorithm(path, expectedHex, algorithm string) error {
	expectedHex = strings.ToLower(strings.TrimSpace(expectedHex))
	if expectedHex == "" {
		return errors.New("missing expected hash")
	}
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	switch algorithm {
	case "sha512":
		h := sha512.New()
		if _, err := io.Copy(h, f); err != nil {
			return err
		}
		if hex.EncodeToString(h.Sum(nil)) != expectedHex {
			return errors.New("sha512 mismatch")
		}
		return nil
	case "sha1":
		h := sha1.New()
		if _, err := io.Copy(h, f); err != nil {
			return err
		}
		if hex.EncodeToString(h.Sum(nil)) != expectedHex {
			return errors.New("sha1 mismatch")
		}
		return nil
	default:
		return errors.New("unsupported hash algorithm")
	}
}

func validateArtifactURL(raw string) error {
	u, err := url.Parse(raw)
	if err != nil {
		return errors.New("invalid artifact_url")
	}
	if u.Scheme != "https" {
		return errors.New("artifact_url must use https")
	}
	if !isAllowedHost(u.Hostname()) {
		return errors.New("artifact_url host not allowed")
	}
	return nil
}

func validateExpectedHash(v string) error {
	if !strings.HasPrefix(v, "sha256:") {
		return errors.New("artifact_hash must start with sha256:")
	}
	hexPart := strings.TrimPrefix(v, "sha256:")
	if len(hexPart) != 64 {
		return errors.New("artifact_hash must include 64 hex characters")
	}
	for _, r := range hexPart {
		isHex := (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F')
		if !isHex {
			return errors.New("artifact_hash contains non-hex characters")
		}
	}
	return nil
}

func normalizeExpectedHash(raw string, expectedLen int, prefix string) (string, error) {
	v := strings.TrimSpace(strings.ToLower(raw))
	if strings.HasPrefix(v, prefix+":") {
		v = strings.TrimPrefix(v, prefix+":")
	}
	if len(v) != expectedLen {
		return "", fmt.Errorf("%s must include %d hex characters", prefix, expectedLen)
	}
	for _, r := range v {
		isHex := (r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')
		if !isHex {
			return "", fmt.Errorf("%s contains non-hex characters", prefix)
		}
	}
	return v, nil
}

func safeInstallFilename(req InstallRequest) string {
	if u, err := url.Parse(req.ArtifactURL); err == nil {
		base := filepath.Base(u.Path)
		if base != "." && base != "/" && isSafeFilename(base) {
			return base
		}
	}
	name := sanitizeID(req.ModID)
	if strings.TrimSpace(req.Version) != "" {
		name = sanitizeID(req.ModID + "-" + req.Version)
	}
	return name + ".jar"
}

func downloadArtifact(rawURL, dest string) error {
	timeout := defaultTimeout
	if v := strings.TrimSpace(os.Getenv("ZLH_MOD_DOWNLOAD_TIMEOUT")); v != "" {
		if d, err := time.ParseDuration(v); err == nil && d > 0 {
			timeout = d
		}
	}

	client := &http.Client{
		Timeout: timeout,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			// Keep downloads pinned to the curated HTTPS host, even across redirects.
			if len(via) >= maxRedirects {
				return errors.New("too many redirects")
			}
			if req.URL.Scheme != "https" {
				return errors.New("redirected to non-https url")
			}
			if !isAllowedHost(req.URL.Hostname()) {
				return errors.New("redirected to disallowed host")
			}
			return nil
		},
	}

	resp, err := client.Get(rawURL)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.Request == nil || resp.Request.URL == nil {
		return errors.New("invalid download response")
	}
	if resp.Request.URL.Scheme != "https" || !isAllowedHost(resp.Request.URL.Hostname()) {
		return errors.New("final download url not allowed")
	}
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download failed: status %d", resp.StatusCode)
	}
	if cl := resp.Header.Get("Content-Length"); cl != "" {
		n, err := strconv.ParseInt(cl, 10, 64)
		if err == nil && n > maxDownloadSize {
			return errors.New("artifact too large")
		}
	}

	out, err := os.Create(dest)
	if err != nil {
		return err
	}
	defer out.Close()

	limited := io.LimitReader(resp.Body, maxDownloadSize+1)
	written, err := io.Copy(out, limited)
	if err != nil {
		return err
	}
	if written > maxDownloadSize {
		return errors.New("artifact exceeds 200MB limit")
	}
	return nil
}

func uniqueRemovedPath(dir, filename string) string {
	candidate := filepath.Join(dir, filename)
	if _, err := os.Stat(candidate); errors.Is(err, os.ErrNotExist) {
		return candidate
	}
	base := filename
	ext := ""
	if strings.HasSuffix(filename, ".disabled") {
		base = strings.TrimSuffix(filename, ".disabled")
		ext = ".disabled"
	}
	if strings.HasSuffix(base, ".jar") {
		base = strings.TrimSuffix(base, ".jar")
		ext = ".jar" + ext
	}
	ts := time.Now().UTC().Format("20060102T150405")
	for i := 1; ; i++ {
		name := fmt.Sprintf("%s-%s-%d%s", base, ts, i, ext)
		candidate = filepath.Join(dir, name)
		if _, err := os.Stat(candidate); errors.Is(err, os.ErrNotExist) {
			return candidate
		}
	}
}

func isAllowedHost(host string) bool {
	for _, allowed := range allowedHosts {
		if strings.EqualFold(host, allowed) {
			return true
		}
	}
	return false
}
