package stdout

import (
	"fmt"
	"io"
	"math"
	"os"
	"path/filepath"
	"runtime"
	"sync"
	"time"

	"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/report"
	"github.com/fatih/color"
)

// UI struct
type UI struct {
	output io.Writer
	*common.UI
	red               *color.Color
	orange            *color.Color
	blue              *color.Color
	cyan              *color.Color
	showItemCnt       bool
	showSymlinkTarget bool
	top               int
	depth             int
	summarize         bool
	noPrefix          bool
	fixedBase         float64
	fixedSuffix       string
	reverseSort       bool
}

var (
	progressRunes      = []rune(`⠇⠏⠋⠙⠹⠸⠼⠴⠦⠧`)
	progressRunesOld   = []rune(`-\\|/`)
	progressRunesCount = len(progressRunes)
)

// CreateStdoutUI creates UI for stdout
func CreateStdoutUI(
	output io.Writer,
	useColors bool,
	showProgress bool,
	showApparentSize bool,
	showRelativeSize bool,
	summarize bool,
	useSIPrefix bool,
	noPrefix bool,
	fixedUnit string,
	top int,
	reverseSort bool,
	depth int,
) *UI {
	ui := &UI{
		UI: &common.UI{
			UseColors:        useColors,
			ShowProgress:     showProgress,
			ShowApparentSize: showApparentSize,
			ShowRelativeSize: showRelativeSize,
			Analyzer:         analyze.CreateTopDirAnalyzer(),
			UseSIPrefix:      useSIPrefix,
		},
		output:      output,
		summarize:   summarize,
		noPrefix:    noPrefix,
		top:         top,
		reverseSort: reverseSort,
		depth:       depth,
	}
	if fixedUnit != "" {
		ui.SetFixedUnit(fixedUnit)
	} else if !noPrefix && !useSIPrefix {
		ui.SetBlockSizeFromEnvironment()
	}
	ui.red = color.New(color.FgRed).Add(color.Bold)
	ui.orange = color.New(color.FgYellow).Add(color.Bold)
	ui.blue = color.New(color.FgBlue).Add(color.Bold)
	ui.cyan = color.New(color.FgCyan).Add(color.Bold)

	if ui.top > 0 || ui.depth > 0 {
		ui.Analyzer = analyze.CreateAnalyzer()
	}

	if !useColors {
		color.NoColor = true
	}

	return ui
}
func (ui *UI) SetFixedUnit(unitChar string) {
	k, m, g := common.Ki, common.Mi, common.Gi
	suffixMap := map[string]string{"k": " KiB", "m": " MiB", "g": " GiB"}

	if ui.UseSIPrefix {
		k, m, g = common.K, common.M, common.G
		suffixMap = map[string]string{"k": " kB", "m": " MB", "g": " GB"}
	}

	switch unitChar {
	case "k":
		ui.fixedBase = k
		ui.fixedSuffix = suffixMap["k"]
	case "m":
		ui.fixedBase = m
		ui.fixedSuffix = suffixMap["m"]
	case "g":
		ui.fixedBase = g
		ui.fixedSuffix = suffixMap["g"]
	}
}

func (ui *UI) SetShowItemCount() {
	ui.showItemCnt = true
}

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

func (ui *UI) UseOldProgressRunes() {
	progressRunes = progressRunesOld
	progressRunesCount = len(progressRunes)
}

// StartUILoop stub
func (ui *UI) StartUILoop() error {
	return nil
}

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

// ListDevices lists mounted devices and shows their disk usage
func (ui *UI) ListDevices(getter device.DevicesInfoGetter) error {
	devices, err := getter.GetDevicesInfo()
	if err != nil {
		return err
	}

	maxDeviceNameLength := maxInt(maxLength(
		devices,
		func(device *device.Device) string { return device.Name },
	), len("Devices"))

	var sizeLength, percentLength int
	if ui.UseColors {
		sizeLength = 20
		percentLength = 16
	} else {
		sizeLength = 9
		percentLength = 5
	}

	lineFormat := fmt.Sprintf(
		"%%%ds %%%ds %%%ds %%%ds %%%ds %%s\n",
		maxDeviceNameLength,
		sizeLength,
		sizeLength,
		sizeLength,
		percentLength,
	)

	fmt.Fprintf(
		ui.output,
		fmt.Sprintf("%%%ds %%9s %%9s %%9s %%5s %%s\n", maxDeviceNameLength),
		"Device",
		"Size",
		"Used",
		"Free",
		"Used%",
		"Mount point",
	)

	for _, device := range devices {
		usedPercent := math.Round(float64(device.Size-device.Free) / float64(device.Size) * 100)

		fmt.Fprintf(
			ui.output,
			lineFormat,
			device.Name,
			ui.formatSize(device.Size),
			ui.formatSize(device.Size-device.Free),
			ui.formatSize(device.Free),
			ui.red.Sprintf("%.f%%", usedPercent),
			device.MountPoint)
	}

	return nil
}

