#!/usr/bin/env bash
set -euo pipefail

# =============================================================================
# setup-create-tui.sh — Scaffoldea aplicación TUI en Go con DevFactory
# Genera un TUI completo para gestionar scripts, comandos, Makefile y builds
# =============================================================================

# --- Colores ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'

log_info()  { echo -e "${BLUE}[INFO]${NC} $1"; }
log_ok()    { echo -e "${GREEN}[OK]${NC} $1"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_step()  { echo -e "${CYAN}[STEP]${NC} $1"; }

# --- Parámetros ---
# Env vars configurables (con defaults razonables):
#   DEVFACTORY_PATH    — ruta local al repo DevFactory (default: $HOME/.local_agentes/backend)
#   DEVFACTORY_MODULE  — nombre del módulo Go de DevFactory
#   GO_NAMESPACE       — namespace para el módulo Go del proyecto (default: github.com/lucasdataproyects)
MODULE_NAME="${1:-}"
TARGET_PATH="${2:-.}"
DEVFACTORY_PATH="${DEVFACTORY_PATH:-$HOME/.local_agentes/backend}"
DEVFACTORY_MODULE="${DEVFACTORY_MODULE:-github.com/lucasdataproyects/devfactory}"
GO_NAMESPACE="${GO_NAMESPACE:-github.com/lucasdataproyects}"

# --- Validar nombre ---
if [[ -z "$MODULE_NAME" ]]; then
    log_error "Uso: setup-create-tui.sh <nombre> [path]"
    echo "STATUS: ERROR"
    exit 1
fi

# Normalizar nombre a kebab-case
MODULE_NAME=$(echo "$MODULE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g')
PROJECT_DIR="$TARGET_PATH/$MODULE_NAME"
GO_MODULE="${GO_NAMESPACE}/$MODULE_NAME"

# --- Check estado existente ---
if [[ -f "$PROJECT_DIR/go.mod" ]]; then
    log_warn "El proyecto TUI $MODULE_NAME ya existe en $PROJECT_DIR"
    echo "STATUS: CONFIGURED"
    exit 0
fi

# --- Verificar dependencias ---
log_step "Verificando dependencias..."

if ! command -v go &>/dev/null; then
    log_error "Go no está instalado. Instala Go 1.22+"
    echo "STATUS: ERROR"
    exit 1
fi

GO_VERSION=$(go version | grep -oP '\d+\.\d+' | head -1)
log_ok "Go $GO_VERSION encontrado"

if [[ ! -d "$DEVFACTORY_PATH" ]]; then
    log_error "DevFactory no encontrado en $DEVFACTORY_PATH — es necesario para los componentes TUI"
    echo "STATUS: ERROR"
    exit 1
fi

log_ok "DevFactory encontrado en $DEVFACTORY_PATH"

# --- Crear estructura ---
log_step "Creando estructura del proyecto TUI '$MODULE_NAME'..."

mkdir -p "$PROJECT_DIR"/{app,views,config}

# =============================================================================
# go.mod
# =============================================================================
log_step "Generando go.mod..."
cat > "$PROJECT_DIR/go.mod" << EOF
module $GO_MODULE

go 1.22.2

require $DEVFACTORY_MODULE v0.0.0

replace $DEVFACTORY_MODULE => $DEVFACTORY_PATH
EOF

# =============================================================================
# go.work
# =============================================================================
log_step "Generando go.work con DevFactory local..."
cat > "$PROJECT_DIR/go.work" << EOF
go 1.22.2

use (
    .
    $DEVFACTORY_PATH
)
EOF
log_ok "go.work enlazado a DevFactory"

# =============================================================================
# config/config.go
# =============================================================================
log_step "Generando config/config.go..."
cat > "$PROJECT_DIR/config/config.go" << 'GOEOF'
package config

import (
	"path/filepath"
)

// CommandEntry representa un comando frecuente configurable.
type CommandEntry struct {
	Name        string
	Command     string
	Description string
}

// BuildConfig representa una configuración de build.
type BuildConfig struct {
	Name        string
	Command     string
	Description string
}

// Config contiene la configuración del TUI.
type Config struct {
	RootDir      string
	Commands     []CommandEntry
	BuildConfigs []BuildConfig
}

// New crea una configuración con defaults sensatos.
func New(dir string) Config {
	absDir, err := filepath.Abs(dir)
	if err != nil {
		absDir = dir
	}
	return Config{
		RootDir: absDir,
		Commands: []CommandEntry{
			{Name: "Go Build", Command: "go build ./...", Description: "Compilar todos los paquetes"},
			{Name: "Go Test", Command: "go test ./... -v", Description: "Ejecutar todos los tests"},
			{Name: "Go Vet", Command: "go vet ./...", Description: "Análisis estático del código"},
			{Name: "Git Status", Command: "git status", Description: "Estado del repositorio"},
			{Name: "Git Log", Command: "git log --oneline -10", Description: "Últimos 10 commits"},
		},
		BuildConfigs: []BuildConfig{
			{Name: "Debug", Command: "go build -gcflags='-N -l' -o build/debug .", Description: "Build con símbolos de debug"},
			{Name: "Release", Command: "go build -trimpath -ldflags='-s -w' -o build/release .", Description: "Build optimizado para producción"},
			{Name: "Linux AMD64", Command: "GOOS=linux GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o build/app-linux-amd64 .", Description: "Cross-compile Linux 64-bit"},
			{Name: "Windows AMD64", Command: "GOOS=windows GOARCH=amd64 go build -trimpath -ldflags='-s -w' -o build/app-windows-amd64.exe .", Description: "Cross-compile Windows 64-bit"},
		},
	}
}
GOEOF

# =============================================================================
# views/keys.go — Constantes de navegación compartidas
# =============================================================================
log_step "Generando views/keys.go..."
cat > "$PROJECT_DIR/views/keys.go" << 'GOEOF'
package views

// Constantes de teclas para navegación.
const (
	KeyQuit   = "ctrl+c"
	KeyEsc    = "esc"
	KeyBack   = "0"
)

// IsBack retorna true si la tecla es de retroceso (Esc o 0).
func IsBack(key string) bool {
	return key == KeyEsc || key == KeyBack
}
GOEOF

# =============================================================================
# views/exec.go — Tipos de mensaje y comandos async
# =============================================================================
log_step "Generando views/exec.go..."
cat > "$PROJECT_DIR/views/exec.go" << 'GOEOF'
package views

import (
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
)

// ExecResultMsg se envía cuando un comando termina de ejecutarse.
type ExecResultMsg struct {
	Output   string
	Error    string
	ExitCode int
	Label    string
}

// ScriptEntry representa un script descubierto.
type ScriptEntry struct {
	Name        string
	Path        string
	Description string
}

// DiscoverScriptsMsg contiene los scripts descubiertos.
type DiscoverScriptsMsg struct {
	Scripts []ScriptEntry
}

// MakeTarget representa un target de Makefile.
type MakeTarget struct {
	Name        string
	Description string
}

// DiscoverMakeTargetsMsg contiene los targets descubiertos.
type DiscoverMakeTargetsMsg struct {
	Targets []MakeTarget
}

// ExecCommand ejecuta un comando shell de forma asíncrona.
func ExecCommand(label, command, workDir string) tea.Cmd {
	return func() tea.Msg {
		cmd := exec.Command("bash", "-c", command)
		cmd.Dir = workDir
		out, err := cmd.CombinedOutput()
		exitCode := 0
		errMsg := ""
		if err != nil {
			errMsg = err.Error()
			if exitErr, ok := err.(*exec.ExitError); ok {
				exitCode = exitErr.ExitCode()
			} else {
				exitCode = 1
			}
		}
		return ExecResultMsg{
			Output:   string(out),
			Error:    errMsg,
			ExitCode: exitCode,
			Label:    label,
		}
	}
}

// DiscoverScripts busca archivos .sh en el directorio raíz.
func DiscoverScripts(rootDir string) tea.Cmd {
	return func() tea.Msg {
		var scripts []ScriptEntry
		skipDirs := map[string]bool{
			".git": true, "node_modules": true, "vendor": true,
			"build": true, ".venv": true, "__pycache__": true,
		}
		_ = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return nil
			}
			if info.IsDir() {
				if skipDirs[info.Name()] {
					return filepath.SkipDir
				}
				return nil
			}
			if strings.HasSuffix(info.Name(), ".sh") {
				relPath, _ := filepath.Rel(rootDir, path)
				desc := extractScriptDescription(path)
				scripts = append(scripts, ScriptEntry{
					Name:        relPath,
					Path:        path,
					Description: desc,
				})
			}
			return nil
		})
		return DiscoverScriptsMsg{Scripts: scripts}
	}
}

