package screens

import (
	"fmt"
	"strings"
	"sync/atomic"

	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/spinner"
	tea "charm.land/bubbletea/v2"

	"git.secluded.site/keld/internal/picker"
	"git.secluded.site/keld/internal/restic"
	"git.secluded.site/keld/internal/theme"
	"git.secluded.site/keld/internal/ui"
)

// filePickerPhase tracks the file picker's internal flow.
type filePickerPhase int

const (
	phaseFileLoading filePickerPhase = iota // spinner while fetching
	phaseFilePicking                        // interactive file picker
	phaseFileNotice                         // notice before auto-advancing
)

// FileLoader fetches the file listing for a snapshot. Injected at
// construction so tests can provide a fake.
type FileLoader func(snapshotID string) ([]restic.LsNode, error)

// filesLoadedMsg carries the result of an async file listing load.
type filesLoadedMsg struct {
	gen   int64
	nodes []restic.LsNode
	err   error
}

// FilePicker is a Screen adapter that wraps the vendored picker
// component for selecting files to restore from a snapshot. It
// handles async loading, error notices, and conversion of selected
// paths to restic --include syntax.
type FilePicker struct {
	loader     FileLoader
	snapshotFn func() string
	styles     *theme.Styles
	spinner    spinner.Model

	phase      filePickerPhase
	gen        int64
	snapshotID string

	// Picking phase.
	picker *picker.Model

	// Notice phase.
	notice string

	// Layout.
	lastSize *tea.WindowSizeMsg

	// Result.
	includes  []string
	selection string
}

// NewFilePicker creates a file picker screen. If loader is nil, the
// screen auto-advances with no includes (full restore). snapshotFn
// returns the snapshot ID from the previous screen.
func NewFilePicker(loader FileLoader, snapshotFn func() string, styles *theme.Styles) *FilePicker {
	sp := spinner.New(spinner.WithSpinner(spinner.Dot))
	sp.Style = sp.Style.Foreground(styles.Accent)

	return &FilePicker{
		loader:     loader,
		snapshotFn: snapshotFn,
		styles:     styles,
		spinner:    sp,
	}
}

// Init starts the async file listing load.
func (fp *FilePicker) Init() tea.Cmd {
	fp.selection = ""
	fp.includes = nil

	fp.snapshotID = fp.snapshotFn()

	if fp.loader == nil {
		return ui.DoneCmd
	}

	fp.phase = phaseFileLoading
	gen := atomic.AddInt64(&fp.gen, 1)

	return tea.Batch(fp.spinner.Tick, func() tea.Msg {
		nodes, err := fp.loader(fp.snapshotID)
		return filesLoadedMsg{gen: gen, nodes: nodes, err: err}
	})
}

// Update handles messages across all phases.
func (fp *FilePicker) Update(msg tea.Msg) (ui.Screen, tea.Cmd) {
	// Always capture the latest window size so it can be applied
	// to the picker when it is built after loading completes.
	if wsm, ok := msg.(tea.WindowSizeMsg); ok {
		fp.lastSize = &wsm
	}

	switch msg := msg.(type) {
	case tea.KeyPressMsg:
		if msg.Code == tea.KeyEscape {
			return fp.handleEsc()
		}

	case filesLoadedMsg:
		return fp.handleLoaded(msg)
	}

	switch fp.phase {
	case phaseFileLoading:
		return fp.updateLoading(msg)
	case phaseFilePicking:
		return fp.updatePicking(msg)
	case phaseFileNotice:
		return fp.updateNotice(msg)
	}

	return fp, nil
}

// handleEsc processes Esc across all phases.
func (fp *FilePicker) handleEsc() (ui.Screen, tea.Cmd) {
	switch fp.phase {
	case phaseFileLoading:
		return fp, ui.BackCmd
	case phaseFilePicking:
		if fp.picker != nil && fp.picker.CurrentDirectory == "." {
			return fp, ui.BackCmd
		}
		// Let the picker navigate to parent directory.
		return fp.updatePicking(tea.KeyPressMsg{Code: tea.KeyEscape})
	case phaseFileNotice:
		return fp, ui.BackCmd
	}
	return fp, ui.BackCmd
}

// handleLoaded processes the async file listing result.
func (fp *FilePicker) handleLoaded(msg filesLoadedMsg) (ui.Screen, tea.Cmd) {
	if msg.gen != atomic.LoadInt64(&fp.gen) {
		return fp, nil
	}

	if msg.err != nil {
		fp.phase = phaseFileNotice
		fp.notice = fmt.Sprintf("Could not list snapshot contents: %v\nProceeding with full restore.", msg.err)
		return fp, nil
	}

	if len(msg.nodes) == 0 {
		fp.phase = phaseFileNotice
		fp.notice = "Snapshot contains no files.\nProceeding with full restore."
		return fp, nil
	}

	fp.buildPicker(msg.nodes)
	fp.phase = phaseFilePicking
	return fp, fp.picker.Init()
}

