package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"time"

	"github.com/steveyegge/beads/internal/beads"
)

// MergeResult contains the outcome of a 3-way merge
type MergeResult struct {
	Merged    []*beads.Issue    // Final merged state
	Conflicts int               // Number of true conflicts resolved
	Strategy  map[string]string // Per-issue: "local", "remote", "merged", "same"
}

// MergeStrategy constants for describing how each issue was merged
const (
	StrategyLocal  = "local"  // Only local changed
	StrategyRemote = "remote" // Only remote changed
	StrategyMerged = "merged" // True conflict, LWW applied
	StrategySame   = "same"   // Both made identical change (or no change)
)

// FieldMergeRule defines how a specific field is merged in conflicts
type FieldMergeRule string

const (
	RuleLWW    FieldMergeRule = "lww"    // Last-Write-Wins by updated_at
	RuleUnion  FieldMergeRule = "union"  // Set union (OR-Set)
	RuleAppend FieldMergeRule = "append" // Append-only merge
)

// FieldRules maps field names to merge rules
// Scalar fields use LWW, collection fields use union/append
var FieldRules = map[string]FieldMergeRule{
	// Scalar fields - LWW by updated_at
	"status":      RuleLWW,
	"priority":    RuleLWW,
	"assignee":    RuleLWW,
	"title":       RuleLWW,
	"description": RuleLWW,
	"design":      RuleLWW,
	"issue_type":  RuleLWW,
	"notes":       RuleLWW,

	// Set fields - union (no data loss)
	"labels":       RuleUnion,
	"dependencies": RuleUnion,

	// Append-only fields
	"comments": RuleAppend,
}

// mergeFieldLevel performs field-by-field merge for true conflicts.
// Returns a new issue with:
// - Scalar fields: from the newer issue (LWW by updated_at, remote wins on tie)
// - Labels: union of both
// - Dependencies: union of both (by DependsOnID+Type)
// - Comments: append from both (deduplicated by ID or content)
func mergeFieldLevel(_base, local, remote *beads.Issue) *beads.Issue {
	// Determine which is newer for LWW scalars
	localNewer := local.UpdatedAt.After(remote.UpdatedAt)

	// Clock skew detection: warn if timestamps differ by more than 24 hours
	timeDiff := local.UpdatedAt.Sub(remote.UpdatedAt)
	if timeDiff < 0 {
		timeDiff = -timeDiff
	}
	if timeDiff > 24*time.Hour {
		fmt.Fprintf(os.Stderr, "Warning: Issue %s has %v timestamp difference (possible clock skew)\n",
			local.ID, timeDiff.Round(time.Hour))
	}

	// Start with a copy of the newer issue for scalar fields
	var merged beads.Issue
	if localNewer {
		merged = *local
	} else {
		merged = *remote
	}

	// Union merge: Labels
	merged.Labels = mergeLabels(local.Labels, remote.Labels)

	// Union merge: Dependencies (by DependsOnID+Type key)
	merged.Dependencies = mergeDependencies(local.Dependencies, remote.Dependencies)

	// Append merge: Comments (deduplicated)
	merged.Comments = mergeComments(local.Comments, remote.Comments)

	return &merged
}

// mergeLabels performs set union on labels
func mergeLabels(local, remote []string) []string {
	seen := make(map[string]bool)
	var result []string

	// Add all local labels
	for _, label := range local {
		if !seen[label] {
			seen[label] = true
			result = append(result, label)
		}
	}

	// Add remote labels not in local
	for _, label := range remote {
		if !seen[label] {
			seen[label] = true
			result = append(result, label)
		}
	}

	// Sort for deterministic output
	sort.Strings(result)
	return result
}

// dependencyKey creates a unique key for deduplication
// Uses DependsOnID + Type as the identity (same target+type = same dependency)
func dependencyKey(d *beads.Dependency) string {
	if d == nil {
		return ""
	}
	return d.DependsOnID + ":" + string(d.Type)
}

