package main

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

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

// exportToJSONLWithStore exports issues to JSONL using the provided store
func exportToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPath string) error {
	// Get all issues
	issues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
	if err != nil {
		return fmt.Errorf("failed to get issues: %w", err)
	}

	// Safety check: prevent exporting empty database over non-empty JSONL
	if len(issues) == 0 {
		existingCount, err := countIssuesInJSONL(jsonlPath)
		if err != nil {
			// If we can't read the file, it might not exist yet, which is fine
			if !os.IsNotExist(err) {
				return fmt.Errorf("warning: failed to read existing JSONL: %w", err)
			}
		} else if existingCount > 0 {
			return fmt.Errorf("refusing to export empty database over non-empty JSONL file (database: 0 issues, JSONL: %d issues). This would result in data loss", existingCount)
		}
	}

	// Sort by ID for consistent output
	sort.Slice(issues, func(i, j int) bool {
		return issues[i].ID < issues[j].ID
	})

	// Populate dependencies for all issues
	allDeps, err := store.GetAllDependencyRecords(ctx)
	if err != nil {
		return fmt.Errorf("failed to get dependencies: %w", err)
	}
	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 {
			return fmt.Errorf("failed to get labels for %s: %w", issue.ID, err)
		}
		issue.Labels = labels
	}

	// Populate comments for all issues
	for _, issue := range issues {
		comments, err := store.GetIssueComments(ctx, issue.ID)
		if err != nil {
			return fmt.Errorf("failed to get comments for %s: %w", issue.ID, err)
		}
		issue.Comments = comments
	}

	// Create temp file for atomic write
	dir := filepath.Dir(jsonlPath)
	base := filepath.Base(jsonlPath)
	tempFile, err := os.CreateTemp(dir, base+".tmp.*")
	if err != nil {
		return fmt.Errorf("failed to create temp file: %w", err)
	}
	tempPath := tempFile.Name()

	// Use defer pattern for proper cleanup
	var writeErr error
	defer func() {
		_ = tempFile.Close()
		if writeErr != nil {
			_ = os.Remove(tempPath) // Remove temp file on error
		}
	}()

	// Write JSONL
	for _, issue := range issues {
		data, marshalErr := json.Marshal(issue)
		if marshalErr != nil {
			writeErr = fmt.Errorf("failed to marshal issue %s: %w", issue.ID, marshalErr)
			return writeErr
		}
		if _, writeErr = tempFile.Write(data); writeErr != nil {
			writeErr = fmt.Errorf("failed to write issue %s: %w", issue.ID, writeErr)
			return writeErr
		}
		if _, writeErr = tempFile.WriteString("\n"); writeErr != nil {
			writeErr = fmt.Errorf("failed to write newline: %w", writeErr)
			return writeErr
		}
	}

	// Close before rename
	if writeErr = tempFile.Close(); writeErr != nil {
		writeErr = fmt.Errorf("failed to close temp file: %w", writeErr)
		return writeErr
	}

	// Atomic rename
	if writeErr = os.Rename(tempPath, jsonlPath); writeErr != nil {
		writeErr = fmt.Errorf("failed to rename temp file: %w", writeErr)
		return writeErr
	}

	return nil
}

// importToJSONLWithStore imports issues from JSONL using the provided store
func importToJSONLWithStore(ctx context.Context, store storage.Storage, jsonlPath string) error {
	// Read JSONL file
	file, err := os.Open(jsonlPath) // #nosec G304 - controlled path from config
	if err != nil {
		return fmt.Errorf("failed to open JSONL: %w", err)
	}
	defer file.Close()
	
	// Parse all issues
	var issues []*types.Issue
	scanner := bufio.NewScanner(file)
	lineNum := 0
	
	for scanner.Scan() {
		lineNum++
		line := scanner.Text()
		
		// Skip empty lines
		if line == "" {
			continue
		}
		
		// Parse JSON
		var issue types.Issue
		if err := json.Unmarshal([]byte(line), &issue); err != nil {
			// Log error but continue - don't fail entire import
			fmt.Fprintf(os.Stderr, "Warning: failed to parse JSONL line %d: %v\n", lineNum, err)
			continue
		}
		
		issues = append(issues, &issue)
	}
	
	if err := scanner.Err(); err != nil {
		return fmt.Errorf("failed to read JSONL: %w", err)
	}
	
	// Use existing import logic with auto-conflict resolution
	opts := ImportOptions{
		DryRun:              false,
		SkipUpdate:          false,
		Strict:              false,
		SkipPrefixValidation: true,  // Skip prefix validation for auto-import
	}
	
	_, err = importIssuesCore(ctx, "", store, issues, opts)
	return err
}

