// Package worktree provides management of isolated git worktree-based development environments.
package worktree

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/aki/amux/internal/config"
	"github.com/aki/amux/internal/filemanager"
	"github.com/aki/amux/internal/git"
	"github.com/aki/amux/internal/hooks"
)

// Manager manages Amux worktrees
type Manager struct {
	configManager *config.Manager
	gitOps        *git.Operations
	worktreesDir  string
	fm            *filemanager.Manager[Worktree]
}

// GetConfigManager returns the config manager
func (m *Manager) GetConfigManager() *config.Manager {
	return m.configManager
}

// NewManager creates a new worktree manager
func NewManager(configManager *config.Manager) (*Manager, error) {
	gitOps := git.NewOperations(configManager.GetProjectRoot())

	if !gitOps.IsGitRepository() {
		return nil, fmt.Errorf("not a git repository")
	}

	worktreesDir := filepath.Join(configManager.GetAmuxDir(), "worktrees")

	return &Manager{
		configManager: configManager,
		gitOps:        gitOps,
		worktreesDir:  worktreesDir,
		fm:            filemanager.NewManager[Worktree](),
	}, nil
}

// Create creates a new worktree
func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Worktree, error) {
	// Determine base branch
	baseBranch := opts.BaseBranch
	if baseBranch == "" {
		var err error
		baseBranch, err = m.gitOps.GetDefaultBranch()
		if err != nil {
			return nil, fmt.Errorf("failed to determine base branch: %w", err)
		}
	}

	// Determine branch name
	branch := opts.Branch
	if branch == "" {
		// Use the provided name as the branch name
		branch = opts.Name
	}

	// Create worktree directory
	worktreePath := filepath.Join(m.worktreesDir, branch)

	// Ensure the worktrees directory exists
	if err := os.MkdirAll(m.worktreesDir, 0o755); err != nil {
		return nil, fmt.Errorf("failed to create worktrees directory: %w", err)
	}

	// Check if branch exists
	exists, err := m.gitOps.BranchExists(branch)
	if err != nil {
		return nil, fmt.Errorf("failed to check branch existence: %w", err)
	}

	// Handle branch creation/checkout based on mode
	switch opts.BranchMode {
	case BranchModeCreate:
		// Create new branch (fail if exists)
		if exists {
			return nil, fmt.Errorf("cannot create branch '%s': already exists. Use -c to checkout existing branch", branch)
		}
		if err := m.gitOps.CreateWorktree(worktreePath, branch, baseBranch); err != nil {
			return nil, fmt.Errorf("failed to create worktree with new branch: %w", err)
		}
	case BranchModeCheckout:
		// Use existing branch (fail if doesn't exist)
		if !exists {
			return nil, fmt.Errorf("cannot checkout '%s': branch does not exist. Use -b to create new branch", branch)
		}
		if err := m.gitOps.CreateWorktreeFromExistingBranch(worktreePath, branch); err != nil {
			return nil, fmt.Errorf("failed to create worktree from existing branch: %w", err)
		}
	default:
		// Should never happen as BranchModeCreate is the default
		return nil, fmt.Errorf("invalid branch mode: %v", opts.BranchMode)
	}

	// Create worktree metadata
	worktree := &Worktree{
		Branch:      branch,
		BaseBranch:  baseBranch,
		Path:        worktreePath,
		Description: opts.Description,
		CreatedAt:   time.Now(),
		AutoCreated: opts.AutoCreated,
	}

	// Save worktree metadata
	if err := m.saveWorktree(ctx, worktree); err != nil {
		// Cleanup on failure
		_ = m.gitOps.RemoveWorktree(worktreePath)
		_ = m.gitOps.DeleteBranch(branch)
		return nil, fmt.Errorf("failed to save worktree metadata: %w", err)
	}

	// Execute hooks unless disabled
	if !opts.NoHooks {
		if err := m.executeHooks(ctx, worktree, hooks.EventWorkspaceCreate); err != nil {
			// Log error but don't fail worktree creation
			slog.Error("hook execution failed", "error", err)
		}
	}

	return worktree, nil
}

// Get retrieves a worktree by its branch name
func (m *Manager) Get(ctx context.Context, branch string) (*Worktree, error) {
	// Construct metadata path for {branch-name}.yaml
	metadataPath := filepath.Join(m.worktreesDir, branch+".yaml")

	// Handle branches with slashes
	if strings.Contains(branch, "/") {
		parts := strings.Split(branch, "/")
		dir := filepath.Join(m.worktreesDir, filepath.Join(parts[:len(parts)-1]...))
		metadataPath = filepath.Join(dir, parts[len(parts)-1]+".yaml")
	}

	worktree, _, err := m.fm.Read(ctx, metadataPath)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, ErrNotFound{Identifier: branch}
		}
		return nil, fmt.Errorf("failed to read worktree: %w", err)
	}

	// Get last modified time from filesystem
	worktree.UpdatedAt = m.getLastModified(worktree.Path)

	// Check consistency status
	m.CheckConsistency(worktree)

	return worktree, nil
}

