package lib

import (
	"context"
	"fmt"
	"os"
	"strings"

	_ "embed" // for embedding config.sh

	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/spinner"
	"github.com/charmbracelet/bubbles/table"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/ddworken/hishtory/client/hctx"
	"github.com/muesli/termenv"
	"golang.org/x/term"
)

const TABLE_HEIGHT = 20
const PADDED_NUM_ENTRIES = TABLE_HEIGHT * 5

var selectedRow string = ""

var baseStyle = lipgloss.NewStyle().
	BorderStyle(lipgloss.NormalBorder()).
	BorderForeground(lipgloss.Color("240"))

type errMsg error

type model struct {
	// context
	ctx *context.Context

	// Model for the loading spinner.
	spinner spinner.Model
	// Whether data is still loading and the spinner should still be displayed.
	isLoading bool

	// Whether the TUI is quitting.
	quitting bool

	// The table used for displaying search results.
	table table.Model
	// The number of entries in the table.
	numEntries int
	// Whether the user has hit enter to select an entry and the TUI is thus about to quit.
	selected bool

	// The search box for the query
	queryInput textinput.Model
	// The query to run. Reset to nil after it was run.
	runQuery *string
	// The previous query that was run.
	lastQuery string

	// Unrecoverable error.
	err error
	// An error while searching. Recoverable and displayed as a warning message.
	searchErr error
	// Whether the device is offline. If so, a warning will be displayed.
	isOffline bool

	// A banner from the backend to be displayed. Generally an empty string.
	banner string
}

type doneDownloadingMsg struct{}
type offlineMsg struct{}
type bannerMsg struct {
	banner string
}

func initialModel(ctx *context.Context, t table.Model, initialQuery string, numEntries int) model {
	s := spinner.New()
	s.Spinner = spinner.Dot
	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
	queryInput := textinput.New()
	queryInput.Placeholder = "ls"
	queryInput.Focus()
	queryInput.CharLimit = 156
	queryInput.Width = 50
	if initialQuery != "" {
		queryInput.SetValue(initialQuery)
	}
	return model{ctx: ctx, spinner: s, isLoading: false, table: t, runQuery: &initialQuery, queryInput: queryInput, numEntries: numEntries}
}

func (m model) Init() tea.Cmd {
	return m.spinner.Tick
}

func runQueryAndUpdateTable(m model, updateTable bool) model {
	if (m.runQuery != nil && *m.runQuery != m.lastQuery) || updateTable {
		if m.runQuery == nil {
			m.runQuery = &m.lastQuery
		}
		rows, numEntries, err := getRows(m.ctx, hctx.GetConf(m.ctx).DisplayedColumns, *m.runQuery, PADDED_NUM_ENTRIES)
		if err != nil {
			m.searchErr = err
			return m
		} else {
			m.searchErr = nil
		}
		m.numEntries = numEntries
		if updateTable {
			t, err := makeTable(m.ctx, rows)
			if err != nil {
				m.err = err
				return m
			}
			m.table = t
		}
		m.table.SetRows(rows)
		m.table.SetCursor(0)
		m.lastQuery = *m.runQuery
		m.runQuery = nil
	}
	if m.table.Cursor() >= m.numEntries {
		// Ensure that we can't scroll past the end of the table
		m.table.SetCursor(m.numEntries - 1)
	}
	return m
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "esc", "ctrl+c":
			m.quitting = true
			return m, tea.Quit
		case "enter":
			if m.numEntries != 0 {
				m.selected = true
			}
			return m, tea.Quit
		default:
			t, cmd1 := m.table.Update(msg)
			m.table = t
			if strings.HasPrefix(msg.String(), "alt+") {
				return m, tea.Batch(cmd1)
			}
			i, cmd2 := m.queryInput.Update(msg)
			m.queryInput = i
			searchQuery := m.queryInput.Value()
			m.runQuery = &searchQuery
			m = runQueryAndUpdateTable(m, false)
			return m, tea.Batch(cmd1, cmd2)
		}
	case tea.WindowSizeMsg:
		m = runQueryAndUpdateTable(m, true)
		return m, nil
	case errMsg:
		m.err = msg
		return m, nil
	case offlineMsg:
		m.isOffline = true
		return m, nil
	case bannerMsg:
		m.banner = msg.banner
		return m, nil
	case doneDownloadingMsg:
		m.isLoading = false
		return m, nil
	default:
		var cmd tea.Cmd
		if m.isLoading {
			m.spinner, cmd = m.spinner.Update(msg)
			return m, cmd
		} else {
			m.table, cmd = m.table.Update(msg)
			return m, cmd
		}
	}
}

