package cli

import (
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"

	tea "charm.land/bubbletea/v2"

	"reasonix/internal/i18n"
	"reasonix/internal/skill"
)

var scopePriority = map[skill.Scope]int{
	skill.ScopeProject: 0,
	skill.ScopeCustom:  1,
	skill.ScopeGlobal:  2,
	skill.ScopeBuiltin: 3,
}

type skillPickerMode string

const (
	pickerSkills        skillPickerMode = "skills"
	pickerSources       skillPickerMode = "sources"
	pickerSourceSkills  skillPickerMode = "source-skills"
	pickerDetail        skillPickerMode = "detail"
	pickerConfirmDelete skillPickerMode = "confirm-delete"
)

type skillPicker struct {
	mode            skillPickerMode
	skills          []skill.Skill
	roots           []skillRootLine
	enabled         map[string]bool
	originalEnabled map[string]bool
	query           string
	sel             int
	sourceSel       int
	sourceSkillSel  int
	showDiagnostics bool
	searchActive    bool
	detailSkill     skill.Skill
	detailBack      skillPickerMode
	detailAction    int
	confirm         int
	deleteSkill     skill.Skill
}

type skillRootLine struct {
	dir        string
	scope      skill.Scope
	status     skill.PathStatus
	skills     int
	configured bool
	diagnostic bool
}

func (m *chatTUI) openSkillPicker() {
	st := m.skillStore()
	skills := st.List()
	if m.ctrl != nil {
		skills = m.ctrl.AllSkills()
	}
	if len(skills) == 0 {
		m.notice(i18n.M.ListSkillsNone)
		return
	}
	sorted := sortedSkills(skills)
	enabled := map[string]bool{}
	original := map[string]bool{}
	disabled := m.disabledSkillNames()
	for _, sk := range sorted {
		on := !disabled[sk.Name]
		enabled[sk.Name] = on
		original[sk.Name] = on
	}
	m.skillPick = &skillPicker{
		mode:            pickerSkills,
		skills:          sorted,
		roots:           skillRootLines(st, sorted),
		enabled:         enabled,
		originalEnabled: original,
	}
}

func (m chatTUI) handleSkillPickerKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	p := m.skillPick
	if p == nil {
		return m, nil
	}

	if p.searchActive {
		switch msg.String() {
		case "esc":
			p.searchActive = false
			return m, nil
		case "enter":
			return m.saveSkillPick()
		case "backspace":
			if len(p.query) > 0 {
				p.query = p.query[:len(p.query)-1]
				p.sel = clampSel(p.sel, p.filteredSkills())
			}
			return m, nil
		case "up", "k":
			if p.sel > 0 {
				p.sel--
			}
			return m, nil
		case "down", "j":
			filtered := p.filteredSkills()
			if p.sel < len(filtered)-1 {
				p.sel++
			}
			return m, nil
		default:
			if t := msg.Text; t != "" {
				p.query += t
			} else if s := msg.String(); len(s) == 1 && s[0] >= 32 && s[0] < 127 {
				p.query += s
			}
			p.sel = clampSel(p.sel, p.filteredSkills())
			return m, nil
		}
	}

	switch p.mode {
	case pickerSkills:
		return m.handleSkillPickerSkillsKey(msg)
	case pickerSources:
		return m.handleSkillPickerSourcesKey(msg)
	case pickerSourceSkills:
		return m.handleSkillPickerSourceSkillsKey(msg)
	case pickerDetail:
		return m.handleSkillPickerDetailKey(msg)
	case pickerConfirmDelete:
		return m.handleSkillPickerConfirmDeleteKey(msg)
	}
	return m, nil
}

