package mods

import (
	"archive/zip"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"path/filepath"
	"strings"
)

const (
	maxMetadataEntrySize = 2 * 1024 * 1024
	maxCompressionRatio  = 200
)

type jarMetadata struct {
	ID               string
	Name             string
	Version          string
	HasFabricMeta    bool
	HasForgeMeta     bool
	HasPluginMeta    bool
	MinecraftVersion string
}

func parseJarMetadata(path string) (jarMetadata, error) {
	r, err := zip.OpenReader(path)
	if err != nil {
		return jarMetadata{}, err
	}
	defer r.Close()

	var out jarMetadata
	for _, f := range r.File {
		name := strings.ToLower(f.Name)

		switch name {
		case "fabric.mod.json":
			out.HasFabricMeta = true
			b, err := readZipEntryLimited(f, maxMetadataEntrySize)
			if err == nil {
				mergeFabricMetadata(&out, b)
			}
		case "meta-inf/mods.toml":
			out.HasForgeMeta = true
			b, err := readZipEntryLimited(f, maxMetadataEntrySize)
			if err == nil {
				mergeModsTOMLMetadata(&out, b)
			}
		case "mcmod.info":
			b, err := readZipEntryLimited(f, maxMetadataEntrySize)
			if err == nil {
				mergeMCModInfoMetadata(&out, b)
			}
		case "plugin.yml":
			out.HasPluginMeta = true
			b, err := readZipEntryLimited(f, maxMetadataEntrySize)
			if err == nil {
				mergePluginYAMLMetadata(&out, b)
			}
		}
	}

	if out.ID == "" && out.Name == "" && out.Version == "" && !out.HasFabricMeta && !out.HasForgeMeta && !out.HasPluginMeta {
		return out, errors.New("no metadata found")
	}
	return out, nil
}

func readZipEntryLimited(f *zip.File, limit int64) ([]byte, error) {
	if f.UncompressedSize64 > uint64(limit) {
		return nil, fmt.Errorf("zip entry too large: %s", f.Name)
	}
	if f.CompressedSize64 > 0 && f.UncompressedSize64/f.CompressedSize64 > maxCompressionRatio {
		return nil, fmt.Errorf("zip entry compression ratio too high: %s", f.Name)
	}

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

	lr := io.LimitReader(rc, limit+1)
	b, err := io.ReadAll(lr)
	if err != nil {
		return nil, err
	}
	if int64(len(b)) > limit {
		return nil, fmt.Errorf("zip entry too large after read: %s", f.Name)
	}
	return b, nil
}

func mergeFabricMetadata(out *jarMetadata, b []byte) {
	var payload struct {
		ID      string      `json:"id"`
		Name    string      `json:"name"`
		Version string      `json:"version"`
		Depends interface{} `json:"depends"`
	}
	if err := json.Unmarshal(b, &payload); err != nil {
		return
	}
	if out.ID == "" {
		out.ID = strings.TrimSpace(payload.ID)
	}
	if out.Name == "" {
		out.Name = strings.TrimSpace(payload.Name)
	}
	if out.Version == "" {
		out.Version = strings.TrimSpace(payload.Version)
	}

	switch d := payload.Depends.(type) {
	case map[string]any:
		if v, ok := d["minecraft"]; ok {
			out.MinecraftVersion = strings.TrimSpace(fmt.Sprint(v))
		}
	}
}

func mergeModsTOMLMetadata(out *jarMetadata, b []byte) {
	lines := strings.Split(string(b), "\n")
	for _, raw := range lines {
		line := strings.TrimSpace(raw)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		key, val, ok := splitKV(line)
		if !ok {
			continue
		}
		switch key {
		case "modId":
			if out.ID == "" {
				out.ID = val
			}
		case "displayName":
			if out.Name == "" {
				out.Name = val
			}
		case "version":
			if out.Version == "" {
				out.Version = val
			}
		case "loaderVersion":
			if out.MinecraftVersion == "" {
				out.MinecraftVersion = val
			}
		}
	}
}

func mergeMCModInfoMetadata(out *jarMetadata, b []byte) {
	var arr []map[string]any
	if err := json.Unmarshal(b, &arr); err == nil && len(arr) > 0 {
		mergeModInfoMap(out, arr[0])
		return
	}
	var obj map[string]any
	if err := json.Unmarshal(b, &obj); err == nil {
		mergeModInfoMap(out, obj)
	}
}

func mergeModInfoMap(out *jarMetadata, obj map[string]any) {
	if out.ID == "" {
		out.ID = strings.TrimSpace(fmt.Sprint(obj["modid"]))
	}
	if out.Name == "" {
		out.Name = strings.TrimSpace(fmt.Sprint(obj["name"]))
	}
	if out.Version == "" {
		out.Version = strings.TrimSpace(fmt.Sprint(obj["version"]))
	}
	if out.MinecraftVersion == "" {
		out.MinecraftVersion = strings.TrimSpace(fmt.Sprint(obj["mcversion"]))
	}
}

func mergePluginYAMLMetadata(out *jarMetadata, b []byte) {
	lines := strings.Split(string(b), "\n")
	for _, raw := range lines {
		line := strings.TrimSpace(raw)
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		key, val, ok := splitKV(line)
		if !ok {
			continue
		}
		switch strings.ToLower(key) {
		case "name":
			if out.Name == "" {
				out.Name = val
			}
		case "version":
			if out.Version == "" {
				out.Version = val
			}
		}
	}
}

func splitKV(line string) (string, string, bool) {
	sep := "="
	idx := strings.Index(line, sep)
	if idx == -1 {
		sep = ":"
		idx = strings.Index(line, sep)
		if idx == -1 {
			return "", "", false
		}
	}
	k := strings.TrimSpace(line[:idx])
	v := strings.TrimSpace(line[idx+1:])
	v = strings.Trim(v, `"'`)
	if k == "" || v == "" {
		return "", "", false
	}
	return k, v, true
}

func fallbackNameFromFilename(filename string) (id string, name string) {
	base := strings.TrimSuffix(filename, filepath.Ext(filename))
	base = strings.TrimSuffix(base, ".jar")
	base = strings.TrimSuffix(base, ".disabled")
	base = strings.TrimSpace(base)
	if base == "" {
		base = "unknown"
	}
	return sanitizeID(base), base
}