func (m model) View() string {
	if m.err != nil {
		return fmt.Sprintf("An unrecoverable error occured: %v\n", m.err)
	}
	if m.selected {
		indexOfCommand := -1
		for i, columnName := range hctx.GetConf(m.ctx).DisplayedColumns {
			if columnName == "Command" {
				indexOfCommand = i
				break
			}
		}
		if indexOfCommand == -1 {
			selectedRow = "Error: Table doesn't have a column named `Command`?"
			return ""
		}
		selectedRow = m.table.SelectedRow()[indexOfCommand]
		return ""
	}
	if m.quitting {
		return ""
	}
	warning := ""
	if m.isOffline {
		warning += "Warning: failed to contact the hishtory backend (are you offline?), so some results may be stale\n\n"
	}
	if m.searchErr != nil {
		warning += fmt.Sprintf("Warning: failed to search: %v\n\n", m.searchErr)
	}
	return fmt.Sprintf("\n%s%s\nSearch Query: %s\n\n%s\n", warning, m.banner, m.queryInput.View(), baseStyle.Render(m.table.View()))
}

func getRows(ctx *context.Context, columnNames []string, query string, numEntries int) ([]table.Row, int, error) {
	db := hctx.GetDb(ctx)
	config := hctx.GetConf(ctx)
	data, err := Search(ctx, db, query, numEntries)
	if err != nil {
		return nil, 0, err
	}
	var rows []table.Row
	lastCommand := ""
	for i := 0; i < numEntries; i++ {
		if i < len(data) {
			entry := data[i]
			if strings.TrimSpace(entry.Command) == strings.TrimSpace(lastCommand) && config.FilterDuplicateCommands {
				continue
			}
			entry.Command = strings.ReplaceAll(entry.Command, "\n", " ") // TODO: handle multi-line commands better here
			row, err := buildTableRow(ctx, columnNames, *entry)
			if err != nil {
				return nil, 0, fmt.Errorf("failed to build row for entry=%#v: %v", entry, err)
			}
			rows = append(rows, row)
			lastCommand = entry.Command
		} else {
			rows = append(rows, table.Row{})
		}
	}
	return rows, len(data), nil
}

func calculateColumnWidths(rows []table.Row) []int {
	numColumns := len(rows[0])
	neededColumnWidth := make([]int, numColumns)
	for _, row := range rows {
		for i, v := range row {
			neededColumnWidth[i] = max(neededColumnWidth[i], len(v))
		}
	}
	return neededColumnWidth
}

func getTerminalSize() (int, int, error) {
	return term.GetSize(2)
}

var bigQueryResults []table.Row

