package desktop

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"sort"
	"strings"
	"sync"

	"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)

var mimeappsWriteMu sync.Mutex

const (
	groupDefaults = "Default Applications"
	groupAdded    = "Added Associations"
	groupRemoved  = "Removed Associations"
)

type MimeAssociations struct {
	Defaults map[string]string
	Added    map[string][]string
	Removed  map[string][]string
}

func newAssociations() *MimeAssociations {
	return &MimeAssociations{
		Defaults: make(map[string]string),
		Added:    make(map[string][]string),
		Removed:  make(map[string][]string),
	}
}

func mimeappsSearchPaths() []string {
	var paths []string
	seen := make(map[string]bool)

	add := func(p string) {
		if p == "" || seen[p] {
			return
		}
		seen[p] = true
		paths = append(paths, p)
	}

	add(filepath.Join(utils.XDGConfigHome(), "mimeapps.list"))

	if env := os.Getenv("XDG_CONFIG_DIRS"); env != "" {
		for d := range strings.SplitSeq(env, ":") {
			add(filepath.Join(strings.TrimSpace(d), "mimeapps.list"))
		}
	} else {
		add("/etc/xdg/mimeapps.list")
	}

	add(filepath.Join(utils.XDGDataHome(), "applications", "mimeapps.list"))

	if env := os.Getenv("XDG_DATA_DIRS"); env != "" {
		for d := range strings.SplitSeq(env, ":") {
			add(filepath.Join(strings.TrimSpace(d), "applications", "mimeapps.list"))
		}
	} else {
		add("/usr/local/share/applications/mimeapps.list")
		add("/usr/share/applications/mimeapps.list")
	}

	return paths
}

func mimeappsWritePath() string {
	return filepath.Join(utils.XDGConfigHome(), "mimeapps.list")
}

func readAssociations(path string) (*MimeAssociations, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, err
	}

	groups := parseGroups(data)
	assoc := newAssociations()

	if g := groups[groupDefaults]; g != nil {
		for mime, val := range g.keys {
			parts := splitList(val)
			if len(parts) > 0 {
				assoc.Defaults[mime] = parts[0]
			}
		}
	}

	if g := groups[groupAdded]; g != nil {
		for mime, val := range g.keys {
			assoc.Added[mime] = splitList(val)
		}
	}

	if g := groups[groupRemoved]; g != nil {
		for mime, val := range g.keys {
			assoc.Removed[mime] = splitList(val)
		}
	}

	return assoc, nil
}

func mergedAssociations() *MimeAssociations {
	merged := newAssociations()

	for _, path := range mimeappsSearchPaths() {
		assoc, err := readAssociations(path)
		if err != nil {
			continue
		}
		for mime, app := range assoc.Defaults {
			if _, ok := merged.Defaults[mime]; !ok {
				merged.Defaults[mime] = app
			}
		}
		for mime, apps := range assoc.Added {
			merged.Added[mime] = append(merged.Added[mime], apps...)
		}
		for mime, apps := range assoc.Removed {
			merged.Removed[mime] = append(merged.Removed[mime], apps...)
		}
	}

	return merged
}

// isSafeIniField rejects values that would corrupt a key=value line in mimeapps.list
func isSafeIniField(s string) bool {
	return !strings.ContainsAny(s, "\n\r[]")
}

func writeUserMimeapps(update func(*MimeAssociations)) error {
	mimeappsWriteMu.Lock()
	defer mimeappsWriteMu.Unlock()

	path := mimeappsWritePath()

	assoc, err := readAssociations(path)
	if err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		assoc = newAssociations()
	}

	update(assoc)

	var buf bytes.Buffer
	w := bufio.NewWriter(&buf)

	var writeErr error
	writeSection := func(name string, entries map[string]string) {
		fmt.Fprintf(w, "[%s]\n", name)
		keys := make([]string, 0, len(entries))
		for k := range entries {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		for _, k := range keys {
			v := entries[k]
			if !isSafeIniField(k) || !isSafeIniField(v) {
				if writeErr == nil {
					writeErr = fmt.Errorf("invalid mimeapps.list field %q=%q", k, v)
				}
				continue
			}
			fmt.Fprintf(w, "%s=%s\n", k, v)
		}
		fmt.Fprintln(w)
	}

	flatten := func(m map[string][]string) map[string]string {
		out := make(map[string]string, len(m))
		for k, list := range m {
			out[k] = strings.Join(list, ";") + ";"
		}
		return out
	}

	writeSection(groupDefaults, assoc.Defaults)
	writeSection(groupAdded, flatten(assoc.Added))
	writeSection(groupRemoved, flatten(assoc.Removed))

	if writeErr != nil {
		return writeErr
	}

	if err := w.Flush(); err != nil {
		return err
	}

	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	return os.WriteFile(path, buf.Bytes(), 0o644)
}

func setDefaultAssociation(mimeType, desktopID string) error {
	return setDefaultAssociations([]string{mimeType}, desktopID)
}

func setDefaultAssociations(mimeTypes []string, desktopID string) error {
	if !strings.HasSuffix(desktopID, ".desktop") {
		desktopID += ".desktop"
	}
	return writeUserMimeapps(func(assoc *MimeAssociations) {
		for _, mimeType := range mimeTypes {
			if mimeType == "" {
				continue
			}
			assoc.Defaults[mimeType] = desktopID
			existing := assoc.Added[mimeType]
			if !slices.Contains(existing, desktopID) {
				assoc.Added[mimeType] = append(existing, desktopID)
			}
			removed, ok := assoc.Removed[mimeType]
			if !ok {
				continue
			}
			filtered := removed[:0]
			for _, id := range removed {
				if id != desktopID {
					filtered = append(filtered, id)
				}
			}
			switch {
			case len(filtered) == 0:
				delete(assoc.Removed, mimeType)
			default:
				assoc.Removed[mimeType] = filtered
			}
		}
	})
}
