package histfile

import (
	"math"
	"os"
	"strconv"
	"sync"

	"github.com/curusarn/resh/internal/histcli"
	"github.com/curusarn/resh/internal/histlist"
	"github.com/curusarn/resh/internal/recio"
	"github.com/curusarn/resh/internal/recordint"
	"github.com/curusarn/resh/internal/records"
	"github.com/curusarn/resh/internal/recutil"
	"github.com/curusarn/resh/record"
	"go.uber.org/zap"
)

// TODO: get rid of histfile - use histio instead
// Histfile writes records to histfile
type Histfile struct {
	sugar *zap.SugaredLogger

	sessionsMutex sync.Mutex
	sessions      map[string]recordint.Collect
	historyPath   string

	// NOTE: we have separate histories which only differ if there was not enough resh_history
	//			resh_history itself is common for both bash and zsh
	bashCmdLines histlist.Histlist
	zshCmdLines  histlist.Histlist

	cliRecords histcli.Histcli

	rio *recio.RecIO
}

// New creates new histfile and runs its gorutines
func New(sugar *zap.SugaredLogger, input chan recordint.Collect, sessionsToDrop chan string,
	reshHistoryPath string, bashHistoryPath string, zshHistoryPath string,
	maxInitHistSize int, minInitHistSizeKB int,
	signals chan os.Signal, shutdownDone chan string) *Histfile {

	rio := recio.New(sugar.With("module", "histfile"))
	hf := Histfile{
		sugar:        sugar.With("module", "histfile"),
		sessions:     map[string]recordint.Collect{},
		historyPath:  reshHistoryPath,
		bashCmdLines: histlist.New(sugar),
		zshCmdLines:  histlist.New(sugar),
		cliRecords:   histcli.New(),
		rio:          &rio,
	}
	go hf.loadHistory(bashHistoryPath, zshHistoryPath, maxInitHistSize, minInitHistSizeKB)
	go hf.writer(input, signals, shutdownDone)
	go hf.sessionGC(sessionsToDrop)
	return &hf
}

// load records from resh history, reverse, enrich and save
func (h *Histfile) loadCliRecords(recs []recordint.Indexed) {
	for _, cmdline := range h.bashCmdLines.List {
		h.cliRecords.AddCmdLine(cmdline)
	}
	for _, cmdline := range h.zshCmdLines.List {
		h.cliRecords.AddCmdLine(cmdline)
	}
	for i := len(recs) - 1; i >= 0; i-- {
		rec := recs[i]
		h.cliRecords.AddRecord(&rec)
	}
	h.sugar.Infow("Resh history loaded",
		"historyRecordsCount", len(h.cliRecords.List),
	)
}

// loadsHistory from resh_history and if there is not enough of it also load native shell histories
func (h *Histfile) loadHistory(bashHistoryPath, zshHistoryPath string, maxInitHistSize, minInitHistSizeKB int) {
	h.sugar.Infow("Checking if resh_history is large enough ...")
	fi, err := os.Stat(h.historyPath)
	var size int
	if err != nil {
		h.sugar.Errorw("Failed to stat resh_history file", "error", err)
	} else {
		size = int(fi.Size())
	}
	useNativeHistories := false
	if size/1024 < minInitHistSizeKB {
		useNativeHistories = true
		h.sugar.Warnw("Resh_history is too small - loading native bash and zsh history ...")
		h.bashCmdLines = records.LoadCmdLinesFromBashFile(h.sugar, bashHistoryPath)
		h.sugar.Infow("Bash history loaded", "cmdLineCount", len(h.bashCmdLines.List))
		h.zshCmdLines = records.LoadCmdLinesFromZshFile(h.sugar, zshHistoryPath)
		h.sugar.Infow("Zsh history loaded", "cmdLineCount", len(h.zshCmdLines.List))
		// no maxInitHistSize when using native histories
		maxInitHistSize = math.MaxInt32
	}
	h.sugar.Debugw("Loading resh history from file ...",
		"historyFile", h.historyPath,
	)
	history, err := h.rio.ReadAndFixFile(h.historyPath, 3)
	if err != nil {
		h.sugar.Panicf("Failed to read file: %w", err)
	}
	h.sugar.Infow("Resh history loaded from file",
		"historyFile", h.historyPath,
		"recordCount", len(history),
	)
	go h.loadCliRecords(history)
	// NOTE: keeping this weird interface for now because we might use it in the future
	//       when we only load bash or zsh history
	reshCmdLines := loadCmdLines(h.sugar, history)
	h.sugar.Infow("Resh history loaded and processed",
		"recordCount", len(reshCmdLines.List),
	)
	if useNativeHistories == false {
		h.bashCmdLines = reshCmdLines
		h.zshCmdLines = histlist.Copy(reshCmdLines)
		return
	}
	h.bashCmdLines.AddHistlist(reshCmdLines)
	h.sugar.Infow("Processed bash history and resh history together", "cmdLinecount", len(h.bashCmdLines.List))
	h.zshCmdLines.AddHistlist(reshCmdLines)
	h.sugar.Infow("Processed zsh history and resh history together", "cmdLineCount", len(h.zshCmdLines.List))
}

