package tui

import (
	"fmt"
	"io"
	"strings"

	"github.com/charmbracelet/bubbles/key"
	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"github.com/shazow/wifitui/wifi"
)

// itemDelegate is our custom list delegate
type itemDelegate struct {
	list.DefaultDelegate
	listModel *ListModel
}

func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
	i, ok := listItem.(connectionItem)
	if !ok {
		// Fallback to default render for any other item types
		d.DefaultDelegate.Render(w, m, index, listItem)
		return
	}

	title := i.Title()

	// Add icons for security
	icon := getIcon(i)
	title = icon + title

	// Define column width for SSID
	ssidColumnWidth := d.listModel.ssidColumnWidth
	titleWidth := lipgloss.Width(title)

	// Truncate title if it's too long
	if titleWidth > ssidColumnWidth {
		title = truncateString(title, ssidColumnWidth)
		titleWidth = ssidColumnWidth
	}
	padding := strings.Repeat(" ", ssidColumnWidth-titleWidth)

	// Apply custom styling based on connection state
	var titleStyle lipgloss.Style
	if !i.IsVisible {
		titleStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Disabled)
	} else if i.IsActive {
		titleStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Success)
	} else if i.IsKnown {
		titleStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Saved)
	} else {
		titleStyle = lipgloss.NewStyle().Foreground(CurrentTheme.Normal)
	}
	title = titleStyle.Render(title)

	// Prepare description parts
	strengthPart := i.Description()
	connectedPart := ""
	if i.IsActive {
		connectedPart = "  (Connected)"
	}

	apCount := ""
	if len(i.AccessPoints) > 1 {
		apCount = "  " + lipgloss.NewStyle().Foreground(CurrentTheme.Subtle).Render(
			fmt.Sprintf("%s×%d", CurrentTheme.AccessPointIcon, len(i.AccessPoints)),
		)
	}

	var desc string
	var sb strings.Builder
	if i.Strength() > 0 {
		sb.WriteString(CurrentTheme.FormatSignalStrength(i.Strength()))
		sb.WriteString(apCount)
		sb.WriteString(connectedPart)
		desc = sb.String()
	} else {
		// Networks that are not visible have Strength=0, show their time ago instead
		sb.WriteString(strengthPart)
		sb.WriteString(apCount)
		sb.WriteString(connectedPart)
		desc = lipgloss.NewStyle().Foreground(CurrentTheme.Subtle).Render(sb.String())
	}

	// Now combine and render the full line
	var line string
	if index == m.Index() {
		// Selected item
		if d.listModel.isForgetting {
			desc = lipgloss.NewStyle().Foreground(CurrentTheme.Error).Render("Forget? (Y/n)")
		}
		line = lipgloss.NewStyle().Foreground(CurrentTheme.Primary).Render("▶ ") + title + padding + " " + desc
	} else {
		// Normal item
		line = "  " + title + padding + " " + desc
	}
	fmt.Fprint(w, line)
}

type ListModel struct {
	list               list.Model
	isForgetting       bool
	scanner            *ScanSchedule
	numScans           int // Number of scans with results since enter
	width              int
	height             int
	window             *WindowState
	ssidColumnWidth    int
	desiredColumnWidth int
}

const (
	defaultSSIDColumnWidth = 30
	maxSSIDColumnWidth     = 60
	listContentOverhead    = 33
	minWindowWidth         = 70
)

// IsConsumingInput returns whether the model is focused on a text input.
func (m *ListModel) IsConsumingInput() bool {
	// The list model does not have any text inputs.
	if m.list.FilterState() == list.Filtering {
		return true
	}
	return false
}

func (m *ListModel) OnEnter() tea.Cmd {
	m.numScans = 0
	return tea.Batch(
		m.scanner.SetSchedule(ScanFast),
		// Start a scan right away
		func() tea.Msg { return scanMsg{} },
	)
}

func NewListModel() *ListModel {
	return NewListModelWithWindow(nil)
}