// mergeDependencies performs set union on dependencies
func mergeDependencies(local, remote []*beads.Dependency) []*beads.Dependency {
	seen := make(map[string]*beads.Dependency)

	// Add all local dependencies
	for _, dep := range local {
		if dep == nil {
			continue
		}
		key := dependencyKey(dep)
		seen[key] = dep
	}

	// Add remote dependencies not in local (or with newer timestamp)
	for _, dep := range remote {
		if dep == nil {
			continue
		}
		key := dependencyKey(dep)
		if existing, ok := seen[key]; ok {
			// Keep the one with newer CreatedAt
			if dep.CreatedAt.After(existing.CreatedAt) {
				seen[key] = dep
			}
		} else {
			seen[key] = dep
		}
	}

	// Collect and sort by key for deterministic output
	keys := make([]string, 0, len(seen))
	for k := range seen {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	result := make([]*beads.Dependency, 0, len(keys))
	for _, k := range keys {
		result = append(result, seen[k])
	}

	return result
}

// commentKey creates a unique key for deduplication
// Uses ID if present, otherwise content hash
func commentKey(c *beads.Comment) string {
	if c == nil {
		return ""
	}
	if c.ID != 0 {
		return fmt.Sprintf("id:%d", c.ID)
	}
	// Fallback to content-based key for comments without ID
	return fmt.Sprintf("content:%s:%s", c.Author, c.Text)
}

// mergeComments performs append-merge on comments with deduplication
func mergeComments(local, remote []*beads.Comment) []*beads.Comment {
	seen := make(map[string]*beads.Comment)

	// Add all local comments
	for _, c := range local {
		if c == nil {
			continue
		}
		key := commentKey(c)
		seen[key] = c
	}

	// Add remote comments not in local
	for _, c := range remote {
		if c == nil {
			continue
		}
		key := commentKey(c)
		if _, ok := seen[key]; !ok {
			seen[key] = c
		}
	}

	// Collect all comments
	result := make([]*beads.Comment, 0, len(seen))
	for _, c := range seen {
		result = append(result, c)
	}

	// Sort by CreatedAt for chronological order
	sort.Slice(result, func(i, j int) bool {
		return result[i].CreatedAt.Before(result[j].CreatedAt)
	})

	return result
}

// MergeIssues performs 3-way merge: base x local x remote -> merged
//
// Algorithm:
// 1. Build lookup maps for base, local, and remote by issue ID
// 2. Collect all unique issue IDs across all three sets
// 3. For each ID, apply MergeIssue to determine final state
// 4. Return merged result with per-issue strategy annotations
func MergeIssues(base, local, remote []*beads.Issue) *MergeResult {
	// Build lookup maps by issue ID
	baseMap := buildIssueMap(base)
	localMap := buildIssueMap(local)
	remoteMap := buildIssueMap(remote)

	// Collect all unique issue IDs
	allIDs := collectUniqueIDs(baseMap, localMap, remoteMap)

	result := &MergeResult{
		Merged:   make([]*beads.Issue, 0, len(allIDs)),
		Strategy: make(map[string]string),
	}

	for _, id := range allIDs {
		baseIssue := baseMap[id]
		localIssue := localMap[id]
		remoteIssue := remoteMap[id]

		merged, strategy := MergeIssue(baseIssue, localIssue, remoteIssue)

		// Always record strategy (even for deletions, for logging/debugging)
		result.Strategy[id] = strategy

		if merged != nil {
			result.Merged = append(result.Merged, merged)
			if strategy == StrategyMerged {
				result.Conflicts++
			}
		}
		// If merged is nil, the issue was deleted (present in base but not in local/remote)
	}

	return result
}

// MergeIssue merges a single issue using 3-way algorithm
//
// Cases:
// - base=nil: First sync (no common ancestor)
//   - local=nil, remote=nil: impossible (would not be in allIDs)
//   - local=nil: return remote (new from remote)
//   - remote=nil: return local (new from local)
//   - both exist: LWW by updated_at (both added independently)
//
// - base!=nil: Standard 3-way merge
//   - base=local=remote: no changes (same)
//   - base=local, remote differs: only remote changed (remote)
//   - base=remote, local differs: only local changed (local)
//   - local=remote (but differs from base): both made identical change (same)
//   - all three differ: true conflict, LWW by updated_at (merged)
//
// - Deletion handling:
//   - local=nil (deleted locally): if remote unchanged from base, delete; else keep remote
//   - remote=nil (deleted remotely): if local unchanged from base, delete; else keep local
func MergeIssue(base, local, remote *beads.Issue) (*beads.Issue, string) {
	// Case: no base state (first sync)
	if base == nil {
		if local == nil && remote == nil {
			// Should not happen (would not be in allIDs)
			return nil, StrategySame
		}
		if local == nil {
			return remote, StrategyRemote
		}
		if remote == nil {
			return local, StrategyLocal
		}
		// Both exist with no base: treat as conflict, use field-level merge
		// This allows labels/comments to be union-merged even in first sync
		return mergeFieldLevel(nil, local, remote), StrategyMerged
	}

	// Case: local deleted
	if local == nil {
		// If remote unchanged from base, honor the local deletion
		if issueEqual(base, remote) {
			return nil, StrategyLocal
		}
		// Remote changed after local deleted: keep remote (remote wins conflict)
		return remote, StrategyMerged
	}

	// Case: remote deleted
	if remote == nil {
		// If local unchanged from base, honor the remote deletion
		if issueEqual(base, local) {
			return nil, StrategyRemote
		}
		// Local changed after remote deleted: keep local (local wins conflict)
		return local, StrategyMerged
	}

	// Standard 3-way cases (all three exist)
	if issueEqual(base, local) && issueEqual(base, remote) {
		// No changes anywhere
		return local, StrategySame
	}

	if issueEqual(base, local) {
		// Only remote changed
		return remote, StrategyRemote
	}

	if issueEqual(base, remote) {
		// Only local changed
		return local, StrategyLocal
	}

	if issueEqual(local, remote) {
		// Both made identical change
		return local, StrategySame
	}

	// True conflict: use field-level merge
	// - Scalar fields use LWW (remote wins on tie)
	// - Labels use union (no data loss)
	// - Dependencies use union (no data loss)
	// - Comments use append (deduplicated)
	return mergeFieldLevel(base, local, remote), StrategyMerged
}

// issueEqual compares two issues for equality (content-level, not pointer)
// Compares all merge-relevant fields: content, status, workflow, assignment
func issueEqual(a, b *beads.Issue) bool {
	if a == nil || b == nil {
		return a == nil && b == nil
	}

	// Core identification
	if a.ID != b.ID {
		return false
	}

	// Issue content
	if a.Title != b.Title ||
		a.Description != b.Description ||
		a.Design != b.Design ||
		a.AcceptanceCriteria != b.AcceptanceCriteria ||
		a.Notes != b.Notes {
		return false
	}

	// Status & workflow
	if a.Status != b.Status ||
		a.Priority != b.Priority ||
		a.IssueType != b.IssueType {
		return false
	}

	// Assignment
	if a.Assignee != b.Assignee {
		return false
	}
	if !intPtrEqual(a.EstimatedMinutes, b.EstimatedMinutes) {
		return false
	}

	// Timestamps (updated_at is crucial for LWW)
	if !a.UpdatedAt.Equal(b.UpdatedAt) {
		return false
	}

	// Closed state
	if !timePtrEqual(a.ClosedAt, b.ClosedAt) ||
		a.CloseReason != b.CloseReason {
		return false
	}

	// Time-based scheduling
	if !timePtrEqual(a.DueAt, b.DueAt) ||
		!timePtrEqual(a.DeferUntil, b.DeferUntil) {
		return false
	}

	// External reference
	if !stringPtrEqual(a.ExternalRef, b.ExternalRef) {
		return false
	}

	// Tombstone fields
	if !timePtrEqual(a.DeletedAt, b.DeletedAt) ||
		a.DeletedBy != b.DeletedBy ||
		a.DeleteReason != b.DeleteReason {
		return false
	}

	// Labels (order-independent comparison)
	if !stringSliceEqual(a.Labels, b.Labels) {
		return false
	}

	return true
}

// buildIssueMap creates a lookup map from issue ID to issue pointer
func buildIssueMap(issues []*beads.Issue) map[string]*beads.Issue {
	m := make(map[string]*beads.Issue, len(issues))
	for _, issue := range issues {
		if issue != nil {
			m[issue.ID] = issue
		}
	}
	return m
}

// collectUniqueIDs gathers all unique issue IDs from the three maps
// Returns sorted for deterministic output
func collectUniqueIDs(base, local, remote map[string]*beads.Issue) []string {
	seen := make(map[string]bool)
	for id := range base {
		seen[id] = true
	}
	for id := range local {
		seen[id] = true
	}
	for id := range remote {
		seen[id] = true
	}

	ids := make([]string, 0, len(seen))
	for id := range seen {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	return ids
}

// Helper functions for pointer comparison

func intPtrEqual(a, b *int) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

func stringPtrEqual(a, b *string) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return *a == *b
}

func timePtrEqual(a, b *time.Time) bool {
	if a == nil && b == nil {
		return true
	}
	if a == nil || b == nil {
		return false
	}
	return a.Equal(*b)
}

func stringSliceEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}
	// Sort copies for order-independent comparison
	aCopy := make([]string, len(a))
	bCopy := make([]string, len(b))
	copy(aCopy, a)
	copy(bCopy, b)
	sort.Strings(aCopy)
	sort.Strings(bCopy)
	for i := range aCopy {
		if aCopy[i] != bCopy[i] {
			return false
		}
	}
	return true
}

