// Package cmd implements interactive subcommands invoked from the CLI.
// setup.go contains the provider onboarding wizard reachable via
// `cliamp setup`. It walks the user through configuring each remote
// provider (Navidrome, Plex, Jellyfin, Spotify, Qobuz, NetEase, YouTube Music),
// validates the connection where possible, and writes the resulting
// TOML section to ~/.config/cliamp/config.toml.
//
// The UI is a small standalone Bubbletea+Lipgloss program — separate from
// the main player Model — because setup runs to completion and exits.
package cmd

import (
	"context"
	"errors"
	"fmt"
	"io/fs"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"

	"github.com/bjarneo/cliamp/external/emby"
	"github.com/bjarneo/cliamp/external/jellyfin"
	"github.com/bjarneo/cliamp/external/navidrome"
	"github.com/bjarneo/cliamp/external/netease"
	"github.com/bjarneo/cliamp/external/plex"
	"github.com/bjarneo/cliamp/internal/appdir"
)

// Setup launches the interactive wizard. Returns nil on clean exit.
func Setup() error {
	prog := tea.NewProgram(newSetupModel())
	_, err := prog.Run()
	return err
}

// ----- Provider specs -----------------------------------------------------

type fieldSpec struct {
	key      string
	label    string
	help     string // shown faintly under the field
	required bool
	secret   bool
	defaultV string
	// onlyIf, when non-nil, hides the field unless the predicate returns true
	// against the current values map. Used by Jellyfin for token-vs-password.
	onlyIf func(map[string]string) bool
}

type providerSpec struct {
	key     string
	name    string
	section string
	intro   []string // pre-form blurb (with help URLs)
	picker  *pickerSpec
	fields  []fieldSpec
	// validate runs a probe against the live server. Nil skips validation.
	validate func(map[string]string) error
	// body returns the TOML block body (no header).
	body func(map[string]string) string
	// extraValidate runs after the form before validate, e.g. to enforce
	// "token OR (user+password)" for Jellyfin.
	extraValidate func(map[string]string) error
}

// pickerSpec is an optional radio choice shown before the fields. The
// selected option's key is stored in values[key]. fieldSpec.onlyIf can
// reference this to show different fields per choice.
type pickerSpec struct {
	key     string
	label   string
	options []pickerOption
}

type pickerOption struct {
	value string
	label string
}

// Picker keys are stored in the values map alongside real field keys; the
// leading underscore distinguishes them from TOML field names.
const (
	keyJellyfinAuth   = "_auth"
	keyEmbyAuth       = "_emby_auth"
	keyNetEaseBrowser = "_netease_browser"
	keyYTMusicMode    = "_mode"
	keySpotifyMode    = "_spotify_mode"
	keyQobuzQuality   = "_qobuz_quality"
)

