package main

import (
	"bufio"
	"cmp"
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/spf13/cobra"
	"github.com/steveyegge/beads/internal/beads"
	"github.com/steveyegge/beads/internal/types"
)

// JiraSyncStats tracks statistics for a Jira sync operation.
type JiraSyncStats struct {
	Pulled   int `json:"pulled"`
	Pushed   int `json:"pushed"`
	Created  int `json:"created"`
	Updated  int `json:"updated"`
	Skipped  int `json:"skipped"`
	Errors   int `json:"errors"`
	Conflicts int `json:"conflicts"`
}

// JiraSyncResult represents the result of a Jira sync operation.
type JiraSyncResult struct {
	Success   bool           `json:"success"`
	Stats     JiraSyncStats  `json:"stats"`
	LastSync  string         `json:"last_sync,omitempty"`
	Error     string         `json:"error,omitempty"`
	Warnings  []string       `json:"warnings,omitempty"`
}

var jiraCmd = &cobra.Command{
	Use:     "jira",
	GroupID: "advanced",
	Short:   "Jira integration commands",
	Long: `Synchronize issues between beads and Jira.

Configuration:
  bd config set jira.url "https://company.atlassian.net"
  bd config set jira.project "PROJ"
  bd config set jira.api_token "YOUR_TOKEN"
  bd config set jira.username "your_email@company.com"  # For Jira Cloud

Environment variables (alternative to config):
  JIRA_API_TOKEN - Jira API token
  JIRA_USERNAME  - Jira username/email

Examples:
  bd jira sync --pull         # Import issues from Jira
  bd jira sync --push         # Export issues to Jira
  bd jira sync                # Bidirectional sync (pull then push)
  bd jira sync --dry-run      # Preview sync without changes
  bd jira status              # Show sync status`,
}