func NewListModelWithWindow(window *WindowState) *ListModel {
	// m needs to be a pointer to be assigned to listModel
	m := &ListModel{
		ssidColumnWidth:    defaultSSIDColumnWidth,
		desiredColumnWidth: defaultSSIDColumnWidth + 2,
		window:             window,
	}
	m.scanner = NewScanSchedule(func() tea.Msg { return scanMsg{} })
	delegate := itemDelegate{
		listModel: m,
	}
	l := list.New([]list.Item{}, delegate, 0, 0)
	l.Title = fmt.Sprintf("%-29s %s", CurrentTheme.TitleIcon+"WiFi Network", "Signal")
	l.SetShowStatusBar(false)
	l.SetShowHelp(false)
	l.Help.Styles.ShortKey = lipgloss.NewStyle().Foreground(CurrentTheme.Primary)
	l.Help.Styles.ShortDesc = lipgloss.NewStyle().Foreground(CurrentTheme.Subtle)
	l.Help.Styles.FullKey = lipgloss.NewStyle().Foreground(CurrentTheme.Primary)
	l.Help.Styles.FullDesc = lipgloss.NewStyle().Foreground(CurrentTheme.Subtle)
	l.AdditionalShortHelpKeys = func() []key.Binding {
		return []key.Binding{
			key.NewBinding(key.WithKeys("s"), key.WithHelp("s", "scan")),
			key.NewBinding(key.WithKeys("f"), key.WithHelp("f", "forget")),
			key.NewBinding(key.WithKeys("c"), key.WithHelp("c", "connect")),
		}
	}
	// Make 'q' the only quit key
	l.KeyMap.Quit = key.NewBinding(key.WithKeys("q"), key.WithHelp("q", "quit"))
	l.KeyMap.ShowFullHelp.SetEnabled(false)
	l.KeyMap.CloseFullHelp.SetEnabled(false)
	l.AdditionalFullHelpKeys = func() []key.Binding {
		return append([]key.Binding{
			key.NewBinding(key.WithKeys("n"), key.WithHelp("n", "new network")),
			key.NewBinding(key.WithKeys("S"), key.WithHelp("S", "active scan")),
		}, l.AdditionalShortHelpKeys()...)
	}

	// Enable the fuzzy finder
	l.SetFilteringEnabled(true)
	l.Styles.Title = lipgloss.NewStyle().Foreground(CurrentTheme.Primary).Bold(true)
	l.Styles.FilterPrompt = lipgloss.NewStyle().Foreground(CurrentTheme.Normal)
	l.Styles.FilterCursor = lipgloss.NewStyle().Foreground(CurrentTheme.Primary)
	m.list = l
	return m
}

func (m *ListModel) SetSize(w, h int) {
	m.list.SetSize(w, h)
}

func (m *ListModel) availableWidth() int {
	h, _ := lipgloss.NewStyle().Margin(1, 2).GetFrameSize()
	listBorderStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).BorderForeground(CurrentTheme.Border)
	bh, _ := listBorderStyle.GetFrameSize()

	return m.window.ContentWidth(h+bh, m.width, 1)
}

// refreshColumns recalculates the desired SSID column width from the latest connection snapshot.
//
// Call this whenever the connection list changes (e.g. after scans or reloads)
// before updateListSize so the rendered columns can adapt to content.
func (m *ListModel) refreshColumns(conns []wifi.Connection) {
	maxW := 0
	for _, c := range conns {
		item := connectionItem{Connection: c}
		w := lipgloss.Width(getIcon(item) + item.Title())
		if w > maxW {
			maxW = w
		}
	}

	if maxW == 0 {
		m.desiredColumnWidth = defaultSSIDColumnWidth + 2
		return
	}

	m.desiredColumnWidth = maxW + 2
}