func providers() []providerSpec {
	return []providerSpec{
		{
			key:     "navidrome",
			name:    "Navidrome / Subsonic",
			section: "navidrome",
			intro: []string{
				"Self-hosted music server using the Subsonic API.",
				"Docs: cliamp.stream → docs/navidrome.md",
			},
			fields: []fieldSpec{
				{key: "url", label: "Server URL", help: "e.g. https://music.example.com", required: true},
				{key: "user", label: "Username", required: true},
				{key: "password", label: "Password", required: true, secret: true},
			},
			validate: func(v map[string]string) error {
				return navidrome.New(v["url"], v["user"], v["password"]).Ping()
			},
			body: func(v map[string]string) string {
				return strings.Join([]string{
					fmt.Sprintf("url      = %q", v["url"]),
					fmt.Sprintf("user     = %q", v["user"]),
					fmt.Sprintf("password = %q", v["password"]),
				}, "\n")
			},
		},
		{
			key:     "plex",
			name:    "Plex Media Server",
			section: "plex",
			intro: []string{
				"Stream from your Plex server using an X-Plex-Token.",
				"Find a token: https://support.plex.tv/articles/204059436",
			},
			fields: []fieldSpec{
				{key: "url", label: "Server URL", help: "e.g. http://192.168.1.10:32400", required: true},
				{key: "token", label: "X-Plex-Token", required: true, secret: true},
			},
			validate: func(v map[string]string) error {
				return plex.NewClient(v["url"], v["token"]).Ping()
			},
			body: func(v map[string]string) string {
				return strings.Join([]string{
					fmt.Sprintf("url   = %q", v["url"]),
					fmt.Sprintf("token = %q", v["token"]),
				}, "\n")
			},
		},
		{
			key:     "jellyfin",
			name:    "Jellyfin",
			section: "jellyfin",
			intro: []string{
				"Authenticate with an API token (Dashboard → API Keys)",
				"or with your username and password.",
			},
			picker: &pickerSpec{
				key:   keyJellyfinAuth,
				label: "Authentication",
				options: []pickerOption{
					{value: "token", label: "API token"},
					{value: "password", label: "Username + password"},
				},
			},
			fields: []fieldSpec{
				{key: "url", label: "Server URL", help: "e.g. https://jellyfin.example.com", required: true},
				{key: "token", label: "API token", required: true, secret: true,
					onlyIf: func(v map[string]string) bool { return v[keyJellyfinAuth] == "token" }},
				{key: "user", label: "Username", required: true,
					onlyIf: func(v map[string]string) bool { return v[keyJellyfinAuth] == "password" }},
				{key: "password", label: "Password", required: true, secret: true,
					onlyIf: func(v map[string]string) bool { return v[keyJellyfinAuth] == "password" }},
			},
			validate: func(v map[string]string) error {
				return jellyfin.NewClient(v["url"], v["token"], "", v["user"], v["password"]).Ping()
			},
			body: func(v map[string]string) string {
				lines := []string{fmt.Sprintf("url      = %q", v["url"])}
				if v[keyJellyfinAuth] == "token" {
					lines = append(lines, fmt.Sprintf("token    = %q", v["token"]))
				} else {
					lines = append(lines,
						fmt.Sprintf("user     = %q", v["user"]),
						fmt.Sprintf("password = %q", v["password"]),
					)
				}
				return strings.Join(lines, "\n")
			},
		},
		{
			key:     "emby",
			name:    "Emby",
			section: "emby",
			intro: []string{
				"Authenticate with an API key (Dashboard → API Keys)",
				"or with your username and password.",
			},
			picker: &pickerSpec{
				key:   keyEmbyAuth,
				label: "Authentication",
				options: []pickerOption{
					{value: "token", label: "API key"},
					{value: "password", label: "Username + password"},
				},
			},
			fields: []fieldSpec{
				{key: "url", label: "Server URL", help: "e.g. https://emby.example.com", required: true},
				{key: "token", label: "API key", required: true, secret: true,
					onlyIf: func(v map[string]string) bool { return v[keyEmbyAuth] == "token" }},
				{key: "user", label: "Username (optional)", help: "multi-user servers: picks your account from /Users",
					onlyIf: func(v map[string]string) bool { return v[keyEmbyAuth] == "token" }},
				{key: "user", label: "Username", required: true,
					onlyIf: func(v map[string]string) bool { return v[keyEmbyAuth] == "password" }},
				{key: "password", label: "Password", required: true, secret: true,
					onlyIf: func(v map[string]string) bool { return v[keyEmbyAuth] == "password" }},
			},
			validate: func(v map[string]string) error {
				if err := emby.NewClient(v["url"], v["token"], "", v["user"], v["password"]).Ping(); err != nil {
					return fmt.Errorf("emby: validation: %w", err)
				}
				return nil
			},
			body: func(v map[string]string) string {
				lines := []string{fmt.Sprintf("url      = %q", v["url"])}
				if v[keyEmbyAuth] == "token" {
					lines = append(lines, fmt.Sprintf("token    = %q", v["token"]))
					if v["user"] != "" {
						lines = append(lines, fmt.Sprintf("user     = %q", v["user"]))
					}
				} else {
					lines = append(lines,
						fmt.Sprintf("user     = %q", v["user"]),
						fmt.Sprintf("password = %q", v["password"]),
					)
				}
				return strings.Join(lines, "\n")
			},
		},
		{
			key:     "spotify",
			name:    "Spotify (Premium)",
			section: "spotify",
			intro: []string{
				"Requires a Spotify Premium account.",
				"",
				"Recommended: register your own Spotify Developer app at",
				"developer.spotify.com/dashboard (redirect URI",
				"http://127.0.0.1:19872/login). Your own client_id gives you a",
				"private Web API rate-limit quota for library and playlists.",
				"Playback is authorized separately, so first use completes two",
				"steps in one browser tab. Apps registered after Nov 27, 2024",
				"can't use /v1/search; that's a Spotify dev-mode restriction.",
				"",
				"Alternative: cliamp ships a built-in client_id (the librespot",
				"keymaster) that bypasses the search restriction. It's shared",
				"with every librespot- and spotify-player-based client, so you",
				"may see occasional 429 errors when the pool is busy.",
			},
			picker: &pickerSpec{
				key:   keySpotifyMode,
				label: "Client ID",
				options: []pickerOption{
					{value: "custom", label: "Use my own Spotify Developer app client_id"},
					{value: "default", label: "Use built-in shared client_id"},
				},
			},
			fields: []fieldSpec{
				{key: "client_id", label: "Client ID", required: true,
					help:   "from developer.spotify.com/dashboard; redirect URI http://127.0.0.1:19872/login",
					onlyIf: func(v map[string]string) bool { return v[keySpotifyMode] == "custom" }},
				{key: "bitrate", label: "Bitrate", help: "96, 160, or 320 kbps", defaultV: "320"},
			},
			extraValidate: func(v map[string]string) error {
				if v["bitrate"] == "" {
					return nil
				}
				if _, err := strconv.Atoi(v["bitrate"]); err != nil {
					return fmt.Errorf("bitrate must be a number")
				}
				return nil
			},
			body: func(v map[string]string) string {
				br := v["bitrate"]
				if br == "" {
					br = "320"
				}
				lines := []string{}
				if v[keySpotifyMode] == "custom" && v["client_id"] != "" {
					lines = append(lines, fmt.Sprintf("client_id = %q", v["client_id"]))
				}
				lines = append(lines, fmt.Sprintf("bitrate   = %s", br))
				return strings.Join(lines, "\n")
			},
		},
		{
			key:     "qobuz",
			name:    "Qobuz",
			section: "qobuz",
			intro: []string{
				"Lossless streaming. Requires an active Qobuz subscription.",
				"",
				"No API credentials needed - cliamp obtains them automatically.",
				"After setup, launch cliamp, select Qobuz, and press Enter to",
				"sign in via OAuth in your browser. Hi-Res tiers require a plan",
				"that includes them.",
			},
			picker: &pickerSpec{
				key:   keyQobuzQuality,
				label: "Stream quality",
				options: []pickerOption{
					{value: "6", label: "FLAC 16-bit/44.1kHz (CD) - recommended"},
					{value: "7", label: "FLAC 24-bit up to 96kHz (Hi-Res)"},
					{value: "27", label: "FLAC 24-bit up to 192kHz (Hi-Res)"},
					{value: "5", label: "MP3 320kbps"},
				},
			},
			body: func(v map[string]string) string {
				q := v[keyQobuzQuality]
				if q == "" {
					q = "6"
				}
				return strings.Join([]string{
					"enabled = true",
					fmt.Sprintf("quality = %s", q),
				}, "\n")
			},
		},
		{
			key:     "netease",
			name:    "NetEase Cloud Music",
			section: "netease",
			intro: []string{
				"Reuses your browser session through yt-dlp cookies.",
				"Sign in at music.163.com first, then pick that browser here.",
			},
			picker: &pickerSpec{
				key:   keyNetEaseBrowser,
				label: "Browser session",
				options: []pickerOption{
					{value: "chrome", label: "Chrome"},
					{value: "safari", label: "Safari"},
					{value: "firefox", label: "Firefox"},
					{value: "brave", label: "Brave"},
					{value: "edge", label: "Edge"},
					{value: "chromium", label: "Chromium"},
					{value: "vivaldi", label: "Vivaldi"},
					{value: "custom", label: "Custom browser/profile"},
				},
			},
			fields: []fieldSpec{
				{key: "cookies_from", label: "Custom browser/profile", help: "e.g. chrome:Profile 1, firefox:default-release", required: true,
					onlyIf: func(v map[string]string) bool { return v[keyNetEaseBrowser] == "custom" }},
			},
			validate: func(v map[string]string) error {
				browser := netEaseCookiesFrom(v)
				if browser == "" {
					return fmt.Errorf("browser is required")
				}
				v["cookies_from"] = browser
				ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
				defer cancel()
				acc, err := netease.CheckLogin(ctx, browser)
				if err != nil {
					return fmt.Errorf("netease: validation: %w", err)
				}
				v["user_id"] = acc.UserID
				return nil
			},
			body: func(v map[string]string) string {
				browser := netEaseCookiesFrom(v)
				lines := []string{
					"enabled      = true",
					fmt.Sprintf("cookies_from = %q", browser),
				}
				if v["user_id"] != "" {
					lines = append(lines, fmt.Sprintf("user_id      = %q", v["user_id"]))
				}
				return strings.Join(lines, "\n")
			},
		},
		{
			key:     "ytmusic",
			name:    "YouTube Music",
			section: "ytmusic",
			intro: []string{
				"Works out of the box with built-in fallback credentials.",
				"Provide your own OAuth client to skip the shared pool, and/or",
				"a browser name for cookie-based age-gated playback.",
			},
			picker: &pickerSpec{
				key:   keyYTMusicMode,
				label: "Mode",
				options: []pickerOption{
					{value: "default", label: "Use built-in credentials (recommended)"},
					{value: "custom", label: "Provide my own OAuth credentials / cookies"},
					{value: "off", label: "Disable YouTube Music"},
				},
			},
			fields: []fieldSpec{
				{key: "client_id", label: "OAuth Client ID",
					onlyIf: func(v map[string]string) bool { return v[keyYTMusicMode] == "custom" }},
				{key: "client_secret", label: "OAuth Client Secret", secret: true,
					onlyIf: func(v map[string]string) bool { return v[keyYTMusicMode] == "custom" }},
				{key: "cookies_from", label: "Cookies from browser", help: "e.g. chrome, firefox; blank to skip",
					onlyIf: func(v map[string]string) bool { return v[keyYTMusicMode] == "custom" }},
			},
			body: func(v map[string]string) string {
				switch v[keyYTMusicMode] {
				case "off":
					return "enabled = false"
				case "custom":
					lines := []string{"enabled = true"}
					if v["client_id"] != "" {
						lines = append(lines, fmt.Sprintf("client_id     = %q", v["client_id"]))
					}
					if v["client_secret"] != "" {
						lines = append(lines, fmt.Sprintf("client_secret = %q", v["client_secret"]))
					}
					if v["cookies_from"] != "" {
						lines = append(lines, fmt.Sprintf("cookies_from  = %q", v["cookies_from"]))
					}
					return strings.Join(lines, "\n")
				default:
					return "enabled = true"
				}
			},
		},
	}
}