var jiraSyncCmd = &cobra.Command{
	Use:   "sync",
	Short: "Synchronize issues with Jira",
	Long: `Synchronize issues between beads and Jira.

Modes:
  --pull         Import issues from Jira into beads
  --push         Export issues from beads to Jira
  (no flags)     Bidirectional sync: pull then push, with conflict resolution

Conflict Resolution:
  By default, newer timestamp wins. Override with:
  --prefer-local   Always prefer local beads version
  --prefer-jira    Always prefer Jira version

Examples:
  bd jira sync --pull                # Import from Jira
  bd jira sync --push --create-only  # Push new issues only
  bd jira sync --dry-run             # Preview without changes
  bd jira sync --prefer-local        # Bidirectional, local wins`,
	Run: func(cmd *cobra.Command, args []string) {
		// Flag errors are unlikely but check one to ensure cobra is working
		pull, _ := cmd.Flags().GetBool("pull")
		push, _ := cmd.Flags().GetBool("push")
		dryRun, _ := cmd.Flags().GetBool("dry-run")
		preferLocal, _ := cmd.Flags().GetBool("prefer-local")
		preferJira, _ := cmd.Flags().GetBool("prefer-jira")
		createOnly, _ := cmd.Flags().GetBool("create-only")
		updateRefs, _ := cmd.Flags().GetBool("update-refs")
		state, _ := cmd.Flags().GetString("state")

		// Block writes in readonly mode (sync modifies data)
		if !dryRun {
			CheckReadonly("jira sync")
		}

		// Validate conflicting flags
		if preferLocal && preferJira {
			fmt.Fprintf(os.Stderr, "Error: cannot use both --prefer-local and --prefer-jira\n")
			os.Exit(1)
		}

		// Ensure store is available
		if err := ensureStoreActive(); err != nil {
			fmt.Fprintf(os.Stderr, "Error: database not available: %v\n", err)
			os.Exit(1)
		}

		// Ensure we have Jira configuration
		if err := validateJiraConfig(); err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}

		// Default mode: bidirectional (pull then push)
		if !pull && !push {
			pull = true
			push = true
		}

		ctx := rootCtx
		result := &JiraSyncResult{Success: true}

		// Step 1: Pull from Jira
		if pull {
			if dryRun {
				fmt.Println("→ [DRY RUN] Would pull issues from Jira")
			} else {
				fmt.Println("→ Pulling issues from Jira...")
			}

			pullStats, err := doPullFromJira(ctx, dryRun, state)
			if err != nil {
				result.Success = false
				result.Error = err.Error()
				if jsonOutput {
					outputJSON(result)
				} else {
					fmt.Fprintf(os.Stderr, "Error pulling from Jira: %v\n", err)
				}
				os.Exit(1)
			}

			result.Stats.Pulled = pullStats.Created + pullStats.Updated
			result.Stats.Created += pullStats.Created
			result.Stats.Updated += pullStats.Updated
			result.Stats.Skipped += pullStats.Skipped

			if !dryRun {
				fmt.Printf("✓ Pulled %d issues (%d created, %d updated)\n",
					result.Stats.Pulled, pullStats.Created, pullStats.Updated)
			}
		}

		// Step 2: Handle conflicts (if bidirectional)
		if pull && push && !dryRun {
			conflicts, err := detectJiraConflicts(ctx)
			if err != nil {
				result.Warnings = append(result.Warnings, fmt.Sprintf("conflict detection failed: %v", err))
			} else if len(conflicts) > 0 {
				result.Stats.Conflicts = len(conflicts)
				if preferLocal {
					fmt.Printf("→ Resolving %d conflicts (preferring local)\n", len(conflicts))
					// Local wins - no action needed, push will overwrite
				} else if preferJira {
					fmt.Printf("→ Resolving %d conflicts (preferring Jira)\n", len(conflicts))
					// Jira wins - re-import conflicting issues
					if err := reimportConflicts(ctx, conflicts); err != nil {
						result.Warnings = append(result.Warnings, fmt.Sprintf("conflict resolution failed: %v", err))
					}
				} else {
					// Default: timestamp-based (newer wins)
					fmt.Printf("→ Resolving %d conflicts (newer wins)\n", len(conflicts))
					if err := resolveConflictsByTimestamp(ctx, conflicts); err != nil {
						result.Warnings = append(result.Warnings, fmt.Sprintf("conflict resolution failed: %v", err))
					}
				}
			}
		}

		// Step 3: Push to Jira
		if push {
			if dryRun {
				fmt.Println("→ [DRY RUN] Would push issues to Jira")
			} else {
				fmt.Println("→ Pushing issues to Jira...")
			}

			pushStats, err := doPushToJira(ctx, dryRun, createOnly, updateRefs)
			if err != nil {
				result.Success = false
				result.Error = err.Error()
				if jsonOutput {
					outputJSON(result)
				} else {
					fmt.Fprintf(os.Stderr, "Error pushing to Jira: %v\n", err)
				}
				os.Exit(1)
			}

			result.Stats.Pushed = pushStats.Created + pushStats.Updated
			result.Stats.Created += pushStats.Created
			result.Stats.Updated += pushStats.Updated
			result.Stats.Skipped += pushStats.Skipped
			result.Stats.Errors += pushStats.Errors

			if !dryRun {
				fmt.Printf("✓ Pushed %d issues (%d created, %d updated)\n",
					result.Stats.Pushed, pushStats.Created, pushStats.Updated)
			}
		}

		// Update last sync timestamp
		if !dryRun && result.Success {
			result.LastSync = time.Now().Format(time.RFC3339)
			if err := store.SetConfig(ctx, "jira.last_sync", result.LastSync); err != nil {
				result.Warnings = append(result.Warnings, fmt.Sprintf("failed to update last_sync: %v", err))
			}
		}

		// Output result
		if jsonOutput {
			outputJSON(result)
		} else if dryRun {
			fmt.Println("\n✓ Dry run complete (no changes made)")
		} else {
			fmt.Println("\n✓ Jira sync complete")
			if len(result.Warnings) > 0 {
				fmt.Println("\nWarnings:")
				for _, w := range result.Warnings {
					fmt.Printf("  - %s\n", w)
				}
			}
		}
	},
}

