package main

import (
	"cmp"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/spf13/cobra"
	"github.com/steveyegge/beads/internal/debug"
	"github.com/steveyegge/beads/internal/storage/sqlite"
	"github.com/steveyegge/beads/internal/types"
	"github.com/steveyegge/beads/internal/util"
	"github.com/steveyegge/beads/internal/validation"
)

// countIssuesInJSONL counts the number of issues in a JSONL file
func countIssuesInJSONL(path string) (int, error) {
	// #nosec G304 - controlled path from config
	file, err := os.Open(path)
	if err != nil {
		return 0, err
	}
	defer func() {
		if err := file.Close(); err != nil {
			fmt.Fprintf(os.Stderr, "Warning: failed to close file: %v\n", err)
		}
	}()

	count := 0
	decoder := json.NewDecoder(file)
	for {
		var issue types.Issue
		if err := decoder.Decode(&issue); err != nil {
			if err.Error() == "EOF" {
				break
			}
			// Return error for corrupt/invalid JSON
			return count, fmt.Errorf("invalid JSON at issue %d: %w", count+1, err)
		}
		count++
	}
	return count, nil
}

// getIssueIDsFromJSONL reads a JSONL file and returns a set of issue IDs
func getIssueIDsFromJSONL(path string) (map[string]bool, error) {
	// #nosec G304 - controlled path from config
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer func() {
		if err := file.Close(); err != nil {
			fmt.Fprintf(os.Stderr, "Warning: failed to close file: %v\n", err)
		}
	}()

	ids := make(map[string]bool)
	decoder := json.NewDecoder(file)
	lineNum := 0
	for {
		var issue types.Issue
		if err := decoder.Decode(&issue); err != nil {
			if err.Error() == "EOF" {
				break
			}
			// Return error for corrupt/invalid JSON
			return ids, fmt.Errorf("invalid JSON at line %d: %w", lineNum+1, err)
		}
		ids[issue.ID] = true
		lineNum++
	}
	return ids, nil
}

// validateExportPath checks if the output path is safe to write to
func validateExportPath(path string) error {
	// Get absolute path to normalize it
	absPath, err := filepath.Abs(path)
	if err != nil {
		return fmt.Errorf("invalid path: %v", err)
	}

	// Convert to lowercase for case-insensitive comparison on Windows
	absPathLower := strings.ToLower(absPath)

	// List of sensitive system directories to avoid
	sensitiveDirs := []string{
		"c:\\windows",
		"c:\\program files",
		"c:\\program files (x86)",
		"c:\\programdata",
		"c:\\system volume information",
		"c:\\$recycle.bin",
		"c:\\boot",
		"c:\\recovery",
	}

	for _, dir := range sensitiveDirs {
		if strings.HasPrefix(absPathLower, strings.ToLower(dir)) {
			return fmt.Errorf("cannot write to sensitive system directory: %s", dir)
		}
	}

	return nil
}