func netEaseCookiesFrom(v map[string]string) string {
	picked := strings.TrimSpace(v[keyNetEaseBrowser])
	if picked == "" || picked == "custom" {
		return strings.TrimSpace(v["cookies_from"])
	}
	return picked
}

// ----- Bubbletea model ----------------------------------------------------

type stage int

const (
	stageMenu stage = iota
	stagePicker
	stageForm
	stageValidating
	stageResult
)

type setupModel struct {
	provs []providerSpec
	w, h  int

	stage      stage
	menuCursor int

	// active provider (index into provs); -1 means none
	pidx int

	// picker state (within a provider)
	pickerCursor int

	// form state
	values  map[string]string
	visible []int // indices into provs[pidx].fields that are currently visible
	fcursor int

	// validating
	spinFrame int

	// result state
	resultErr     error
	resultWarning bool   // validation failed but config still valid
	resultText    string // overall message (e.g. "Saved [navidrome] section.")
	saveFailed    error  // io error writing config (rare; shown as error result)
	awaitingSave  bool   // true after a failed validate: waiting for y/n

	// cached so we don't hit os.UserHomeDir per-frame
	cfgPath string
}

func newSetupModel() *setupModel {
	cfgPath, _ := configFilePath()
	return &setupModel{
		provs:   providers(),
		stage:   stageMenu,
		pidx:    -1,
		values:  map[string]string{},
		cfgPath: cfgPath,
	}
}