// AnalyzePath analyzes recursively disk usage in given path
func (ui *UI) AnalyzePath(path string, _ fs.Item) error {
	// When path is a regular file, create a File item directly so that
	// apparent-size (GetSize) and disk-usage (GetUsage) are both correct.
	// Running a file through AnalyzeDir + UpdateStats would report the
	// default 4096-byte directory overhead instead of the real values.
	info, err := os.Stat(path)
	if err == nil && info.Mode().IsRegular() {
		file := analyze.CreateFileItem(filepath.Base(path), info)
		ui.printTotalItem(file)
		return nil
	}

	var (
		dir             fs.Item
		wait            sync.WaitGroup
		updateStatsDone chan struct{}
	)
	updateStatsDone = make(chan struct{}, 1)

	if ui.ShowProgress {
		wait.Add(1)
		go func() {
			defer wait.Done()
			ui.updateProgress(updateStatsDone)
		}()
	}

	wait.Add(1)
	go func() {
		defer wait.Done()
		dir = ui.Analyzer.AnalyzeDir(path, ui.CreateIgnoreFunc(), ui.CreateFileTypeFilter())
		if ui.IsFilteringFiles() {
			dir.UpdateStatsWithFileFiltering(make(fs.HardLinkedItems, 10))
		} else {
			dir.UpdateStats(make(fs.HardLinkedItems, 10))
		}
		updateStatsDone <- struct{}{}
	}()

	wait.Wait()

	switch {
	case ui.top > 0:
		ui.printTopFiles(dir)
	case ui.depth > 0:
		ui.printDirWithDepth(dir, 0)
	case ui.summarize:
		ui.printTotalItem(dir)
	default:
		ui.showDir(dir)
	}

	return nil
}

// ReadFromStorage reads analysis data from persistent key-value storage
func (ui *UI) ReadFromStorage(storagePath, path string) error {
	storage := analyze.NewStorage(storagePath, path)
	closeFn := storage.Open()
	defer closeFn()

	dir, err := storage.GetDirForPath(path)
	if err != nil {
		return err
	}

	switch {
	case ui.top > 0:
		ui.printTopFiles(dir)
	case ui.summarize:
		ui.printTotalItem(dir)
	default:
		ui.showDir(dir)
	}
	return nil
}

func (ui *UI) showDir(dir fs.Item) {
	sortOrder := fs.SortDesc
	if ui.reverseSort {
		sortOrder = fs.SortAsc
	}

	sort := fs.SortBySize
	if ui.ShowApparentSize {
		sort = fs.SortByApparentSize
	}

	for file := range dir.GetFiles(sort, sortOrder) {
		ui.printItem(file)
	}
}

func (ui *UI) printTopFiles(file fs.Item) {
	collected := analyze.CollectTopFiles(file, ui.top)
	for _, file := range collected {
		ui.printItemPath(file)
	}
}

func (ui *UI) printTotalItem(file fs.Item) {
	var lineFormat string
	if ui.UseColors {
		lineFormat = "%20s %s\n"
	} else {
		lineFormat = "%9s %s\n"
	}

	var size int64
	if ui.ShowApparentSize {
		size = file.GetSize()
	} else {
		size = file.GetUsage()
	}

	fmt.Fprintf(
		ui.output,
		lineFormat,
		ui.formatSize(size),
		file.GetName(),
	)
}