// validateDatabaseFingerprint checks that the database belongs to this repository
func validateDatabaseFingerprint(store storage.Storage, log *daemonLogger) error {
	ctx := context.Background()

	// Get stored repo ID
	storedRepoID, err := store.GetMetadata(ctx, "repo_id")
	if err != nil && err.Error() != "metadata key not found: repo_id" {
		return fmt.Errorf("failed to read repo_id: %w", err)
	}

	// If no repo_id, this is a legacy database - require explicit migration
	if storedRepoID == "" {
		return fmt.Errorf(`
LEGACY DATABASE DETECTED!

This database was created before version 0.17.5 and lacks a repository fingerprint.
To continue using this database, you must explicitly set its repository ID:

  bd migrate --update-repo-id

This ensures the database is bound to this repository and prevents accidental
database sharing between different repositories.

If this is a fresh clone, run:
  rm -rf .beads && bd init

Note: Auto-claiming legacy databases is intentionally disabled to prevent
silent corruption when databases are copied between repositories.
`)
	}

	// Validate repo ID matches current repository
	currentRepoID, err := beads.ComputeRepoID()
	if err != nil {
		log.log("Warning: could not compute current repository ID: %v", err)
		return nil
	}

	if storedRepoID != currentRepoID {
		return fmt.Errorf(`
DATABASE MISMATCH DETECTED!

This database belongs to a different repository:
  Database repo ID:  %s
  Current repo ID:   %s

This usually means:
  1. You copied a .beads directory from another repo (don't do this!)
  2. Git remote URL changed (run 'bd migrate --update-repo-id')
  3. Database corruption
  4. bd was upgraded and URL canonicalization changed

Solutions:
  - If remote URL changed: bd migrate --update-repo-id
  - If bd was upgraded: bd migrate --update-repo-id
  - If wrong database: rm -rf .beads && bd init
  - If correct database: BEADS_IGNORE_REPO_MISMATCH=1 bd daemon
    (Warning: This can cause data corruption across clones!)
`, storedRepoID[:8], currentRepoID[:8])
	}

	log.log("Repository fingerprint validated: %s", currentRepoID[:8])
	return nil
}

// createExportFunc creates a function that only exports database to JSONL
// and optionally commits/pushes (no git pull or import). Used for mutation events.
func createExportFunc(ctx context.Context, store storage.Storage, autoCommit, autoPush bool, log daemonLogger) func() {
	return func() {
		exportCtx, exportCancel := context.WithTimeout(ctx, 30*time.Second)
		defer exportCancel()

		log.log("Starting export...")

		jsonlPath := findJSONLPath()
		if jsonlPath == "" {
			log.log("Error: JSONL path not found")
			return
		}

		// Check for exclusive lock
		beadsDir := filepath.Dir(jsonlPath)
		skip, holder, err := types.ShouldSkipDatabase(beadsDir)
		if skip {
			if err != nil {
				log.log("Skipping export (lock check failed: %v)", err)
			} else {
				log.log("Skipping export (locked by %s)", holder)
			}
			return
		}
		if holder != "" {
			log.log("Removed stale lock (%s), proceeding", holder)
		}

		// Pre-export validation
		if err := validatePreExport(exportCtx, store, jsonlPath); err != nil {
			log.log("Pre-export validation failed: %v", err)
			return
		}

		// Export to JSONL
		if err := exportToJSONLWithStore(exportCtx, store, jsonlPath); err != nil {
			log.log("Export failed: %v", err)
			return
		}
		log.log("Exported to JSONL")

		// Auto-commit if enabled
		if autoCommit {
			hasChanges, err := gitHasChanges(exportCtx, jsonlPath)
			if err != nil {
				log.log("Error checking git status: %v", err)
				return
			}

			if hasChanges {
				message := fmt.Sprintf("bd daemon export: %s", time.Now().Format("2006-01-02 15:04:05"))
				if err := gitCommit(exportCtx, jsonlPath, message); err != nil {
					log.log("Commit failed: %v", err)
					return
				}
				log.log("Committed changes")

				// Auto-push if enabled
				if autoPush {
					if err := gitPush(exportCtx); err != nil {
						log.log("Push failed: %v", err)
						return
					}
					log.log("Pushed to remote")
				}
			}
		}

		log.log("Export complete")
	}
}