func (m chatTUI) handleSkillPickerSkillsKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	p := m.skillPick
	switch msg.String() {
	case "esc":
		m.skillPick = nil
	case "up", "k":
		if p.sel > 0 {
			p.sel--
		}
	case "down", "j":
		if p.sel < len(p.skills)-1 {
			p.sel++
		}
	case "enter":
		return m.saveSkillPick()
	case " ", "space":
		p.toggleSelectedSkill()
	case "right", "l":
		if sk, ok := p.selectedSkill(); ok {
			p.openDetail(sk, pickerSkills)
		}
	case "/":
		p.searchActive = true
		p.query = ""
	case "s":
		p.mode = pickerSources
		p.searchActive = false
		p.sourceSel = 0
	case "r":
		m.rescanSkills()
	}
	return m, nil
}

func (m chatTUI) handleSkillPickerSourcesKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	p := m.skillPick
	visible := p.visibleRoots()
	switch msg.String() {
	case "esc":
		m.skillPick = nil
	case "up", "k":
		if p.sourceSel > 0 {
			p.sourceSel--
		}
	case "down", "j":
		if p.sourceSel < len(visible)-1 {
			p.sourceSel++
		}
	case "enter", "right", "l":
		if len(visible) > 0 {
			p.mode = pickerSourceSkills
			p.sourceSkillSel = 0
		}
	case "d":
		p.showDiagnostics = !p.showDiagnostics
		p.sourceSel = clampSel(p.sourceSel, visible)
	case "s":
		p.mode = pickerSkills
		p.sourceSel = 0
		p.showDiagnostics = false
	case "r":
		m.rescanSkills()
	}
	return m, nil
}

func (m chatTUI) handleSkillPickerSourceSkillsKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	p := m.skillPick
	skills := p.selectedRootSkills()
	switch msg.String() {
	case "esc", "left", "h":
		p.mode = pickerSources
	case "up", "k":
		if p.sourceSkillSel > 0 {
			p.sourceSkillSel--
		}
	case "down", "j":
		if p.sourceSkillSel < len(skills)-1 {
			p.sourceSkillSel++
		}
	case "enter", "right", "l":
		if p.sourceSkillSel >= 0 && p.sourceSkillSel < len(skills) {
			p.openDetail(skills[p.sourceSkillSel], pickerSourceSkills)
		}
	case " ", "space":
		if p.sourceSkillSel >= 0 && p.sourceSkillSel < len(skills) {
			p.toggleSkill(skills[p.sourceSkillSel].Name)
		}
	}
	p.sourceSkillSel = clampSel(p.sourceSkillSel, skills)
	return m, nil
}

func (m chatTUI) handleSkillPickerDetailKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	p := m.skillPick
	actions := skillActionsFor(p.detailSkill)
	p.detailAction = clampInt(p.detailAction, len(actions))
	switch msg.String() {
	case "esc", "left", "h":
		p.mode = p.detailBack
		p.detailAction = 0
	case "up", "k":
		if p.detailAction > 0 {
			p.detailAction--
		}
	case "down", "j":
		if p.detailAction < len(actions)-1 {
			p.detailAction++
		}
	case "enter":
		if len(actions) > 0 {
			return m.applySkillAction(p.detailSkill, actions[p.detailAction])
		}
	case " ", "space":
		p.toggleSkill(p.detailSkill.Name)
	}
	p.detailAction = clampInt(p.detailAction, len(actions))
	return m, nil
}

func (m chatTUI) handleSkillPickerConfirmDeleteKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	p := m.skillPick
	switch msg.String() {
	case "up", "k", "down", "j":
		if p.confirm == 0 {
			p.confirm = 1
		} else {
			p.confirm = 0
		}
	case "y":
		p.confirm = 0
		return m.deleteSkillPick(p.deleteSkill)
	case "n", "esc":
		p.mode = pickerDetail
	case "enter":
		if p.confirm == 0 {
			return m.deleteSkillPick(p.deleteSkill)
		}
		p.mode = pickerDetail
	}
	return m, nil
}