func (m *setupModel) Init() tea.Cmd {
	return func() tea.Msg { return tea.RequestWindowSize() }
}

// ----- Messages -----------------------------------------------------------

type validateDoneMsg struct{ err error }

type spinTickMsg struct{}

func spinTickCmd() tea.Cmd {
	return tea.Tick(120*time.Millisecond, func(time.Time) tea.Msg { return spinTickMsg{} })
}

func runValidateCmd(spec providerSpec, values map[string]string) tea.Cmd {
	return func() tea.Msg {
		if spec.validate == nil {
			return validateDoneMsg{}
		}
		return validateDoneMsg{err: spec.validate(values)}
	}
}

// ----- Update -------------------------------------------------------------

func (m *setupModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.w, m.h = msg.Width, msg.Height
		return m, nil

	case tea.KeyPressMsg:
		return m.handleKey(msg)

	case tea.PasteMsg:
		return m.handlePaste(msg.Content)

	case spinTickMsg:
		if m.stage == stageValidating {
			m.spinFrame++
			return m, spinTickCmd()
		}
		return m, nil

	case validateDoneMsg:
		return m.onValidateDone(msg.err)
	}
	return m, nil
}

// handlePaste appends bracketed-paste content into the active form field.
// Newlines and other control characters are stripped — the wizard's fields
// are all single-line, and pasted credentials often have a trailing newline
// from the source app.
func (m *setupModel) handlePaste(content string) (tea.Model, tea.Cmd) {
	if content == "" || m.stage != stageForm || len(m.visible) == 0 {
		return m, nil
	}
	clean := sanitizePaste(content)
	if clean == "" {
		return m, nil
	}
	field := m.provs[m.pidx].fields[m.visible[m.fcursor]]
	m.values[field.key] += clean
	return m, nil
}

