// cctuip is a Co-op Cloud TUI.
package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"sort"
	"strings"

	abraClient "coopcloud.tech/abra/pkg/client"
	"coopcloud.tech/abra/pkg/config"
	"coopcloud.tech/abra/pkg/recipe"
	"coopcloud.tech/tagcmp"
	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/spinner"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	dockerClient "github.com/docker/docker/client"
	"github.com/evertras/bubble-table/table"
	"golang.org/x/exp/slices"
	"golang.org/x/term"
)

// help is the cctuip CLI help output.
const help = `cctuip [options]

cctuip is a Co-op Cloud TUI.

Options:
  -h    output help
`

var helpFlag bool

// handleCliFlags parses CLI flags.
func handleCliFlags() error {
	flag.BoolVar(&helpFlag, "h", false, "output help")
	flag.Parse()
	return nil
}

// getClient retrieves a docker client from the abra API.
func getClient(server string) (*dockerClient.Client, error) {
	cl, err := abraClient.New(server)
	if err != nil {
		return nil, fmt.Errorf("getClient: %s", err)
	}
	return cl, nil
}

// getApps retrieves app metadata from the abra API.
func getApps() ([]config.App, error) {
	appFiles, err := config.LoadAppFiles("")
	if err != nil {
		return []config.App{}, fmt.Errorf("getApps: %s", err)
	}

	apps, err := config.GetApps(appFiles, "")
	if err != nil {
		return []config.App{}, fmt.Errorf("getApps: %s", err)
	}

	sort.Sort(config.ByName(apps))

	return apps, nil
}

// getNumServersAndRecipes totals servers and recipes.
func getNumServersAndRecipes(apps []config.App) (int, int) {
	var (
		servers []string
		recipes []string
	)

	for _, app := range apps {
		if !slices.Contains(servers, app.Server) {
			servers = append(servers, app.Server)
		}
		if !slices.Contains(recipes, app.Recipe) {
			recipes = append(recipes, app.Recipe)
		}
	}

	return len(servers), len(recipes)
}

// errorMsg delivers errors to the UI.
type errorMsg struct{ err error }

// Error implements error output rendering.
func (e errorMsg) Error() string { return e.err.Error() }

// appsDeployStatusMsg delivers the deployment status of all apps to the UI.
type appsDeployStatusMsg map[string]map[string]string

// getAppsDeployStatus retrieves apps deployment status from a server.
func getAppsDeployStatus(m model) tea.Msg {
	var apps []config.App

	for _, row := range m.table.GetVisibleRows() {
		apps = append(apps, row.Data["app"].(config.App))
	}

	statuses, err := config.GetAppStatuses(apps, true)
	if err != nil {
		return errorMsg{err}
	}

	catl, err := recipe.ReadRecipeCatalogue()
	if err != nil {
		return errorMsg{err}
	}

	for _, app := range apps {
		var newUpdates []string
		if status, ok := statuses[app.StackName()]; ok {
			if version, ok := status["version"]; ok {
				updates, err := recipe.GetRecipeCatalogueVersions(app.Recipe, catl)
				if err != nil {
					return errorMsg{err}
				}

				parsedVersion, err := tagcmp.Parse(version)
				if err != nil {
					statuses[app.StackName()]["updates"] = "🤷"
					continue
				}

				for _, update := range updates {
					parsedUpdate, err := tagcmp.Parse(update)
					if err != nil {
						statuses[app.StackName()]["updates"] = "🤷"
						continue
					}

					if update != version && parsedUpdate.IsGreaterThan(parsedVersion) {
						newUpdates = append(newUpdates, update)
					}
				}

				if len(newUpdates) == 0 {
					statuses[app.StackName()]["updates"] = "✅"
				} else {
					statuses[app.StackName()]["updates"] = fmt.Sprintf("%v", len(newUpdates))
				}
			}
		}
	}

	return appsDeployStatusMsg(statuses)
}

func renderAppsDeployStatus(m *model, appStatuses appsDeployStatusMsg) table.Model {
	for _, row := range m.table.GetVisibleRows() {
		app := row.Data["app"].(config.App)
		appStatus := appStatuses[app.StackName()]
		var (
			version      = appStatus["version"]
			updates      = appStatus["updates"]
			status       = appStatus["status"]
			chaos        = appStatus["chaos"]
			chaosVersion = appStatus["chaosVersion"]
			autoUpdate   = appStatus["autoUpdate"]
		)
		if status == "" {
			row.Data["status"] = "🤷"
		} else {
			row.Data["status"] = status
		}
		if version == "" {
			row.Data["version"] = "🤷"
		} else {
			row.Data["version"] = version
			row.Data["updates"] = updates
		}
		if chaos == "" {
			row.Data["chaos"] = "🤷"
		} else {
			row.Data["chaos"] = chaos
		}
		if chaosVersion == "" {
			row.Data["chaos-version"] = "🤷"
		} else {
			row.Data["chaos-version"] = chaosVersion
		}
		if autoUpdate == "" {
			row.Data["autoUpdate"] = "🤷"
		} else {
			row.Data["autoUpdate"] = autoUpdate
		}
	}
	return m.table
}

type initTableMsg struct{ table table.Model }