func (m chatTUI) saveSkillPick() (tea.Model, tea.Cmd) {
	p := m.skillPick
	if p == nil {
		return m, nil
	}
	changes := p.changedEnabled()
	m.skillPick = nil
	if len(changes) == 0 {
		m.notice(i18n.M.SkillPickerNoChanges)
		return m, nil
	}
	m.skillSaveEnabledChanges(changes)
	if m.pendingModelSwitch != nil {
		return m, m.pendingModelSwitch
	}
	return m, nil
}

func (m chatTUI) deleteSkillPick(sk skill.Skill) (tea.Model, tea.Cmd) {
	p := m.skillPick
	target, ok, err := skillDeleteTarget(sk)
	if err != nil {
		m.notice("skill delete: " + err.Error())
		if p != nil {
			p.mode = pickerDetail
		}
		return m, nil
	}
	if !ok {
		m.notice("skill delete: built-in skills cannot be removed")
		if p != nil {
			p.mode = pickerDetail
		}
		return m, nil
	}
	if err := os.RemoveAll(target); err != nil {
		m.notice("skill delete: " + err.Error())
		if p != nil {
			p.mode = pickerDetail
		}
		return m, nil
	}
	if p != nil {
		delete(p.enabled, sk.Name)
		delete(p.originalEnabled, sk.Name)
		p.mode = pickerSkills
		p.detailAction = 0
	}
	m.notice(fmt.Sprintf(i18n.M.SkillPickerDeletedFmt, sk.Name))
	m.refreshSkillPickerData()
	m.scheduleSkillSessionRefresh("skill delete", "deleted skill "+sk.Name+" — refreshing session")
	return m, m.pendingModelSwitch
}

func (m *chatTUI) rescanSkills() {
	m.refreshSkillPickerData()
	m.notice(i18n.M.SkillPickerRescanned)
}

func (m *chatTUI) refreshSkillPickerData() {
	st := m.skillStore()
	skills := st.List()
	m.skills = skills
	m.invalidateSlashCatalog()
	if m.skillPick != nil {
		sorted := sortedSkills(skills)
		m.skillPick.skills = sorted
		m.skillPick.roots = skillRootLines(st, sorted)
		m.skillPick.syncEnabledMaps(sorted, m.disabledSkillNames())
		if m.skillPick.searchActive && m.skillPick.query != "" {
			m.skillPick.sel = clampSel(m.skillPick.sel, m.skillPick.filteredSkills())
		} else {
			m.skillPick.sel = clampSel(m.skillPick.sel, sorted)
		}
		m.skillPick.sourceSel = clampSel(m.skillPick.sourceSel, m.skillPick.visibleRoots())
		m.skillPick.sourceSkillSel = clampSel(m.skillPick.sourceSkillSel, m.skillPick.selectedRootSkills())
	}
}

func (p *skillPicker) filteredSkills() []skill.Skill {
	if p.query == "" {
		return p.skills
	}
	q := strings.ToLower(p.query)
	var out []skill.Skill
	for _, s := range p.skills {
		if strings.Contains(strings.ToLower(s.SlashName()), q) ||
			strings.Contains(strings.ToLower(s.Plugin), q) ||
			strings.Contains(strings.ToLower(s.Description), q) {
			out = append(out, s)
		}
	}
	return out
}

func (p *skillPicker) visibleRoots() []skillRootLine {
	if p.showDiagnostics {
		return p.roots
	}
	var out []skillRootLine
	for _, r := range p.roots {
		if !r.diagnostic || r.configured || r.skills > 0 {
			out = append(out, r)
		}
	}
	return out
}

func (p *skillPicker) selectedSkill() (skill.Skill, bool) {
	if p == nil {
		return skill.Skill{}, false
	}
	skills := p.skills
	if p.searchActive && p.query != "" {
		skills = p.filteredSkills()
	}
	if len(skills) == 0 {
		return skill.Skill{}, false
	}
	p.sel = clampSel(p.sel, skills)
	return skills[p.sel], true
}

