package convoy

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"os/exec"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/help"
	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
)

// convoyIDPattern validates convoy IDs to prevent SQL injection.
var convoyIDPattern = regexp.MustCompile(`^hq-[a-zA-Z0-9-]+$`)

// subprocessTimeout is the timeout for bd and sqlite3 calls.
const subprocessTimeout = 5 * time.Second

// IssueItem represents a tracked issue within a convoy.
type IssueItem struct {
	ID     string
	Title  string
	Status string
}

// ConvoyItem represents a convoy with its tracked issues.
type ConvoyItem struct {
	ID       string
	Title    string
	Status   string
	Issues   []IssueItem
	Progress string // e.g., "2/5"
	Expanded bool
}

// Model is the bubbletea model for the convoy TUI.
type Model struct {
	convoys   []ConvoyItem
	cursor    int    // Current selection index in flattened view
	townBeads string // Path to town beads directory
	err       error

	// UI state
	keys     KeyMap
	help     help.Model
	showHelp bool
	width    int
	height   int
}

// New creates a new convoy TUI model.
func New(townBeads string) Model {
	return Model{
		townBeads: townBeads,
		keys:      DefaultKeyMap(),
		help:      help.New(),
		convoys:   make([]ConvoyItem, 0),
	}
}

// Init initializes the model.
func (m Model) Init() tea.Cmd {
	return m.fetchConvoys
}

// fetchConvoysMsg is the result of fetching convoys.
type fetchConvoysMsg struct {
	convoys []ConvoyItem
	err     error
}

// fetchConvoys fetches convoy data from beads.
func (m Model) fetchConvoys() tea.Msg {
	convoys, err := loadConvoys(m.townBeads)
	return fetchConvoysMsg{convoys: convoys, err: err}
}

// loadConvoys loads convoy data from the beads directory.
func loadConvoys(townBeads string) ([]ConvoyItem, error) {
	ctx, cancel := context.WithTimeout(context.Background(), subprocessTimeout)
	defer cancel()

	// Get list of open convoys
	listArgs := []string{"list", "--type=convoy", "--json"}
	listCmd := exec.CommandContext(ctx, "bd", listArgs...)
	listCmd.Dir = townBeads
	var stdout bytes.Buffer
	listCmd.Stdout = &stdout

	if err := listCmd.Run(); err != nil {
		return nil, fmt.Errorf("listing convoys: %w", err)
	}

	var rawConvoys []struct {
		ID     string `json:"id"`
		Title  string `json:"title"`
		Status string `json:"status"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &rawConvoys); err != nil {
		return nil, fmt.Errorf("parsing convoy list: %w", err)
	}

	convoys := make([]ConvoyItem, 0, len(rawConvoys))
	for _, rc := range rawConvoys {
		issues, completed, total := loadTrackedIssues(townBeads, rc.ID)
		convoys = append(convoys, ConvoyItem{
			ID:       rc.ID,
			Title:    rc.Title,
			Status:   rc.Status,
			Issues:   issues,
			Progress: fmt.Sprintf("%d/%d", completed, total),
			Expanded: false,
		})
	}

	return convoys, nil
}

// loadTrackedIssues loads issues tracked by a convoy.
func loadTrackedIssues(townBeads, convoyID string) ([]IssueItem, int, int) {
	// Validate convoy ID to prevent SQL injection
	if !convoyIDPattern.MatchString(convoyID) {
		return nil, 0, 0
	}

	ctx, cancel := context.WithTimeout(context.Background(), subprocessTimeout)
	defer cancel()

	dbPath := filepath.Join(townBeads, "beads.db")

	// Query tracked issues from SQLite (ID validated above)
	query := fmt.Sprintf(`
		SELECT d.depends_on_id
		FROM dependencies d
		WHERE d.issue_id = '%s' AND d.type = 'tracks'
	`, convoyID)

	cmd := exec.CommandContext(ctx, "sqlite3", "-json", dbPath, query) //nolint:gosec // G204: sqlite3 with controlled query
	var stdout bytes.Buffer
	cmd.Stdout = &stdout

	if err := cmd.Run(); err != nil {
		return nil, 0, 0
	}

	var deps []struct {
		DependsOnID string `json:"depends_on_id"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &deps); err != nil {
		return nil, 0, 0
	}

	// Collect issue IDs, handling external references
	issueIDs := make([]string, 0, len(deps))
	for _, dep := range deps {
		issueID := dep.DependsOnID
		if strings.HasPrefix(issueID, "external:") {
			parts := strings.SplitN(issueID, ":", 3)
			if len(parts) == 3 {
				issueID = parts[2]
			}
		}
		issueIDs = append(issueIDs, issueID)
	}

	// Batch fetch all issue details in one call
	detailsMap := getIssueDetailsBatch(townBeads, issueIDs)

	issues := make([]IssueItem, 0, len(deps))
	completed := 0
	for _, id := range issueIDs {
		if issue, ok := detailsMap[id]; ok {
			issues = append(issues, issue)
			if issue.Status == "closed" {
				completed++
			}
		}
	}

	// Sort by status (open first, then closed)
	sort.Slice(issues, func(i, j int) bool {
		if issues[i].Status == issues[j].Status {
			return issues[i].ID < issues[j].ID
		}
		return issues[i].Status != "closed" // open comes first
	})

	return issues, completed, len(issues)
}