func extractScriptDescription(path string) string {
	data, err := os.ReadFile(path)
	if err != nil {
		return ""
	}
	lines := strings.Split(string(data), "\n")
	limit := len(lines)
	if limit > 10 {
		limit = 10
	}
	for _, line := range lines[:limit] {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "# ") && !strings.HasPrefix(trimmed, "#!") {
			return strings.TrimPrefix(trimmed, "# ")
		}
	}
	return ""
}

// DiscoverMakeTargets parsea el Makefile buscando targets.
func DiscoverMakeTargets(rootDir string) tea.Cmd {
	return func() tea.Msg {
		makefilePath := filepath.Join(rootDir, "Makefile")
		data, err := os.ReadFile(makefilePath)
		if err != nil {
			return DiscoverMakeTargetsMsg{Targets: nil}
		}

		var targets []MakeTarget
		lines := strings.Split(string(data), "\n")
		targetRegex := regexp.MustCompile(`^([a-zA-Z_][a-zA-Z0-9_-]*)\s*:`)
		var lastComment string

		for _, line := range lines {
			if strings.HasPrefix(line, "## ") {
				lastComment = strings.TrimPrefix(line, "## ")
			} else if match := targetRegex.FindStringSubmatch(line); match != nil {
				target := match[1]
				if target != ".PHONY" {
					desc := lastComment
					lastComment = ""
					parts := strings.SplitN(desc, ":", 2)
					if len(parts) == 2 {
						desc = strings.TrimSpace(parts[1])
					}
					targets = append(targets, MakeTarget{
						Name:        target,
						Description: desc,
					})
				}
			} else {
				lastComment = ""
			}
		}
		return DiscoverMakeTargetsMsg{Targets: targets}
	}
}
GOEOF