func (ui *UI) printItem(file fs.Item) {
	var lineFormat string
	if ui.showItemCnt {
		if ui.UseColors {
			lineFormat = "%s %23s %25s %s\n"
		} else {
			lineFormat = "%s %9s %11s %s\n"
		}
	} else {
		if ui.UseColors {
			lineFormat = "%s %23s %s\n"
		} else {
			lineFormat = "%s %9s %s\n"
		}
	}

	var size int64
	if ui.ShowApparentSize {
		size = file.GetSize()
	} else {
		size = file.GetUsage()
	}

	name := file.GetName()
	if file.IsDir() {
		name = ui.blue.Sprint("/" + file.GetName())
	}

	// Append symlink target with cyan name (like ls --color)
	if ui.showSymlinkTarget {
		if si, ok := file.(fs.SymlinkItem); ok {
			if target := si.GetSymlinkTarget(); target != "" {
				name = ui.cyan.Sprint(file.GetName()) + " -> " + target
			}
		}
	}

	if ui.showItemCnt {
		fmt.Fprintf(
			ui.output,
			lineFormat,
			string(file.GetFlag()),
			ui.formatSize(size),
			ui.formatCount(file.GetItemCount()),
			name,
		)
		return
	}

	fmt.Fprintf(
		ui.output,
		lineFormat,
		string(file.GetFlag()),
		ui.formatSize(size),
		name,
	)
}

func (ui *UI) printItemPath(file fs.Item) {
	var lineFormat string
	if ui.UseColors {
		lineFormat = "%20s %s\n"
	} else {
		lineFormat = "%9s %s\n"
	}

	var size int64
	if ui.ShowApparentSize {
		size = file.GetSize()
	} else {
		size = file.GetUsage()
	}

	if file.IsDir() {
		fmt.Fprintf(ui.output,
			lineFormat,
			ui.formatSize(size),
			ui.blue.Sprint(file.GetPath()))
	} else {
		fmt.Fprintf(ui.output,
			lineFormat,
			ui.formatSize(size),
			file.GetPath())
	}
}

func (ui *UI) printDirWithDepth(dir fs.Item, currentDepth int) {
	// Print current directory
	ui.printItemPath(dir)

	// If we haven't reached the max depth, print contents
	if currentDepth < ui.depth && dir.IsDir() {
		sortOrder := fs.SortDesc
		if ui.reverseSort {
			sortOrder = fs.SortAsc
		}

		files := dir.GetFiles(fs.SortBySize, sortOrder)

		// Print all files at this depth level
		for file := range files {
			if file.IsDir() {
				// Recurse into subdirectories
				ui.printDirWithDepth(file, currentDepth+1)
			} else {
				// Print regular files
				ui.printItemPath(file)
			}
		}
	}
}

// ReadAnalysis reads analysis report from JSON file
func (ui *UI) ReadAnalysis(input io.Reader) error {
	var (
		dir      fs.Item
		wait     sync.WaitGroup
		err      error
		doneChan chan struct{}
	)

	if ui.ShowProgress {
		wait.Add(1)
		doneChan = make(chan struct{})
		go func() {
			defer wait.Done()
			ui.showReadingProgress(doneChan)
		}()
	}

	wait.Add(1)
	go func() {
		defer wait.Done()
		dir, err = report.ReadAnalysis(input)
		if err != nil {
			if ui.ShowProgress {
				doneChan <- struct{}{}
			}
			return
		}
		runtime.GC()

		if ui.IsFilteringFiles() {
			dir.UpdateStatsWithFileFiltering(make(fs.HardLinkedItems, 10))
		} else {
			dir.UpdateStats(make(fs.HardLinkedItems, 10))
		}

		if ui.ShowProgress {
			doneChan <- struct{}{}
		}
	}()

	wait.Wait()

	if err != nil {
		return err
	}

	if ui.summarize {
		ui.printTotalItem(dir)
	} else {
		ui.showDir(dir)
	}

	return nil
}

func (ui *UI) showReadingProgress(doneChan chan struct{}) {
	emptyRow := "\r"
	for j := 0; j < 40; j++ {
		emptyRow += " "
	}

	i := 0
	for {
		fmt.Fprint(ui.output, emptyRow)

		select {
		case <-doneChan:
			fmt.Fprint(ui.output, "\r")
			return
		default:
		}

		fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
		fmt.Fprint(ui.output, "Reading analysis from file...")

		time.Sleep(100 * time.Millisecond)
		i++
		i %= progressRunesCount
	}
}