// List returns all worktrees
func (m *Manager) List(ctx context.Context, opts ListOptions) ([]*Worktree, error) {
	// Ensure worktrees directory exists
	if err := os.MkdirAll(m.worktreesDir, 0o755); err != nil {
		return nil, fmt.Errorf("failed to create worktrees directory: %w", err)
	}

	// Find all .yaml files (except worktree.yaml for backward compatibility)
	var worktrees []*Worktree
	err := filepath.Walk(m.worktreesDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			// Filesystem errors should terminate the walk
			return err
		}

		if !info.IsDir() && strings.HasSuffix(info.Name(), ".yaml") && info.Name() != "worktree.yaml" {
			// Found a metadata file
			worktreePtr, _, err := m.fm.Read(ctx, path)
			if err != nil {
				// Log parse errors but continue with other worktrees
				slog.Warn("failed to read worktree metadata",
					"path", path,
					"error", err)
				return nil
			}
			worktree := *worktreePtr

			// Get last modified time from filesystem
			worktree.UpdatedAt = m.getLastModified(worktree.Path)

			// Check consistency status
			m.CheckConsistency(&worktree)

			worktrees = append(worktrees, &worktree)
		}
		return nil
	})
	if err != nil {
		return nil, fmt.Errorf("failed to walk worktrees directory: %w", err)
	}

	// Sort by creation time (newest first)
	sort.Slice(worktrees, func(i, j int) bool {
		return worktrees[i].CreatedAt.After(worktrees[j].CreatedAt)
	})

	return worktrees, nil
}

// getLastModified gets the last modified time of any file in the worktree
func (m *Manager) getLastModified(worktreePath string) time.Time {
	var lastMod time.Time

	// Walk through the worktree directory
	err := filepath.Walk(worktreePath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil //nolint:nilerr // Continue walking even if there's an error
		}

		// Skip .git directory and .amux directory
		if strings.Contains(path, "/.git/") || strings.Contains(path, "/.amux/") {
			if info.IsDir() {
				return filepath.SkipDir
			}
			return nil
		}

		// Update last modified time if this file is newer
		if info.ModTime().After(lastMod) {
			lastMod = info.ModTime()
		}

		return nil
	})

	// If no files found or error, use directory's own modified time
	if err != nil || lastMod.IsZero() {
		if info, err := os.Stat(worktreePath); err == nil {
			lastMod = info.ModTime()
		}
	}

	return lastMod
}

// ResolveWorktree finds a worktree by branch name
func (m *Manager) ResolveWorktree(ctx context.Context, identifier Identifier) (*Worktree, error) {
	// Branch name is the identifier
	wt, err := m.Get(ctx, string(identifier))
	if err != nil {
		return nil, err
	}
	return wt, nil
}

