package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"syscall"

	"github.com/otiai10/copy"
	"github.com/urfave/cli/v3"
	"github.com/x-motemen/ghq/logger"
)

func doMigrate(ctx context.Context, cmd *cli.Command) error {
	var (
		repoDir     = cmd.Args().First()
		dry         = cmd.Bool("dry-run")
		skipConfirm = cmd.Bool("y")
		w           = cmd.Root().Writer
	)

	if repoDir == "" {
		return fmt.Errorf("repository directory is required")
	}

	// Resolve directory (supports both absolute and relative paths)
	absDir, err := filepath.Abs(repoDir)
	if err != nil {
		return fmt.Errorf("failed to resolve directory path: %w", err)
	}

	// Check if the directory exists
	if _, err := os.Stat(absDir); os.IsNotExist(err) {
		return fmt.Errorf("directory %q does not exist", absDir)
	} else if err != nil {
		return fmt.Errorf("failed to access directory %q: %w", absDir, err)
	}

	// Detect VCS backend
	vcsBackend := findVCSBackend(absDir, "")
	if vcsBackend == nil {
		return fmt.Errorf("failed to detect VCS backend in %q", absDir)
	}

	// Refuse to migrate a linked Git checkout (worktree or submodule).
	// These have a .git file referencing a parent repo; moving them alone
	// breaks the link.
	if vcsBackend == GitBackend {
		if linked, target, err := isLinkedGitDir(absDir); err != nil {
			return fmt.Errorf("failed to check .git link status: %w", err)
		} else if linked {
			return fmt.Errorf("directory %q has a .git file linking to %q; it is a worktree or submodule and cannot be migrated independently", absDir, target)
		}
	}

	// Get remote URL
	if vcsBackend.RemoteURL == nil {
		return fmt.Errorf("migrate is not supported for this VCS backend")
	}
	remoteURL, err := vcsBackend.RemoteURL(absDir)
	if err != nil {
		return fmt.Errorf("failed to get remote URL: %w", err)
	}

	// Parse the remote URL
	u, err := newURL(remoteURL, false, false)
	if err != nil {
		return fmt.Errorf("failed to parse remote URL %q: %w", remoteURL, err)
	}

	// Derive destination path
	localRepo, err := LocalRepositoryFromURL(u, false)
	if err != nil {
		return fmt.Errorf("failed to derive destination path: %w", err)
	}

	destPath := localRepo.FullPath

	// Check if source and destination are the same
	if absDir == destPath {
		return fmt.Errorf("repository is already at the correct location: %s", destPath)
	}

	// Check if destination already exists
	if _, err := os.Stat(destPath); err == nil {
		return fmt.Errorf("destination directory %q already exists", destPath)
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("failed to check destination directory: %w", err)
	}

	// Check for linked worktrees before dry-run return so we can report them
	var hasWorktrees bool
	if vcsBackend == GitBackend {
		hasWorktrees, err = hasLinkedWorktrees(absDir)
		if err != nil {
			return fmt.Errorf("failed to check for linked worktrees: %w", err)
		}
	}

	// Dry-run mode
	if dry {
		fmt.Fprintf(w, "Would migrate %s to %s\n", absDir, destPath)
		if hasWorktrees {
			fmt.Fprintf(w, "Would run 'git worktree repair' to update linked worktrees\n")
		}
		return nil
	}

	// Confirmation prompt (skip if -y flag is set)
	if !skipConfirm {
		ok, err := confirm(fmt.Sprintf("Migrate %s to %s?", absDir, destPath))
		if err != nil {
			return err
		}
		if !ok {
			return fmt.Errorf("migration aborted by user")
		}
	}

	// Create parent directories
	destDir := filepath.Dir(destPath)
	if err := os.MkdirAll(destDir, 0755); err != nil {
		return fmt.Errorf("failed to create parent directories: %w", err)
	}

	// Move the repository
	if err := moveDir(absDir, destPath); err != nil {
		return fmt.Errorf("failed to move repository: %w", err)
	}

	// Repair linked worktrees so their .git files reference the new location.
	//
	// For each worktree, two pointers exist:
	//   back-pointer:    .git/worktrees/<name>/gitdir  → worktree working dir
	//   forward ref:     <worktree>/.git               → main repo's .git/worktrees/<name>
	//
	// External worktrees (outside the repo) didn't move, so only the forward
	// ref is stale. Internal worktrees (inside the repo) moved along with
	// the repo, so BOTH pointers are stale. We fix the back-pointers first
	// so that "git worktree repair" can match entries to update the forward refs.
	if hasWorktrees {
		wtPaths, wtErr := repairWorktreeBackPointers(absDir, destPath)
		if wtErr != nil {
			logger.Log("warning", fmt.Sprintf("failed to discover linked worktree paths: %v", wtErr))
		} else if len(wtPaths) > 0 {
			args := append([]string{"worktree", "repair"}, wtPaths...)
			cmd := exec.Command("git", args...)
			cmd.Dir = destPath
			if out, err := cmd.CombinedOutput(); err != nil {
				logger.Log("warning", fmt.Sprintf("git worktree repair failed: %v\n%s", err, out))
			}
		}
	}

	fmt.Fprintln(w, destPath)
	return nil
}

// moveDir attempts to move directory from src to dst, with fallback for cross-device moves
func moveDir(src, dst string) error {
	// Try atomic rename first
	renameErr := os.Rename(src, dst)
	if renameErr == nil {
		return nil
	}

	// Check for cross-device error
	var linkError *os.LinkError
	isCrossDevice := errors.As(renameErr, &linkError) && errors.Is(linkError.Err, syscall.EXDEV)

	if !isCrossDevice {
		return renameErr
	}

	// Fallback: copy directory tree using otiai10/copy, then remove source
	opt := copy.Options{
		// Preserve symlinks as-is
		OnSymlink: func(src string) copy.SymlinkAction {
			return copy.Shallow
		},
		// Skip special files (pipes, sockets, devices) as they're uncommon in repos
		Skip: func(srcinfo os.FileInfo, src, dest string) (bool, error) {
			mode := srcinfo.Mode()
			// Skip if not regular file, directory, or symlink
			if !mode.IsRegular() && !mode.IsDir() && mode&os.ModeSymlink == 0 {
				return true, nil
			}
			return false, nil
		},
	}

	copyErr := copy.Copy(src, dst, opt)
	if copyErr != nil {
		// Attempt to cleanup partial copy
		if cleanupErr := os.RemoveAll(dst); cleanupErr != nil {
			return fmt.Errorf("copy failed: %w (cleanup also failed: %v)", copyErr, cleanupErr)
		}
		return copyErr
	}

	return os.RemoveAll(src)
}