func sanitizePaste(s string) string {
	var b strings.Builder
	b.Grow(len(s))
	for _, r := range s {
		// Drop newlines and other ASCII control chars; keep spaces and tabs (tabs as space).
		switch {
		case r == '\n' || r == '\r':
			continue
		case r == '\t':
			b.WriteRune(' ')
		case r < 0x20:
			continue
		default:
			b.WriteRune(r)
		}
	}
	return b.String()
}

func (m *setupModel) handleKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	if msg.String() == "ctrl+c" {
		return m, tea.Quit
	}

	switch m.stage {
	case stageMenu:
		return m.menuKey(msg)
	case stagePicker:
		return m.pickerKey(msg)
	case stageForm:
		return m.formKey(msg)
	case stageValidating:
		// Ignore input while probing.
		return m, nil
	case stageResult:
		return m.resultKey(msg)
	}
	return m, nil
}

func (m *setupModel) menuKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	switch msg.String() {
	case "q", "esc":
		return m, tea.Quit
	case "up", "k":
		if m.menuCursor > 0 {
			m.menuCursor--
		}
	case "down", "j":
		if m.menuCursor < len(m.provs)-1 {
			m.menuCursor++
		}
	case "enter":
		m.startProvider(m.menuCursor)
	}
	return m, nil
}

func (m *setupModel) startProvider(idx int) {
	m.pidx = idx
	m.values = map[string]string{}
	m.fcursor = 0
	m.pickerCursor = 0
	m.resultErr = nil
	m.resultText = ""
	m.resultWarning = false
	m.awaitingSave = false
	m.saveFailed = nil

	if m.provs[idx].picker != nil {
		m.stage = stagePicker
	} else {
		m.refreshVisibleFields()
		m.stage = stageForm
	}
}

func (m *setupModel) pickerKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	p := m.provs[m.pidx].picker
	switch msg.String() {
	case "esc":
		m.stage = stageMenu
		return m, nil
	case "up", "k":
		if m.pickerCursor > 0 {
			m.pickerCursor--
		}
	case "down", "j":
		if m.pickerCursor < len(p.options)-1 {
			m.pickerCursor++
		}
	case "enter":
		m.values[p.key] = p.options[m.pickerCursor].value
		// Pickers can short-circuit with no fields (e.g. ytmusic "default" / "off").
		m.refreshVisibleFields()
		if len(m.visible) == 0 {
			return m.submitForm()
		}
		m.stage = stageForm
	}
	return m, nil
}

func (m *setupModel) formKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	if len(m.visible) == 0 {
		// Defensive: shouldn't happen; submit anyway.
		return m.submitForm()
	}
	field := m.provs[m.pidx].fields[m.visible[m.fcursor]]
	cur := m.values[field.key]

	switch msg.Code {
	case tea.KeyEscape:
		m.stage = stageMenu
		return m, nil
	case tea.KeyUp:
		if m.fcursor > 0 {
			m.fcursor--
		}
		return m, nil
	case tea.KeyTab, tea.KeyDown:
		// Shift+Tab arrives with ModShift; treat it as "previous field".
		if msg.Mod&tea.ModShift != 0 {
			if m.fcursor > 0 {
				m.fcursor--
			}
			return m, nil
		}
		if m.fcursor < len(m.visible)-1 {
			m.fcursor++
		}
		return m, nil
	case tea.KeyEnter:
		// Enter on the last field submits; otherwise advance.
		if m.fcursor < len(m.visible)-1 {
			m.fcursor++
			return m, nil
		}
		return m.submitForm()
	case tea.KeyBackspace:
		if cur != "" {
			m.values[field.key] = removeLastRune(cur)
		}
		return m, nil
	case tea.KeySpace:
		m.values[field.key] = cur + " "
		return m, nil
	}

	// Treat ctrl+s / ctrl+d as submit shortcuts.
	if s := msg.String(); s == "ctrl+s" || s == "ctrl+d" {
		return m.submitForm()
	}

	if len(msg.Text) > 0 {
		m.values[field.key] = cur + msg.Text
	}
	return m, nil
}