// Remove deletes a worktree by branch name
func (m *Manager) Remove(ctx context.Context, identifier Identifier, opts RemoveOptions) error {
	worktree, err := m.ResolveWorktree(ctx, identifier)
	if err != nil {
		// If worktree not found, still try to clean up the directory
		if _, ok := err.(ErrNotFound); ok {
			// Try to clean up the directory if it exists
			worktreeDir := filepath.Join(m.worktreesDir, string(identifier))
			if _, err := os.Stat(worktreeDir); err == nil {
				if err := os.RemoveAll(worktreeDir); err != nil {
					return fmt.Errorf("failed to remove worktree directory: %w", err)
				}
			}
			return nil
		}
		return err
	}

	// Safety check: prevent removing worktree while working inside it
	if !opts.SkipSafetyCheck && opts.CurrentDir != "" {
		if err := m.checkCurrentDirectorySafety(worktree.Path, opts.CurrentDir); err != nil {
			return err
		}
	}

	// Execute hooks before removal unless disabled
	if !opts.NoHooks {
		if err := m.executeHooks(ctx, worktree, hooks.EventWorkspaceRemove); err != nil {
			// Log error but continue with removal
			slog.Error("hook execution failed", "error", err)
		}
	}

	// Check consistency to determine the right cleanup approach
	m.CheckConsistency(worktree)

	// Handle different inconsistency cases
	switch worktree.Status {
	case StatusConsistent:
		// Normal case: both worktree and folder exist
		if err := m.gitOps.RemoveWorktree(worktree.Path); err != nil {
			return fmt.Errorf("failed to remove worktree: %w", err)
		}
	case StatusFolderMissing:
		// Case 1: Folder deleted but git worktree exists
		// Need to prune the worktree reference
		_ = m.gitOps.PruneWorktrees()
	case StatusWorktreeMissing:
		// Case 2: Git worktree removed but folder exists
		// Nothing special needed, will clean up folder below
	case StatusOrphaned:
		// Both are missing, just clean up metadata
	}

	// Delete branch
	if err := m.gitOps.DeleteBranch(worktree.Branch); err != nil {
		// If branch doesn't exist or is checked out in a non-existent worktree, continue
		if !strings.Contains(err.Error(), "not found") &&
			!strings.Contains(err.Error(), "checked out at") {
			return fmt.Errorf("failed to delete branch: %w", err)
		}
	}

	// Clean up worktree directory
	worktreeDir := filepath.Join(m.worktreesDir, worktree.Branch)
	if _, err := os.Stat(worktreeDir); err == nil {
		if err := os.RemoveAll(worktreeDir); err != nil {
			return fmt.Errorf("failed to remove worktree directory: %w", err)
		}
	}

	// Clean up metadata file ({branch-name}.yaml)
	metadataPath := filepath.Join(m.worktreesDir, worktree.Branch+".yaml")

	// Handle branches with slashes
	if strings.Contains(worktree.Branch, "/") {
		parts := strings.Split(worktree.Branch, "/")
		dir := filepath.Join(m.worktreesDir, filepath.Join(parts[:len(parts)-1]...))
		metadataPath = filepath.Join(dir, parts[len(parts)-1]+".yaml")
	}

	if _, err := os.Stat(metadataPath); err == nil {
		if err := os.Remove(metadataPath); err != nil {
			return fmt.Errorf("failed to remove metadata file: %w", err)
		}
	}

	return nil
}

// Cleanup removes old worktrees based on last modified time
func (m *Manager) Cleanup(ctx context.Context, opts CleanupOptions) ([]string, error) {
	worktrees, err := m.List(ctx, ListOptions{})
	if err != nil {
		return nil, err
	}

	cutoff := time.Now().AddDate(0, 0, -opts.Days)
	var removed []string

	for _, worktree := range worktrees {
		if worktree.UpdatedAt.Before(cutoff) {
			if !opts.DryRun {
				if err := m.Remove(ctx, Identifier(worktree.Branch), RemoveOptions{NoHooks: false}); err != nil {
					// Log error but continue with other worktrees
					fmt.Fprintf(os.Stderr, "Failed to remove worktree %s: %v\n", worktree.Branch, err)
					continue
				}
			}
			removed = append(removed, worktree.Branch)
		}
	}

	return removed, nil
}

// CheckConsistency checks the consistency status of a worktree
func (m *Manager) CheckConsistency(worktree *Worktree) {
	// Check if worktree folder exists
	if _, err := os.Stat(worktree.Path); err == nil {
		worktree.PathExists = true
	} else {
		worktree.PathExists = false
	}

	// Check if git worktree exists
	worktrees, err := m.gitOps.ListWorktrees()
	worktree.WorktreeExists = false
	if err == nil {
		for _, wt := range worktrees {
			// Normalize paths for comparison
			wtPath := filepath.Clean(wt.Path)
			wsPath := filepath.Clean(worktree.Path)

			// Try to resolve symlinks for both paths to handle macOS /var -> /private/var
			// We need to resolve even non-existent paths by resolving their parent directories
			wtPathResolved := wtPath
			wsPathResolved := wsPath

			// For worktree path
			if resolvedWt, err := filepath.EvalSymlinks(wtPath); err == nil {
				wtPathResolved = resolvedWt
			} else {
				// If path doesn't exist, try to resolve parent directory
				wtDir := filepath.Dir(wtPath)
				if resolvedDir, err := filepath.EvalSymlinks(wtDir); err == nil {
					wtPathResolved = filepath.Join(resolvedDir, filepath.Base(wtPath))
				}
			}

			// For workspace path
			if resolvedWs, err := filepath.EvalSymlinks(wsPath); err == nil {
				wsPathResolved = resolvedWs
			} else {
				// If path doesn't exist, try to resolve parent directory
				wsDir := filepath.Dir(wsPath)
				if resolvedDir, err := filepath.EvalSymlinks(wsDir); err == nil {
					wsPathResolved = filepath.Join(resolvedDir, filepath.Base(wsPath))
				}
			}

			if wtPathResolved == wsPathResolved {
				worktree.WorktreeExists = true
				break
			}
		}
	}

	// Determine status based on existence flags
	if worktree.PathExists && worktree.WorktreeExists {
		worktree.Status = StatusConsistent
	} else if !worktree.PathExists && worktree.WorktreeExists {
		worktree.Status = StatusFolderMissing
	} else if worktree.PathExists && !worktree.WorktreeExists {
		worktree.Status = StatusWorktreeMissing
	} else {
		worktree.Status = StatusOrphaned
	}
}

