package tui

import (
	"fmt"
	"io"
	"os"
	"os/signal"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"time"
	"unicode"

	log "github.com/sirupsen/logrus"

	"github.com/dundee/gdu/v5/internal/common"
	"github.com/dundee/gdu/v5/pkg/analyze"
	"github.com/dundee/gdu/v5/pkg/device"
	"github.com/dundee/gdu/v5/pkg/fs"
	"github.com/dundee/gdu/v5/pkg/remove"
	"github.com/dundee/gdu/v5/pkg/timefilter"
	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

// UI struct
type UI struct {
	app        common.TermApplication
	screen     tcell.Screen
	output     io.Writer
	currentDir fs.Item
	topDir     fs.Item
	getter     device.DevicesInfoGetter
	*common.UI
	grid                    *tview.Grid
	header                  *tview.TextView
	footer                  *tview.Flex
	footerLabel             *tview.TextView
	currentDirLabel         *tview.TextView
	pages                   *tview.Pages
	progress                *tview.TextView
	progressBar             *ProgressBar
	status                  *tview.TextView
	help                    *tview.Flex
	table                   *tview.Table
	filteringInput          *tview.InputField
	typeFilteringInput      *tview.InputField
	done                    chan struct{}
	remover                 func(fs.Item, fs.Item) error
	emptier                 func(fs.Item, fs.Item) error
	trasher                 func(fs.Item, fs.Item) error
	exec                    func(argv0 string, argv []string, envv []string) error
	changeCwdFn             func(string) error
	linkedItems             fs.HardLinkedItems
	ignoredRows             map[int]struct{}
	markedRows              map[int]struct{}
	markedPaths             []string
	deleteQueue             chan deleteQueueItem
	resultRow               ResultRow
	topDirPath              string
	currentDirPath          string
	filterValue             string
	typeFilterValue         string
	sortBy                  string
	sortOrder               string
	footerTextColor         string
	footerBackgroundColor   string
	footerNumberColor       string
	headerTextColor         string
	headerBackgroundColor   string
	defaultSortBy           string
	defaultSortOrder        string
	exportName              string
	devices                 []*device.Device
	selectedTextColor       tcell.Color
	selectedBackgroundColor tcell.Color
	markedTextColor         tcell.Color
	markedBackgroundColor   tcell.Color
	currentItemNameMaxLen   int
	activeWorkers           int
	deleteWorkersCount      int
	statusMut               sync.RWMutex
	workersMut              sync.Mutex
	askBeforeDelete         bool
	showItemCount           bool
	showSymlinkTarget       bool
	showMtime               bool
	filtering               bool
	typeFiltering           bool
	headerHidden            bool
	useOldSizeBar           bool
	showBarPercentage       bool
	noDelete                bool
	noViewFile              bool
	noSpawnShell            bool
	deleteInBackground      bool
	timeFilter              *timefilter.TimeFilter
	timeFilterLoc           *time.Location
	noDeleteWithFilter      bool
	collapsePath            bool
	browseParentDirs        bool
	showDiskProgressBar     bool
	currentDeviceSize       int64
	confirmQuit             bool
	scanning                bool
	scanCancelled           bool
	scanStart               time.Time
	scanDuration            time.Duration
	previewing              bool
	previewSavedDir         fs.Item
	progressFlex            *tview.Flex
}

type deleteQueueItem struct {
	item   fs.Item
	action DeleteAction
}

// ResultRow is a struct for a row in the result table
type ResultRow struct {
	NumberColor    string
	DirectoryColor string
}

// Option is optional function customizing the behaviour of UI
type Option func(ui *UI)

// CreateUI creates the whole UI app
func CreateUI(
	app common.TermApplication,
	screen tcell.Screen,
	output io.Writer,
	useColors bool,
	showApparentSize bool,
	showRelativeSize bool,
	useSIPrefix bool,
	opts ...Option,
) *UI {
	ui := &UI{
		UI: &common.UI{
			UseColors:        useColors,
			ShowApparentSize: showApparentSize,
			ShowRelativeSize: showRelativeSize,
			Analyzer:         analyze.CreateAnalyzer(),
			UseSIPrefix:      useSIPrefix,
		},
		app:                     app,
		screen:                  screen,
		output:                  output,
		askBeforeDelete:         true,
		confirmQuit:             true,
		showItemCount:           false,
		remover:                 remove.ItemFromDir,
		emptier:                 remove.EmptyFileFromDir,
		trasher:                 remove.MoveItemToTrash,
		exec:                    Execute,
		linkedItems:             make(fs.HardLinkedItems, 10),
		selectedTextColor:       tview.Styles.TitleColor,
		selectedBackgroundColor: tview.Styles.MoreContrastBackgroundColor,
		markedTextColor:         tview.Styles.PrimaryTextColor,
		markedBackgroundColor:   tview.Styles.ContrastBackgroundColor,
		currentItemNameMaxLen:   70,
		defaultSortBy:           "size",
		defaultSortOrder:        "desc",
		ignoredRows:             make(map[int]struct{}),
		markedRows:              make(map[int]struct{}),
		exportName:              "export.json",
		noDelete:                false,
		noViewFile:              false,
		noSpawnShell:            false,
		deleteQueue:             make(chan deleteQueueItem, 1000),
		deleteWorkersCount:      3 * runtime.GOMAXPROCS(0),
	}
	if !useSIPrefix {
		ui.SetBlockSizeFromEnvironment()
	}
	for _, o := range opts {
		o(ui)
	}

	ui.resetSorting()

	app.SetBeforeDrawFunc(func(screen tcell.Screen) bool {
		screen.Clear()
		return false
	})

	ui.app.SetInputCapture(ui.keyPressed)
	ui.app.SetMouseCapture(ui.onMouse)

	ui.header = tview.NewTextView()
	ui.header.SetText(" gdu ~ Use arrow keys to navigate, press ? for help ")
	ui.header.SetTextColor(tcell.GetColor(ui.headerTextColor))
	ui.header.SetBackgroundColor(tcell.GetColor(ui.headerBackgroundColor))

	ui.currentDirLabel = tview.NewTextView()
	ui.currentDirLabel.SetTextColor(tcell.ColorDefault)
	ui.currentDirLabel.SetBackgroundColor(tcell.ColorDefault)

	ui.table = tview.NewTable().SetSelectable(true, false)
	ui.table.SetBackgroundColor(tcell.ColorDefault)
	ui.table.SetSelectedFunc(ui.fileItemSelected)

	if ui.UseColors {
		ui.table.SetSelectedStyle(tcell.Style{}.
			Foreground(ui.selectedTextColor).
			Background(ui.selectedBackgroundColor).Bold(true))
	} else {
		ui.table.SetSelectedStyle(tcell.Style{}.
			Foreground(tcell.ColorWhite).
			Background(tcell.ColorGray).Bold(true))
	}

	ui.footerLabel = tview.NewTextView().SetDynamicColors(true)
	ui.footerLabel.SetTextColor(tcell.GetColor(ui.footerTextColor))
	ui.footerLabel.SetBackgroundColor(tcell.GetColor(ui.footerBackgroundColor))
	ui.footerLabel.SetText(" No items to display. ")

	ui.footer = tview.NewFlex()
	ui.footer.AddItem(ui.footerLabel, 0, 1, false)

	ui.createGrid()

	ui.pages = tview.NewPages().
		AddPage("background", ui.grid, true, true)
	ui.pages.SetBackgroundColor(tcell.ColorDefault)

	ui.app.SetRoot(ui.pages, true)

	return ui
}

// createGrid creates the main grid layout
func (ui *UI) createGrid() {
	if ui.headerHidden {
		ui.grid = tview.NewGrid().SetRows(1, 0, 1).SetColumns(0)
		ui.grid.AddItem(ui.currentDirLabel, 0, 0, 1, 1, 0, 0, false).
			AddItem(ui.table, 1, 0, 1, 1, 0, 0, true).
			AddItem(ui.footer, 2, 0, 1, 1, 0, 0, false)
	} else {
		ui.grid = tview.NewGrid().SetRows(1, 1, 0, 1).SetColumns(0)
		ui.grid.AddItem(ui.header, 0, 0, 1, 1, 0, 0, false).
			AddItem(ui.currentDirLabel, 1, 0, 1, 1, 0, 0, false).
			AddItem(ui.table, 2, 0, 1, 1, 0, 0, true).
			AddItem(ui.footer, 3, 0, 1, 1, 0, 0, false)
	}
}

// SetSelectedTextColor sets the color for the highlighted selected text
func (ui *UI) SetSelectedTextColor(color tcell.Color) {
	ui.selectedTextColor = color
}

// SetSelectedBackgroundColor sets the color for the highlighted selected text
func (ui *UI) SetSelectedBackgroundColor(color tcell.Color) {
	ui.selectedBackgroundColor = color
}

// SetMarkedTextColor sets the text color for marked items
func (ui *UI) SetMarkedTextColor(color tcell.Color) {
	ui.markedTextColor = color
}

// SetMarkedBackgroundColor sets the background color for marked items
func (ui *UI) SetMarkedBackgroundColor(color tcell.Color) {
	ui.markedBackgroundColor = color
}

// SetFooterTextColor sets the color for the footer text
func (ui *UI) SetFooterTextColor(color string) {
	ui.footerTextColor = color
}

// SetFooterBackgroundColor sets the color for the footer background
func (ui *UI) SetFooterBackgroundColor(color string) {
	ui.footerBackgroundColor = color
}

// SetFooterNumberColor sets the color for the footer number
func (ui *UI) SetFooterNumberColor(color string) {
	ui.footerNumberColor = color
}

// SetHeaderTextColor sets the color for the header text
func (ui *UI) SetHeaderTextColor(color string) {
	ui.headerTextColor = color
}

// SetHeaderBackgroundColor sets the color for the header background
func (ui *UI) SetHeaderBackgroundColor(color string) {
	ui.headerBackgroundColor = color
}

// SetHeaderHidden sets the flag to hide the header
func (ui *UI) SetHeaderHidden() {
	ui.headerHidden = true
}

// SetResultRowDirectoryColor sets the color for the result row directory
func (ui *UI) SetResultRowDirectoryColor(color string) {
	ui.resultRow.DirectoryColor = color
}

// SetResultRowNumberColor sets the color for the result row number
func (ui *UI) SetResultRowNumberColor(color string) {
	ui.resultRow.NumberColor = color
}

// SetCurrentItemNameMaxLen sets the maximum length of the path of the currently processed item
// to be shown in the progress modal
func (ui *UI) SetCurrentItemNameMaxLen(maxLen int) {
	ui.currentItemNameMaxLen = maxLen
}

// UseOldSizeBar uses the old size bar (# chars) instead of the new one (unicode block elements)
func (ui *UI) UseOldSizeBar() {
	ui.useOldSizeBar = true
}

// SetShowBarPercentage shows the numeric usage percentage next to the size bar
func (ui *UI) SetShowBarPercentage() {
	ui.showBarPercentage = true
}

// SetChangeCwdFn sets function that can be used to change current working dir
// during dir browsing
func (ui *UI) SetChangeCwdFn(fn func(string) error) {
	ui.changeCwdFn = fn
}

// SetDeleteInParallel sets the flag to delete files in parallel
func (ui *UI) SetDeleteInParallel() {
	ui.remover = remove.ItemFromDirParallel
}

// StartUILoop starts tview application
func (ui *UI) StartUILoop() error {
	signals := make(chan os.Signal, 1)
	signal.Notify(
		signals,
		syscall.SIGHUP,
		syscall.SIGINT,
		syscall.SIGQUIT,
		syscall.SIGILL,
		syscall.SIGTRAP,
		syscall.SIGABRT,
		syscall.SIGPIPE,
		syscall.SIGTERM,
	)
	defer signal.Stop(signals)

	return ui.runUILoop(signals)
}

func (ui *UI) runUILoop(signals <-chan os.Signal) error {
	stopSignals := make(chan struct{})
	signalsDone := make(chan struct{})
	go func() {
		defer close(signalsDone)
		ui.handleSignals(signals, stopSignals)
	}()

	err := ui.app.Run()
	close(stopSignals)
	<-signalsDone
	return err
}

func (ui *UI) handleSignals(signals <-chan os.Signal, stop <-chan struct{}) {
	for {
		select {
		case <-stop:
			return
		case current, ok := <-signals:
			if !ok {
				return
			}
			log.Printf("Got signal: %s", current)
			ui.postSignalEvent(signalEvent(current), stop)
		}
	}
}

func (ui *UI) postSignalEvent(event *tcell.EventKey, stop <-chan struct{}) {
	if err := ui.screen.PostEvent(event); err == nil {
		return
	}

	retry := time.NewTicker(time.Millisecond)
	defer retry.Stop()
	for {
		select {
		case <-stop:
			return
		case <-retry.C:
			if err := ui.screen.PostEvent(event); err == nil {
				return
			}
		}
	}
}

func signalEvent(current os.Signal) *tcell.EventKey {
	modifiers := tcell.ModNone
	if current == syscall.SIGINT {
		modifiers = tcell.ModCtrl
	}
	return tcell.NewEventKey(tcell.KeyExit, 0, modifiers)
}

func (ui *UI) handleSignalEvent(event *tcell.EventKey) {
	if event.Modifiers() == tcell.ModCtrl && (ui.cancelScan() || ui.scanning) {
		return
	}
	ui.printMarkedPaths()
	ui.app.Stop()
}

func (ui *UI) cancelScan() bool {
	if !ui.scanning || ui.scanCancelled {
		return false
	}

	ui.Analyzer.Cancel()
	ui.scanCancelled = true
	ui.progress.SetTitle(" Stopping scan... ")
	ui.progress.SetText("Stopping scan and keeping results...")
	return true
}

// SetConfirmQuit sets whether gdu asks for confirmation before quitting
// after a scan that took a noticeable amount of time
func (ui *UI) SetConfirmQuit(value bool) {
	ui.confirmQuit = value
}

// SetShowItemCount sets the flag to show number of items in directory
func (ui *UI) SetShowItemCount() {
	ui.showItemCount = true
}

// SetShowMTime sets the flag to show last modification time of items in directory
func (ui *UI) SetShowMTime() {
	ui.showMtime = true
}

// SetShowSymlinkTarget enables displaying the symlink target (name -> target)
func (ui *UI) SetShowSymlinkTarget(value bool) {
	ui.showSymlinkTarget = value
}

// SetNoDelete disables all write operations
func (ui *UI) SetNoDelete() {
	ui.noDelete = true
}

// SetNoSpawnShell disables shell spawning
func (ui *UI) SetNoSpawnShell() {
	ui.noSpawnShell = true
}

func (ui *UI) SetNoViewFile() {
	ui.noViewFile = true
}

// SetNoDelete disables delete when time filters are active
func (ui *UI) SetNoDeleteWithFilter() {
	ui.noDeleteWithFilter = true
}

// SetBrowseParentDirs enables navigating above the launch directory
func (ui *UI) SetBrowseParentDirs() {
	ui.browseParentDirs = true
}

// SetCollapsePath sets the flag to collapse paths
func (ui *UI) SetCollapsePath(value bool) {
	ui.collapsePath = value
}

// SetShowDiskProgressBar sets whether to show a progress bar when scanning a whole disk
func (ui *UI) SetShowDiskProgressBar(value bool) {
	ui.showDiskProgressBar = value
}

// SetDeleteInBackground sets the flag to delete files in background
func (ui *UI) SetDeleteInBackground() {
	ui.deleteInBackground = true

	for i := 0; i < ui.deleteWorkersCount; i++ {
		go ui.deleteWorker()
	}
	go ui.updateStatusWorker()
}

func (ui *UI) resetSorting() {
	ui.sortBy = ui.defaultSortBy
	ui.sortOrder = ui.defaultSortOrder
}

func (ui *UI) rescanDir() {
	ui.linkedItems = make(fs.HardLinkedItems)
	err := ui.AnalyzePath(ui.currentDirPath, ui.currentDir.GetParent())
	if err != nil {
		ui.showErr("Error rescanning path", err)
	}
}

func (ui *UI) fileItemSelected(row, column int) {
	if ui.currentDir == nil {
		return // Add this check to handle nil case
	}

	selectedDirCell := ui.table.GetCell(row, column)

	// Check if the selectedDirCell is nil before using it
	if selectedDirCell == nil || selectedDirCell.GetReference() == nil {
		return
	}

	selectedDir := selectedDirCell.GetReference().(fs.Item)
	if selectedDir == nil || !selectedDir.IsDir() {
		return
	}

	origDir := ui.currentDir
	ui.currentDir = selectedDir
	ui.hideFilterInput()
	ui.hideTypeFilterInput()
	ui.markedRows = make(map[int]struct{})
	ui.ignoredRows = make(map[int]struct{})
	ui.showDir()

	// while previewing a mid-scan snapshot there is no stable top dir to anchor
	// the "select last visited" logic to, so just render the navigated dir
	if ui.previewing {
		return
	}

	if row != 0 || origDir.GetPath() == ui.topDir.GetPath() {
		return
	}

	// we are going up in the directory tree, select the last visited directory
	if origDir.GetParent() != nil {
		nestedDir := origDir
		for nestedDir.GetParent() != nil {
			if selectedDir.GetName() == nestedDir.GetParent().GetName() {
				sortBy, sortOrder := ui.getSortParams()
				index := -1
				i := 0
				for item := range ui.currentDir.GetFiles(sortBy, sortOrder) {
					if item.GetName() == nestedDir.GetName() {
						index = i
						break
					}
					i++
				}
				if index >= 0 {
					if ui.currentDir.GetPath() != ui.topDir.GetPath() {
						index++
					}
					ui.table.Select(index, 0)
				}
				break
			}
			nestedDir = nestedDir.GetParent()
		}
	}
}

func (ui *UI) deviceItemSelected(row, column int) {
	var err error
	selectedDevice, ok := ui.table.GetCell(row, column).GetReference().(*device.Device)
	if !ok {
		return
	}

	paths := device.GetNestedMountpointsPaths(selectedDevice.MountPoint, ui.devices)
	ui.IgnoreDirPathPatterns, err = common.CreateIgnorePattern(paths)
	if err != nil {
		log.Printf("Creating path patterns for other devices failed: %s", paths)
	}

	ui.resetSorting()

	ui.currentDeviceSize = selectedDevice.Size
	ui.Analyzer.ResetProgress()
	ui.linkedItems = make(fs.HardLinkedItems)
	err = ui.AnalyzePath(selectedDevice.MountPoint, nil)
	if err != nil {
		ui.showErr("Error analyzing device", err)
	}
}

func (ui *UI) confirmDeletion(action DeleteAction) {
	if ui.noDelete {
		previousHeaderText := ui.header.GetText(false)

		// show feedback to user
		ui.header.SetText(" Deletion is disabled!")

		go func() {
			time.Sleep(2 * time.Second)
			ui.app.QueueUpdateDraw(func() {
				ui.header.Clear()
				ui.header.SetText(previousHeaderText)
			})
		}()

		return
	}

	// Check if deletion is allowed with active time filters
	if ui.noDeleteWithFilter {
		modal := tview.NewModal().
			SetText("Deletion is disabled when a time filter is active.\n\n" +
				"To override, set GDU_ALLOW_DELETE_WITH_FILTER=1").
			AddButtons([]string{"OK"}).
			SetDoneFunc(func(buttonIndex int, buttonLabel string) {
				ui.pages.RemovePage("confirm")
			})
		if !ui.UseColors {
			modal.SetBackgroundColor(tcell.ColorGray)
		}
		ui.pages.AddPage("confirm", modal, true, true)
		return
	}

	if len(ui.markedRows) > 0 {
		ui.confirmDeletionMarked(action)
	} else {
		ui.confirmDeletionSelected(action)
	}
}

func (ui *UI) confirmDeletionSelected(action DeleteAction) {
	row, column := ui.table.GetSelection()
	selectedFile := ui.table.GetCell(row, column).GetReference().(fs.Item)
	modal := tview.NewModal().
		SetText(
			"Are you sure you want to " +
				action.Verb() +
				" \"" +
				tview.Escape(selectedFile.GetName()) +
				"\"?",
		).
		AddButtons([]string{"no", "yes", "don't ask me again"}).
		SetDoneFunc(func(buttonIndex int, buttonLabel string) {
			switch buttonIndex {
			case 2:
				ui.askBeforeDelete = false
				fallthrough
			case 1:
				ui.deleteSelected(action)
			}
			ui.pages.RemovePage("confirm")
		})

	if !ui.UseColors {
		modal.SetBackgroundColor(tcell.ColorGray)
	} else {
		modal.SetBackgroundColor(tcell.ColorBlack)
	}
	modal.SetBorderColor(tcell.ColorDefault)

	ui.pages.AddPage("confirm", modal, true, true)
}

// SetTimeFilterWithInfo sets both the time filter function and stores the filter info for display
func (ui *UI) SetTimeFilterWithInfo(tf *timefilter.TimeFilter, loc *time.Location) {
	ui.timeFilter = tf
	ui.timeFilterLoc = loc

	if tf != nil && !tf.IsEmpty() {
		timeFilterFunc := func(mtime time.Time) bool {
			return tf.IncludeByTimeFilter(mtime, loc)
		}
		ui.SetTimeFilter(timeFilterFunc)
		if !ui.isDeleteAllowedWithFilter() {
			ui.SetNoDeleteWithFilter()
		}
	}
}

// hasActiveTimeFilter returns true if any time filter is active
func (ui *UI) hasActiveTimeFilter() bool {
	return ui.timeFilter != nil && !ui.timeFilter.IsEmpty()
}

// formatTimeFilterInfo formats the time filter information for display
func (ui *UI) formatTimeFilterInfo() string {
	if !ui.hasActiveTimeFilter() {
		return ""
	}

	return ui.timeFilter.FormatForDisplay(ui.timeFilterLoc)
}

// isDeleteAllowedWithFilter checks if deletion is allowed when filters are active
func (ui *UI) isDeleteAllowedWithFilter() bool {
	if !ui.hasActiveTimeFilter() {
		return true
	}

	// Check environment variable override
	if os.Getenv("GDU_ALLOW_DELETE_WITH_FILTER") == "1" {
		return true
	}

	return false
}

// sanitizePathForDisplay replaces control characters (which can include
// terminal escape sequences) with the Unicode replacement character. A
// scanned directory entry's name has no character restrictions, and these
// paths are printed to the real terminal after the tview screen has
// stopped, bypassing tview's own protective rendering entirely.
func sanitizePathForDisplay(s string) string {
	return strings.Map(func(r rune) rune {
		if unicode.IsControl(r) {
			return '�'
		}
		return r
	}, s)
}

// printMarkedPaths prints the paths of the marked items to the output
func (ui *UI) printMarkedPaths() {
	for _, path := range ui.markedPaths {
		fmt.Fprintf(ui.output, "%s\n", sanitizePathForDisplay(path))
	}
}