// getIssueDetailsBatch fetches details for multiple issues in a single bd show call.
// Returns a map from issue ID to details.
func getIssueDetailsBatch(townBeads string, issueIDs []string) map[string]IssueItem {
	result := make(map[string]IssueItem)
	if len(issueIDs) == 0 {
		return result
	}

	ctx, cancel := context.WithTimeout(context.Background(), subprocessTimeout)
	defer cancel()

	// Build args: bd show id1 id2 id3 ... --json
	args := append([]string{"show"}, issueIDs...)
	args = append(args, "--json")

	cmd := exec.CommandContext(ctx, "bd", args...) //nolint:gosec // G204: bd is a trusted internal tool
	cmd.Dir = townBeads
	var stdout bytes.Buffer
	cmd.Stdout = &stdout

	if err := cmd.Run(); err != nil {
		return result // Return empty map on error
	}

	var issues []struct {
		ID     string `json:"id"`
		Title  string `json:"title"`
		Status string `json:"status"`
	}
	if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
		return result
	}

	for _, issue := range issues {
		result[issue.ID] = IssueItem{
			ID:     issue.ID,
			Title:  issue.Title,
			Status: issue.Status,
		}
	}

	return result
}

// Update handles messages.
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.help.Width = msg.Width
		return m, nil

	case fetchConvoysMsg:
		m.err = msg.err
		m.convoys = msg.convoys
		return m, nil

	case tea.KeyMsg:
		switch {
		case key.Matches(msg, m.keys.Quit):
			return m, tea.Quit

		case key.Matches(msg, m.keys.Help):
			m.showHelp = !m.showHelp
			return m, nil

		case key.Matches(msg, m.keys.Up):
			if m.cursor > 0 {
				m.cursor--
			}
			return m, nil

		case key.Matches(msg, m.keys.Down):
			max := m.maxCursor()
			if m.cursor < max {
				m.cursor++
			}
			return m, nil

		case key.Matches(msg, m.keys.Top):
			m.cursor = 0
			return m, nil

		case key.Matches(msg, m.keys.Bottom):
			m.cursor = m.maxCursor()
			return m, nil

		case key.Matches(msg, m.keys.Toggle):
			m.toggleExpand()
			return m, nil

		// Number keys for direct convoy access
		case msg.String() >= "1" && msg.String() <= "9":
			n := int(msg.String()[0] - '0')
			if n <= len(m.convoys) {
				m.jumpToConvoy(n - 1)
			}
			return m, nil
		}
	}

	return m, nil
}

// maxCursor returns the maximum valid cursor position.
func (m Model) maxCursor() int {
	count := 0
	for _, c := range m.convoys {
		count++ // convoy itself
		if c.Expanded {
			count += len(c.Issues)
		}
	}
	if count == 0 {
		return 0
	}
	return count - 1
}

// cursorToConvoyIndex returns the convoy index and issue index for the current cursor.
// Returns (convoyIdx, issueIdx) where issueIdx is -1 if on a convoy row.
func (m Model) cursorToConvoyIndex() (int, int) {
	pos := 0
	for ci, c := range m.convoys {
		if pos == m.cursor {
			return ci, -1
		}
		pos++
		if c.Expanded {
			for ii := range c.Issues {
				if pos == m.cursor {
					return ci, ii
				}
				pos++
			}
		}
	}
	return -1, -1
}

// toggleExpand toggles expansion of the convoy at the current cursor.
func (m *Model) toggleExpand() {
	ci, ii := m.cursorToConvoyIndex()
	if ci >= 0 && ii == -1 {
		// On a convoy row, toggle it
		m.convoys[ci].Expanded = !m.convoys[ci].Expanded
	}
}

// jumpToConvoy moves the cursor to a specific convoy by index.
func (m *Model) jumpToConvoy(convoyIdx int) {
	if convoyIdx < 0 || convoyIdx >= len(m.convoys) {
		return
	}
	pos := 0
	for ci, c := range m.convoys {
		if ci == convoyIdx {
			m.cursor = pos
			return
		}
		pos++
		if c.Expanded {
			pos += len(c.Issues)
		}
	}
}

// View renders the model.
func (m Model) View() string {
	return m.renderView()
}
