package workspace

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

	"github.com/moby/moby/api/types/mount"
	"github.com/schmitthub/clawker/internal/config"
	"github.com/schmitthub/clawker/internal/containerfs"
	"github.com/schmitthub/clawker/internal/docker"
	"github.com/schmitthub/clawker/internal/logger"
)

// SetupMountsConfig holds configuration for workspace mount setup
type SetupMountsConfig struct {
	// Log is the logger instance for diagnostic file logging.
	Log *logger.Logger
	// ModeOverride is the CLI flag value (empty means use config default)
	ModeOverride string
	// Cfg is the config.Config interface for reading paths and constants.
	Cfg config.Config
	// ProjectName is the resolved project name for volume naming.
	// Resolved from project.ProjectManager at the command level.
	// Empty string when no project is registered.
	ProjectName string
	// AgentName is the agent name for volume naming
	AgentName string
	// WorkDir is the host working directory for workspace mounts.
	// If empty, falls back to os.Getwd() for backward compatibility.
	WorkDir string
	// ProjectRootDir is the main repository root when using a worktree.
	// If set, the .git directory will be mounted at the same absolute path
	// in the container to allow git worktree references to resolve.
	ProjectRootDir string
	// ContainerPath is the container-side mount destination for the workspace.
	// Set to the host absolute path for Claude Code /resume compatibility.
	// Must be set by callers (CreateContainer passes the resolved working directory).
	ContainerPath string
}

// SetupMountsResult holds the results from setting up workspace mounts.
type SetupMountsResult struct {
	// Mounts is the list of mounts to add to the container's HostConfig.
	Mounts []mount.Mount
	// ConfigVolumeResult tracks which config volumes were newly created.
	// Used by container init orchestration to decide whether to copy host config.
	ConfigVolumeResult ConfigVolumeResult
	// WorkspaceVolumeName is the name of the workspace volume created during setup.
	// Non-empty only for snapshot mode. Used for cleanup on init failure.
	WorkspaceVolumeName string
	// ContainerPath is the resolved container-side workspace mount path.
	ContainerPath string
}