func makeTableColumns(ctx *context.Context, columnNames []string, rows []table.Row) ([]table.Column, error) {
	// Handle an initial query with no results
	if len(rows) == 0 || len(rows[0]) == 0 {
		allRows, _, err := getRows(ctx, columnNames, "", 25)
		if err != nil {
			return nil, err
		}
		return makeTableColumns(ctx, columnNames, allRows)
	}

	// Calculate the minimum amount of space that we need for each column for the current actual search
	columnWidths := calculateColumnWidths(rows)
	totalWidth := 20
	for i, name := range columnNames {
		columnWidths[i] = max(columnWidths[i], len(name))
		totalWidth += columnWidths[i]
	}

	// Calculate the maximum column width that is useful for each column if we search for the empty string
	if bigQueryResults == nil {
		bigRows, _, err := getRows(ctx, columnNames, "", 1000)
		if err != nil {
			return nil, err
		}
		bigQueryResults = bigRows
	}
	maximumColumnWidths := calculateColumnWidths(bigQueryResults)

	// Get the actual terminal width. If we're below this, opportunistically add some padding aiming for the maximum column widths
	terminalWidth, _, err := getTerminalSize()
	if err != nil {
		return nil, fmt.Errorf("failed to get terminal size: %v", err)
	}
	for totalWidth < (terminalWidth - len(columnNames)) {
		prevTotalWidth := totalWidth
		for i := range columnNames {
			if columnWidths[i] < maximumColumnWidths[i]+5 {
				columnWidths[i] += 1
				totalWidth += 1
			}
		}
		if totalWidth == prevTotalWidth {
			break
		}
	}

	// And if we are too large from the initial query, let's shrink things to make the table fit. We'll use the heuristic of always shrinking the widest column.
	for totalWidth > terminalWidth {
		largestColumnIdx := -1
		largestColumnSize := -1
		for i := range columnNames {
			if columnWidths[i] > largestColumnSize {
				largestColumnIdx = i
				largestColumnSize = columnWidths[i]
			}
		}
		columnWidths[largestColumnIdx] -= 2
		totalWidth -= 2
	}

	// And finally, create some actual columns!
	columns := make([]table.Column, 0)
	for i, name := range columnNames {
		columns = append(columns, table.Column{Title: name, Width: columnWidths[i]})
	}
	return columns, nil
}

func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func makeTable(ctx *context.Context, rows []table.Row) (table.Model, error) {
	config := hctx.GetConf(ctx)
	columns, err := makeTableColumns(ctx, config.DisplayedColumns, rows)
	if err != nil {
		return table.Model{}, err
	}
	km := table.KeyMap{
		LineUp: key.NewBinding(
			key.WithKeys("up", "alt+OA"),
			key.WithHelp("↑", "scroll up"),
		),
		LineDown: key.NewBinding(
			key.WithKeys("down", "alt+OB"),
			key.WithHelp("↓", "scroll down"),
		),
		PageUp: key.NewBinding(
			key.WithKeys("pgup"),
			key.WithHelp("pgup", "page up"),
		),
		PageDown: key.NewBinding(
			key.WithKeys("pgdown"),
			key.WithHelp("pgdn", "page down"),
		),
		GotoTop: key.NewBinding(
			key.WithKeys("home"),
			key.WithHelp("home", "go to start"),
		),
		GotoBottom: key.NewBinding(
			key.WithKeys("end"),
			key.WithHelp("end", "go to end"),
		),
	}
	_, terminalHeight, err := getTerminalSize()
	if err != nil {
		return table.Model{}, err
	}
	tableHeight := min(TABLE_HEIGHT, terminalHeight-12)
	t := table.New(
		table.WithColumns(columns),
		table.WithRows(rows),
		table.WithFocused(true),
		table.WithHeight(tableHeight),
		table.WithKeyMap(km),
	)

	s := table.DefaultStyles()
	s.Header = s.Header.
		BorderStyle(lipgloss.NormalBorder()).
		BorderForeground(lipgloss.Color("240")).
		BorderBottom(true).
		Bold(false)
	s.Selected = s.Selected.
		Foreground(lipgloss.Color("229")).
		Background(lipgloss.Color("57")).
		Bold(false)
	t.SetStyles(s)
	t.Focus()
	return t, nil
}

func TuiQuery(ctx *context.Context, gitCommit, initialQuery string) error {
	lipgloss.SetColorProfile(termenv.ANSI)
	rows, numEntries, err := getRows(ctx, hctx.GetConf(ctx).DisplayedColumns, initialQuery, PADDED_NUM_ENTRIES)
	if err != nil {
		return err
	}
	t, err := makeTable(ctx, rows)
	if err != nil {
		return err
	}
	p := tea.NewProgram(initialModel(ctx, t, initialQuery, numEntries), tea.WithOutput(os.Stderr))
	// Blocking: Start the TUI
	err = p.Start()
	if err != nil {
		return err
	}
	if selectedRow == "" && os.Getenv("HISHTORY_TERM_INTEGRATION") != "" {
		// Print out the initialQuery instead so that we don't clear the terminal
		selectedRow = initialQuery
	}
	fmt.Printf("%s\n", selectedRow)
	return nil
}
