package main

import (
	"context"
	"database/sql"
	"flag"
	"fmt"
	"strings"
	"time"
)

// backfillCandidate is the minimal projection we read from the cards table
// for the backfill loop. Avoids loading the full Card row + history we do not
// need.
type backfillCandidate struct {
	ID         string
	Title      string
	ColumnID   string
	ColumnName string
}

// runBackfillJira walks every active kanban card that is not yet linked to a
// Jira issue and creates one via the configured jira module's handler. It is
// the only sanctioned way to backport existing kanban cards into Jira because
// the event dispatcher only ever fires on NEW kanban mutations.
//
// Batching: cards are processed in groups of --batch-size with a
// --pause-sec sleep between groups so we stay well below Jira's REST quota.
// --limit caps the total number of cards processed (0 = no cap).
// --column restricts to a single kanban column by name (case-insensitive).
// --dry-run lists the candidates without calling Jira.
func runBackfillJira(args []string) error {
	fs := flag.NewFlagSet("kanban backfill-jira", flag.ContinueOnError)
	dbPath := fs.String("db", "operations.db", "SQLite database path")
	batchSize := fs.Int("batch-size", 10, "Cards processed per batch before pausing")
	pauseSec := fs.Int("pause-sec", 5, "Seconds to sleep between batches")
	limit := fs.Int("limit", 0, "Maximum cards to process total (0 = no limit)")
	columnFilter := fs.String("column", "", "Only backfill cards whose column name matches (case-insensitive)")
	dryRun := fs.Bool("dry-run", false, "List candidates and exit without calling Jira")
	if err := fs.Parse(args); err != nil {
		return err
	}
	if *batchSize <= 0 {
		return fmt.Errorf("--batch-size must be > 0")
	}

	db, err := openDB(*dbPath)
	if err != nil {
		return fmt.Errorf("open db: %w", err)
	}
	defer db.Close()

	mod, cfg, err := activeJiraModule(db)
	if err != nil {
		return fmt.Errorf("module config: %w", err)
	}

	candidates, err := listBackfillCandidates(db, *columnFilter, *limit)
	if err != nil {
		return err
	}
	if len(candidates) == 0 {
		fmt.Println("no cards to backfill (all active cards have jira_key set or filter is empty)")
		return nil
	}

	fmt.Printf("backfill plan: %d cards across columns; batch=%d pause=%ds dry_run=%v\n",
		len(candidates), *batchSize, *pauseSec, *dryRun)
	fmt.Printf("module: %q (project=%s, board=%d, issue_type=%q)\n",
		mod.Name, cfg.ProjectKey, cfg.BoardID, cfg.IssueType)
	fmt.Println()

	if *dryRun {
		printCandidates(candidates)
		return nil
	}

	h := &jiraHandler{}
	var ok, failed int
	for i := 0; i < len(candidates); i++ {
		c := candidates[i]
		if i > 0 && i%*batchSize == 0 {
			fmt.Printf("--- batch boundary (%d/%d) — sleeping %ds ---\n", i, len(candidates), *pauseSec)
			time.Sleep(time.Duration(*pauseSec) * time.Second)
		}
		status, err := h.Handle(context.Background(), db, mod, Event{
			Type:   "card.created",
			CardID: c.ID,
		})
		now := time.Now().UTC().Format(time.RFC3339)
		if err != nil {
			failed++
			_ = db.updateCardJiraSync(c.ID, "", now, err.Error())
			fmt.Printf("[%4d/%4d] FAIL  %-40s  http=%d  err=%s\n",
				i+1, len(candidates), truncateInline(c.Title, 40), status, truncateInline(err.Error(), 80))
			continue
		}
		ok++
		statusName := cfg.StatusMap[c.ColumnName]
		_ = db.updateCardJiraSync(c.ID, statusName, now, "")
		// After Handle() the card row now carries the assigned jira_key.
		linked, _ := db.lookupCardJiraKey(c.ID)
		fmt.Printf("[%4d/%4d] OK    %-40s  -> %-12s  status=%s\n",
			i+1, len(candidates), truncateInline(c.Title, 40), linked, statusName)
	}
	fmt.Println()
	fmt.Printf("done: %d ok · %d failed · %d total\n", ok, failed, len(candidates))
	if failed > 0 {
		return fmt.Errorf("%d cards failed to backfill", failed)
	}
	return nil
}

// listBackfillCandidates returns active (not deleted, not archived) cards
// without a Jira link, optionally filtered by column name. Newest-first so
// recent cards are mirrored first.
func listBackfillCandidates(db *DB, columnFilter string, limit int) ([]backfillCandidate, error) {
	q := `
SELECT c.id, c.title, c.column_id, col.name
FROM cards c
JOIN columns col ON col.id = c.column_id
WHERE (c.jira_key IS NULL OR c.jira_key = '')
  AND c.deleted_at IS NULL
  AND c.archived_at IS NULL
`
	args := []interface{}{}
	if strings.TrimSpace(columnFilter) != "" {
		q += " AND LOWER(col.name) = LOWER(?) "
		args = append(args, columnFilter)
	}
	q += " ORDER BY c.created_at ASC "
	if limit > 0 {
		q += " LIMIT ? "
		args = append(args, limit)
	}
	rows, err := db.conn.Query(q, args...)
	if err != nil {
		return nil, fmt.Errorf("list candidates: %w", err)
	}
	defer rows.Close()
	out := []backfillCandidate{}
	for rows.Next() {
		var c backfillCandidate
		if err := rows.Scan(&c.ID, &c.Title, &c.ColumnID, &c.ColumnName); err != nil {
			return nil, err
		}
		out = append(out, c)
	}
	return out, rows.Err()
}

// lookupCardJiraKey returns the jira_key column for a card, or empty string.
// Used to print the freshly-assigned key in the progress log.
func (db *DB) lookupCardJiraKey(cardID string) (string, error) {
	var k sql.NullString
	err := db.conn.QueryRow(`SELECT jira_key FROM cards WHERE id = ?`, cardID).Scan(&k)
	if err != nil {
		return "", err
	}
	if !k.Valid {
		return "", nil
	}
	return k.String, nil
}

func printCandidates(cs []backfillCandidate) {
	byCol := map[string]int{}
	for _, c := range cs {
		byCol[c.ColumnName]++
	}
	fmt.Println("candidates by column:")
	for col, n := range byCol {
		fmt.Printf("  %-25s  %d\n", col, n)
	}
	fmt.Println()
	fmt.Printf("first 20 of %d:\n", len(cs))
	for i, c := range cs {
		if i >= 20 {
			break
		}
		fmt.Printf("  %-12s  [%s]  %s\n", c.ID, c.ColumnName, truncateInline(c.Title, 60))
	}
}

func truncateInline(s string, n int) string {
	if len(s) <= n {
		return s
	}
	return s[:n-1] + "…"
}