// createAutoImportFunc creates a function that pulls from git and imports JSONL
// to database (no export). Used for file system change events.
func createAutoImportFunc(ctx context.Context, store storage.Storage, log daemonLogger) func() {
	return func() {
		importCtx, importCancel := context.WithTimeout(ctx, 1*time.Minute)
		defer importCancel()

		log.log("Starting auto-import...")

		jsonlPath := findJSONLPath()
		if jsonlPath == "" {
			log.log("Error: JSONL path not found")
			return
		}

		// Check for exclusive lock
		beadsDir := filepath.Dir(jsonlPath)
		skip, holder, err := types.ShouldSkipDatabase(beadsDir)
		if skip {
			if err != nil {
				log.log("Skipping import (lock check failed: %v)", err)
			} else {
				log.log("Skipping import (locked by %s)", holder)
			}
			return
		}
		if holder != "" {
			log.log("Removed stale lock (%s), proceeding", holder)
		}

		// Check JSONL modification time to avoid redundant imports
		jsonlInfo, err := os.Stat(jsonlPath)
		if err != nil {
		 log.log("Failed to stat JSONL: %v", err)
		 return
	}

	// Get database modification time
	dbPath := filepath.Join(beadsDir, "beads.db")
	dbInfo, err := os.Stat(dbPath)
	if err != nil {
		log.log("Failed to stat database: %v", err)
		return
	}

	// Skip if JSONL is older than database (nothing new to import)
	if !jsonlInfo.ModTime().After(dbInfo.ModTime()) {
		log.log("Skipping import: JSONL not newer than database")
		return
	}

	// Pull from git
	if err := gitPull(importCtx); err != nil {
		log.log("Pull failed: %v", err)
		return
	}
	log.log("Pulled from remote")

	// Count issues before import
		beforeCount, err := countDBIssues(importCtx, store)
		if err != nil {
			log.log("Failed to count issues before import: %v", err)
			return
		}

		// Import from JSONL
		if err := importToJSONLWithStore(importCtx, store, jsonlPath); err != nil {
			log.log("Import failed: %v", err)
			return
		}
		log.log("Imported from JSONL")

		// Validate import
		afterCount, err := countDBIssues(importCtx, store)
		if err != nil {
			log.log("Failed to count issues after import: %v", err)
			return
		}

		if err := validatePostImport(beforeCount, afterCount); err != nil {
			log.log("Post-import validation failed: %v", err)
			return
		}

		log.log("Auto-import complete")
	}
}

// createSyncFunc creates a function that performs full sync cycle (export, commit, pull, import, push)
func createSyncFunc(ctx context.Context, store storage.Storage, autoCommit, autoPush bool, log daemonLogger) func() {
	return func() {
		syncCtx, syncCancel := context.WithTimeout(ctx, 2*time.Minute)
		defer syncCancel()

		log.log("Starting sync cycle...")

		jsonlPath := findJSONLPath()
		if jsonlPath == "" {
			log.log("Error: JSONL path not found")
			return
		}

		// Check for exclusive lock before processing database
		beadsDir := filepath.Dir(jsonlPath)
		skip, holder, err := types.ShouldSkipDatabase(beadsDir)
		if skip {
			if err != nil {
				log.log("Skipping database (lock check failed: %v)", err)
			} else {
				log.log("Skipping database (locked by %s)", holder)
			}
			return
		}
		if holder != "" {
			log.log("Removed stale lock (%s), proceeding with sync", holder)
		}

		// Integrity check: validate before export
		if err := validatePreExport(syncCtx, store, jsonlPath); err != nil {
			log.log("Pre-export validation failed: %v", err)
			return
		}

		// Check for duplicate IDs (database corruption)
		if err := checkDuplicateIDs(syncCtx, store); err != nil {
			log.log("Duplicate ID check failed: %v", err)
			return
		}

		// Check for orphaned dependencies (warns but doesn't fail)
		if orphaned, err := checkOrphanedDeps(syncCtx, store); err != nil {
			log.log("Orphaned dependency check failed: %v", err)
		} else if len(orphaned) > 0 {
			log.log("Found %d orphaned dependencies: %v", len(orphaned), orphaned)
		}

		if err := exportToJSONLWithStore(syncCtx, store, jsonlPath); err != nil {
			log.log("Export failed: %v", err)
			return
		}
		log.log("Exported to JSONL")

		if autoCommit {
			hasChanges, err := gitHasChanges(syncCtx, jsonlPath)
			if err != nil {
				log.log("Error checking git status: %v", err)
				return
			}

			if hasChanges {
				message := fmt.Sprintf("bd daemon sync: %s", time.Now().Format("2006-01-02 15:04:05"))
				if err := gitCommit(syncCtx, jsonlPath, message); err != nil {
					log.log("Commit failed: %v", err)
					return
				}
				log.log("Committed changes")
			}
		}

		if err := gitPull(syncCtx); err != nil {
		log.log("Pull failed: %v", err)
		return
		}
		log.log("Pulled from remote")

		// Count issues before import for validation
	beforeCount, err := countDBIssues(syncCtx, store)
	if err != nil {
		log.log("Failed to count issues before import: %v", err)
		return
	}

	if err := importToJSONLWithStore(syncCtx, store, jsonlPath); err != nil {
		log.log("Import failed: %v", err)
		return
	}
	log.log("Imported from JSONL")

	// Validate import didn't cause data loss
	afterCount, err := countDBIssues(syncCtx, store)
	if err != nil {
		log.log("Failed to count issues after import: %v", err)
		return
	}

	if err := validatePostImport(beforeCount, afterCount); err != nil {
		log.log("Post-import validation failed: %v", err)
		return
	}

		if autoPush && autoCommit {
			if err := gitPush(syncCtx); err != nil {
				log.log("Push failed: %v", err)
				return
			}
			log.log("Pushed to remote")
		}

		log.log("Sync cycle complete")
	}
}