# =============================================================================
# views/menu.go
# =============================================================================
log_step "Generando views/menu.go..."
cat > "$PROJECT_DIR/views/menu.go" << GOEOF
package views

import (
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"$DEVFACTORY_MODULE/tui"
)

// View identifica la vista activa.
type View int

const (
	ViewMenu View = iota
	ViewScripts
	ViewCommands
	ViewMakefiles
	ViewBuilds
)

// MenuModel es el menú principal del TUI.
type MenuModel struct {
	list tui.ListModel
	done bool
}

// NewMenu crea el menú principal con las 4 opciones.
func NewMenu() MenuModel {
	items := []tui.ListItem{
		{Title: "Scripts", Description: "Buscar y ejecutar scripts .sh del repositorio", Value: ViewScripts},
		{Title: "Comandos", Description: "Ejecutar comandos frecuentes configurados", Value: ViewCommands},
		{Title: "Make", Description: "Ejecutar targets del Makefile", Value: ViewMakefiles},
		{Title: "Builds", Description: "Gestionar configuraciones de build", Value: ViewBuilds},
	}
	list := tui.NewList(items)
	list = list.WithHeight(10).WithWidth(60)
	return MenuModel{list: list}
}

// SelectedView retorna la vista seleccionada.
func (m MenuModel) SelectedView() View {
	item := m.list.SelectedItem()
	if item == nil {
		return ViewMenu
	}
	if v, ok := item.Value.(View); ok {
		return v
	}
	return ViewMenu
}

// Init implementa tea.Model.
func (m MenuModel) Init() tea.Cmd {
	return nil
}

// Update implementa tea.Model.
func (m MenuModel) Update(msg tea.Msg) (MenuModel, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "enter", " ":
			m.list.Select()
			m.done = true
			return m, nil
		}
	}
	updated, cmd := m.list.Update(msg)
	m.list = updated.(tui.ListModel)
	return m, cmd
}

// View implementa tea.Model.
func (m MenuModel) View() string {
	titleStyle := lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color("6")).
		MarginBottom(1)

	title := titleStyle.Render("Menú Principal")
	return title + "\n" + m.list.View()
}

// IsDone retorna true si el usuario seleccionó una opción.
func (m MenuModel) IsDone() bool {
	return m.done
}

// Reset limpia el estado de selección.
func (m *MenuModel) Reset() {
	m.done = false
}
GOEOF

# =============================================================================
# views/scripts.go
# =============================================================================
log_step "Generando views/scripts.go..."
cat > "$PROJECT_DIR/views/scripts.go" << GOEOF
package views

import (
	"fmt"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"$DEVFACTORY_MODULE/tui"
)

type scriptsState int

const (
	scriptsLoading scriptsState = iota
	scriptsList
	scriptsRunning
	scriptsOutput
)

// ScriptsModel gestiona la vista de scripts.
type ScriptsModel struct {
	state      scriptsState
	list       tui.FilteredListModel
	spinner    tui.SpinnerModel
	rootDir    string
	output     string
	exitCode   int
	runLabel   string
	scrollOff  int
	viewHeight int
}

// NewScripts crea la vista de scripts.
func NewScripts(rootDir string) ScriptsModel {
	list := tui.NewFilteredList(nil, "Buscar script...")
	list.ListModel = list.ListModel.WithHeight(15).WithWidth(60)
	return ScriptsModel{
		state:      scriptsLoading,
		list:       list,
		spinner:    tui.NewSpinner("Buscando scripts..."),
		rootDir:    rootDir,
		viewHeight: 20,
	}
}

// Init lanza el descubrimiento de scripts.
func (m ScriptsModel) Init() tea.Cmd {
	return tea.Batch(m.spinner.Init(), DiscoverScripts(m.rootDir))
}