var exportCmd = &cobra.Command{
	Use:     "export",
	GroupID: "sync",
	Short:   "Export issues to JSONL format",
	Long: `Export all issues to JSON Lines format (one JSON object per line).
Issues are sorted by ID for consistent diffs.

Output to stdout by default, or use -o flag for file output.

Examples:
  bd export --status open -o open-issues.jsonl
  bd export --type bug --priority-max 1
  bd export --created-after 2025-01-01 --assignee alice`,
	Run: func(cmd *cobra.Command, args []string) {
		format, _ := cmd.Flags().GetString("format")
		output, _ := cmd.Flags().GetString("output")
		statusFilter, _ := cmd.Flags().GetString("status")
		force, _ := cmd.Flags().GetBool("force")

		// Additional filter flags
		assignee, _ := cmd.Flags().GetString("assignee")
		issueType, _ := cmd.Flags().GetString("type")
		labels, _ := cmd.Flags().GetStringSlice("label")
		labelsAny, _ := cmd.Flags().GetStringSlice("label-any")
		priorityMinStr, _ := cmd.Flags().GetString("priority-min")
		priorityMaxStr, _ := cmd.Flags().GetString("priority-max")
		createdAfter, _ := cmd.Flags().GetString("created-after")
		createdBefore, _ := cmd.Flags().GetString("created-before")
		updatedAfter, _ := cmd.Flags().GetString("updated-after")
		updatedBefore, _ := cmd.Flags().GetString("updated-before")

		debug.Logf("Debug: export flags - output=%q, force=%v\n", output, force)

		if format != "jsonl" {
			fmt.Fprintf(os.Stderr, "Error: only 'jsonl' format is currently supported\n")
			os.Exit(1)
		}

		// Export command requires direct database access for consistent snapshot
		// If daemon is connected, close it and open direct connection
		if daemonClient != nil {
			debug.Logf("Debug: export command forcing direct mode (closes daemon connection)\n")
			_ = daemonClient.Close()
			daemonClient = nil
		}

		// Note: We used to check database file timestamps here, but WAL files
		// get created when opening the DB, making timestamp checks unreliable.
		// Instead, we check issue counts after loading (see below).

		// Ensure we have a direct store connection
		if store == nil {
			var err error
			if dbPath == "" {
				fmt.Fprintf(os.Stderr, "Error: no database path found\n")
				os.Exit(1)
			}
			store, err = sqlite.NewWithTimeout(rootCtx, dbPath, lockTimeout)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error: failed to open database: %v\n", err)
				os.Exit(1)
			}
			defer func() { _ = store.Close() }()
		}

		// Normalize labels: trim, dedupe, remove empty
		labels = util.NormalizeLabels(labels)
		labelsAny = util.NormalizeLabels(labelsAny)

		// Build filter
		// Tombstone export logic:
		// - No status filter → include tombstones for sync propagation
		// - --status=tombstone → include only tombstones (filter handles this)
		// - --status=<other> → exclude tombstones (user wants specific status)
		filter := types.IssueFilter{}
		if statusFilter != "" {
			status := types.Status(statusFilter)
			filter.Status = &status
			// Only include tombstones if explicitly filtering for them
			filter.IncludeTombstones = (status == types.StatusTombstone)
		} else {
			// No status filter: include tombstones for sync propagation
			filter.IncludeTombstones = true
		}
		if assignee != "" {
			filter.Assignee = &assignee
		}
		if issueType != "" {
			t := types.IssueType(issueType)
			filter.IssueType = &t
		}
		if len(labels) > 0 {
			filter.Labels = labels
		}
		if len(labelsAny) > 0 {
			filter.LabelsAny = labelsAny
		}

		// Priority exact match (use Changed() to properly handle P0)
		if cmd.Flags().Changed("priority") {
			priorityStr, _ := cmd.Flags().GetString("priority")
			priority, err := validation.ValidatePriority(priorityStr)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error parsing --priority: %v\n", err)
				os.Exit(1)
			}
			filter.Priority = &priority
		}

		// Priority ranges
		if cmd.Flags().Changed("priority-min") {
			priorityMin, err := validation.ValidatePriority(priorityMinStr)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error parsing --priority-min: %v\n", err)
				os.Exit(1)
			}
			filter.PriorityMin = &priorityMin
		}
		if cmd.Flags().Changed("priority-max") {
			priorityMax, err := validation.ValidatePriority(priorityMaxStr)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error parsing --priority-max: %v\n", err)
				os.Exit(1)
			}
			filter.PriorityMax = &priorityMax
		}

		// Date ranges
		if createdAfter != "" {
			t, err := parseTimeFlag(createdAfter)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error parsing --created-after: %v\n", err)
				os.Exit(1)
			}
			filter.CreatedAfter = &t
		}
		if createdBefore != "" {
			t, err := parseTimeFlag(createdBefore)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error parsing --created-before: %v\n", err)
				os.Exit(1)
			}
			filter.CreatedBefore = &t
		}
		if updatedAfter != "" {
			t, err := parseTimeFlag(updatedAfter)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error parsing --updated-after: %v\n", err)
				os.Exit(1)
			}
			filter.UpdatedAfter = &t
		}
		if updatedBefore != "" {
			t, err := parseTimeFlag(updatedBefore)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error parsing --updated-before: %v\n", err)
				os.Exit(1)
			}
			filter.UpdatedBefore = &t
		}

		// Get all issues
		ctx := rootCtx
		issues, err := store.SearchIssues(ctx, "", filter)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error: %v\n", err)
			os.Exit(1)
		}

		// Safety check: prevent exporting empty database over non-empty JSONL
		if len(issues) == 0 && output != "" && !force {
			existingCount, err := countIssuesInJSONL(output)
			if err != nil {
				// If we can't read the file, it might not exist yet, which is fine
				if !os.IsNotExist(err) {
					fmt.Fprintf(os.Stderr, "Warning: failed to read existing JSONL: %v\n", err)
				}
			} else if existingCount > 0 {
				fmt.Fprintf(os.Stderr, "Error: refusing to export empty database over non-empty JSONL file\n")
				fmt.Fprintf(os.Stderr, "  Database has 0 issues, JSONL has %d issues\n", existingCount)
				fmt.Fprintf(os.Stderr, "  This would result in data loss!\n")
				fmt.Fprintf(os.Stderr, "Hint: Use --force to override this safety check, or delete the JSONL file first:\n")
				fmt.Fprintf(os.Stderr, "  bd export -o %s --force\n", output)
				fmt.Fprintf(os.Stderr, "  rm %s\n", output)
				os.Exit(1)
			}
		}

		// Safety check: prevent exporting stale database that would lose issues
		if output != "" && !force {
			debug.Logf("Debug: checking staleness - output=%s, force=%v\n", output, force)

			// Read existing JSONL to get issue IDs
			jsonlIDs, err := getIssueIDsFromJSONL(output)
			if err != nil && !os.IsNotExist(err) {
				fmt.Fprintf(os.Stderr, "Warning: failed to read existing JSONL for staleness check: %v\n", err)
			}

			if err == nil && len(jsonlIDs) > 0 {
				// Build set of DB issue IDs
				dbIDs := make(map[string]bool)
				for _, issue := range issues {
					dbIDs[issue.ID] = true
				}

				// Check if JSONL has any issues that DB doesn't have
				var missingIDs []string
				for id := range jsonlIDs {
					if !dbIDs[id] {
						missingIDs = append(missingIDs, id)
					}
				}

				debug.Logf("Debug: JSONL has %d issues, DB has %d issues, missing %d\n",
					len(jsonlIDs), len(issues), len(missingIDs))

				if len(missingIDs) > 0 {
					slices.Sort(missingIDs)
					fmt.Fprintf(os.Stderr, "Error: refusing to export stale database that would lose issues\n")
					fmt.Fprintf(os.Stderr, "  Database has %d issues\n", len(issues))
					fmt.Fprintf(os.Stderr, "  JSONL has %d issues\n", len(jsonlIDs))
					fmt.Fprintf(os.Stderr, "  Export would lose %d issue(s):\n", len(missingIDs))

					// Show first 10 missing issues
					showCount := len(missingIDs)
					if showCount > 10 {
						showCount = 10
					}
					for i := 0; i < showCount; i++ {
						fmt.Fprintf(os.Stderr, "    - %s\n", missingIDs[i])
					}
					if len(missingIDs) > 10 {
						fmt.Fprintf(os.Stderr, "    ... and %d more\n", len(missingIDs)-10)
					}

					fmt.Fprintf(os.Stderr, "\n")
					fmt.Fprintf(os.Stderr, "This usually means:\n")
					fmt.Fprintf(os.Stderr, "  1. You need to run 'bd import -i %s' to sync the latest changes\n", output)
					fmt.Fprintf(os.Stderr, "  2. Or another workspace added issues that weren't synced to this database\n")
					fmt.Fprintf(os.Stderr, "\n")
					fmt.Fprintf(os.Stderr, "To force export anyway (will lose these issues):\n")
					fmt.Fprintf(os.Stderr, "  bd export -o %s --force\n", output)
					os.Exit(1)
				}
			}
		}

		// Filter out wisps - they should never be exported to JSONL
		// Wisps exist only in SQLite and are shared via .beads/redirect, not JSONL.
		filtered := make([]*types.Issue, 0, len(issues))
		for _, issue := range issues {
			if !issue.Ephemeral {
				filtered = append(filtered, issue)
			}
		}
		issues = filtered

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

		// Populate dependencies for all issues in one query (avoids N+1 problem)
		allDeps, err := store.GetAllDependencyRecords(ctx)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Error getting dependencies: %v\n", err)
			os.Exit(1)
		}
		for _, issue := range issues {
			issue.Dependencies = allDeps[issue.ID]
		}

		// Populate labels for all issues
		for _, issue := range issues {
			labels, err := store.GetLabels(ctx, issue.ID)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error getting labels for %s: %v\n", issue.ID, err)
				os.Exit(1)
			}
			issue.Labels = labels
		}

		// Open output
		out := os.Stdout
		var tempFile *os.File
		var tempPath string
		var finalPath string
		if output != "" {
			// Validate output path before creating files
			if err := validateExportPath(output); err != nil {
				fmt.Fprintf(os.Stderr, "Error: %v\n", err)
				os.Exit(1)
			}

			// Create temporary file in same directory for atomic rename
			dir := filepath.Dir(output)
			base := filepath.Base(output)
			var err error
			tempFile, err = os.CreateTemp(dir, base+".tmp.*")
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error creating temporary file: %v\n", err)
				os.Exit(1)
			}
			tempPath = tempFile.Name()
			finalPath = output

			// Ensure cleanup on failure
			defer func() {
				if tempFile != nil {
					_ = tempFile.Close()
					_ = os.Remove(tempPath) // Clean up temp file if we haven't renamed it
				}
			}()

			out = tempFile
		}

		// Write JSONL (timestamp-only deduplication DISABLED due to bd-160)
		encoder := json.NewEncoder(out)
		exportedIDs := make([]string, 0, len(issues))
		skippedCount := 0
		for _, issue := range issues {
			if err := encoder.Encode(issue); err != nil {
				fmt.Fprintf(os.Stderr, "Error encoding issue %s: %v\n", issue.ID, err)
				os.Exit(1)
			}

			exportedIDs = append(exportedIDs, issue.ID)
		}

		// Report skipped issues if any (helps debugging bd-159)
		if skippedCount > 0 && (output == "" || output == findJSONLPath()) {
			fmt.Fprintf(os.Stderr, "Skipped %d issue(s) with timestamp-only changes\n", skippedCount)
		}

		// Only clear dirty issues and auto-flush state if exporting to the default JSONL path
		// This prevents clearing dirty flags when exporting to custom paths (e.g., bd export -o backup.jsonl)
		if output == "" || output == findJSONLPath() {
			// Clear only the issues that were actually exported (fixes bd-52 race condition)
			if err := store.ClearDirtyIssuesByID(ctx, exportedIDs); err != nil {
				fmt.Fprintf(os.Stderr, "Warning: failed to clear dirty issues: %v\n", err)
			}

			// Clear auto-flush state since we just manually exported
			// This cancels any pending auto-flush timer and marks DB as clean
			clearAutoFlushState()

			// Store JSONL file hash for integrity validation
			// nolint:gosec // G304: finalPath is validated JSONL export path
			jsonlData, err := os.ReadFile(finalPath)
			if err == nil {
				hasher := sha256.New()
				hasher.Write(jsonlData)
				fileHash := hex.EncodeToString(hasher.Sum(nil))
				if err := store.SetJSONLFileHash(ctx, fileHash); err != nil {
					fmt.Fprintf(os.Stderr, "Warning: failed to update jsonl_file_hash: %v\n", err)
				}
			}
		}

		// If writing to file, atomically replace the target file
		if tempFile != nil {
			// Close the temp file before renaming
			if err := tempFile.Close(); err != nil {
				fmt.Fprintf(os.Stderr, "Warning: failed to close temporary file: %v\n", err)
			}
			tempFile = nil // Prevent cleanup

			// Atomically replace the target file
			if err := os.Rename(tempPath, finalPath); err != nil {
				_ = os.Remove(tempPath) // Clean up on failure
				fmt.Fprintf(os.Stderr, "Error replacing output file: %v\n", err)
				os.Exit(1)
			}

			// Set appropriate file permissions (0600: rw-------)
			// Skip chmod for symlinks - os.Chmod follows symlinks and would change the target's
			// permissions, which may be in a read-only location (e.g., /nix/store on NixOS).
			if info, err := os.Lstat(finalPath); err == nil && info.Mode()&os.ModeSymlink == 0 {
				if err := os.Chmod(finalPath, 0600); err != nil {
					fmt.Fprintf(os.Stderr, "Warning: failed to set file permissions: %v\n", err)
				}
			}

			// Verify JSONL file integrity after export
			actualCount, err := countIssuesInJSONL(finalPath)
			if err != nil {
				fmt.Fprintf(os.Stderr, "Error: Export verification failed: %v\n", err)
				os.Exit(1)
			}
			if actualCount != len(exportedIDs) {
				fmt.Fprintf(os.Stderr, "Error: Export verification failed\n")
				fmt.Fprintf(os.Stderr, "  Expected: %d issues\n", len(exportedIDs))
				fmt.Fprintf(os.Stderr, "  JSONL file: %d lines\n", actualCount)
				fmt.Fprintf(os.Stderr, "  Mismatch indicates export failed to write all issues\n")
				os.Exit(1)
			}

			// Update database mtime to be >= JSONL mtime (fixes #278, #301, #321)
			// Only do this when exporting to default JSONL path (not arbitrary outputs)
			// This prevents validatePreExport from incorrectly blocking on next export
			if output == "" || output == findJSONLPath() {
				beadsDir := filepath.Dir(finalPath)
				dbPath := filepath.Join(beadsDir, "beads.db")
				if err := TouchDatabaseFile(dbPath, finalPath); err != nil {
					// Log warning but don't fail export
					fmt.Fprintf(os.Stderr, "Warning: failed to update database mtime: %v\n", err)
				}
			}
		}

		// Output statistics if JSON format requested
		if jsonOutput {
			stats := map[string]interface{}{
				"success":      true,
				"exported":     len(exportedIDs),
				"skipped":      skippedCount,
				"total_issues": len(issues),
			}
			if output != "" {
				stats["output_file"] = output
			}
			data, _ := json.MarshalIndent(stats, "", "  ")
			fmt.Fprintln(os.Stderr, string(data))
		}
	},
}