var jiraStatusCmd = &cobra.Command{
	Use:   "status",
	Short: "Show Jira sync status",
	Long: `Show the current Jira sync status, including:
  - Last sync timestamp
  - Configuration status
  - Number of issues with Jira links
  - Issues pending push (no external_ref)`,
	Run: func(cmd *cobra.Command, args []string) {
		ctx := rootCtx

		// Ensure store is available
		if err := ensureStoreActive(); err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}

		// Get configuration
		jiraURL, _ := store.GetConfig(ctx, "jira.url")
		jiraProject, _ := store.GetConfig(ctx, "jira.project")
		lastSync, _ := store.GetConfig(ctx, "jira.last_sync")

		// Check if configured
		configured := jiraURL != "" && jiraProject != ""

		// Count issues with Jira links
		allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}

		withJiraRef := 0
		pendingPush := 0
		for _, issue := range allIssues {
			if issue.ExternalRef != nil && isJiraExternalRef(*issue.ExternalRef, jiraURL) {
				withJiraRef++
			} else if issue.ExternalRef == nil {
				// Only count issues without any external_ref as pending push
				pendingPush++
			}
			// Issues with non-Jira external_ref are not counted in either category
		}

		if jsonOutput {
			outputJSON(map[string]interface{}{
				"configured":       configured,
				"jira_url":         jiraURL,
				"jira_project":     jiraProject,
				"last_sync":        lastSync,
				"total_issues":     len(allIssues),
				"with_jira_ref":    withJiraRef,
				"pending_push":     pendingPush,
			})
			return
		}

		fmt.Println("Jira Sync Status")
		fmt.Println("================")
		fmt.Println()

		if !configured {
			fmt.Println("Status: Not configured")
			fmt.Println()
			fmt.Println("To configure Jira integration:")
			fmt.Println("  bd config set jira.url \"https://company.atlassian.net\"")
			fmt.Println("  bd config set jira.project \"PROJ\"")
			fmt.Println("  bd config set jira.api_token \"YOUR_TOKEN\"")
			fmt.Println("  bd config set jira.username \"your@email.com\"")
			return
		}

		fmt.Printf("Jira URL:     %s\n", jiraURL)
		fmt.Printf("Project:      %s\n", jiraProject)
		if lastSync != "" {
			fmt.Printf("Last Sync:    %s\n", lastSync)
		} else {
			fmt.Println("Last Sync:    Never")
		}
		fmt.Println()
		fmt.Printf("Total Issues: %d\n", len(allIssues))
		fmt.Printf("With Jira:    %d\n", withJiraRef)
		fmt.Printf("Local Only:   %d\n", pendingPush)

		if pendingPush > 0 {
			fmt.Println()
			fmt.Printf("Run 'bd jira sync --push' to push %d local issue(s) to Jira\n", pendingPush)
		}
	},
}

func init() {
	// Sync command flags
	jiraSyncCmd.Flags().Bool("pull", false, "Pull issues from Jira")
	jiraSyncCmd.Flags().Bool("push", false, "Push issues to Jira")
	jiraSyncCmd.Flags().Bool("dry-run", false, "Preview sync without making changes")
	jiraSyncCmd.Flags().Bool("prefer-local", false, "Prefer local version on conflicts")
	jiraSyncCmd.Flags().Bool("prefer-jira", false, "Prefer Jira version on conflicts")
	jiraSyncCmd.Flags().Bool("create-only", false, "Only create new issues, don't update existing")
	jiraSyncCmd.Flags().Bool("update-refs", true, "Update external_ref after creating Jira issues")
	jiraSyncCmd.Flags().String("state", "all", "Issue state to sync: open, closed, all")

	jiraCmd.AddCommand(jiraSyncCmd)
	jiraCmd.AddCommand(jiraStatusCmd)
	rootCmd.AddCommand(jiraCmd)
}

// validateJiraConfig checks that required Jira configuration is present.
func validateJiraConfig() error {
	if err := ensureStoreActive(); err != nil {
		return fmt.Errorf("database not available: %w", err)
	}

	ctx := rootCtx
	jiraURL, _ := store.GetConfig(ctx, "jira.url")
	jiraProject, _ := store.GetConfig(ctx, "jira.project")

	if jiraURL == "" {
		return fmt.Errorf("jira.url not configured\nRun: bd config set jira.url \"https://company.atlassian.net\"")
	}
	if jiraProject == "" {
		return fmt.Errorf("jira.project not configured\nRun: bd config set jira.project \"PROJ\"")
	}

	// Check for API token (from config or env)
	apiToken, _ := store.GetConfig(ctx, "jira.api_token")
	if apiToken == "" && os.Getenv("JIRA_API_TOKEN") == "" {
		return fmt.Errorf("Jira API token not configured\nRun: bd config set jira.api_token \"YOUR_TOKEN\"\nOr: export JIRA_API_TOKEN=YOUR_TOKEN")
	}

	return nil
}

// PullStats tracks pull operation statistics.
type PullStats struct {
	Created int
	Updated int
	Skipped int
}