func (m *ListModel) updateListSize() {
	_, v := lipgloss.NewStyle().Margin(1, 2).GetFrameSize()
	listBorderStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).BorderForeground(CurrentTheme.Border)
	_, bv := listBorderStyle.GetFrameSize()

	availableWidth := m.availableWidth()

	// Calculate ssidColumnWidth
	// Reserve space for: "  " (2) + " " (1) + Description (~30)
	// We want roughly 33 chars reserved for the non-SSID parts of the line.
	availableForSSID := availableWidth - listContentOverhead

	// Target is content width (which includes padding)
	targetSSIDWidth := m.desiredColumnWidth

	// Clamp between 30 and 60
	if targetSSIDWidth < defaultSSIDColumnWidth {
		targetSSIDWidth = defaultSSIDColumnWidth
	} else if targetSSIDWidth > maxSSIDColumnWidth {
		targetSSIDWidth = maxSSIDColumnWidth
	}

	// Ensure we don't exceed available space, but respect the absolute minimum of 30
	if targetSSIDWidth > availableForSSID {
		targetSSIDWidth = availableForSSID
	}
	// Hard floor of 30, even if it causes overflow (preserves usability of column)
	if targetSSIDWidth < defaultSSIDColumnWidth {
		targetSSIDWidth = defaultSSIDColumnWidth
	}

	m.ssidColumnWidth = targetSSIDWidth

	// Update list title to match the new column width
	titlePrefix := CurrentTheme.TitleIcon + "WiFi Network"
	gap := m.ssidColumnWidth - lipgloss.Width(titlePrefix)
	if gap < 0 {
		gap = 0
	}
	m.list.Title = fmt.Sprintf("%s%s %s", titlePrefix, strings.Repeat(" ", gap), "Signal")

	// Subtract 2 for the padding spaces in the help string format: " %s "
	m.list.Help.Width = availableWidth - 2

	helpStr := fmt.Sprintf("\n\n %s ", m.list.Help.View(m))
	helpHeight := lipgloss.Height(helpStr)

	// extraVerticalSpace covers margins + border + status bar (2 lines)
	extraVerticalSpace := v + bv + 2

	windowHeight := m.window.BaseHeight(m.height)
	m.list.SetSize(availableWidth, windowHeight-extraVerticalSpace-helpHeight)
}

func (m *ListModel) SetItems(items []list.Item) {
	m.list.SetItems(items)
}

func (m *ListModel) newEditModel(item *connectionItem) *EditModel {
	if m.window != nil {
		return NewEditModelWithWindow(item, m.window)
	}
	editModel := NewEditModel(item)
	editModel.applyWindowWidth(m.width)
	return editModel
}