func (p *skillPicker) toggleSelectedSkill() {
	if sk, ok := p.selectedSkill(); ok {
		p.toggleSkill(sk.Name)
	}
}

func (p *skillPicker) openDetail(sk skill.Skill, back skillPickerMode) {
	p.detailSkill = sk
	p.detailBack = back
	p.detailAction = 0
	p.mode = pickerDetail
}

func (p *skillPicker) skillEnabled(name string) bool {
	if p == nil || p.enabled == nil {
		return true
	}
	if enabled, ok := p.enabled[name]; ok {
		return enabled
	}
	return true
}

func (p *skillPicker) toggleSkill(name string) {
	if p.enabled == nil {
		p.enabled = map[string]bool{}
	}
	if p.originalEnabled == nil {
		p.originalEnabled = map[string]bool{}
	}
	if _, ok := p.originalEnabled[name]; !ok {
		p.originalEnabled[name] = p.skillEnabled(name)
	}
	p.enabled[name] = !p.skillEnabled(name)
}

func (p *skillPicker) changedEnabled() map[string]bool {
	changes := map[string]bool{}
	if p == nil {
		return changes
	}
	for name, enabled := range p.enabled {
		original, ok := p.originalEnabled[name]
		if !ok {
			original = true
		}
		if enabled != original {
			changes[name] = enabled
		}
	}
	return changes
}

func (p *skillPicker) syncEnabledMaps(skills []skill.Skill, disabled map[string]bool) {
	nextEnabled := map[string]bool{}
	nextOriginal := map[string]bool{}
	for _, sk := range skills {
		enabled := !disabled[sk.Name]
		if p.enabled != nil {
			if existing, ok := p.enabled[sk.Name]; ok {
				enabled = existing
			}
		}
		original := !disabled[sk.Name]
		if p.originalEnabled != nil {
			if existing, ok := p.originalEnabled[sk.Name]; ok {
				original = existing
			}
		}
		nextEnabled[sk.Name] = enabled
		nextOriginal[sk.Name] = original
	}
	p.enabled = nextEnabled
	p.originalEnabled = nextOriginal
}

func (p *skillPicker) selectedRoot() (skillRootLine, bool) {
	if p == nil {
		return skillRootLine{}, false
	}
	roots := p.visibleRoots()
	if len(roots) == 0 {
		return skillRootLine{}, false
	}
	p.sourceSel = clampSel(p.sourceSel, roots)
	return roots[p.sourceSel], true
}

func (p *skillPicker) selectedRootSkills() []skill.Skill {
	root, ok := p.selectedRoot()
	if !ok {
		return nil
	}
	var out []skill.Skill
	for _, sk := range p.skills {
		if skillInRoot(sk, root) {
			out = append(out, sk)
		}
	}
	return out
}

func skillInRoot(sk skill.Skill, root skillRootLine) bool {
	if root.scope == skill.ScopeBuiltin {
		return sk.Scope == skill.ScopeBuiltin
	}
	if sk.Scope != root.scope || sk.Path == "" || sk.Scope == skill.ScopeBuiltin {
		return false
	}
	cleanPath := filepath.Clean(sk.Path)
	cleanRoot := filepath.Clean(root.dir)
	prefix := cleanRoot + string(filepath.Separator)
	return cleanPath == cleanRoot || strings.HasPrefix(cleanPath, prefix)
}

