package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/spf13/cobra"
	"github.com/steveyegge/beads/internal/merge"
	"github.com/steveyegge/beads/internal/ui"
)

var resolveConflictsCmd = &cobra.Command{
	Use:     "resolve-conflicts [file]",
	GroupID: GroupMaintenance,
	Short:   "Resolve git merge conflicts in JSONL files",
	Long: `Resolve git merge conflict markers in beads JSONL files.

When git merges fail to auto-resolve, JSONL files can end up with conflict
markers (<<<<<<, =======, >>>>>>). This command parses those markers and
resolves the conflicts using beads merge semantics.

Modes:
  mechanical (default)  Uses deterministic merge rules (updated_at wins, etc.)
  interactive           Prompts for each conflict (not yet implemented)

The file defaults to .beads/beads.jsonl if not specified.

Examples:
  bd resolve-conflicts                    # Resolve conflicts in .beads/beads.jsonl
  bd resolve-conflicts --dry-run          # Show what would be resolved
  bd resolve-conflicts custom.jsonl       # Resolve conflicts in custom file
  bd resolve-conflicts --json             # Output results as JSON`,
	Args: cobra.MaximumNArgs(1),
	// PreRun disables PersistentPreRun for this command (no database needed)
	PreRun: func(cmd *cobra.Command, args []string) {},
	Run:    runResolveConflicts,
}

var (
	resolveConflictsMode   string
	resolveConflictsDryRun bool
	resolveConflictsJSON   bool
	resolveConflictsPath   string
)

func init() {
	resolveConflictsCmd.Flags().StringVar(&resolveConflictsMode, "mode", "mechanical", "Resolution mode: mechanical, interactive")
	resolveConflictsCmd.Flags().BoolVar(&resolveConflictsDryRun, "dry-run", false, "Show what would be resolved without making changes")
	resolveConflictsCmd.Flags().BoolVar(&resolveConflictsJSON, "json", false, "Output results as JSON")
	resolveConflictsCmd.Flags().StringVar(&resolveConflictsPath, "path", ".", "Path to repository with .beads directory")
	rootCmd.AddCommand(resolveConflictsCmd)
}

// conflictRegion represents a single conflict in the file
type conflictRegion struct {
	StartLine int      // Line number where <<<<<<< starts
	EndLine   int      // Line number where >>>>>>> ends
	LeftSide  []string // Lines between <<<<<<< and =======
	RightSide []string // Lines between ======= and >>>>>>>
	LeftLabel string   // Label after <<<<<<< (e.g., "HEAD")
	RightLabel string  // Label after >>>>>>> (e.g., "branch-name")
}

// resolveConflictsResult is the JSON output structure
type resolveConflictsResult struct {
	FilePath         string                   `json:"file_path"`
	DryRun           bool                     `json:"dry_run"`
	Mode             string                   `json:"mode"`
	ConflictsFound   int                      `json:"conflicts_found"`
	ConflictsResolved int                     `json:"conflicts_resolved"`
	Status           string                   `json:"status"` // "success", "no_conflicts", "dry_run", "error"
	BackupPath       string                   `json:"backup_path,omitempty"`
	Error            string                   `json:"error,omitempty"`
	Conflicts        []conflictResolutionInfo `json:"conflicts,omitempty"`
}

type conflictResolutionInfo struct {
	LineRange  string `json:"line_range"`
	LeftLabel  string `json:"left_label"`
	RightLabel string `json:"right_label"`
	Resolution string `json:"resolution"` // "merged", "left", "right", "both"
	IssueID    string `json:"issue_id,omitempty"`
}