// doPullFromJira imports issues from Jira using the Python script.
func doPullFromJira(ctx context.Context, dryRun bool, state string) (*PullStats, error) {
	stats := &PullStats{}

	// Find the Python script
	scriptPath, err := findJiraScript("jira2jsonl.py")
	if err != nil {
		return stats, fmt.Errorf("jira2jsonl.py not found: %w", err)
	}

	// Build command
	args := []string{scriptPath, "--from-config"}
	if state != "" && state != "all" {
		args = append(args, "--state", state)
	}

	// Run Python script to get JSONL output
	cmd := exec.CommandContext(ctx, "python3", args...)
	cmd.Stderr = os.Stderr

	output, err := cmd.Output()
	if err != nil {
		return stats, fmt.Errorf("failed to fetch from Jira: %w", err)
	}

	if dryRun {
		// Count issues in output
		scanner := bufio.NewScanner(strings.NewReader(string(output)))
		count := 0
		for scanner.Scan() {
			if strings.TrimSpace(scanner.Text()) != "" {
				count++
			}
		}
		fmt.Printf("  Would import %d issues from Jira\n", count)
		return stats, nil
	}

	// Parse JSONL and import
	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	var issues []*types.Issue

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}

		var issue types.Issue
		if err := json.Unmarshal([]byte(line), &issue); err != nil {
			return stats, fmt.Errorf("failed to parse issue JSON: %w", err)
		}
		issues = append(issues, &issue)
	}

	if err := scanner.Err(); err != nil {
		return stats, fmt.Errorf("failed to read JSONL: %w", err)
	}

	// Import issues using shared logic
	opts := ImportOptions{
		DryRun:     false,
		SkipUpdate: false,
	}

	result, err := importIssuesCore(ctx, dbPath, store, issues, opts)
	if err != nil {
		return stats, fmt.Errorf("import failed: %w", err)
	}

	stats.Created = result.Created
	stats.Updated = result.Updated
	stats.Skipped = result.Skipped

	return stats, nil
}

// PushStats tracks push operation statistics.
type PushStats struct {
	Created int
	Updated int
	Skipped int
	Errors  int
}

// doPushToJira exports issues to Jira using the Python script.
func doPushToJira(ctx context.Context, dryRun bool, createOnly bool, updateRefs bool) (*PushStats, error) {
	stats := &PushStats{}

	// Find the Python script
	scriptPath, err := findJiraScript("jsonl2jira.py")
	if err != nil {
		return stats, fmt.Errorf("jsonl2jira.py not found: %w", err)
	}

	// Get all issues
	issues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
	if err != nil {
		return stats, fmt.Errorf("failed to get issues: %w", err)
	}

	// Sort by ID for consistent output
	slices.SortFunc(issues, func(a, b *types.Issue) int {
		return cmp.Compare(a.ID, b.ID)
	})

	// Generate JSONL for export
	var jsonlLines []string
	for _, issue := range issues {
		data, err := json.Marshal(issue)
		if err != nil {
			return stats, fmt.Errorf("failed to encode issue %s: %w", issue.ID, err)
		}
		jsonlLines = append(jsonlLines, string(data))
	}

	jsonlContent := strings.Join(jsonlLines, "\n")

	// Build command
	args := []string{scriptPath, "--from-config"}
	if dryRun {
		args = append(args, "--dry-run")
	}
	if createOnly {
		args = append(args, "--create-only")
	}
	if updateRefs {
		args = append(args, "--update-refs")
	}

	cmd := exec.CommandContext(ctx, "python3", args...)
	cmd.Stdin = strings.NewReader(jsonlContent)
	cmd.Stderr = os.Stderr

	output, err := cmd.Output()
	if err != nil {
		return stats, fmt.Errorf("failed to push to Jira: %w", err)
	}

	// Parse output for statistics and external_ref updates
	scanner := bufio.NewScanner(strings.NewReader(string(output)))
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}

		// Parse mapping output: {"bd_id": "...", "jira_key": "...", "external_ref": "..."}
		var mapping struct {
			BDID        string `json:"bd_id"`
			JiraKey     string `json:"jira_key"`
			ExternalRef string `json:"external_ref"`
		}
		if err := json.Unmarshal([]byte(line), &mapping); err == nil && mapping.BDID != "" {
			stats.Created++

			// Update external_ref if requested
			if updateRefs && !dryRun && mapping.ExternalRef != "" {
				updates := map[string]interface{}{
					"external_ref": mapping.ExternalRef,
				}
				if err := store.UpdateIssue(ctx, mapping.BDID, updates, actor); err != nil {
					fmt.Fprintf(os.Stderr, "Warning: failed to update external_ref for %s: %v\n", mapping.BDID, err)
					stats.Errors++
				}
			}
		}
	}

	return stats, nil
}

