package tui

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"sync/atomic"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-lens/internal/schema/common"
	"github.com/davecgh/go-spew/spew"
	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
	"go.etcd.io/bbolt"
)

type Config struct {
	LoadBufferSize      int
	SearchHistorySize   int
	LoadingIndicatorLag time.Duration
}

var DefaultConfig = Config{
	LoadBufferSize:      100,
	SearchHistorySize:   100,
	LoadingIndicatorLag: 500 * time.Millisecond,
}

type Primitive interface {
	tview.Primitive

	Mount(ctx context.Context) error
	Update(ctx context.Context) error
	Unmount()
}

type UI struct {
	*tview.Box

	// Need to use context while updating pages those read data from a database.
	// Context should be shared among all mount and updates. Current TUI library
	// doesn't use contexts at all, so I do that feature by myself.
	//nolint:containedctx
	ctx    context.Context
	onStop func()

	app *tview.Application
	db  *bbolt.DB

	pageHistory []Primitive
	mountedPage Primitive

	pageToMount Primitive

	pageStub tview.Primitive

	infoBar    *tview.TextView
	searchBar  *InputFieldWithHistory
	loadingBar *LoadingBar
	helpBar    *tview.TextView

	helpPage *HelpPage

	searchErrorBar *tview.TextView

	isSearching    bool
	isLoading      atomic.Bool
	isShowingError bool
	isShowingHelp  bool

	loadBufferSize int

	rootParser common.Parser

	loadingIndicatorLag time.Duration

	cancelLoading func()

	filters          map[string]func(string) (any, error)
	compositeFilters map[string]func(string) (map[string]any, error)
	filterHints      map[string]string
}

func NewUI(
	ctx context.Context,
	app *tview.Application,
	db *bbolt.DB,
	rootParser common.Parser,
	cfg *Config,
) *UI {
	spew.Config.DisableMethods = true

	if cfg == nil {
		cfg = &DefaultConfig
	}

	ui := &UI{
		Box: tview.NewBox(),

		app:        app,
		db:         db,
		rootParser: rootParser,

		filters:          make(map[string]func(string) (any, error)),
		compositeFilters: make(map[string]func(string) (map[string]any, error)),
		filterHints:      make(map[string]string),

		loadBufferSize:      cfg.LoadBufferSize,
		loadingIndicatorLag: cfg.LoadingIndicatorLag,
	}

	ui.ctx, ui.onStop = context.WithCancel(ctx)

	backgroundColor := ui.GetBackgroundColor()
	textColor := tview.Styles.PrimaryTextColor

	inverseBackgroundColor := textColor
	inverseTextColor := backgroundColor

	alertTextColor := tcell.ColorRed

	ui.pageStub = tview.NewBox()

	ui.infoBar = tview.NewTextView()
	ui.infoBar.SetBackgroundColor(inverseBackgroundColor)
	ui.infoBar.SetTextColor(inverseTextColor)
	ui.infoBar.SetText(
		fmt.Sprintf(" %s (press h for help, q to quit) ", db.Path()),
	)

	ui.searchBar = NewInputFieldWithHistory(cfg.SearchHistorySize)
	ui.searchBar.SetFieldBackgroundColor(backgroundColor)
	ui.searchBar.SetFieldTextColor(textColor)
	ui.searchBar.SetLabelColor(textColor)
	ui.searchBar.Focus(nil)
	ui.searchBar.SetLabel("/")

	ui.searchErrorBar = tview.NewTextView()
	ui.searchErrorBar.SetBackgroundColor(backgroundColor)
	ui.searchErrorBar.SetTextColor(alertTextColor)

	ui.helpBar = tview.NewTextView()
	ui.helpBar.SetBackgroundColor(inverseBackgroundColor)
	ui.helpBar.SetTextColor(inverseTextColor)
	ui.helpBar.SetText(" Press Enter for next page or Escape to exit help ")

	ui.loadingBar = NewLoadingBar(ui.triggerDraw)

	ui.pageToMount = NewBucketsView(ui, NewFilter(nil))

	return ui
}

func (ui *UI) checkFilterExists(typ string) bool {
	if _, ok := ui.filters[typ]; ok {
		return true
	}
	if _, ok := ui.compositeFilters[typ]; ok {
		return true
	}
	return false
}

func (ui *UI) AddFilter(
	typ string,
	parser func(string) (any, error),
	helpHint string,
) error {
	if ui.checkFilterExists(typ) {
		return fmt.Errorf("filter %s already exists", typ)
	}
	ui.filters[typ] = parser
	ui.filterHints[typ] = helpHint
	return nil
}

func (ui *UI) AddCompositeFilter(
	typ string,
	parser func(string) (map[string]any, error),
	helpHint string,
) error {
	if ui.checkFilterExists(typ) {
		return fmt.Errorf("filter %s already exists", typ)
	}
	ui.compositeFilters[typ] = parser
	ui.filterHints[typ] = helpHint
	return nil
}

func (ui *UI) stopOnError(err error) {
	if err != nil {
		ui.onStop()
		ui.app.QueueEvent(tcell.NewEventError(err))
	}
}

