package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	_ "github.com/ncruces/go-sqlite3/driver"
	_ "github.com/ncruces/go-sqlite3/embed"
	"github.com/spf13/cobra"
	"github.com/steveyegge/beads/internal/ui"
)

var repairCmd = &cobra.Command{
	Use:     "repair",
	GroupID: GroupMaintenance,
	Short:   "Repair corrupted database by cleaning orphaned references",
	// Note: This command is in noDbCommands list (main.go) to skip normal db init.
	// We open SQLite directly, bypassing migration invariant checks.
	Long: `Repair a database that won't open due to orphaned foreign key references.

When the database has orphaned dependencies or labels, the migration invariant
check fails and prevents the database from opening. This creates a chicken-and-egg
problem where 'bd doctor --fix' can't run because it can't open the database.

This command opens SQLite directly (bypassing invariant checks) and cleans:
  - Orphaned dependencies (issue_id not in issues)
  - Orphaned dependencies (depends_on_id not in issues, excluding external refs)
  - Orphaned labels (issue_id not in issues)
  - Orphaned comments (issue_id not in issues)
  - Orphaned events (issue_id not in issues)

After repair, normal bd commands should work again.

Examples:
  bd repair              # Repair database in current directory
  bd repair --dry-run    # Show what would be cleaned without making changes
  bd repair --path /other/repo  # Repair database in another location
  bd repair --json       # Output results as JSON`,
	Run: runRepair,
}

var (
	repairDryRun bool
	repairPath   string
	repairJSON   bool
)

func init() {
	repairCmd.Flags().BoolVar(&repairDryRun, "dry-run", false, "Show what would be cleaned without making changes")
	repairCmd.Flags().StringVar(&repairPath, "path", ".", "Path to repository with .beads directory")
	repairCmd.Flags().BoolVar(&repairJSON, "json", false, "Output results as JSON")
	rootCmd.AddCommand(repairCmd)
}

// outputJSONAndExit outputs the repair result as JSON and exits
func outputJSONAndExit(result repairResult, exitCode int) {
	data, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		fmt.Fprintf(os.Stderr, `{"error": "failed to marshal JSON: %v"}`, err)
		os.Exit(1)
	}
	fmt.Println(string(data))
	os.Exit(exitCode)
}