// saveWorktree saves worktree metadata to disk with file locking
func (m *Manager) saveWorktree(ctx context.Context, worktree *Worktree) error {
	// Save {branch-name}.yaml next to the worktree directory
	// For branch "feature/auth", creates:
	//   - .amux/worktrees/feature/auth.yaml (metadata)
	//   - .amux/worktrees/feature/auth/ (git worktree)

	// Get the directory containing the metadata file
	metadataDir := m.worktreesDir
	metadataFile := worktree.Branch + ".yaml"

	// Handle branches with slashes by creating parent directories
	if strings.Contains(worktree.Branch, "/") {
		parts := strings.Split(worktree.Branch, "/")
		// All but the last part become directories
		metadataDir = filepath.Join(m.worktreesDir, filepath.Join(parts[:len(parts)-1]...))
		metadataFile = parts[len(parts)-1] + ".yaml"
	}

	// Ensure metadata directory exists
	if err := os.MkdirAll(metadataDir, 0o755); err != nil {
		return fmt.Errorf("failed to create metadata directory: %w", err)
	}

	metadataPath := filepath.Join(metadataDir, metadataFile)
	return m.fm.Write(ctx, metadataPath, worktree)
}

// executeHooks runs hooks for the given worktree event
func (m *Manager) executeHooks(ctx context.Context, wt *Worktree, event hooks.Event) error {
	configDir := m.configManager.GetAmuxDir()

	// Load hooks configuration
	hooksConfig, err := hooks.LoadConfig(configDir)
	if err != nil {
		return fmt.Errorf("failed to load hooks: %w", err)
	}

	// Get hooks for this event
	eventHooks := hooksConfig.GetHooksForEvent(event)
	if len(eventHooks) == 0 {
		return nil // No hooks configured
	}

	// Check if hooks are trusted
	trusted, err := hooks.IsTrusted(configDir, hooksConfig)
	if err != nil {
		return fmt.Errorf("failed to check hook trust: %w", err)
	}

	if !trusted {
		// Don't execute untrusted hooks
		return nil
	}

	// Prepare environment variables
	env := map[string]string{
		"AMUX_WORKTREE_BRANCH":      wt.Branch,
		"AMUX_WORKTREE_PATH":        wt.Path,
		"AMUX_WORKTREE_BASE_BRANCH": wt.BaseBranch,
		"AMUX_EVENT":                string(event),
		"AMUX_EVENT_TIME":           time.Now().Format(time.RFC3339),
		"AMUX_PROJECT_ROOT":         m.configManager.GetProjectRoot(),
		"AMUX_CONFIG_DIR":           configDir,
	}

	// Execute hooks in worktree directory
	executor := hooks.NewExecutor(configDir, env).WithWorkingDir(wt.Path)
	return executor.ExecuteHooks(ctx, event, eventHooks)
}

// checkCurrentDirectorySafety checks if the current directory is inside the worktree
func (m *Manager) checkCurrentDirectorySafety(worktreePath, currentDir string) error {
	// Resolve current directory to handle OS-level symlinks (e.g., macOS /var -> /private/var)
	resolvedCwd, err := filepath.EvalSymlinks(currentDir)
	if err != nil {
		// If we can't resolve, use original path
		resolvedCwd = currentDir
	}

	// Check both original and resolved paths
	if strings.HasPrefix(currentDir, worktreePath) || strings.HasPrefix(resolvedCwd, worktreePath) {
		return fmt.Errorf("cannot remove worktree while working inside it")
	}

	return nil
}

// RemoveWithSessionCheck removes a worktree with active session checking
func (m *Manager) RemoveWithSessionCheck(ctx context.Context, identifier Identifier, force bool) error {
	// Resolve worktree
	worktree, err := m.ResolveWorktree(ctx, identifier)
	if err != nil {
		return err
	}

	// Check for active sessions
	sessionIDs, err := worktree.SessionIDs()
	if err != nil {
		return fmt.Errorf("failed to check active sessions: %w", err)
	}

	// If there are active sessions and force is not specified, return error
	if len(sessionIDs) > 0 && !force {
		return fmt.Errorf("cannot remove worktree '%s' - currently in use by %d session(s)", worktree.Branch, len(sessionIDs))
	}

	// Proceed with normal removal
	return m.Remove(ctx, identifier, RemoveOptions{})
}