// Base state storage functions for sync_base.jsonl

const syncBaseFileName = "sync_base.jsonl"

// loadBaseState loads the last-synced state from .beads/sync_base.jsonl
// Returns empty slice if file doesn't exist (first sync scenario)
func loadBaseState(beadsDir string) ([]*beads.Issue, error) {
	baseStatePath := filepath.Join(beadsDir, syncBaseFileName)

	// Check if file exists
	if _, err := os.Stat(baseStatePath); os.IsNotExist(err) {
		// First sync: no base state
		return nil, nil
	}

	// Read and parse JSONL file
	file, err := os.Open(baseStatePath)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	var issues []*beads.Issue
	scanner := bufio.NewScanner(file)
	// Increase buffer for large issues
	buf := make([]byte, 0, 64*1024)
	scanner.Buffer(buf, 1024*1024)

	lineNum := 0
	for scanner.Scan() {
		lineNum++
		line := scanner.Text()
		if line == "" {
			continue
		}

		var issue beads.Issue
		if err := json.Unmarshal([]byte(line), &issue); err != nil {
			fmt.Fprintf(os.Stderr, "Warning: Skipping malformed line %d in sync_base.jsonl: %v\n", lineNum, err)
			continue
		}
		issues = append(issues, &issue)
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return issues, nil
}

// saveBaseState writes the merged state to .beads/sync_base.jsonl
// This becomes the base for the next 3-way merge
func saveBaseState(beadsDir string, issues []*beads.Issue) error {
	baseStatePath := filepath.Join(beadsDir, syncBaseFileName)

	// Write to temp file first for atomicity
	tempPath := baseStatePath + ".tmp"
	file, err := os.Create(tempPath)
	if err != nil {
		return err
	}

	encoder := json.NewEncoder(file)
	encoder.SetEscapeHTML(false)

	for _, issue := range issues {
		if err := encoder.Encode(issue); err != nil {
			_ = file.Close() // Best-effort cleanup
			_ = os.Remove(tempPath)
			return err
		}
	}

	if err := file.Close(); err != nil {
		_ = os.Remove(tempPath) // Best-effort cleanup
		return err
	}

	// Atomic rename
	return os.Rename(tempPath, baseStatePath)
}