func runRepair(cmd *cobra.Command, args []string) {
	// Find .beads directory
	beadsDir := filepath.Join(repairPath, ".beads")
	if _, err := os.Stat(beadsDir); os.IsNotExist(err) {
		if repairJSON {
			outputJSONAndExit(repairResult{
				Status: "error",
				Error:  fmt.Sprintf(".beads directory not found at %s", beadsDir),
			}, 1)
		}
		fmt.Fprintf(os.Stderr, "Error: .beads directory not found at %s\n", beadsDir)
		os.Exit(1)
	}

	// Find database file
	dbPath := filepath.Join(beadsDir, "beads.db")
	if _, err := os.Stat(dbPath); os.IsNotExist(err) {
		if repairJSON {
			outputJSONAndExit(repairResult{
				Status: "error",
				Error:  fmt.Sprintf("database not found at %s", dbPath),
			}, 1)
		}
		fmt.Fprintf(os.Stderr, "Error: database not found at %s\n", dbPath)
		os.Exit(1)
	}

	if !repairJSON {
		fmt.Printf("Repairing database: %s\n", dbPath)
		if repairDryRun {
			fmt.Println("[DRY-RUN] No changes will be made")
		}
		fmt.Println()
	}

	// Open database directly, bypassing beads storage layer
	db, err := openRepairDB(dbPath)
	if err != nil {
		if repairJSON {
			outputJSONAndExit(repairResult{
				DatabasePath: dbPath,
				Status:       "error",
				Error:        fmt.Sprintf("opening database: %v", err),
			}, 1)
		}
		fmt.Fprintf(os.Stderr, "Error opening database: %v\n", err)
		os.Exit(1)
	}
	defer db.Close()

	// Collect repair statistics
	stats := repairStats{}

	// Helper for consistent error output
	exitWithError := func(msg string, err error) {
		if repairJSON {
			outputJSONAndExit(repairResult{
				DatabasePath: dbPath,
				Status:       "error",
				Error:        fmt.Sprintf("%s: %v", msg, err),
			}, 1)
		}
		fmt.Fprintf(os.Stderr, "Error %s: %v\n", msg, err)
		os.Exit(1)
	}

	// 1. Find and clean orphaned dependencies (issue_id not in issues)
	orphanedIssueID, err := findOrphanedDepsIssueID(db)
	if err != nil {
		exitWithError("checking orphaned deps (issue_id)", err)
	}
	stats.orphanedDepsIssueID = len(orphanedIssueID)

	// 2. Find and clean orphaned dependencies (depends_on_id not in issues)
	orphanedDependsOn, err := findOrphanedDepsDependsOn(db)
	if err != nil {
		exitWithError("checking orphaned deps (depends_on_id)", err)
	}
	stats.orphanedDepsDependsOn = len(orphanedDependsOn)

	// 3. Find and clean orphaned labels
	orphanedLabels, err := findOrphanedLabels(db)
	if err != nil {
		exitWithError("checking orphaned labels", err)
	}
	stats.orphanedLabels = len(orphanedLabels)

	// 4. Find and clean orphaned comments
	orphanedCommentsList, err := findOrphanedComments(db)
	if err != nil {
		exitWithError("checking orphaned comments", err)
	}
	stats.orphanedComments = len(orphanedCommentsList)

	// 5. Find and clean orphaned events
	orphanedEventsList, err := findOrphanedEvents(db)
	if err != nil {
		exitWithError("checking orphaned events", err)
	}
	stats.orphanedEvents = len(orphanedEventsList)

	// Build JSON result structure (used for both JSON output and tracking)
	jsonResult := repairResult{
		DatabasePath: dbPath,
		DryRun:       repairDryRun,
		OrphanCounts: repairOrphanCounts{
			DependenciesIssueID:   stats.orphanedDepsIssueID,
			DependenciesDependsOn: stats.orphanedDepsDependsOn,
			Labels:                stats.orphanedLabels,
			Comments:              stats.orphanedComments,
			Events:                stats.orphanedEvents,
			Total:                 stats.total(),
		},
	}

	// Build orphan details
	for _, dep := range orphanedIssueID {
		jsonResult.OrphanDetails.DependenciesIssueID = append(jsonResult.OrphanDetails.DependenciesIssueID,
			repairDepDetail{IssueID: dep.issueID, DependsOnID: dep.dependsOnID})
	}
	for _, dep := range orphanedDependsOn {
		jsonResult.OrphanDetails.DependenciesDependsOn = append(jsonResult.OrphanDetails.DependenciesDependsOn,
			repairDepDetail{IssueID: dep.issueID, DependsOnID: dep.dependsOnID})
	}
	for _, label := range orphanedLabels {
		jsonResult.OrphanDetails.Labels = append(jsonResult.OrphanDetails.Labels,
			repairLabelDetail{IssueID: label.issueID, Label: label.label})
	}
	for _, comment := range orphanedCommentsList {
		jsonResult.OrphanDetails.Comments = append(jsonResult.OrphanDetails.Comments,
			repairCommentDetail{ID: comment.id, IssueID: comment.issueID, Author: comment.author})
	}
	for _, event := range orphanedEventsList {
		jsonResult.OrphanDetails.Events = append(jsonResult.OrphanDetails.Events,
			repairEventDetail{ID: event.id, IssueID: event.issueID, EventType: event.eventType})
	}

	// Handle no orphans case
	if stats.total() == 0 {
		if repairJSON {
			jsonResult.Status = "no_orphans"
			outputJSONAndExit(jsonResult, 0)
		}
		fmt.Printf("%s No orphaned references found - database is clean\n", ui.RenderPass("✓"))
		return
	}

	// Print findings (text mode)
	if !repairJSON {
		fmt.Printf("Found %d orphaned reference(s):\n", stats.total())
		if stats.orphanedDepsIssueID > 0 {
			fmt.Printf("  • %d dependencies with missing issue_id\n", stats.orphanedDepsIssueID)
			for _, dep := range orphanedIssueID {
				fmt.Printf("    - %s → %s\n", dep.issueID, dep.dependsOnID)
			}
		}
		if stats.orphanedDepsDependsOn > 0 {
			fmt.Printf("  • %d dependencies with missing depends_on_id\n", stats.orphanedDepsDependsOn)
			for _, dep := range orphanedDependsOn {
				fmt.Printf("    - %s → %s\n", dep.issueID, dep.dependsOnID)
			}
		}
		if stats.orphanedLabels > 0 {
			fmt.Printf("  • %d labels with missing issue_id\n", stats.orphanedLabels)
			for _, label := range orphanedLabels {
				fmt.Printf("    - %s: %s\n", label.issueID, label.label)
			}
		}
		if stats.orphanedComments > 0 {
			fmt.Printf("  • %d comments with missing issue_id\n", stats.orphanedComments)
			for _, comment := range orphanedCommentsList {
				fmt.Printf("    - %s (by %s)\n", comment.issueID, comment.author)
			}
		}
		if stats.orphanedEvents > 0 {
			fmt.Printf("  • %d events with missing issue_id\n", stats.orphanedEvents)
			for _, event := range orphanedEventsList {
				fmt.Printf("    - %s: %s\n", event.issueID, event.eventType)
			}
		}
		fmt.Println()
	}

	// Handle dry-run
	if repairDryRun {
		if repairJSON {
			jsonResult.Status = "dry_run"
			outputJSONAndExit(jsonResult, 0)
		}
		fmt.Printf("[DRY-RUN] Would delete %d orphaned reference(s)\n", stats.total())
		return
	}

	// Create backup before destructive operations
	backupPath := dbPath + ".pre-repair"
	if !repairJSON {
		fmt.Printf("Creating backup: %s\n", filepath.Base(backupPath))
	}
	if err := copyFile(dbPath, backupPath); err != nil {
		if repairJSON {
			jsonResult.Status = "error"
			jsonResult.Error = fmt.Sprintf("creating backup: %v", err)
			outputJSONAndExit(jsonResult, 1)
		}
		fmt.Fprintf(os.Stderr, "Error creating backup: %v\n", err)
		fmt.Fprintf(os.Stderr, "Aborting repair. Fix backup issue and retry.\n")
		os.Exit(1)
	}
	jsonResult.BackupPath = backupPath
	if !repairJSON {
		fmt.Printf("  %s Backup created\n\n", ui.RenderPass("✓"))
	}

	// Apply repairs in a transaction
	if !repairJSON {
		fmt.Println("Cleaning orphaned references...")
	}

	tx, err := db.Begin()
	if err != nil {
		if repairJSON {
			jsonResult.Status = "error"
			jsonResult.Error = fmt.Sprintf("starting transaction: %v", err)
			outputJSONAndExit(jsonResult, 1)
		}
		fmt.Fprintf(os.Stderr, "Error starting transaction: %v\n", err)
		os.Exit(1)
	}

	var repairErr error

	// Delete orphaned deps (issue_id) and mark affected issues dirty
	if len(orphanedIssueID) > 0 && repairErr == nil {
		// Note: orphanedIssueID contains deps where issue_id doesn't exist,
		// so we can't mark them dirty (the issue is gone). But for depends_on orphans,
		// the issue_id still exists and should be marked dirty.
		result, err := tx.Exec(`
			DELETE FROM dependencies
			WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = dependencies.issue_id)
		`)
		if err != nil {
			repairErr = fmt.Errorf("deleting orphaned deps (issue_id): %w", err)
		} else if !repairJSON {
			deleted, _ := result.RowsAffected()
			fmt.Printf("  %s Deleted %d dependencies with missing issue_id\n", ui.RenderPass("✓"), deleted)
		}
	}

	// Delete orphaned deps (depends_on_id) and mark parent issues dirty
	if len(orphanedDependsOn) > 0 && repairErr == nil {
		// Mark parent issues as dirty for export
		for _, dep := range orphanedDependsOn {
			_, _ = tx.Exec("INSERT OR IGNORE INTO dirty_issues (issue_id) VALUES (?)", dep.issueID)
		}

		result, err := tx.Exec(`
			DELETE FROM dependencies
			WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = dependencies.depends_on_id)
			  AND dependencies.depends_on_id NOT LIKE 'external:%'
		`)
		if err != nil {
			repairErr = fmt.Errorf("deleting orphaned deps (depends_on_id): %w", err)
		} else if !repairJSON {
			deleted, _ := result.RowsAffected()
			fmt.Printf("  %s Deleted %d dependencies with missing depends_on_id\n", ui.RenderPass("✓"), deleted)
		}
	}

	// Delete orphaned labels
	if len(orphanedLabels) > 0 && repairErr == nil {
		// Labels reference non-existent issues, so no dirty marking needed
		result, err := tx.Exec(`
			DELETE FROM labels
			WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = labels.issue_id)
		`)
		if err != nil {
			repairErr = fmt.Errorf("deleting orphaned labels: %w", err)
		} else if !repairJSON {
			deleted, _ := result.RowsAffected()
			fmt.Printf("  %s Deleted %d labels with missing issue_id\n", ui.RenderPass("✓"), deleted)
		}
	}

	// Delete orphaned comments
	if len(orphanedCommentsList) > 0 && repairErr == nil {
		// Comments reference non-existent issues, so no dirty marking needed
		result, err := tx.Exec(`
			DELETE FROM comments
			WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = comments.issue_id)
		`)
		if err != nil {
			repairErr = fmt.Errorf("deleting orphaned comments: %w", err)
		} else if !repairJSON {
			deleted, _ := result.RowsAffected()
			fmt.Printf("  %s Deleted %d comments with missing issue_id\n", ui.RenderPass("✓"), deleted)
		}
	}

	// Delete orphaned events
	if len(orphanedEventsList) > 0 && repairErr == nil {
		// Events reference non-existent issues, so no dirty marking needed
		result, err := tx.Exec(`
			DELETE FROM events
			WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = events.issue_id)
		`)
		if err != nil {
			repairErr = fmt.Errorf("deleting orphaned events: %w", err)
		} else if !repairJSON {
			deleted, _ := result.RowsAffected()
			fmt.Printf("  %s Deleted %d events with missing issue_id\n", ui.RenderPass("✓"), deleted)
		}
	}

	// Commit or rollback
	if repairErr != nil {
		_ = tx.Rollback()
		if repairJSON {
			jsonResult.Status = "error"
			jsonResult.Error = repairErr.Error()
			outputJSONAndExit(jsonResult, 1)
		}
		fmt.Fprintf(os.Stderr, "\n%s Error: %v\n", ui.RenderFail("✗"), repairErr)
		fmt.Fprintf(os.Stderr, "Transaction rolled back. Database unchanged.\n")
		fmt.Fprintf(os.Stderr, "Backup available at: %s\n", backupPath)
		os.Exit(1)
	}

	if err := tx.Commit(); err != nil {
		if repairJSON {
			jsonResult.Status = "error"
			jsonResult.Error = fmt.Sprintf("committing transaction: %v", err)
			outputJSONAndExit(jsonResult, 1)
		}
		fmt.Fprintf(os.Stderr, "\n%s Error committing transaction: %v\n", ui.RenderFail("✗"), err)
		fmt.Fprintf(os.Stderr, "Backup available at: %s\n", backupPath)
		os.Exit(1)
	}

	// Run WAL checkpoint to persist changes
	if !repairJSON {
		fmt.Print("  Running WAL checkpoint... ")
	}
	if _, err := db.Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
		if !repairJSON {
			fmt.Printf("%s %v\n", ui.RenderFail("✗"), err)
		}
	} else if !repairJSON {
		fmt.Printf("%s\n", ui.RenderPass("✓"))
	}

	// Final output
	if repairJSON {
		jsonResult.Status = "success"
		outputJSONAndExit(jsonResult, 0)
	}

	fmt.Println()
	fmt.Printf("%s Repair complete. Try running 'bd doctor' to verify.\n", ui.RenderPass("✓"))
	fmt.Printf("Backup preserved at: %s\n", filepath.Base(backupPath))
}