func (m *setupModel) submitForm() (tea.Model, tea.Cmd) {
	spec := m.provs[m.pidx]

	// Apply defaults for blank fields.
	for _, f := range spec.fields {
		if m.values[f.key] == "" && f.defaultV != "" {
			m.values[f.key] = f.defaultV
		}
	}

	// Required-field check (only for visible fields).
	for i, idx := range m.visible {
		f := spec.fields[idx]
		if f.required && strings.TrimSpace(m.values[f.key]) == "" {
			m.fcursor = i
			m.resultErr = fmt.Errorf("%s is required", f.label)
			m.resultText = ""
			m.stage = stageResult
			return m, nil
		}
	}

	if spec.extraValidate != nil {
		if err := spec.extraValidate(m.values); err != nil {
			m.resultErr = err
			m.stage = stageResult
			return m, nil
		}
	}

	// Light URL sanity check before any network call.
	if u, ok := m.values["url"]; ok && u != "" {
		clean := strings.TrimRight(u, "/")
		if !looksLikeHTTPURL(clean) {
			m.resultErr = fmt.Errorf("URL must start with http:// or https://")
			m.stage = stageResult
			return m, nil
		}
		m.values["url"] = clean
	}

	if spec.validate == nil {
		// No probe; save immediately.
		return m, m.persistAndDone(false)
	}

	m.stage = stageValidating
	m.spinFrame = 0
	return m, tea.Batch(spinTickCmd(), runValidateCmd(spec, m.values))
}

func (m *setupModel) onValidateDone(err error) (tea.Model, tea.Cmd) {
	if err == nil {
		return m, m.persistAndDone(false)
	}
	m.resultErr = err
	m.awaitingSave = true
	m.stage = stageResult
	return m, nil
}

// persistAndDone writes the section and transitions to result. warn=true
// indicates the user opted to save despite a failed probe.
func (m *setupModel) persistAndDone(warn bool) tea.Cmd {
	spec := m.provs[m.pidx]
	body := spec.body(m.values)
	if err := saveSection(spec.section, body); err != nil {
		m.saveFailed = err
		m.stage = stageResult
		m.awaitingSave = false
		return nil
	}
	m.stage = stageResult
	m.awaitingSave = false
	m.resultWarning = warn
	m.resultText = fmt.Sprintf("Saved [%s] section.", spec.section)
	return nil
}

func (m *setupModel) resultKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	if m.awaitingSave {
		switch strings.ToLower(msg.String()) {
		case "y":
			m.persistAndDone(true)
			return m, nil
		case "n", "esc":
			m.awaitingSave = false
			m.resultErr = nil
			m.stage = stageMenu
			return m, nil
		}
		return m, nil
	}

	switch msg.String() {
	case "enter", "esc", "q", " ":
		m.stage = stageMenu
		return m, nil
	}
	return m, nil
}

// refreshVisibleFields recomputes the visible-field index list based on
// the current values map, then resets the cursor to a sensible position.
func (m *setupModel) refreshVisibleFields() {
	spec := m.provs[m.pidx]
	m.visible = m.visible[:0]
	for i, f := range spec.fields {
		if f.onlyIf == nil || f.onlyIf(m.values) {
			m.visible = append(m.visible, i)
		}
	}
	if m.fcursor >= len(m.visible) {
		m.fcursor = 0
	}
}

// ----- Rendering ----------------------------------------------------------

var (
	// Color choices match the existing player palette; ANSI numbers give
	// good contrast against any terminal theme.
	titleStyle  = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(10)).Bold(true)
	hintStyle   = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(7))
	dimStyle    = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(8))
	accentStyle = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(11)).Bold(true)
	errStyle    = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(9)).Bold(true)
	okStyle     = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(10)).Bold(true)
	warnStyle   = lipgloss.NewStyle().Foreground(lipgloss.ANSIColor(11)).Bold(true)
	cardStyle   = lipgloss.NewStyle().
			Border(lipgloss.RoundedBorder()).
			BorderForeground(lipgloss.ANSIColor(8)).
			Padding(1, 2)
	activeFieldStyle = lipgloss.NewStyle().
				Border(lipgloss.NormalBorder(), false, false, false, true).
				BorderForeground(lipgloss.ANSIColor(11)).
				PaddingLeft(1)
	inactiveFieldStyle = lipgloss.NewStyle().PaddingLeft(2)
)

const (
	maxCardWidth = 78
	logoLine1    = "  cliamp setup"
	logoLine2    = "  configure remote providers"
)

func (m *setupModel) View() tea.View {
	header := titleStyle.Render(logoLine1) + "\n" +
		hintStyle.Render(logoLine2) + "\n"

	var body string
	switch m.stage {
	case stageMenu:
		body = m.viewMenu()
	case stagePicker:
		body = m.viewPicker()
	case stageForm:
		body = m.viewForm()
	case stageValidating:
		body = m.viewValidating()
	case stageResult:
		body = m.viewResult()
	}

	footer := "\n" + m.viewFooter()
	view := tea.NewView(header + "\n" + body + footer)
	view.AltScreen = true
	return view
}