func runResolveConflicts(cmd *cobra.Command, args []string) {
	// Determine file path
	var filePath string
	if len(args) > 0 {
		filePath = args[0]
	} else {
		filePath = filepath.Join(resolveConflictsPath, ".beads", "beads.jsonl")
	}

	// Validate mode
	if resolveConflictsMode != "mechanical" && resolveConflictsMode != "interactive" {
		outputResolveError(filePath, fmt.Sprintf("invalid mode: %s (use 'mechanical' or 'interactive')", resolveConflictsMode))
		os.Exit(1)
	}

	if resolveConflictsMode == "interactive" {
		outputResolveError(filePath, "interactive mode not yet implemented")
		os.Exit(1)
	}

	// Check file exists
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		outputResolveError(filePath, fmt.Sprintf("file not found: %s", filePath))
		os.Exit(1)
	}

	// Read file content
	content, err := os.ReadFile(filePath) // #nosec G304 -- user-provided path for conflict resolution
	if err != nil {
		outputResolveError(filePath, fmt.Sprintf("reading file: %v", err))
		os.Exit(1)
	}

	// Parse conflicts
	conflicts, cleanLines, err := parseConflicts(string(content))
	if err != nil {
		outputResolveError(filePath, fmt.Sprintf("parsing conflicts: %v", err))
		os.Exit(1)
	}

	result := resolveConflictsResult{
		FilePath:       filePath,
		DryRun:         resolveConflictsDryRun,
		Mode:           resolveConflictsMode,
		ConflictsFound: len(conflicts),
	}

	// No conflicts case
	if len(conflicts) == 0 {
		result.Status = "no_conflicts"
		if resolveConflictsJSON {
			outputResolveJSON(result, 0)
		}
		fmt.Printf("%s No conflict markers found in %s\n", ui.RenderPass("✓"), filePath)
		return
	}

	if !resolveConflictsJSON {
		fmt.Printf("Found %d conflict region(s) in %s\n", len(conflicts), filePath)
		if resolveConflictsDryRun {
			fmt.Println("[DRY-RUN] No changes will be made")
		}
		fmt.Println()
	}

	// Resolve each conflict
	var resolvedLines []string
	resolvedLines = append(resolvedLines, cleanLines...)

	for i, conflict := range conflicts {
		resolution, info := resolveConflict(conflict, i+1)
		result.Conflicts = append(result.Conflicts, info)

		if !resolveConflictsJSON && !resolveConflictsDryRun {
			fmt.Printf("  Conflict %d (lines %d-%d): %s\n", i+1, conflict.StartLine, conflict.EndLine, info.Resolution)
		}

		resolvedLines = append(resolvedLines, resolution...)
		result.ConflictsResolved++
	}

	// Dry-run output
	if resolveConflictsDryRun {
		result.Status = "dry_run"
		if resolveConflictsJSON {
			outputResolveJSON(result, 0)
		} else {
			fmt.Printf("[DRY-RUN] Would resolve %d conflict(s)\n", len(conflicts))
			for i, info := range result.Conflicts {
				fmt.Printf("  %d. Lines %s: %s", i+1, info.LineRange, info.Resolution)
				if info.IssueID != "" {
					fmt.Printf(" (issue: %s)", info.IssueID)
				}
				fmt.Println()
			}
		}
		return
	}

	// Create backup
	backupPath := filePath + ".pre-resolve"
	if err := copyFile(filePath, backupPath); err != nil {
		outputResolveError(filePath, fmt.Sprintf("creating backup: %v", err))
		os.Exit(1)
	}
	result.BackupPath = backupPath

	if !resolveConflictsJSON {
		fmt.Printf("  Backup created: %s\n", filepath.Base(backupPath))
	}

	// Write resolved content
	output := strings.Join(resolvedLines, "\n")
	if len(resolvedLines) > 0 {
		output += "\n"
	}
	if err := os.WriteFile(filePath, []byte(output), 0644); err != nil { // #nosec G306 -- standard file permissions
		outputResolveError(filePath, fmt.Sprintf("writing file: %v", err))
		os.Exit(1)
	}

	result.Status = "success"
	if resolveConflictsJSON {
		outputResolveJSON(result, 0)
	} else {
		fmt.Println()
		fmt.Printf("%s Resolved %d conflict(s) in %s\n", ui.RenderPass("✓"), len(conflicts), filepath.Base(filePath))
		fmt.Printf("Backup preserved at: %s\n", filepath.Base(backupPath))
	}
}