func (ui *UI) stop() {
	ui.onStop()
	ui.app.Stop()
}

func (ui *UI) movePrevPage() {
	if len(ui.pageHistory) != 0 {
		ui.mountedPage.Unmount()
		ui.mountedPage = ui.pageHistory[len(ui.pageHistory)-1]
		ui.pageHistory = ui.pageHistory[:len(ui.pageHistory)-1]
		ui.triggerDraw()
	}
}

func (ui *UI) moveNextPage(page Primitive) {
	ui.pageToMount = page
	ui.triggerDraw()
}

func (ui *UI) triggerDraw() {
	go ui.app.QueueUpdateDraw(func() {})
}

func (ui *UI) Draw(screen tcell.Screen) {
	if ui.isLoading.Load() {
		ui.draw(screen)
		return
	}

	ui.isLoading.Store(true)

	ctx, cancel := context.WithCancel(ui.ctx)

	ready := make(chan struct{})
	go func() {
		ui.load(ctx)

		cancel()
		close(ready)
		ui.isLoading.Store(false)
	}()

	select {
	case <-ready:
	case <-time.After(ui.loadingIndicatorLag):
		ui.loadingBar.Start(ui.ctx)
		ui.cancelLoading = cancel

		go func() {
			<-ready
			ui.loadingBar.Stop()
			ui.triggerDraw()
		}()
	}

	ui.draw(screen)
}

func (ui *UI) load(ctx context.Context) {
	if ui.mountedPage == nil && ui.pageToMount == nil {
		ui.stop()
		return
	}

	if ui.pageToMount != nil {
		ui.mountAndUpdate(ctx)
	} else {
		ui.update(ctx)
	}
}

func (ui *UI) draw(screen tcell.Screen) {
	ui.DrawForSubclass(screen, ui)
	x, y, width, height := ui.GetInnerRect()

	var (
		pageToDraw tview.Primitive
		barToDraw  tview.Primitive
	)

	switch {
	case ui.isShowingHelp:
		if ui.helpPage == nil {
			var filters []string
			for f := range ui.filters {
				filters = append(filters, f)
			}
			for f := range ui.compositeFilters {
				filters = append(filters, f)
			}
			ui.helpPage = NewHelpPage(filters, ui.filterHints)
		}
		pageToDraw = ui.helpPage
	case ui.mountedPage != nil:
		pageToDraw = ui.mountedPage
	default:
		pageToDraw = ui.pageStub
	}

	pageToDraw.SetRect(x, y, width, height-1)
	pageToDraw.Draw(screen)

	// Search bar uses cursor and we need to hide it when another bar is drawn.
	screen.HideCursor()

	switch {
	case ui.isLoading.Load():
		barToDraw = ui.loadingBar
	case ui.isSearching:
		barToDraw = ui.searchBar
	case ui.isShowingError:
		barToDraw = ui.searchErrorBar
	case ui.isShowingHelp:
		barToDraw = ui.helpBar
	default:
		barToDraw = ui.infoBar
	}

	barToDraw.SetRect(x, y+height-1, width, 1)
	barToDraw.Draw(screen)
}

func (ui *UI) mountAndUpdate(ctx context.Context) {
	defer func() {
		// Operation succeeded or was canceled, either way reset page to mount.
		ui.pageToMount = nil
	}()

	// Mount should use app global context.
	//nolint:contextcheck
	err := ui.pageToMount.Mount(ui.ctx)
	if err != nil {
		ui.stopOnError(err)
		return
	}

	x, y, width, height := ui.GetInnerRect()
	ui.pageToMount.SetRect(x, y, width, height-1)

	s := loadOp(ctx, ui.pageToMount.Update)
	if s.err != nil {
		ui.pageToMount.Unmount()
		ui.stopOnError(s.err)
		return
	}
	// Update was canceled.
	if !s.done {
		ui.pageToMount.Unmount()
		return
	}

	if ui.mountedPage != nil {
		ui.pageHistory = append(ui.pageHistory, ui.mountedPage)
	}
	ui.mountedPage = ui.pageToMount
}

func (ui *UI) update(ctx context.Context) {
	x, y, width, height := ui.GetInnerRect()
	ui.mountedPage.SetRect(x, y, width, height-1)

	s := loadOp(ctx, ui.mountedPage.Update)
	if s.err != nil {
		ui.stopOnError(s.err)
		return
	}
}

type status struct {
	done bool
	err  error
}

func loadOp(ctx context.Context, op func(ctx context.Context) error) status {
	errCh := make(chan error)
	go func() {
		errCh <- op(ctx)
	}()

	select {
	case <-ctx.Done():
		return status{done: false, err: nil}
	case err := <-errCh:
		return status{done: true, err: err}
	}
}

func (ui *UI) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
	return ui.WrapInputHandler(func(event *tcell.EventKey, _ func(tview.Primitive)) {
		switch {
		case ui.isLoading.Load():
			ui.handleInputOnLoading(event)
		case ui.isShowingHelp:
			ui.handleInputOnShowingHelp(event)
		case ui.isShowingError:
			ui.handleInputOnShowingError()
		case ui.isSearching:
			ui.handleInputOnSearching(event)
		default:
			ui.handleInput(event)
		}
	})
}