func init() {
	exportCmd.Flags().StringP("format", "f", "jsonl", "Export format (jsonl)")
	exportCmd.Flags().StringP("output", "o", "", "Output file (default: stdout)")
	exportCmd.Flags().StringP("status", "s", "", "Filter by status")
	exportCmd.Flags().Bool("force", false, "Force export even if database is empty")
	exportCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output export statistics in JSON format")

	// Filter flags
	registerPriorityFlag(exportCmd, "")
	exportCmd.Flags().StringP("assignee", "a", "", "Filter by assignee")
	exportCmd.Flags().StringP("type", "t", "", "Filter by type (bug, feature, task, epic, chore, merge-request, molecule, gate)")
	exportCmd.Flags().StringSliceP("label", "l", []string{}, "Filter by labels (AND: must have ALL)")
	exportCmd.Flags().StringSlice("label-any", []string{}, "Filter by labels (OR: must have AT LEAST ONE)")

	// Priority filters
	exportCmd.Flags().String("priority-min", "", "Filter by minimum priority (inclusive, 0-4 or P0-P4)")
	exportCmd.Flags().String("priority-max", "", "Filter by maximum priority (inclusive, 0-4 or P0-P4)")

	// Date filters
	exportCmd.Flags().String("created-after", "", "Filter issues created after date (YYYY-MM-DD or RFC3339)")
	exportCmd.Flags().String("created-before", "", "Filter issues created before date (YYYY-MM-DD or RFC3339)")
	exportCmd.Flags().String("updated-after", "", "Filter issues updated after date (YYYY-MM-DD or RFC3339)")
	exportCmd.Flags().String("updated-before", "", "Filter issues updated before date (YYYY-MM-DD or RFC3339)")

	rootCmd.AddCommand(exportCmd)
}