// Update implementa tea.Model.
func (m ScriptsModel) Update(msg tea.Msg) (ScriptsModel, tea.Cmd) {
	switch msg := msg.(type) {
	case DiscoverScriptsMsg:
		items := make([]tui.ListItem, len(msg.Scripts))
		for i, s := range msg.Scripts {
			items[i] = tui.ListItem{
				Title:       s.Name,
				Description: s.Description,
				Value:       s.Path,
			}
		}
		m.list.ListModel.SetItems(items)
		m.list = tui.NewFilteredList(items, "Buscar script...")
		m.list.ListModel = m.list.ListModel.WithHeight(15).WithWidth(60)
		m.state = scriptsList
		return m, nil

	case ExecResultMsg:
		m.output = msg.Output
		if msg.Error != "" && msg.Output == "" {
			m.output = msg.Error
		}
		m.exitCode = msg.ExitCode
		m.runLabel = msg.Label
		m.state = scriptsOutput
		m.scrollOff = 0
		return m, nil

	case tea.KeyMsg:
		switch m.state {
		case scriptsList:
			switch msg.String() {
			case "enter":
				item := m.list.SelectedItem()
				if item != nil {
					path := item.Value.(string)
					m.state = scriptsRunning
					m.spinner.SetMessage("Ejecutando " + item.Title + "...")
					return m, tea.Batch(m.spinner.Init(), ExecCommand(item.Title, "bash "+path, m.rootDir))
				}
			}
		case scriptsOutput:
			switch msg.String() {
			case "up", "k":
				if m.scrollOff > 0 {
					m.scrollOff--
				}
				return m, nil
			case "down", "j":
				lines := strings.Split(m.output, "\n")
				if m.scrollOff < len(lines)-m.viewHeight {
					m.scrollOff++
				}
				return m, nil
			}
		}
	case tea.WindowSizeMsg:
		m.viewHeight = msg.Height - 6
	}

	if m.state == scriptsLoading || m.state == scriptsRunning {
		updated, cmd := m.spinner.Update(msg)
		m.spinner = updated.(tui.SpinnerModel)
		return m, cmd
	}

	if m.state == scriptsList {
		updated, cmd := m.list.Update(msg)
		m.list = updated.(tui.FilteredListModel)
		return m, cmd
	}

	return m, nil
}

// View implementa tea.Model.
func (m ScriptsModel) View() string {
	titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6"))

	switch m.state {
	case scriptsLoading:
		return titleStyle.Render("Scripts") + "\n\n" + m.spinner.View()
	case scriptsList:
		return titleStyle.Render("Scripts") + "\n\n" + m.list.View()
	case scriptsRunning:
		return titleStyle.Render("Scripts") + "\n\n" + m.spinner.View()
	case scriptsOutput:
		header := titleStyle.Render("Scripts > " + m.runLabel)
		statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
		if m.exitCode != 0 {
			statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
		}
		status := statusStyle.Render(fmt.Sprintf("Exit: %d", m.exitCode))

		lines := strings.Split(m.output, "\n")
		end := m.scrollOff + m.viewHeight
		if end > len(lines) {
			end = len(lines)
		}
		visible := lines
		if m.scrollOff < len(lines) {
			visible = lines[m.scrollOff:end]
		}
		content := strings.Join(visible, "\n")

		return header + "  " + status + "\n\n" + content
	}
	return ""
}

// HandleBack retorna true si la tecla es back y la vista puede retroceder.
func (m *ScriptsModel) HandleBack() bool {
	if m.state == scriptsOutput {
		m.state = scriptsList
		m.output = ""
		m.scrollOff = 0
		return false
	}
	if m.state == scriptsList {
		return true
	}
	return false
}
GOEOF

# =============================================================================
# views/commands.go
# =============================================================================
log_step "Generando views/commands.go..."
cat > "$PROJECT_DIR/views/commands.go" << GOEOF
package views

import (
	"fmt"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"$GO_MODULE/config"
	"$DEVFACTORY_MODULE/tui"
)

type commandsState int

const (
	commandsList commandsState = iota
	commandsRunning
	commandsOutput
)

// CommandsModel gestiona la vista de comandos frecuentes.
type CommandsModel struct {
	state      commandsState
	list       tui.ListModel
	spinner    tui.SpinnerModel
	rootDir    string
	output     string
	exitCode   int
	runLabel   string
	scrollOff  int
	viewHeight int
}

// NewCommands crea la vista de comandos frecuentes.
func NewCommands(cfg config.Config) CommandsModel {
	items := make([]tui.ListItem, len(cfg.Commands))
	for i, c := range cfg.Commands {
		items[i] = tui.ListItem{
			Title:       c.Name,
			Description: c.Description + " — " + c.Command,
			Value:       c.Command,
		}
	}
	list := tui.NewList(items)
	list = list.WithHeight(12).WithWidth(60)
	return CommandsModel{
		state:      commandsList,
		list:       list,
		spinner:    tui.NewSpinner("Ejecutando..."),
		rootDir:    cfg.RootDir,
		viewHeight: 20,
	}
}

// Init implementa tea.Model.
func (m CommandsModel) Init() tea.Cmd {
	return nil
}

// Update implementa tea.Model.
func (m CommandsModel) Update(msg tea.Msg) (CommandsModel, tea.Cmd) {
	switch msg := msg.(type) {
	case ExecResultMsg:
		m.output = msg.Output
		if msg.Error != "" && msg.Output == "" {
			m.output = msg.Error
		}
		m.exitCode = msg.ExitCode
		m.runLabel = msg.Label
		m.state = commandsOutput
		m.scrollOff = 0
		return m, nil

	case tea.KeyMsg:
		switch m.state {
		case commandsList:
			if msg.String() == "enter" {
				item := m.list.SelectedItem()
				if item != nil {
					cmd := item.Value.(string)
					m.state = commandsRunning
					m.spinner.SetMessage("Ejecutando " + item.Title + "...")
					return m, tea.Batch(m.spinner.Init(), ExecCommand(item.Title, cmd, m.rootDir))
				}
			}
		case commandsOutput:
			switch msg.String() {
			case "up", "k":
				if m.scrollOff > 0 {
					m.scrollOff--
				}
				return m, nil
			case "down", "j":
				lines := strings.Split(m.output, "\n")
				if m.scrollOff < len(lines)-m.viewHeight {
					m.scrollOff++
				}
				return m, nil
			}
		}
	case tea.WindowSizeMsg:
		m.viewHeight = msg.Height - 6
	}

	if m.state == commandsRunning {
		updated, cmd := m.spinner.Update(msg)
		m.spinner = updated.(tui.SpinnerModel)
		return m, cmd
	}

	if m.state == commandsList {
		updated, cmd := m.list.Update(msg)
		m.list = updated.(tui.ListModel)
		return m, cmd
	}

	return m, nil
}