// repairStats tracks what was found/cleaned
type repairStats struct {
	orphanedDepsIssueID   int
	orphanedDepsDependsOn int
	orphanedLabels        int
	orphanedComments      int
	orphanedEvents        int
}

func (s repairStats) total() int {
	return s.orphanedDepsIssueID + s.orphanedDepsDependsOn + s.orphanedLabels + s.orphanedComments + s.orphanedEvents
}

// orphanedDep represents an orphaned dependency
type orphanedDep struct {
	issueID     string
	dependsOnID string
}

// orphanedLabel represents an orphaned label
type orphanedLabel struct {
	issueID string
	label   string
}

// orphanedComment represents an orphaned comment
type orphanedComment struct {
	id      int
	issueID string
	author  string
}

// orphanedEvent represents an orphaned event
type orphanedEvent struct {
	id        int
	issueID   string
	eventType string
}

// repairResult is the JSON output structure
type repairResult struct {
	DatabasePath  string              `json:"database_path"`
	DryRun        bool                `json:"dry_run"`
	OrphanCounts  repairOrphanCounts  `json:"orphan_counts"`
	OrphanDetails repairOrphanDetails `json:"orphan_details"`
	Status        string              `json:"status"` // "success", "no_orphans", "dry_run", "error"
	BackupPath    string              `json:"backup_path,omitempty"`
	Error         string              `json:"error,omitempty"`
}