func (m *ListModel) Update(msg tea.Msg) (Component, tea.Cmd) {
	var cmds []tea.Cmd

	oldIndex := m.list.Index()

	if m.isForgetting {
		selected, ok := m.list.SelectedItem().(connectionItem)
		if !ok {
			m.isForgetting = false
		} else {
			finished, cmd := forgetHandler(msg, selected)
			if finished {
				m.isForgetting = false
				return m, cmd
			}
		}
		// Don't let other events pass through while forgetting
		return m, nil
	}

	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		if m.window != nil {
			m.window.Update(msg)
		}
		if msg.Width < minWindowWidth {
			m.width = minWindowWidth
		} else {
			m.width = msg.Width
		}
		m.height = msg.Height
		m.updateListSize()
		return m, nil
	case connectionsLoadedMsg:
		m.refreshColumns(msg)
		items := make([]list.Item, len(msg))
		for i, c := range msg {
			items[i] = connectionItem{Connection: c}
		}
		m.list.SetItems(items)
		m.updateListSize()
		return m, nil
	case scanFinishedMsg:
		m.refreshColumns(msg)
		items := make([]list.Item, len(msg))
		for i, c := range msg {
			items[i] = connectionItem{Connection: c}
		}
		m.list.SetItems(items)
		m.updateListSize()
		if len(items) > 0 {
			m.numScans++
		}
		if m.numScans == 3 {
			// Slow down scanner after a few scans
			m.scanner.SetSchedule(ScanSlow)
		}
		return m, nil
	case tea.KeyMsg:
		if m.list.FilterState() == list.Filtering {
			break
		}
		switch msg.String() {
		case "?":
			m.list.Help.ShowAll = !m.list.Help.ShowAll
			m.updateListSize()
			return m, nil
		case "q":
			if m.list.FilterState() != list.Filtering {
				return m, tea.Quit
			}
		case "n":
			editModel := m.newEditModel(nil)
			return editModel, nil
		case "s":
			return m, func() tea.Msg { return scanMsg{} }
		case "S":
			enabled, cmd := m.scanner.Toggle()
			var msg string
			if enabled {
				msg = "Active Scan enabled"
			} else {
				msg = "Active Scan disabled"
			}
			return m, tea.Batch(cmd, func() tea.Msg {
				return statusMsg{status: msg}
			})
		case "f":
			if len(m.list.Items()) > 0 {
				selected, ok := m.list.SelectedItem().(connectionItem)
				if ok && selected.IsKnown {
					m.isForgetting = true
					return m, nil
				}
			}
		case "c":
			if len(m.list.Items()) > 0 {
				selected, ok := m.list.SelectedItem().(connectionItem)
				if ok {
					if selected.IsKnown {
						return m, func() tea.Msg { return connectMsg{item: selected} }
					} else {
						editModel := m.newEditModel(&selected)
						return editModel, nil
					}
				}
			}
		case "enter":
			if len(m.list.Items()) > 0 {
				selected, ok := m.list.SelectedItem().(connectionItem)
				if !ok {
					break
				}
				editModel := m.newEditModel(&selected)
				return editModel, nil
			}
		}
	}

	// The list bubble needs to be updated.
	newList, newCmd := m.list.Update(msg)
	m.list = newList
	cmds = append(cmds, newCmd)

	if m.isForgetting && m.list.Index() != oldIndex {
		m.isForgetting = false
	}

	cmds = append(cmds, m.scanner.Update(msg))
	return m, tea.Batch(cmds...)
}

func (m *ListModel) OnLeave() tea.Cmd {
	m.isForgetting = false
	return m.scanner.SetSchedule(ScanOff)
}

func (m *ListModel) View() string {
	var viewBuilder strings.Builder
	listBorderStyle := lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), true).BorderForeground(CurrentTheme.Border).Width(m.availableWidth())

	help := fmt.Sprintf("\n\n %s ", m.list.Help.View(m))
	viewBuilder.WriteString(listBorderStyle.Render(m.list.View() + help))

	// Custom status bar
	statusText := ""
	if len(m.list.Items()) > 0 {
		statusText = fmt.Sprintf("%d/%d", m.list.Index()+1, len(m.list.Items()))
	}
	viewBuilder.WriteString("\n")
	viewBuilder.WriteString(statusText)
	return lipgloss.NewStyle().Margin(1, 2).Render(viewBuilder.String())
}

func (m *ListModel) FullHelp() [][]key.Binding {
	return m.list.FullHelp()
}

func (m *ListModel) ShortHelp() []key.Binding {
	h := m.list.ShortHelp()
	// Remove up/down from short help
	return h[2:]
}

// truncateString truncates a string to maxW visual width, appending an ellipsis if truncated.
func truncateString(s string, maxW int) string {
	if lipgloss.Width(s) <= maxW {
		return s
	}
	// We need to fit "…" so target is maxW - 1
	target := maxW - 1
	var w int
	var sb strings.Builder
	for _, r := range s {
		rw := lipgloss.Width(string(r))
		if w+rw > target {
			break
		}
		sb.WriteRune(r)
		w += rw
	}
	sb.WriteString("…")
	return sb.String()
}

func getIcon(i connectionItem) string {
	var icon string
	if i.IsVisible {
		switch i.Security {
		case wifi.SecurityOpen:
			icon = CurrentTheme.NetworkOpenIcon
		case wifi.SecurityUnknown:
			icon = CurrentTheme.NetworkUnknownIcon
		default:
			icon = CurrentTheme.NetworkSecureIcon
		}
	}
	if i.IsKnown {
		icon = CurrentTheme.NetworkSavedIcon
	}
	return icon
}