// View implementa tea.Model.
func (m CommandsModel) View() string {
	titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6"))

	switch m.state {
	case commandsList:
		return titleStyle.Render("Comandos") + "\n\n" + m.list.View()
	case commandsRunning:
		return titleStyle.Render("Comandos") + "\n\n" + m.spinner.View()
	case commandsOutput:
		header := titleStyle.Render("Comandos > " + m.runLabel)
		statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
		if m.exitCode != 0 {
			statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
		}
		status := statusStyle.Render(fmt.Sprintf("Exit: %d", m.exitCode))

		lines := strings.Split(m.output, "\n")
		end := m.scrollOff + m.viewHeight
		if end > len(lines) {
			end = len(lines)
		}
		visible := lines
		if m.scrollOff < len(lines) {
			visible = lines[m.scrollOff:end]
		}
		content := strings.Join(visible, "\n")

		return header + "  " + status + "\n\n" + content
	}
	return ""
}

// HandleBack retorna true si debe volver al menú.
func (m *CommandsModel) HandleBack() bool {
	if m.state == commandsOutput {
		m.state = commandsList
		m.output = ""
		m.scrollOff = 0
		return false
	}
	if m.state == commandsList {
		return true
	}
	return false
}
GOEOF

# =============================================================================
# views/makefiles.go
# =============================================================================
log_step "Generando views/makefiles.go..."
cat > "$PROJECT_DIR/views/makefiles.go" << GOEOF
package views

import (
	"fmt"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"$DEVFACTORY_MODULE/tui"
)

type makeState int

const (
	makeLoading makeState = iota
	makeList
	makeRunning
	makeOutput
	makeEmpty
)

// MakefilesModel gestiona la vista de Makefile targets.
type MakefilesModel struct {
	state      makeState
	list       tui.FilteredListModel
	spinner    tui.SpinnerModel
	rootDir    string
	output     string
	exitCode   int
	runLabel   string
	scrollOff  int
	viewHeight int
}

// NewMakefiles crea la vista de Makefile targets.
func NewMakefiles(rootDir string) MakefilesModel {
	list := tui.NewFilteredList(nil, "Buscar target...")
	list.ListModel = list.ListModel.WithHeight(15).WithWidth(60)
	return MakefilesModel{
		state:      makeLoading,
		list:       list,
		spinner:    tui.NewSpinner("Buscando targets..."),
		rootDir:    rootDir,
		viewHeight: 20,
	}
}

// Init lanza el descubrimiento de targets.
func (m MakefilesModel) Init() tea.Cmd {
	return tea.Batch(m.spinner.Init(), DiscoverMakeTargets(m.rootDir))
}

// Update implementa tea.Model.
func (m MakefilesModel) Update(msg tea.Msg) (MakefilesModel, tea.Cmd) {
	switch msg := msg.(type) {
	case DiscoverMakeTargetsMsg:
		if len(msg.Targets) == 0 {
			m.state = makeEmpty
			return m, nil
		}
		items := make([]tui.ListItem, len(msg.Targets))
		for i, t := range msg.Targets {
			items[i] = tui.ListItem{
				Title:       t.Name,
				Description: t.Description,
				Value:       t.Name,
			}
		}
		m.list = tui.NewFilteredList(items, "Buscar target...")
		m.list.ListModel = m.list.ListModel.WithHeight(15).WithWidth(60)
		m.state = makeList
		return m, nil

	case ExecResultMsg:
		m.output = msg.Output
		if msg.Error != "" && msg.Output == "" {
			m.output = msg.Error
		}
		m.exitCode = msg.ExitCode
		m.runLabel = msg.Label
		m.state = makeOutput
		m.scrollOff = 0
		return m, nil

	case tea.KeyMsg:
		switch m.state {
		case makeList:
			if msg.String() == "enter" {
				item := m.list.SelectedItem()
				if item != nil {
					target := item.Value.(string)
					m.state = makeRunning
					m.spinner.SetMessage("Ejecutando make " + target + "...")
					return m, tea.Batch(m.spinner.Init(), ExecCommand("make "+target, "make "+target, m.rootDir))
				}
			}
		case makeOutput:
			switch msg.String() {
			case "up", "k":
				if m.scrollOff > 0 {
					m.scrollOff--
				}
				return m, nil
			case "down", "j":
				lines := strings.Split(m.output, "\n")
				if m.scrollOff < len(lines)-m.viewHeight {
					m.scrollOff++
				}
				return m, nil
			}
		}
	case tea.WindowSizeMsg:
		m.viewHeight = msg.Height - 6
	}

	if m.state == makeLoading || m.state == makeRunning {
		updated, cmd := m.spinner.Update(msg)
		m.spinner = updated.(tui.SpinnerModel)
		return m, cmd
	}

	if m.state == makeList {
		updated, cmd := m.list.Update(msg)
		m.list = updated.(tui.FilteredListModel)
		return m, cmd
	}

	return m, nil
}