type repairOrphanCounts struct {
	DependenciesIssueID   int `json:"dependencies_issue_id"`
	DependenciesDependsOn int `json:"dependencies_depends_on"`
	Labels                int `json:"labels"`
	Comments              int `json:"comments"`
	Events                int `json:"events"`
	Total                 int `json:"total"`
}

type repairOrphanDetails struct {
	DependenciesIssueID   []repairDepDetail     `json:"dependencies_issue_id,omitempty"`
	DependenciesDependsOn []repairDepDetail     `json:"dependencies_depends_on,omitempty"`
	Labels                []repairLabelDetail   `json:"labels,omitempty"`
	Comments              []repairCommentDetail `json:"comments,omitempty"`
	Events                []repairEventDetail   `json:"events,omitempty"`
}

type repairDepDetail struct {
	IssueID     string `json:"issue_id"`
	DependsOnID string `json:"depends_on_id"`
}

type repairLabelDetail struct {
	IssueID string `json:"issue_id"`
	Label   string `json:"label"`
}

type repairCommentDetail struct {
	ID      int    `json:"id"`
	IssueID string `json:"issue_id"`
	Author  string `json:"author"`
}

type repairEventDetail struct {
	ID        int    `json:"id"`
	IssueID   string `json:"issue_id"`
	EventType string `json:"event_type"`
}