// updateLoading forwards messages to the spinner during loading.
func (fp *FilePicker) updateLoading(msg tea.Msg) (ui.Screen, tea.Cmd) {
	var cmd tea.Cmd
	fp.spinner, cmd = fp.spinner.Update(msg)
	return fp, cmd
}

// updatePicking forwards messages to the picker.
func (fp *FilePicker) updatePicking(msg tea.Msg) (ui.Screen, tea.Cmd) {
	if fp.picker == nil {
		return fp, nil
	}

	// Handle window resize for the picker.
	if wsm, ok := msg.(tea.WindowSizeMsg); ok {
		fp.picker.AutoHeight = false
		fp.picker.SetHeight(max(1, wsm.Height))
	}

	updated, cmd := fp.picker.Update(msg)
	fp.picker = &updated

	if fp.picker.Confirmed {
		fp.resolveSelection()
		return fp, ui.DoneCmd
	}

	return fp, cmd
}

// updateNotice waits for any key press to acknowledge, then advances.
func (fp *FilePicker) updateNotice(msg tea.Msg) (ui.Screen, tea.Cmd) {
	if _, ok := msg.(tea.KeyPressMsg); ok {
		fp.selection = "all files"
		return fp, ui.DoneCmd
	}
	return fp, nil
}

// buildPicker constructs the picker model from the loaded nodes.
// If a WindowSizeMsg was received during loading, it is applied
// to the picker immediately so it has correct dimensions.
func (fp *FilePicker) buildPicker(nodes []restic.LsNode) {
	sfs := restic.NewSnapshotFS(nodes)
	p := picker.New(sfs)
	p.Cursor = strings.TrimSuffix(theme.Cursor, " ")
	p.Styles = fp.styles.Picker
	fp.picker = &p

	if fp.lastSize != nil {
		fp.picker.AutoHeight = false
		fp.picker.SetHeight(max(1, fp.lastSize.Height))
	}
}

// resolveSelection reads the picker's selection state and converts
// it to restic --include paths.
func (fp *FilePicker) resolveSelection() {
	sel := fp.picker.Selection
	if sel.AllSelected() {
		fp.selection = "all files"
		fp.includes = nil
		return
	}

	paths := sel.SelectedPaths()
	if paths == nil {
		// Nothing toggled — treat as full restore.
		fp.selection = "all files"
		fp.includes = nil
		return
	}

	fp.includes = make([]string, len(paths))
	for i, p := range paths {
		fp.includes[i] = "/" + p
	}
	fp.selection = fmt.Sprintf("%d path(s)", len(fp.includes))
}

// View renders the current phase.
func (fp *FilePicker) View() string {
	switch fp.phase {
	case phaseFileLoading:
		return fp.spinner.View() + " Loading snapshot contents…"
	case phaseFilePicking:
		if fp.picker == nil {
			return ""
		}
		return fp.picker.View()
	case phaseFileNotice:
		return fp.notice + "\n\nPress any key to continue."
	}
	return ""
}

// Title returns the screen's display title.
func (fp *FilePicker) Title() string { return "Select files to restore" }

// KeyBindings returns bindings for the help bar.
func (fp *FilePicker) KeyBindings() []key.Binding {
	switch fp.phase {
	case phaseFilePicking:
		return []key.Binding{
			key.NewBinding(key.WithKeys("↑/↓"), key.WithHelp("↑/↓", "navigate")),
			key.NewBinding(key.WithKeys("space"), key.WithHelp("space", "toggle")),
			key.NewBinding(key.WithKeys("→"), key.WithHelp("→", "open")),
			key.NewBinding(key.WithKeys("←"), key.WithHelp("←", "back")),
			key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "confirm")),
		}
	case phaseFileNotice:
		return []key.Binding{
			key.NewBinding(key.WithKeys("enter"), key.WithHelp("any key", "continue")),
		}
	default:
		return []key.Binding{
			key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "confirm")),
		}
	}
}

// Selection returns a summary for breadcrumb display.
func (fp *FilePicker) Selection() string { return fp.selection }

// Includes returns the restic --include paths, or nil for a full
// restore. Each path has a leading "/" for restic's absolute syntax.
func (fp *FilePicker) Includes() []string { return fp.includes }