// View implementa tea.Model.
func (m MakefilesModel) View() string {
	titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6"))
	warnStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("3"))

	switch m.state {
	case makeLoading:
		return titleStyle.Render("Make") + "\n\n" + m.spinner.View()
	case makeEmpty:
		return titleStyle.Render("Make") + "\n\n" + warnStyle.Render("No se encontró Makefile en "+m.rootDir)
	case makeList:
		return titleStyle.Render("Make") + "\n\n" + m.list.View()
	case makeRunning:
		return titleStyle.Render("Make") + "\n\n" + m.spinner.View()
	case makeOutput:
		header := titleStyle.Render("Make > " + m.runLabel)
		statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
		if m.exitCode != 0 {
			statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
		}
		status := statusStyle.Render(fmt.Sprintf("Exit: %d", m.exitCode))

		lines := strings.Split(m.output, "\n")
		end := m.scrollOff + m.viewHeight
		if end > len(lines) {
			end = len(lines)
		}
		visible := lines
		if m.scrollOff < len(lines) {
			visible = lines[m.scrollOff:end]
		}
		content := strings.Join(visible, "\n")

		return header + "  " + status + "\n\n" + content
	}
	return ""
}

// HandleBack retorna true si debe volver al menú.
func (m *MakefilesModel) HandleBack() bool {
	if m.state == makeOutput {
		m.state = makeList
		m.output = ""
		m.scrollOff = 0
		return false
	}
	if m.state == makeList || m.state == makeEmpty {
		return true
	}
	return false
}
GOEOF

# =============================================================================
# views/builds.go
# =============================================================================
log_step "Generando views/builds.go..."
cat > "$PROJECT_DIR/views/builds.go" << GOEOF
package views

import (
	"fmt"
	"strings"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"$GO_MODULE/config"
	"$DEVFACTORY_MODULE/tui"
)

type buildsState int

const (
	buildsList buildsState = iota
	buildsRunning
	buildsOutput
)

// BuildsModel gestiona la vista de builds.
type BuildsModel struct {
	state      buildsState
	list       tui.ListModel
	spinner    tui.SpinnerModel
	rootDir    string
	output     string
	exitCode   int
	runLabel   string
	startTime  time.Time
	elapsed    time.Duration
	scrollOff  int
	viewHeight int
}

// NewBuilds crea la vista de builds.
func NewBuilds(cfg config.Config) BuildsModel {
	items := make([]tui.ListItem, len(cfg.BuildConfigs))
	for i, b := range cfg.BuildConfigs {
		items[i] = tui.ListItem{
			Title:       b.Name,
			Description: b.Description,
			Value:       b.Command,
		}
	}
	list := tui.NewList(items)
	list = list.WithHeight(12).WithWidth(60)
	return BuildsModel{
		state:      buildsList,
		list:       list,
		spinner:    tui.NewSpinner("Building..."),
		rootDir:    cfg.RootDir,
		viewHeight: 20,
	}
}

// Init implementa tea.Model.
func (m BuildsModel) Init() tea.Cmd {
	return nil
}

// Update implementa tea.Model.
func (m BuildsModel) Update(msg tea.Msg) (BuildsModel, tea.Cmd) {
	switch msg := msg.(type) {
	case ExecResultMsg:
		m.elapsed = time.Since(m.startTime)
		m.output = msg.Output
		if msg.Error != "" && msg.Output == "" {
			m.output = msg.Error
		}
		m.exitCode = msg.ExitCode
		m.runLabel = msg.Label
		m.state = buildsOutput
		m.scrollOff = 0
		return m, nil

	case tea.KeyMsg:
		switch m.state {
		case buildsList:
			if msg.String() == "enter" {
				item := m.list.SelectedItem()
				if item != nil {
					cmd := item.Value.(string)
					m.state = buildsRunning
					m.startTime = time.Now()
					m.spinner.SetMessage("Building " + item.Title + "...")
					return m, tea.Batch(m.spinner.Init(), ExecCommand(item.Title, cmd, m.rootDir))
				}
			}
		case buildsOutput:
			switch msg.String() {
			case "up", "k":
				if m.scrollOff > 0 {
					m.scrollOff--
				}
				return m, nil
			case "down", "j":
				lines := strings.Split(m.output, "\n")
				if m.scrollOff < len(lines)-m.viewHeight {
					m.scrollOff++
				}
				return m, nil
			}
		}
	case tea.WindowSizeMsg:
		m.viewHeight = msg.Height - 6
	}

	if m.state == buildsRunning {
		updated, cmd := m.spinner.Update(msg)
		m.spinner = updated.(tui.SpinnerModel)
		return m, cmd
	}

	if m.state == buildsList {
		updated, cmd := m.list.Update(msg)
		m.list = updated.(tui.ListModel)
		return m, cmd
	}

	return m, nil
}