// sessionGC reads sessionIDs from channel and deletes them from histfile struct
func (h *Histfile) sessionGC(sessionsToDrop chan string) {
	for {
		func() {
			session := <-sessionsToDrop
			sugar := h.sugar.With("sessionID", session)
			sugar.Debugw("Got session to drop")
			h.sessionsMutex.Lock()
			defer h.sessionsMutex.Unlock()
			if part1, found := h.sessions[session]; found == true {
				sugar.Infow("Dropping session")
				delete(h.sessions, session)
				go h.rio.AppendToFile(h.historyPath, []record.V1{part1.Rec})
			} else {
				sugar.Infow("No hanging parts for session - nothing to drop")
			}
		}()
	}
}

// writer reads records from channel, merges them and writes them to file
func (h *Histfile) writer(collect chan recordint.Collect, signals chan os.Signal, shutdownDone chan string) {
	for {
		func() {
			select {
			case rec := <-collect:
				part := "2"
				if rec.Rec.PartOne {
					part = "1"
				}
				sugar := h.sugar.With(
					"recordCmdLine", rec.Rec.CmdLine,
					"recordPart", part,
					"recordShell", rec.Shell,
				)
				sugar.Debugw("Got record")
				h.sessionsMutex.Lock()
				defer h.sessionsMutex.Unlock()

				// allows nested sessions to merge records properly
				mergeID := rec.SessionID + "_" + strconv.Itoa(rec.Shlvl)
				sugar = sugar.With("mergeID", mergeID)
				if rec.Rec.PartOne {
					if _, found := h.sessions[mergeID]; found {
						msg := "Got another first part of the records before merging the previous one - overwriting!"
						if rec.Shell == "zsh" {
							sugar.Warnw(msg)
						} else {
							sugar.Infow(msg + " Unfortunately this is normal in bash, it can't be prevented.")
						}
					}
					h.sessions[mergeID] = rec
				} else {
					if part1, found := h.sessions[mergeID]; found == false {
						sugar.Warnw("Got second part of record and nothing to merge it with - ignoring!")
					} else {
						delete(h.sessions, mergeID)
						go h.mergeAndWriteRecord(sugar, part1, rec)
					}
				}
			case sig := <-signals:
				sugar := h.sugar.With(
					"signal", sig.String(),
				)
				sugar.Infow("Got signal")
				h.sessionsMutex.Lock()
				defer h.sessionsMutex.Unlock()
				sugar.Debugw("Unlocked mutex")

				for sessID, rec := range h.sessions {
					sugar.Warnw("Writing incomplete record for session",
						"sessionID", sessID,
					)
					h.writeRecord(sugar, rec.Rec)
				}
				sugar.Debugw("Shutdown successful")
				shutdownDone <- "histfile"
				return
			}
		}()
	}
}

func (h *Histfile) writeRecord(sugar *zap.SugaredLogger, rec record.V1) {
	h.rio.AppendToFile(h.historyPath, []record.V1{rec})
}

func (h *Histfile) mergeAndWriteRecord(sugar *zap.SugaredLogger, part1 recordint.Collect, part2 recordint.Collect) {
	rec, err := recutil.Merge(&part1, &part2)
	if err != nil {
		sugar.Errorw("Error while merging records", "error", err)
		return
	}

	func() {
		cmdLine := rec.CmdLine
		h.bashCmdLines.AddCmdLine(cmdLine)
		h.zshCmdLines.AddCmdLine(cmdLine)
		h.cliRecords.AddRecord(&recordint.Indexed{
			// TODO: is this what we want?
			Rec: rec,
		})
	}()

	h.rio.AppendToFile(h.historyPath, []record.V1{rec})
}

// TODO: use errors in RecIO
// func writeRecord(sugar *zap.SugaredLogger, rec record.V1, outputPath string) {
// 	recJSON, err := json.Marshal(rec)
// 	if err != nil {
// 		sugar.Errorw("Marshalling error", "error", err)
// 		return
// 	}
// 	f, err := os.OpenFile(outputPath,
// 		os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// 	if err != nil {
// 		sugar.Errorw("Could not open file", "error", err)
// 		return
// 	}
// 	defer f.Close()
// 	_, err = f.Write(append(recJSON, []byte("\n")...))
// 	if err != nil {
// 		sugar.Errorw("Error while writing record",
// 			"recordRaw", rec,
// 			"error", err,
// 		)
// 		return
// 	}
// }

// DumpCliRecords returns enriched records
func (h *Histfile) DumpCliRecords() histcli.Histcli {
	// don't forget locks in the future
	return h.cliRecords
}

func loadCmdLines(sugar *zap.SugaredLogger, recs []recordint.Indexed) histlist.Histlist {
	hl := histlist.New(sugar)
	// go from bottom and deduplicate
	var cmdLines []string
	cmdLinesSet := map[string]bool{}
	for i := len(recs) - 1; i >= 0; i-- {
		cmdLine := recs[i].Rec.CmdLine
		if cmdLinesSet[cmdLine] {
			continue
		}
		cmdLinesSet[cmdLine] = true
		cmdLines = append([]string{cmdLine}, cmdLines...)
		// if len(cmdLines) > limit {
		// 	break
		// }
	}
	// add everything to histlist
	for _, cmdLine := range cmdLines {
		hl.AddCmdLine(cmdLine)
	}
	return hl
}