// SetupMounts prepares workspace mounts for container creation.
// It handles workspace mode resolution, strategy creation/preparation,
// .clawkerignore pattern loading, config volumes, and docker socket mounting.
//
// Returns a result containing the mounts and config volume creation state.
func SetupMounts(ctx context.Context, client *docker.Client, cfg SetupMountsConfig) (*SetupMountsResult, error) {
	// Validate ContainerPath early — before any config or Docker access.
	containerPath := cfg.ContainerPath
	if containerPath == "" {
		return nil, fmt.Errorf("container workspace path is required (ContainerPath must be set on SetupMountsConfig)")
	}
	if !filepath.IsAbs(containerPath) {
		return nil, fmt.Errorf("container mount path must be absolute, got %q", containerPath)
	}

	var mounts []mount.Mount

	// Get host path (working directory)
	hostPath := cfg.WorkDir
	if hostPath == "" {
		var err error
		hostPath, err = os.Getwd()
		if err != nil {
			return nil, fmt.Errorf("failed to get working directory: %w", err)
		}
	}

	// Determine workspace mode (CLI flag overrides config default)
	project := cfg.Cfg.Project()
	modeStr := cfg.ModeOverride
	if modeStr == "" {
		modeStr = project.Workspace.DefaultMode
	}

	mode, err := config.ParseMode(modeStr)
	if err != nil {
		return nil, fmt.Errorf("invalid workspace mode: %w", err)
	}

	// Load .clawkerignore patterns (empty when no project is registered)
	var ignorePatterns []string
	ignoreFile, err := cfg.Cfg.GetProjectIgnoreFile()
	if err != nil {
		if !errors.Is(err, config.ErrNotInProject) {
			return nil, fmt.Errorf("failed to resolve ignore file: %w", err)
		}
	} else {
		ignorePatterns, err = docker.LoadIgnorePatterns(ignoreFile)
		if err != nil {
			return nil, fmt.Errorf("failed to load %s: %w", ignoreFile, err)
		}
	}

	// Create workspace strategy
	wsCfg := Config{
		HostPath:       hostPath,
		RemotePath:     containerPath,
		ProjectName:    cfg.ProjectName,
		AgentName:      cfg.AgentName,
		IgnorePatterns: ignorePatterns,
	}

	strategy, err := NewStrategy(mode, wsCfg, cfg.Log)
	if err != nil {
		return nil, fmt.Errorf("failed to create workspace strategy: %w", err)
	}

	cfg.Log.Debug().
		Str("mode", string(mode)).
		Str("strategy", strategy.Name()).
		Msg("using workspace strategy")

	// Prepare workspace resources (important for snapshot mode)
	if err := strategy.Prepare(ctx, client); err != nil {
		return nil, fmt.Errorf("failed to prepare workspace: %w", err)
	}

	// Track workspace volume name for cleanup on init failure (snapshot mode only)
	var wsVolumeName string
	if ss, ok := strategy.(*SnapshotStrategy); ok && ss.WasCreated() {
		wsVolumeName = ss.VolumeName()
	}

	// Get workspace mount
	wsMounts, err := strategy.GetMounts()
	if err != nil {
		return nil, fmt.Errorf("failed to get workspace mounts: %w", err)
	}
	mounts = append(mounts, wsMounts...)

	// Mount main repo's .git directory for worktree support
	if cfg.ProjectRootDir != "" {
		gitMount, err := buildWorktreeGitMount(cfg.ProjectRootDir)
		if err != nil {
			return nil, err
		}
		mounts = append(mounts, *gitMount)
		cfg.Log.Debug().
			Str("gitdir", gitMount.Source).
			Msg("mounting main repo .git for worktree")
	}

	// Ensure config volumes (returns creation state for init orchestration)
	configResult, err := EnsureConfigVolumes(ctx, client, cfg.ProjectName, cfg.AgentName)
	if err != nil {
		return nil, fmt.Errorf("failed to create config volumes: %w", err)
	}
	configMounts, err := GetConfigVolumeMounts(cfg.ProjectName, cfg.AgentName)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve config volume names: %w", err)
	}
	mounts = append(mounts, configMounts...)

	// Bind mount host ~/.claude/projects/ on top of the config volume so
	// auto-memory and session jsonls are shared across container runs.
	// Resolution failure is a hard error — clawker is not useful without
	// host Claude Code installed, so we'd rather fail loud than mask a
	// misconfigured $CLAUDE_CONFIG_DIR. Users who genuinely don't want the
	// bind can set agent.claude_code.mount_projects: false. Missing
	// projects/ subdir under an existing config dir is still a soft skip
	// (Claude Code creates it on first session).
	//
	// Container UID is host-derived (see consts.ContainerUID() /
	// consts.HostUID()) so the bind mount is writable by construction.
	if project.Agent.ClaudeCode.MountProjectsEnabled() {
		src, ok, resolveErr := containerfs.ResolveHostProjectsDir()
		if resolveErr != nil {
			return nil, fmt.Errorf(
				"mount_projects is enabled but the host claude config dir could not be resolved: %w. "+
					"Run claude on the host first, or set agent.claude_code.mount_projects: false to opt out",
				resolveErr)
		}
		if !ok {
			cfg.Log.Debug().Msg("skip ~/.claude/projects bind: host dir does not exist (Claude Code has not created it yet)")
		} else {
			projectsMount, err := GetClaudeProjectsMount(src)
			if err != nil {
				return nil, fmt.Errorf("build claude projects mount: %w", err)
			}
			mounts = append(mounts, projectsMount)
			cfg.Log.Debug().Str("src", src).Msg("mounted host ~/.claude/projects/")
		}
	}

	// Ensure and mount shared directory (if enabled)
	if project.Agent.SharedDirEnabled() {
		sharePath, err := cfg.Cfg.ShareSubdir()
		if err != nil {
			return nil, fmt.Errorf("failed to ensure share directory: %w", err)
		}
		mounts = append(mounts, GetShareVolumeMount(sharePath))
	}

	// Add docker socket mount if enabled
	if project.Security.DockerSocket {
		mounts = append(mounts, GetDockerSocketMount())
	}

	return &SetupMountsResult{
		Mounts:              mounts,
		ConfigVolumeResult:  configResult,
		WorkspaceVolumeName: wsVolumeName,
		ContainerPath:       containerPath,
	}, nil
}

// buildWorktreeGitMount creates a bind mount for the main repository's .git directory.
// This is needed for worktree support because worktrees use a .git file that references
// the main repo's .git directory. By mounting at the same absolute path, git commands
// work correctly inside the container.
func buildWorktreeGitMount(projectRootDir string) (*mount.Mount, error) {
	// Resolve symlinks to match git's behavior. Git records absolute paths with
	// symlinks resolved (e.g., on macOS /var -> /private/var). The mount target
	// must match the path git wrote in the worktree's .git file.
	resolvedRoot, err := filepath.EvalSymlinks(projectRootDir)
	if err != nil {
		return nil, fmt.Errorf("failed to resolve symlinks for project root %s: %w", projectRootDir, err)
	}

	// Validate .git exists and is a directory before mounting
	gitDir := filepath.Join(resolvedRoot, ".git")
	gitInfo, err := os.Stat(gitDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("main repository .git not found at %s (required for worktree support)", gitDir)
		}
		return nil, fmt.Errorf("cannot access .git directory at %s: %w", gitDir, err)
	}
	if !gitInfo.IsDir() {
		return nil, fmt.Errorf(".git at %s is not a directory (expected main repository, got worktree)", gitDir)
	}

	return &mount.Mount{
		Type:     mount.TypeBind,
		Source:   gitDir,
		Target:   gitDir, // Same absolute path preserves worktree references
		ReadOnly: false,
	}, nil
}