// parseConflicts extracts conflict regions and non-conflicted lines from content
func parseConflicts(content string) ([]conflictRegion, []string, error) {
	var conflicts []conflictRegion
	var cleanLines []string

	scanner := bufio.NewScanner(strings.NewReader(content))
	lineNum := 0
	var current *conflictRegion
	inLeft := false
	inRight := false

	for scanner.Scan() {
		lineNum++
		line := scanner.Text()

		// Detect conflict start
		if strings.HasPrefix(line, "<<<<<<<") {
			if current != nil {
				return nil, nil, fmt.Errorf("nested conflict at line %d", lineNum)
			}
			current = &conflictRegion{
				StartLine:  lineNum,
				LeftLabel:  strings.TrimSpace(strings.TrimPrefix(line, "<<<<<<<")),
			}
			inLeft = true
			continue
		}

		// Detect conflict separator
		if strings.HasPrefix(line, "=======") && current != nil {
			inLeft = false
			inRight = true
			continue
		}

		// Detect conflict end
		if strings.HasPrefix(line, ">>>>>>>") && current != nil {
			current.EndLine = lineNum
			current.RightLabel = strings.TrimSpace(strings.TrimPrefix(line, ">>>>>>>"))
			conflicts = append(conflicts, *current)
			current = nil
			inLeft = false
			inRight = false
			continue
		}

		// Collect lines
		if current != nil {
			if inLeft {
				current.LeftSide = append(current.LeftSide, line)
			} else if inRight {
				current.RightSide = append(current.RightSide, line)
			}
		} else {
			cleanLines = append(cleanLines, line)
		}
	}

	if current != nil {
		return nil, nil, fmt.Errorf("unclosed conflict starting at line %d", current.StartLine)
	}

	return conflicts, cleanLines, scanner.Err()
}

// resolveConflict resolves a single conflict region using merge semantics
func resolveConflict(conflict conflictRegion, _ int) ([]string, conflictResolutionInfo) {
	info := conflictResolutionInfo{
		LineRange:  fmt.Sprintf("%d-%d", conflict.StartLine, conflict.EndLine),
		LeftLabel:  conflict.LeftLabel,
		RightLabel: conflict.RightLabel,
	}

	// Try to parse left and right as JSON issues
	var leftIssues, rightIssues []merge.Issue

	for _, line := range conflict.LeftSide {
		if strings.TrimSpace(line) == "" {
			continue
		}
		var issue merge.Issue
		if err := json.Unmarshal([]byte(line), &issue); err == nil {
			issue.RawLine = line
			leftIssues = append(leftIssues, issue)
		}
	}

	for _, line := range conflict.RightSide {
		if strings.TrimSpace(line) == "" {
			continue
		}
		var issue merge.Issue
		if err := json.Unmarshal([]byte(line), &issue); err == nil {
			issue.RawLine = line
			rightIssues = append(rightIssues, issue)
		}
	}

	// If we couldn't parse as JSON, keep both sides
	if len(leftIssues) == 0 && len(rightIssues) == 0 {
		info.Resolution = "kept_both_unparseable"
		var result []string
		result = append(result, conflict.LeftSide...)
		result = append(result, conflict.RightSide...)
		return result, info
	}

	// If only one side has valid JSON, use that
	if len(leftIssues) > 0 && len(rightIssues) == 0 {
		info.Resolution = "left_only_valid"
		if len(leftIssues) == 1 {
			info.IssueID = leftIssues[0].ID
		}
		return conflict.LeftSide, info
	}

	if len(rightIssues) > 0 && len(leftIssues) == 0 {
		info.Resolution = "right_only_valid"
		if len(rightIssues) == 1 {
			info.IssueID = rightIssues[0].ID
		}
		return conflict.RightSide, info
	}

	// Both sides have valid JSON - merge them
	// Use the 3-way merge logic with empty base (both sides added the same issue)
	var result []string
	mergedIDs := make(map[string]bool)

	for _, left := range leftIssues {
		// Find matching right issue by ID
		var matchingRight *merge.Issue
		for i := range rightIssues {
			if rightIssues[i].ID == left.ID {
				matchingRight = &rightIssues[i]
				break
			}
		}

		if matchingRight != nil {
			// Merge the two versions
			merged := mergeIssueConflict(left, *matchingRight)
			mergedJSON, err := json.Marshal(merged)
			if err != nil {
				// Fall back to left on marshal error
				result = append(result, left.RawLine)
			} else {
				result = append(result, string(mergedJSON))
			}
			mergedIDs[left.ID] = true
			info.IssueID = left.ID
			info.Resolution = "merged"
		} else {
			// No matching right issue - keep left
			result = append(result, left.RawLine)
		}
	}

	// Add any right issues that weren't merged
	for _, right := range rightIssues {
		if !mergedIDs[right.ID] {
			result = append(result, right.RawLine)
		}
	}

	if info.Resolution == "" {
		info.Resolution = "merged_multiple"
	}

	return result, info
}