// card wraps body in the standard rounded-border card sized to the terminal.
func (m *setupModel) card(body string) string {
	return cardStyle.Width(min(maxCardWidth, m.viewWidth())).Render(body)
}

func (m *setupModel) viewMenu() string {
	var b strings.Builder
	b.WriteString(accentStyle.Render("Pick a provider to configure"))
	b.WriteString("\n\n")
	for i, p := range m.provs {
		marker := "  "
		nameRender := p.name
		if i == m.menuCursor {
			marker = accentStyle.Render("▸ ")
			nameRender = accentStyle.Render(p.name)
		}
		b.WriteString(marker)
		b.WriteString(nameRender)
		b.WriteString("\n")
		// Render the first intro line as a one-liner per item, faintly.
		if len(p.intro) > 0 {
			indent := "    "
			b.WriteString(indent)
			b.WriteString(hintStyle.Render(p.intro[0]))
			b.WriteString("\n")
		}
	}
	b.WriteString("\n")
	b.WriteString(dimStyle.Render("config: " + m.cfgPath))
	return m.card(b.String())
}

func (m *setupModel) viewPicker() string {
	spec := m.provs[m.pidx]
	p := spec.picker
	var b strings.Builder
	b.WriteString(accentStyle.Render(spec.name))
	b.WriteString("\n\n")
	for _, line := range spec.intro {
		b.WriteString(hintStyle.Render(line))
		b.WriteString("\n")
	}
	b.WriteString("\n")
	b.WriteString(accentStyle.Render(p.label))
	b.WriteString("\n\n")
	for i, opt := range p.options {
		marker := "  "
		text := opt.label
		if i == m.pickerCursor {
			marker = accentStyle.Render("▸ ")
			text = accentStyle.Render(opt.label)
		}
		b.WriteString(marker)
		b.WriteString(text)
		b.WriteString("\n")
	}
	return m.card(b.String())
}

func (m *setupModel) viewForm() string {
	spec := m.provs[m.pidx]
	var b strings.Builder
	b.WriteString(accentStyle.Render(spec.name))
	b.WriteString("\n\n")
	for _, line := range spec.intro {
		b.WriteString(hintStyle.Render(line))
		b.WriteString("\n")
	}
	b.WriteString("\n")

	for i, idx := range m.visible {
		f := spec.fields[idx]
		val := m.values[f.key]
		display := val
		if f.secret {
			display = strings.Repeat("•", utf8.RuneCountInString(val))
		}
		labelLine := f.label
		if f.required {
			labelLine += accentStyle.Render(" *")
		}
		valueLine := display
		if i == m.fcursor {
			// Active: show cursor caret at end of value.
			valueLine = display + accentStyle.Render("▎")
			block := labelLine + "\n" + valueLine
			if f.help != "" {
				block += "\n" + dimStyle.Render(f.help)
			}
			b.WriteString(activeFieldStyle.Render(block))
		} else {
			if valueLine == "" {
				valueLine = dimStyle.Render("(empty)")
			}
			block := labelLine + "\n" + valueLine
			b.WriteString(inactiveFieldStyle.Render(block))
		}
		b.WriteString("\n\n")
	}
	return m.card(strings.TrimRight(b.String(), "\n"))
}

func (m *setupModel) viewValidating() string {
	frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
	frame := frames[m.spinFrame%len(frames)]
	body := accentStyle.Render(frame) + " " +
		"Verifying connection to " + m.provs[m.pidx].name + "…"
	return cardStyle.Width(min(maxCardWidth, m.viewWidth())).Render(body)
}