// findJiraScript locates the Jira Python script.
func findJiraScript(name string) (string, error) {
	// Check common locations
	locations := []string{
		// Relative to current working directory
		filepath.Join("examples", "jira-import", name),
		// Relative to executable
		"",
	}

	// Add executable-relative path
	if exe, err := os.Executable(); err == nil {
		exeDir := filepath.Dir(exe)
		locations = append(locations, filepath.Join(exeDir, "examples", "jira-import", name))
		locations = append(locations, filepath.Join(exeDir, "..", "examples", "jira-import", name))
	}

	// Check BEADS_DIR or current .beads location
	if beadsDir := beads.FindBeadsDir(); beadsDir != "" {
		repoRoot := filepath.Dir(beadsDir)
		locations = append(locations, filepath.Join(repoRoot, "examples", "jira-import", name))
	}

	for _, loc := range locations {
		if loc == "" {
			continue
		}
		if _, err := os.Stat(loc); err == nil {
			absPath, err := filepath.Abs(loc)
			if err == nil {
				return absPath, nil
			}
			return loc, nil
		}
	}

	return "", fmt.Errorf("script not found: %s (looked in examples/jira-import/)", name)
}

// JiraConflict represents a conflict between local and Jira versions.
type JiraConflict struct {
	IssueID        string
	LocalUpdated   time.Time
	JiraUpdated    time.Time
	JiraExternalRef string
}

// detectJiraConflicts finds issues that have been modified both locally and in Jira.
func detectJiraConflicts(ctx context.Context) ([]JiraConflict, error) {
	// Get last sync time
	lastSyncStr, _ := store.GetConfig(ctx, "jira.last_sync")
	if lastSyncStr == "" {
		// No previous sync - no conflicts possible
		return nil, nil
	}

	lastSync, err := time.Parse(time.RFC3339, lastSyncStr)
	if err != nil {
		return nil, fmt.Errorf("invalid last_sync timestamp: %w", err)
	}

	// Get all issues with Jira refs that were updated since last sync
	allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
	if err != nil {
		return nil, err
	}

	// Get jiraURL for validation
	jiraURL, _ := store.GetConfig(ctx, "jira.url")

	var conflicts []JiraConflict
	for _, issue := range allIssues {
		if issue.ExternalRef == nil || !isJiraExternalRef(*issue.ExternalRef, jiraURL) {
			continue
		}

		// Check if updated since last sync
		if issue.UpdatedAt.After(lastSync) {
			// This is a potential conflict - for now, mark as conflict
			// TODO: In a full implementation, we'd fetch the Jira issue and compare timestamps
			conflicts = append(conflicts, JiraConflict{
				IssueID:         issue.ID,
				LocalUpdated:    issue.UpdatedAt,
				JiraExternalRef: *issue.ExternalRef,
			})
		}
	}

	return conflicts, nil
}

// reimportConflicts re-imports conflicting issues from Jira (Jira wins).
// NOTE: This is a placeholder - full implementation requires fetching individual
// issues from Jira API and updating local copies.
func reimportConflicts(_ context.Context, conflicts []JiraConflict) error {
	if len(conflicts) == 0 {
		return nil
	}
	fmt.Fprintf(os.Stderr, "Warning: conflict resolution (--prefer-jira) not fully implemented\n")
	fmt.Fprintf(os.Stderr, "  %d issue(s) may have conflicts that need manual review:\n", len(conflicts))
	for _, c := range conflicts {
		fmt.Fprintf(os.Stderr, "    - %s (local updated: %s)\n", c.IssueID, c.LocalUpdated.Format(time.RFC3339))
	}
	return nil
}

// resolveConflictsByTimestamp resolves conflicts by keeping the newer version.
// NOTE: This is a placeholder - full implementation requires fetching Jira
// timestamps and comparing with local timestamps.
func resolveConflictsByTimestamp(_ context.Context, conflicts []JiraConflict) error {
	if len(conflicts) == 0 {
		return nil
	}
	fmt.Fprintf(os.Stderr, "Warning: timestamp-based conflict resolution not fully implemented\n")
	fmt.Fprintf(os.Stderr, "  %d issue(s) may have conflicts - local version will be pushed:\n", len(conflicts))
	for _, c := range conflicts {
		fmt.Fprintf(os.Stderr, "    - %s\n", c.IssueID)
	}
	return nil
}

// isJiraExternalRef checks if an external_ref URL matches the configured Jira instance.
// It validates both the URL structure (/browse/PROJECT-123) and optionally the host.
func isJiraExternalRef(externalRef, jiraURL string) bool {
	// Must contain /browse/ pattern
	if !strings.Contains(externalRef, "/browse/") {
		return false
	}

	// If jiraURL is provided, validate the host matches
	if jiraURL != "" {
		jiraURL = strings.TrimSuffix(jiraURL, "/")
		if !strings.HasPrefix(externalRef, jiraURL) {
			return false
		}
	}

	return true
}
