package config

import (
	"os"
	"path/filepath"
	"slices"
	"strings"
)

type Histories map[HistoryKey]*History

func NewHistories() *Histories {
	return &Histories{}
}

// NOTE: History objects are created only once.
func (h Histories) GetHistory(key HistoryKey, unique bool) *History {
	if m, ok := h[key]; ok {
		return m
	} else {
		m := &History{
			key:          key,
			unique:       unique,
			loaded:       HistoryEntries{},
			append:       HistoryEntries{},
			loadedUnique: uniqueMap{},
			appendUnique: uniqueMap{},
		}
		h[key] = m
		return m
	}
}

// key is status.mode: `search`, `exec jj` or `exec sh`
type HistoryKey string

// for simplicity we just store an []string per key.
// it has most recently used items at the end.
type HistoryEntries []string

type History struct {
	key HistoryKey

	// content is loaded once and lazily from fs
	loaded HistoryEntries

	// only content generated by the current runtime
	// will be appended to filesystem on termination.
	append HistoryEntries

	// appendOnly becomes true after read from fs
	appendOnly bool

	// unique optimizations
	unique       bool
	loadedUnique uniqueMap
	appendUnique uniqueMap
}

func (h *History) Entries() HistoryEntries {
	if !h.appendOnly {
		h.appendOnly = true
		h.loaded = h.load()
		if h.unique {
			h.loaded, h.loadedUnique = h.dedup(h.loaded)
		}
	}
	return append(h.loaded, h.append...)
}

func (h *History) Append(line string) {
	if h.unique {
		// check first, most likely smaller
		if _, ok := h.appendUnique[line]; ok {
			return
		}
		if _, ok := h.loadedUnique[line]; ok {
			return
		}
		h.appendUnique[line] = true
	}
	h.append = append(h.append, line)
}

// Should be called ONLY at program termination to
// flush new history entries into the filesystem.
func (h Histories) Flush() {
	for _, history := range h {
		history.flush()
	}
}

func (h *History) load() HistoryEntries {
	file := h.historyFile()
	data, err := os.ReadFile(file)
	if err != nil {
		return HistoryEntries{}
	}
	return HistoryEntries(strings.Split(string(data), "\n"))
}

func (h *History) flush() {
	content := slices.DeleteFunc(h.append, func(entry string) bool {
		return len(strings.TrimSpace(entry)) == 0
	})
	if len(content) == 0 {
		return
	}
	file := h.historyFile()
	basedir := filepath.Dir(file)
	if err := os.MkdirAll(basedir, 0755); err != nil {
		return // do nothing, could not have directory
	}
	f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return
	}
	defer f.Close()
	data := []byte(strings.Join([]string(content), "\n") + "\n")
	f.Write(data)
}

func (h *History) historyFile() string {
	return filepath.Join(h.historyDir(), strings.ReplaceAll(string(h.key), " ", "_"))
}

func (h *History) historyDir() string {
	var cacheDir string
	if dir, err := os.UserCacheDir(); err == nil {
		cacheDir = dir
	} else {
		cacheDir = os.TempDir()
	}
	return filepath.Join(cacheDir, "jjui", "history")
}

type uniqueMap map[string]any

func (h *History) dedup(entries HistoryEntries) (HistoryEntries, uniqueMap) {
	seen := uniqueMap{}
	entries = slices.DeleteFunc(entries, func(line string) bool {
		if _, ok := seen[line]; ok {
			return true
		}
		seen[line] = nil
		return false
	})
	return entries, seen
}