// mergeIssueConflict merges two conflicting issue versions
// Uses similar logic to internal/merge but simplified for conflict resolution
func mergeIssueConflict(left, right merge.Issue) merge.Issue {
	result := merge.Issue{
		ID:        left.ID,
		CreatedAt: left.CreatedAt,
		CreatedBy: left.CreatedBy,
	}

	// Title: prefer later updated_at
	result.Title = pickByUpdatedAt(left.Title, right.Title, left.UpdatedAt, right.UpdatedAt)

	// Description: prefer later updated_at
	result.Description = pickByUpdatedAt(left.Description, right.Description, left.UpdatedAt, right.UpdatedAt)

	// Notes: concatenate if different
	if left.Notes == right.Notes {
		result.Notes = left.Notes
	} else if left.Notes == "" {
		result.Notes = right.Notes
	} else if right.Notes == "" {
		result.Notes = left.Notes
	} else {
		result.Notes = left.Notes + "\n\n---\n\n" + right.Notes
	}

	// Status: closed wins
	if left.Status == "closed" || right.Status == "closed" {
		result.Status = "closed"
	} else if left.Status == "tombstone" || right.Status == "tombstone" {
		result.Status = "tombstone"
	} else {
		result.Status = pickByUpdatedAt(left.Status, right.Status, left.UpdatedAt, right.UpdatedAt)
	}

	// Priority: lower number (higher priority) wins
	if left.Priority != 0 && right.Priority != 0 {
		if left.Priority < right.Priority {
			result.Priority = left.Priority
		} else {
			result.Priority = right.Priority
		}
	} else if left.Priority != 0 {
		result.Priority = left.Priority
	} else {
		result.Priority = right.Priority
	}

	// IssueType: prefer left
	if left.IssueType != "" {
		result.IssueType = left.IssueType
	} else {
		result.IssueType = right.IssueType
	}

	// UpdatedAt: max
	result.UpdatedAt = maxTimeStr(left.UpdatedAt, right.UpdatedAt)

	// ClosedAt: max (if status is closed)
	if result.Status == "closed" {
		result.ClosedAt = maxTimeStr(left.ClosedAt, right.ClosedAt)
		// CloseReason and ClosedBySession from whichever has later ClosedAt
		if isTimeAfterStr(left.ClosedAt, right.ClosedAt) {
			result.CloseReason = left.CloseReason
			result.ClosedBySession = left.ClosedBySession
		} else {
			result.CloseReason = right.CloseReason
			result.ClosedBySession = right.ClosedBySession
		}
	}

	// Dependencies: union
	depMap := make(map[string]merge.Dependency)
	for _, dep := range left.Dependencies {
		key := fmt.Sprintf("%s:%s:%s", dep.IssueID, dep.DependsOnID, dep.Type)
		depMap[key] = dep
	}
	for _, dep := range right.Dependencies {
		key := fmt.Sprintf("%s:%s:%s", dep.IssueID, dep.DependsOnID, dep.Type)
		if _, exists := depMap[key]; !exists {
			depMap[key] = dep
		}
	}
	for _, dep := range depMap {
		result.Dependencies = append(result.Dependencies, dep)
	}

	// Tombstone fields
	if result.Status == "tombstone" {
		if isTimeAfterStr(left.DeletedAt, right.DeletedAt) {
			result.DeletedAt = left.DeletedAt
			result.DeletedBy = left.DeletedBy
			result.DeleteReason = left.DeleteReason
			result.OriginalType = left.OriginalType
		} else {
			result.DeletedAt = right.DeletedAt
			result.DeletedBy = right.DeletedBy
			result.DeleteReason = right.DeleteReason
			result.OriginalType = right.OriginalType
		}
	}

	return result
}

func pickByUpdatedAt(left, right, leftTime, rightTime string) string {
	if left == right {
		return left
	}
	if isTimeAfterStr(leftTime, rightTime) {
		return left
	}
	return right
}

func maxTimeStr(t1, t2 string) string {
	if t1 == "" {
		return t2
	}
	if t2 == "" {
		return t1
	}
	if isTimeAfterStr(t1, t2) {
		return t1
	}
	return t2
}

func isTimeAfterStr(t1, t2 string) bool {
	if t1 == "" {
		return false
	}
	if t2 == "" {
		return true
	}
	// Simple string comparison works for RFC3339 timestamps
	return t1 > t2
}

func outputResolveError(filePath, errMsg string) {
	if resolveConflictsJSON {
		result := resolveConflictsResult{
			FilePath: filePath,
			Status:   "error",
			Error:    errMsg,
		}
		outputResolveJSON(result, 1)
	} else {
		fmt.Fprintf(os.Stderr, "Error: %s\n", errMsg)
	}
}

func outputResolveJSON(result resolveConflictsResult, 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)
}