// initTable loads the table layout from local-first data sources (~/.abra).
func initTable(m model) tea.Msg {
	var rows []table.Row

	for _, app := range m.apps {
		rows = append(rows, table.NewRow(table.RowData{
			"domain":        app.Domain,
			"server":        app.Server,
			"recipe":        app.Recipe,
			"status":        "🤷",
			"version":       "🤷",
			"updates":       "🤷",
			"chaos":         "🤷",
			"chaos-version": "🤷",
			"auto-update":   "🤷",
			"app":           app, // attach app itself for faster lookups
		}))
	}

	colStyle := lipgloss.NewStyle().Align(lipgloss.Left)
	columns := []table.Column{
		table.NewFlexColumn("domain", "Domain", 2).WithFiltered(true).WithStyle(colStyle),
		table.NewFlexColumn("server", "Server", 1).WithFiltered(true).WithStyle(colStyle),
		table.NewFlexColumn("recipe", "Recipe", 1).WithFiltered(true).WithStyle(colStyle),
		table.NewFlexColumn("status", "Status", 1).WithStyle(colStyle),
		table.NewFlexColumn("version", "Version", 2).WithStyle(colStyle),
		table.NewFlexColumn("updates", "Updates", 2).WithStyle(colStyle),
		table.NewFlexColumn("chaos", "Chaos", 1).WithStyle(colStyle),
		table.NewFlexColumn("chaos-version", "Chaos version", 2).WithStyle(colStyle),
		table.NewFlexColumn("auto-update", "Auto-update", 1).WithStyle(colStyle),
	}

	width, height, err := term.GetSize(0)
	if err != nil {
		log.Fatal(err) // TODO
	}

	keymap := table.DefaultKeyMap()
	keymap.Filter = key.NewBinding(key.WithKeys("/", "f"))
	keymap.PageDown = key.NewBinding(key.WithKeys("right", "l", "pgdown", "ctrl+d"))
	keymap.PageUp = key.NewBinding(key.WithKeys("left", "h", "pgup", "ctrl+u"))

	t := table.
		New(columns).
		Filtered(true).
		Focused(true).
		WithPageSize(height - 10).
		WithRows([]table.Row(rows)).
		WithTargetWidth(width).
		WithKeyMap(keymap)

	return initTableMsg{table: t}
}

// model is the TUI application state.
type model struct {
	apps []config.App

	numApps    int
	numServers int
	numRecipes int

	numFilteredApps    int
	numFilteredServers int
	numFilteredRecipes int

	table         table.Model
	spinner       spinner.Model
	pollingStatus bool

	err error
}

// getFilteredApps retrieves all visible apps.
func (m model) getFilteredApps() []config.App {
	var servers []config.App
	for _, row := range m.table.GetVisibleRows() {
		servers = append(servers, row.Data["app"].(config.App))
	}
	return servers
}

// updateCount updates the apps/servers/recipes count.
func (m *model) updateCount() {
	if m.table.GetIsFilterActive() {
		apps := m.getFilteredApps()
		m.numFilteredApps = len(apps)
		m.numFilteredServers, m.numFilteredRecipes = getNumServersAndRecipes(apps)
	} else {
		m.numFilteredApps = m.numApps
		m.numFilteredServers = m.numServers
		m.numFilteredRecipes = m.numRecipes
	}
}

// Init initialises a new model. All I/O happens here and the results are fed
// into the UI for further updates and rendering.
func (m model) Init() tea.Cmd {
	return tea.Batch(
		func() tea.Msg { return initTable(m) },
		m.spinner.Tick,
	)
}

// Update handles updates to the TUI via I/O and the user.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmd tea.Cmd
	var cmds []tea.Cmd

	m.table, cmd = m.table.Update(msg)
	cmds = append(cmds, cmd)

	m.spinner, cmd = m.spinner.Update(msg)
	cmds = append(cmds, cmd)

	switch msg := msg.(type) {
	case tea.KeyMsg:
		m.updateCount()

		switch msg.String() {
		case "q":
			return m, tea.Quit
		case "s":
			if !m.table.GetIsFilterInputFocused() {
				m.pollingStatus = true
				return m, func() tea.Msg { return getAppsDeployStatus(m) }
			}
		}
	case initTableMsg:
		m.table = msg.table
	case appsDeployStatusMsg:
		m.pollingStatus = false
		m.table = renderAppsDeployStatus(&m, msg)
	case tea.WindowSizeMsg:
		m.table = m.table.WithTargetWidth(msg.Width)
		m.table = m.table.WithPageSize(msg.Height - 10)
	case errorMsg:
		m.err = msg // TODO
	}

	return m, tea.Batch(cmds...)
}

// View renders the UI.
func (m model) View() string {
	if m.err != nil {
		// TODO
		return fmt.Sprintf("\nWe had some trouble: %v\n\n", m.err)
	}

	body := strings.Builder{}

	body.WriteString(fmt.Sprintf(
		"Servers: %v, Apps: %v, Recipes: %v\n",
		m.numFilteredServers, m.numFilteredApps, m.numFilteredRecipes,
	))

	body.WriteString(m.table.View() + "\n")
	body.WriteString("cctuip v0.1.0")

	if m.pollingStatus {
		body.WriteString(fmt.Sprintf(" %s querying app status...", m.spinner.View()))
	}

	return body.String()
}

// main is the command-line entrypoint.
func main() {
	handleCliFlags()

	if helpFlag {
		fmt.Print(help)
		os.Exit(0)
	}

	apps, err := getApps()
	if err != nil {
		log.Fatal(err)
	}

	numServers, numRecipes := getNumServersAndRecipes(apps)
	if err != nil {
		log.Fatal(err)
	}

	s := spinner.New()
	s.Spinner = spinner.Dot
	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))

	numApps := len(apps)
	m := model{
		apps:               apps,
		numApps:            numApps,
		numServers:         numServers,
		numRecipes:         numRecipes,
		numFilteredApps:    numApps,
		numFilteredServers: numServers,
		numFilteredRecipes: numRecipes,
		spinner:            s,
	}

	p := tea.NewProgram(m, tea.WithAltScreen())
	if _, err := p.Run(); err != nil {
		log.Fatalf("oops, cctuip exploded: %s", err)
	}
}