func (m *setupModel) viewResult() string {
	spec := m.provs[m.pidx]
	var b strings.Builder
	b.WriteString(accentStyle.Render(spec.name))
	b.WriteString("\n\n")

	if m.saveFailed != nil {
		b.WriteString(errStyle.Render("✗ Failed to write config"))
		b.WriteString("\n\n")
		b.WriteString(m.saveFailed.Error())
		b.WriteString("\n\n")
		b.WriteString(hintStyle.Render("Press any key to return to the menu."))
		return m.card(b.String())
	}

	if m.awaitingSave {
		b.WriteString(errStyle.Render("✗ Validation failed"))
		b.WriteString("\n\n")
		b.WriteString(m.resultErr.Error())
		b.WriteString("\n\n")
		b.WriteString(hintStyle.Render("The config will still load on next launch — useful when the server is offline now."))
		b.WriteString("\n\n")
		b.WriteString(accentStyle.Render("Save anyway?  ") + "[y/N]")
		return m.card(b.String())
	}

	if m.resultErr != nil {
		b.WriteString(errStyle.Render("✗ "))
		b.WriteString(m.resultErr.Error())
		b.WriteString("\n\n")
		b.WriteString(hintStyle.Render("Press any key to return to the menu."))
		return m.card(b.String())
	}

	if m.resultWarning {
		b.WriteString(warnStyle.Render("⚠ Saved without verification"))
	} else {
		b.WriteString(okStyle.Render("✓ " + m.resultText))
	}
	b.WriteString("\n\n")
	b.WriteString(dimStyle.Render(m.cfgPath))
	b.WriteString("\n\n")
	b.WriteString(hintStyle.Render("Press any key to configure another provider, or q to quit."))
	return m.card(b.String())
}

func (m *setupModel) viewFooter() string {
	var keys string
	switch m.stage {
	case stageMenu:
		keys = "↑/↓ pick   enter select   q quit"
	case stagePicker:
		keys = "↑/↓ pick   enter confirm   esc back"
	case stageForm:
		keys = "↑/↓ field   enter submit   esc back"
	case stageValidating:
		keys = "ctrl+c cancel"
	case stageResult:
		if m.awaitingSave {
			keys = "y save anyway   n cancel"
		} else {
			keys = "any key continue   q quit"
		}
	}
	return dimStyle.Render(keys)
}

func (m *setupModel) viewWidth() int {
	if m.w <= 4 {
		return maxCardWidth
	}
	return m.w - 2
}

// ----- Helpers ------------------------------------------------------------

func looksLikeHTTPURL(s string) bool {
	u, err := url.Parse(s)
	if err != nil {
		return false
	}
	return u.Scheme == "http" || u.Scheme == "https"
}

func configFilePath() (string, error) {
	dir, err := appdir.Dir()
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, "config.toml"), nil
}

// removeLastRune trims the final UTF-8 rune from s.
func removeLastRune(s string) string {
	if len(s) > 0 {
		_, size := utf8.DecodeLastRuneInString(s)
		return s[:len(s)-size]
	}
	return s
}

// saveSection rewrites or appends a [section] block in config.toml. The
// body is the raw TOML between the header and the next section/EOF; it
// must not contain a header line itself. Existing content for the same
// section is replaced; everything else is preserved as-is.
func saveSection(section, body string) error {
	path, err := configFilePath()
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}

	header := "[" + section + "]"
	block := header + "\n" + strings.TrimRight(body, "\n") + "\n"

	data, err := os.ReadFile(path)
	if err != nil {
		if !errors.Is(err, fs.ErrNotExist) {
			return err
		}
		return os.WriteFile(path, []byte(block), 0o644)
	}

	lines := strings.Split(string(data), "\n")
	start, end := findSection(lines, section)
	if start < 0 {
		out := strings.TrimRight(string(data), "\n")
		if out != "" {
			out += "\n\n"
		}
		out += block
		return os.WriteFile(path, []byte(out), 0o644)
	}

	before := strings.TrimRight(strings.Join(lines[:start], "\n"), "\n")
	after := ""
	if end < len(lines) {
		after = strings.TrimLeft(strings.Join(lines[end:], "\n"), "\n")
	}
	var b strings.Builder
	if before != "" {
		b.WriteString(before)
		b.WriteString("\n\n")
	}
	b.WriteString(block)
	if after != "" {
		b.WriteString("\n")
		b.WriteString(after)
		if !strings.HasSuffix(after, "\n") {
			b.WriteString("\n")
		}
	}
	return os.WriteFile(path, []byte(b.String()), 0o644)
}

// findSection returns [start, end) line indices for the [name] block in
// lines, where start points at the header line and end points at the
// next section header (or len(lines) if it's the last block). Returns
// (-1, -1) if the section is absent.
func findSection(lines []string, name string) (int, int) {
	target := "[" + strings.ToLower(name) + "]"
	start := -1
	for i, l := range lines {
		t := strings.ToLower(strings.TrimSpace(l))
		if t == target {
			start = i
			break
		}
	}
	if start < 0 {
		return -1, -1
	}
	for i := start + 1; i < len(lines); i++ {
		t := strings.TrimSpace(lines[i])
		if strings.HasPrefix(t, "[") && strings.HasSuffix(t, "]") {
			return start, i
		}
	}
	return start, len(lines)
}