func (ui *UI) handleInput(event *tcell.EventKey) {
	m, k, r := event.Modifiers(), event.Key(), event.Rune()

	switch {
	case k == tcell.KeyEsc:
		ui.movePrevPage()
	case m == 0 && k == tcell.KeyRune && r == 'h':
		ui.isShowingHelp = true
	case m == 0 && k == tcell.KeyRune && r == '/':
		ui.isSearching = true
	case m == 0 && k == tcell.KeyRune && r == 'q':
		ui.stop()
	default:
		if ui.mountedPage != nil {
			ui.mountedPage.InputHandler()(event, func(tview.Primitive) {})
		}
	}
}

func (ui *UI) handleInputOnLoading(event *tcell.EventKey) {
	switch k, r := event.Key(), event.Rune(); {
	case k == tcell.KeyEsc:
		ui.cancelLoading()
	case k == tcell.KeyRune && r == 'q':
		ui.stop()
	}
}

func (ui *UI) handleInputOnShowingError() {
	ui.isShowingError = false
	ui.isSearching = true
}

func (ui *UI) handleInputOnShowingHelp(event *tcell.EventKey) {
	k, r := event.Key(), event.Rune()

	switch {
	case k == tcell.KeyEsc:
		ui.isShowingHelp = false
	case k == tcell.KeyRune && r == 'q':
		ui.stop()
	default:
		ui.helpPage.InputHandler()(event, func(tview.Primitive) {})
	}
}

func (ui *UI) handleInputOnSearching(event *tcell.EventKey) {
	m, k := event.Modifiers(), event.Key()

	switch {
	case k == tcell.KeyEnter:
		prompt := ui.searchBar.GetText()

		res, err := ui.processPrompt(prompt)
		if err != nil {
			ui.isShowingError = true
			ui.isSearching = false
			ui.searchErrorBar.SetText(err.Error() + " (press any key to continue)")
			return
		}

		switch ui.mountedPage.(type) {
		case *BucketsView:
			ui.moveNextPage(NewBucketsView(ui, res))
		case *RecordsView:
			bucket := ui.mountedPage.(*RecordsView).bucket
			ui.moveNextPage(NewRecordsView(ui, bucket, res))
		}

		if ui.searchBar.GetText() != "" {
			ui.searchBar.AddToHistory(ui.searchBar.GetText())
		}

		ui.searchBar.SetText("")
		ui.isSearching = false
	case k == tcell.KeyEsc:
		ui.isSearching = false
	case (k == tcell.KeyBackspace2 || m&tcell.ModCtrl != 0 && k == tcell.KeyETB) && len(ui.searchBar.GetText()) == 0:
		ui.isSearching = false
	default:
		ui.searchBar.InputHandler()(event, func(tview.Primitive) {})
	}

	ui.Box.MouseHandler()
}

func (ui *UI) WithPrompt(prompt string) error {
	filter, err := ui.processPrompt(prompt)
	if err != nil {
		return err
	}

	ui.pageToMount = NewBucketsView(ui, filter)

	if prompt != "" {
		ui.searchBar.AddToHistory(prompt)
	}

	return nil
}

func (ui *UI) processPrompt(prompt string) (filter *Filter, err error) {
	if prompt == "" {
		return NewFilter(nil), nil
	}

	filterMap := make(map[string]any)

	for _, filterString := range strings.Split(prompt, "+") {
		parts := strings.Split(filterString, ":")
		if len(parts) != 2 {
			return nil, errors.New("expected 'tag:value [+ tag:value]...'")
		}

		filterTag := strings.TrimSpace(parts[0])
		filterValueString := strings.TrimSpace(parts[1])

		if _, exists := filterMap[filterTag]; exists {
			return nil, fmt.Errorf("duplicate filter tag '%s'", filterTag)
		}

		parser, ok := ui.filters[filterTag]
		if ok {
			filterValue, err := parser(filterValueString)
			if err != nil {
				return nil, fmt.Errorf("can't parse '%s' filter value: %w", filterTag, err)
			}

			filterMap[filterTag] = filterValue
			continue
		}

		compositeParser, ok := ui.compositeFilters[filterTag]
		if ok {
			compositeFilterValue, err := compositeParser(filterValueString)
			if err != nil {
				return nil, fmt.Errorf(
					"can't parse '%s' filter value '%s': %w",
					filterTag, filterValueString, err,
				)
			}

			for tag, value := range compositeFilterValue {
				if _, exists := filterMap[tag]; exists {
					return nil, fmt.Errorf(
						"found duplicate filter tag '%s' while processing composite filter with tag '%s'",
						tag, filterTag,
					)
				}

				filterMap[tag] = value
			}
			continue
		}

		return nil, fmt.Errorf("unknown filter tag '%s'", filterTag)
	}

	return NewFilter(filterMap), nil
}