func skillRootLines(st *skill.Store, skills []skill.Skill) []skillRootLine {
	storeRoots := st.Roots()
	lines := make([]skillRootLine, len(storeRoots))
	for i, r := range storeRoots {
		lines[i] = skillRootLine{
			dir:    r.Dir,
			scope:  r.Scope,
			status: r.Status,
		}
	}
	for _, s := range skills {
		if s.Scope == skill.ScopeBuiltin {
			continue
		}
		cleanPath := filepath.Clean(s.Path)
		for i := range lines {
			if lines[i].scope != s.Scope {
				continue
			}
			// Trailing separator: directory-boundary match so /skills can't match /skills-extra.
			prefix := filepath.Clean(lines[i].dir) + string(filepath.Separator)
			if strings.HasPrefix(cleanPath, prefix) {
				lines[i].skills++
				break
			}
		}
	}
	for i := range lines {
		if lines[i].scope == skill.ScopeCustom {
			lines[i].configured = true
		} else {
			lines[i].diagnostic = true
		}
	}
	builtinCount := 0
	for _, s := range skills {
		if s.Scope == skill.ScopeBuiltin {
			builtinCount++
		}
	}
	if builtinCount > 0 {
		lines = append(lines, skillRootLine{
			dir:        i18n.M.SkillPickerBuiltinSource,
			scope:      skill.ScopeBuiltin,
			status:     skill.StatusOK,
			skills:     builtinCount,
			diagnostic: true,
		})
	}
	return lines
}

type skillActionKind string

const (
	skillActionToggle skillActionKind = "toggle"
	skillActionDelete skillActionKind = "delete"
)

type skillActionItem struct {
	kind  skillActionKind
	label string
}

func skillActionsFor(s skill.Skill) []skillActionItem {
	actions := []skillActionItem{{
		kind:  skillActionToggle,
		label: i18n.M.SkillPickerActionToggle,
	}}
	if _, ok, _ := skillDeleteTarget(s); ok {
		actions = append(actions, skillActionItem{
			kind:  skillActionDelete,
			label: i18n.M.SkillPickerActionDelete,
		})
	}
	return actions
}

func (m chatTUI) applySkillAction(sk skill.Skill, action skillActionItem) (tea.Model, tea.Cmd) {
	p := m.skillPick
	if p == nil {
		return m, nil
	}
	switch action.kind {
	case skillActionToggle:
		p.toggleSkill(sk.Name)
	case skillActionDelete:
		p.deleteSkill = sk
		p.confirm = 1
		p.mode = pickerConfirmDelete
	}
	return m, nil
}

func skillDeleteTarget(s skill.Skill) (string, bool, error) {
	if s.Scope == skill.ScopeBuiltin {
		return "", false, nil
	}
	path := strings.TrimSpace(s.Path)
	if path == "" || path == "(builtin)" {
		return "", false, nil
	}
	clean := filepath.Clean(path)
	info, err := os.Stat(clean)
	if err != nil {
		return "", false, err
	}
	if filepath.Base(clean) == skill.SkillFile {
		dir := filepath.Dir(clean)
		if dir == "." || dir == string(filepath.Separator) {
			return "", false, fmt.Errorf("refusing to remove unsafe skill directory %q", dir)
		}
		return dir, true, nil
	}
	if info.Mode().IsRegular() {
		return clean, true, nil
	}
	return "", false, fmt.Errorf("skill path is not removable: %s", clean)
}

func skillDeleteTargetLabel(s skill.Skill) string {
	target, ok, _ := skillDeleteTarget(s)
	if !ok {
		return ""
	}
	return target
}

func sortedSkills(skills []skill.Skill) []skill.Skill {
	sorted := make([]skill.Skill, len(skills))
	copy(sorted, skills)
	sort.SliceStable(sorted, func(i, j int) bool {
		pi := scopePriority[sorted[i].Scope]
		pj := scopePriority[sorted[j].Scope]
		if pi != pj {
			return pi < pj
		}
		return sorted[i].Name < sorted[j].Name
	})
	return sorted
}

func clampSel[T any](sel int, items []T) int {
	if len(items) == 0 {
		return 0
	}
	if sel < 0 {
		return 0
	}
	if sel >= len(items) {
		return len(items) - 1
	}
	return sel
}

func clampInt(sel, total int) int {
	if total <= 0 {
		return 0
	}
	if sel < 0 {
		return 0
	}
	if sel >= total {
		return total - 1
	}
	return sel
}