// View implementa tea.Model.
func (m BuildsModel) View() string {
	titleStyle := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("6"))

	switch m.state {
	case buildsList:
		return titleStyle.Render("Builds") + "\n\n" + m.list.View()
	case buildsRunning:
		return titleStyle.Render("Builds") + "\n\n" + m.spinner.View()
	case buildsOutput:
		header := titleStyle.Render("Builds > " + m.runLabel)
		statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("2"))
		if m.exitCode != 0 {
			statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1"))
		}
		status := statusStyle.Render(fmt.Sprintf("Exit: %d", m.exitCode))
		elapsed := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Render(fmt.Sprintf("(%s)", m.elapsed.Round(time.Millisecond)))

		lines := strings.Split(m.output, "\n")
		end := m.scrollOff + m.viewHeight
		if end > len(lines) {
			end = len(lines)
		}
		visible := lines
		if m.scrollOff < len(lines) {
			visible = lines[m.scrollOff:end]
		}
		content := strings.Join(visible, "\n")

		return header + "  " + status + "  " + elapsed + "\n\n" + content
	}
	return ""
}

// HandleBack retorna true si debe volver al menú.
func (m *BuildsModel) HandleBack() bool {
	if m.state == buildsOutput {
		m.state = buildsList
		m.output = ""
		m.scrollOff = 0
		return false
	}
	if m.state == buildsList {
		return true
	}
	return false
}
GOEOF

# =============================================================================
# app/model.go — Modelo principal
# =============================================================================
log_step "Generando app/model.go..."
cat > "$PROJECT_DIR/app/model.go" << GOEOF
package app

import (
	"fmt"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"

	"$GO_MODULE/config"
	"$GO_MODULE/views"
	"$DEVFACTORY_MODULE/tui"
)

// Model es el modelo principal de la aplicación TUI.
type Model struct {
	tui.BaseModel
	config      config.Config
	currentView views.View
	menu        views.MenuModel
	scripts     views.ScriptsModel
	commands    views.CommandsModel
	makefiles   views.MakefilesModel
	builds      views.BuildsModel
	width       int
	height      int
	scriptsInit bool
	makeInit    bool
}

// New crea el modelo principal.
func New(cfg config.Config) Model {
	return Model{
		BaseModel:   tui.NewBaseModel(),
		config:      cfg,
		currentView: views.ViewMenu,
		menu:        views.NewMenu(),
		scripts:     views.NewScripts(cfg.RootDir),
		commands:    views.NewCommands(cfg),
		makefiles:   views.NewMakefiles(cfg.RootDir),
		builds:      views.NewBuilds(cfg),
	}
}

// Init implementa tea.Model.
func (m Model) Init() tea.Cmd {
	return nil
}

// Update implementa tea.Model.
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.width = msg.Width
		m.height = msg.Height
		m.SetSize(msg.Width, msg.Height)

	case tea.KeyMsg:
		key := msg.String()

		// Ctrl+C siempre sale
		if key == views.KeyQuit {
			return m, tea.Quit
		}

		// Manejar back (Esc/0)
		if views.IsBack(key) {
			switch m.currentView {
			case views.ViewMenu:
				return m, tea.Quit
			case views.ViewScripts:
				if m.scripts.HandleBack() {
					m.currentView = views.ViewMenu
					m.menu.Reset()
					return m, nil
				}
				return m, nil
			case views.ViewCommands:
				if m.commands.HandleBack() {
					m.currentView = views.ViewMenu
					m.menu.Reset()
					return m, nil
				}
				return m, nil
			case views.ViewMakefiles:
				if m.makefiles.HandleBack() {
					m.currentView = views.ViewMenu
					m.menu.Reset()
					return m, nil
				}
				return m, nil
			case views.ViewBuilds:
				if m.builds.HandleBack() {
					m.currentView = views.ViewMenu
					m.menu.Reset()
					return m, nil
				}
				return m, nil
			}
		}
	}

	// Delegar al sub-modelo activo
	var cmd tea.Cmd
	switch m.currentView {
	case views.ViewMenu:
		m.menu, cmd = m.menu.Update(msg)
		if m.menu.IsDone() {
			selected := m.menu.SelectedView()
			m.currentView = selected
			m.menu.Reset()
			switch selected {
			case views.ViewScripts:
				if !m.scriptsInit {
					m.scriptsInit = true
					return m, m.scripts.Init()
				}
			case views.ViewMakefiles:
				if !m.makeInit {
					m.makeInit = true
					return m, m.makefiles.Init()
				}
			}
			return m, nil
		}
	case views.ViewScripts:
		m.scripts, cmd = m.scripts.Update(msg)
	case views.ViewCommands:
		m.commands, cmd = m.commands.Update(msg)
	case views.ViewMakefiles:
		m.makefiles, cmd = m.makefiles.Update(msg)
	case views.ViewBuilds:
		m.builds, cmd = m.builds.Update(msg)
	}

	return m, cmd
}