// openRepairDB opens SQLite directly for repair, bypassing all beads layer code
func openRepairDB(dbPath string) (*sql.DB, error) {
	// Build connection string with pragmas
	busyMs := int64(30 * time.Second / time.Millisecond)
	if v := strings.TrimSpace(os.Getenv("BD_LOCK_TIMEOUT")); v != "" {
		if d, err := time.ParseDuration(v); err == nil {
			busyMs = int64(d / time.Millisecond)
		}
	}

	connStr := fmt.Sprintf("file:%s?_pragma=busy_timeout(%d)&_pragma=foreign_keys(OFF)&_time_format=sqlite",
		dbPath, busyMs)

	return sql.Open("sqlite3", connStr)
}

// findOrphanedDepsIssueID finds dependencies where issue_id doesn't exist
func findOrphanedDepsIssueID(db *sql.DB) ([]orphanedDep, error) {
	rows, err := db.Query(`
		SELECT d.issue_id, d.depends_on_id
		FROM dependencies d
		WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = d.issue_id)
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var orphans []orphanedDep
	for rows.Next() {
		var dep orphanedDep
		if err := rows.Scan(&dep.issueID, &dep.dependsOnID); err != nil {
			return nil, err
		}
		orphans = append(orphans, dep)
	}
	return orphans, rows.Err()
}

// findOrphanedDepsDependsOn finds dependencies where depends_on_id doesn't exist
func findOrphanedDepsDependsOn(db *sql.DB) ([]orphanedDep, error) {
	rows, err := db.Query(`
		SELECT d.issue_id, d.depends_on_id
		FROM dependencies d
		WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = d.depends_on_id)
		  AND d.depends_on_id NOT LIKE 'external:%'
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var orphans []orphanedDep
	for rows.Next() {
		var dep orphanedDep
		if err := rows.Scan(&dep.issueID, &dep.dependsOnID); err != nil {
			return nil, err
		}
		orphans = append(orphans, dep)
	}
	return orphans, rows.Err()
}

// findOrphanedLabels finds labels where issue_id doesn't exist
func findOrphanedLabels(db *sql.DB) ([]orphanedLabel, error) {
	rows, err := db.Query(`
		SELECT l.issue_id, l.label
		FROM labels l
		WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = l.issue_id)
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var labels []orphanedLabel
	for rows.Next() {
		var label orphanedLabel
		if err := rows.Scan(&label.issueID, &label.label); err != nil {
			return nil, err
		}
		labels = append(labels, label)
	}
	return labels, rows.Err()
}

// findOrphanedComments finds comments where issue_id doesn't exist
func findOrphanedComments(db *sql.DB) ([]orphanedComment, error) {
	rows, err := db.Query(`
		SELECT c.id, c.issue_id, c.author
		FROM comments c
		WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = c.issue_id)
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var comments []orphanedComment
	for rows.Next() {
		var comment orphanedComment
		if err := rows.Scan(&comment.id, &comment.issueID, &comment.author); err != nil {
			return nil, err
		}
		comments = append(comments, comment)
	}
	return comments, rows.Err()
}

// findOrphanedEvents finds events where issue_id doesn't exist
func findOrphanedEvents(db *sql.DB) ([]orphanedEvent, error) {
	rows, err := db.Query(`
		SELECT e.id, e.issue_id, e.event_type
		FROM events e
		WHERE NOT EXISTS (SELECT 1 FROM issues WHERE id = e.issue_id)
	`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var events []orphanedEvent
	for rows.Next() {
		var event orphanedEvent
		if err := rows.Scan(&event.id, &event.issueID, &event.eventType); err != nil {
			return nil, err
		}
		events = append(events, event)
	}
	return events, rows.Err()
}