func (ui *UI) updateProgress(updateStatsDone <-chan struct{}) {
	emptyRow := "\r"
	for j := 0; j < 100; j++ {
		emptyRow += " "
	}

	analysisDoneChan := ui.Analyzer.GetDone()
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()

	i := 0
	for {
		select {
		case <-ticker.C:
			progress := ui.Analyzer.GetProgress()
			fmt.Fprint(ui.output, emptyRow)
			fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
			fmt.Fprint(ui.output, "Scanning... Total items: "+
				ui.red.Sprint(common.FormatNumber(int64(progress.ItemCount)))+
				" size: "+
				ui.formatSize(progress.TotalUsage))
			i++
			i %= progressRunesCount
		case <-analysisDoneChan:
			ticker.Stop()
			fmt.Fprint(ui.output, emptyRow)
			for {
				fmt.Fprint(ui.output, emptyRow)
				fmt.Fprintf(ui.output, "\r %s ", string(progressRunes[i]))
				fmt.Fprint(ui.output, "Calculating disk usage...")
				time.Sleep(100 * time.Millisecond)
				i++
				i %= progressRunesCount

				select {
				case <-updateStatsDone:
					fmt.Fprint(ui.output, emptyRow)
					fmt.Fprint(ui.output, "\r")
					return
				default:
				}
			}
		}
	}
}

func (ui *UI) formatCount(count int64) string {
	count64 := float64(count)

	switch {
	case count64 >= common.G:
		return ui.red.Sprintf("%.1f", float64(count)/float64(common.G)) + "G"
	case count64 >= common.M:
		return ui.red.Sprintf("%.1f", float64(count)/float64(common.M)) + "M"
	case count64 >= common.K:
		return ui.red.Sprintf("%.1f", float64(count)/float64(common.K)) + "k"
	default:
		return ui.red.Sprintf("%d", count)
	}
}

func (ui *UI) formatSize(size int64) string {
	if ui.noPrefix {
		return ui.orange.Sprintf("%d", size)
	}
	if formatted, ok := ui.FormatBlockSize(size); ok {
		return ui.orange.Sprint(formatted)
	}
	if ui.fixedBase > 0 {
		val := float64(size) / ui.fixedBase
		return ui.orange.Sprintf("%.1f", val) + ui.fixedSuffix
	}
	if ui.UseSIPrefix {
		return ui.formatWithDecPrefix(size)
	}
	return ui.formatWithBinPrefix(size)
}

func (ui *UI) formatWithBinPrefix(size int64) string {
	fsize := float64(size)
	asize := math.Abs(fsize)

	switch {
	case asize >= common.Ei:
		return ui.orange.Sprintf("%.1f", fsize/common.Ei) + " EiB"
	case asize >= common.Pi:
		return ui.orange.Sprintf("%.1f", fsize/common.Pi) + " PiB"
	case asize >= common.Ti:
		return ui.orange.Sprintf("%.1f", fsize/common.Ti) + " TiB"
	case asize >= common.Gi:
		return ui.orange.Sprintf("%.1f", fsize/common.Gi) + " GiB"
	case asize >= common.Mi:
		return ui.orange.Sprintf("%.1f", fsize/common.Mi) + " MiB"
	case asize >= common.Ki:
		return ui.orange.Sprintf("%.1f", fsize/common.Ki) + " KiB"
	default:
		return ui.orange.Sprintf("%d", size) + " B"
	}
}

func (ui *UI) formatWithDecPrefix(size int64) string {
	fsize := float64(size)
	asize := math.Abs(fsize)

	switch {
	case asize >= common.E:
		return ui.orange.Sprintf("%.1f", fsize/common.E) + " EB"
	case asize >= common.P:
		return ui.orange.Sprintf("%.1f", fsize/common.P) + " PB"
	case asize >= common.T:
		return ui.orange.Sprintf("%.1f", fsize/common.T) + " TB"
	case asize >= common.G:
		return ui.orange.Sprintf("%.1f", fsize/common.G) + " GB"
	case asize >= common.M:
		return ui.orange.Sprintf("%.1f", fsize/common.M) + " MB"
	case asize >= common.K:
		return ui.orange.Sprintf("%.1f", fsize/common.K) + " kB"
	default:
		return ui.orange.Sprintf("%d", size) + " B"
	}
}

func maxLength(list []*device.Device, keyGetter func(*device.Device) string) int {
	maxLen := 0
	var s string
	for _, item := range list {
		s = keyGetter(item)
		if len(s) > maxLen {
			maxLen = len(s)
		}
	}
	return maxLen
}

func maxInt(x, y int) int {
	if x > y {
		return x
	}
	return y
}