// View implementa tea.Model.
func (m Model) View() string {
	headerStyle := lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color("0")).
		Background(lipgloss.Color("6")).
		Padding(0, 1).
		Width(m.width)

	header := headerStyle.Render(fmt.Sprintf(" TUI Manager — %s", m.config.RootDir))

	var content string
	switch m.currentView {
	case views.ViewMenu:
		content = m.menu.View()
	case views.ViewScripts:
		content = m.scripts.View()
	case views.ViewCommands:
		content = m.commands.View()
	case views.ViewMakefiles:
		content = m.makefiles.View()
	case views.ViewBuilds:
		content = m.builds.View()
	}

	statusStyle := lipgloss.NewStyle().
		Foreground(lipgloss.Color("8")).
		Width(m.width)

	var hints string
	switch m.currentView {
	case views.ViewMenu:
		hints = "↑↓: navegar | Enter: seleccionar | Esc: salir"
	default:
		hints = "↑↓: navegar | Enter: ejecutar | /: filtrar | Esc/0: volver"
	}
	statusBar := statusStyle.Render(hints)

	return header + "\n\n" + content + "\n\n" + statusBar
}
GOEOF

# =============================================================================
# main.go
# =============================================================================
log_step "Generando main.go..."
cat > "$PROJECT_DIR/main.go" << GOEOF
package main

import (
	"fmt"
	"os"

	tea "github.com/charmbracelet/bubbletea"

	"$GO_MODULE/app"
	"$GO_MODULE/config"
)

func main() {
	dir := "."
	if len(os.Args) > 1 {
		dir = os.Args[1]
	}

	cfg := config.New(dir)
	model := app.New(cfg)

	p := tea.NewProgram(model, tea.WithAltScreen())
	if _, err := p.Run(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		os.Exit(1)
	}
}
GOEOF

# =============================================================================
# Makefile
# =============================================================================
log_step "Generando Makefile..."
cat > "$PROJECT_DIR/Makefile" << MKEOF
.PHONY: run build test clean install help

MODULE_NAME := $MODULE_NAME
BUILD_DIR := build

## run: Ejecutar el TUI en modo desarrollo
run:
	go run . .

## build: Compilar el binario
build:
	@mkdir -p \$(BUILD_DIR)
	go build -trimpath -ldflags='-s -w' -o \$(BUILD_DIR)/\$(MODULE_NAME) .
	@echo "Built \$(BUILD_DIR)/\$(MODULE_NAME)"

## test: Ejecutar tests
test:
	go test ./... -v

## clean: Limpiar artefactos
clean:
	rm -rf \$(BUILD_DIR)

## install: Instalar en ~/.local/bin
install: build
	cp \$(BUILD_DIR)/\$(MODULE_NAME) \$(HOME)/.local/bin/\$(MODULE_NAME)
	@echo "Installed to \$(HOME)/.local/bin/\$(MODULE_NAME)"

## tidy: go mod tidy
tidy:
	go mod tidy

## help: Mostrar esta ayuda
help:
	@grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':'
MKEOF

# =============================================================================
# .gitignore
# =============================================================================
cat > "$PROJECT_DIR/.gitignore" << 'EOF'
build/
*.exe
*.dll
*.so
*.dylib
EOF

# =============================================================================
# go mod tidy
# =============================================================================
log_step "Ejecutando go mod tidy..."
cd "$PROJECT_DIR"
go mod tidy 2>/dev/null || log_warn "go mod tidy falló — revisa el go.work"

# =============================================================================
# Resumen
# =============================================================================
echo ""
log_ok "Proyecto TUI '$MODULE_NAME' creado en $PROJECT_DIR"
echo ""
echo -e "${CYAN}Estructura:${NC}"
echo "  $MODULE_NAME/"
echo "  ├── main.go           — Entry point (fullscreen TUI)"
echo "  ├── app/"
echo "  │   └── model.go      — Modelo principal y navegación"
echo "  ├── views/"
echo "  │   ├── keys.go       — Constantes de teclas"
echo "  │   ├── exec.go       — Comandos async y descubrimiento"
echo "  │   ├── menu.go       — Menú principal (4 opciones)"
echo "  │   ├── scripts.go    — Buscar y ejecutar scripts .sh"
echo "  │   ├── commands.go   — Comandos frecuentes"
echo "  │   ├── makefiles.go  — Targets del Makefile"
echo "  │   └── builds.go     — Configuraciones de build"
echo "  ├── config/"
echo "  │   └── config.go     — Configuración (editable)"
echo "  ├── Makefile           — run, build, test, install"
echo "  └── go.work            — Enlace a DevFactory"
echo ""
echo -e "${CYAN}Comandos:${NC}"
echo "  make run     — Ejecutar el TUI"
echo "  make build   — Compilar binario"
echo "  make install — Instalar en ~/.local/bin"
echo "  make test    — Ejecutar tests"
echo ""
echo -e "${CYAN}Navegación:${NC}"
echo "  ↑↓           — Moverse por las listas"
echo "  Enter        — Seleccionar / ejecutar"
echo "  /            — Filtrar (en scripts y make)"
echo "  Esc / 0      — Volver atrás"
echo "  Esc (menú)   — Salir del TUI"
echo ""
echo "STATUS: READY"
