package orch

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"ai-workflow-skill/internal/protocol"
	"ai-workflow-skill/internal/store"

	"github.com/spf13/cobra"
)

func newDispatchWorkspacePreparer(cmd *cobra.Command, opts dispatchOptions) store.DispatchWorkspacePreparer {
	ctx := cmd.Context()

	return func(task store.Task, attemptNo int) (store.DispatchWorkspace, func(), error) {
		effectiveOpts, useWorktree := resolveDispatchWorktreeOptions(task, opts)
		if !useWorktree {
			return store.DispatchWorkspace{}, func() {}, nil
		}
		return provisionDispatchWorkspace(ctx, effectiveOpts, task, attemptNo)
	}
}

func newAttemptReuseWorkspacePreparer(cmd *cobra.Command, task store.Task, attempt *store.TaskAttempt) store.DispatchWorkspacePreparer {
	if attempt == nil || attempt.WorktreePath == "" {
		return nil
	}

	workspaceRoot, ok := deriveWorkspaceRootFromAttempt(task.RunID, task.TaskID, attempt.WorktreePath)
	if !ok {
		return nil
	}

	baseRef := attempt.BaseRef
	if strings.TrimSpace(baseRef) == "" {
		baseRef = attempt.BaseCommit
	}

	opts := dispatchOptions{
		repoPath:       attempt.WorktreePath,
		workspaceRoot:  workspaceRoot,
		strictWorktree: true,
		baseRef:        baseRef,
	}

	return newDispatchWorkspacePreparer(cmd, opts)
}

func dispatchUsesWorktree(opts dispatchOptions) bool {
	return strings.TrimSpace(opts.workspaceRoot) != "" ||
		opts.strictWorktree
}

func resolveDispatchWorktreeOptions(task store.Task, opts dispatchOptions) (dispatchOptions, bool) {
	if dispatchUsesWorktree(opts) {
		return opts, true
	}
	if !taskLooksLikeCodeWork(task) {
		return opts, false
	}

	auto := opts
	auto.strictWorktree = true
	return auto, true
}

func taskLooksLikeCodeWork(task store.Task) bool {
	if acceptanceJSONLooksCodeLike(task.AcceptanceJSON) {
		return true
	}
	return roleLooksCodeLike(task.DefaultTo)
}

func acceptanceJSONLooksCodeLike(raw json.RawMessage) bool {
	if len(raw) == 0 {
		return false
	}

	var value any
	if err := json.Unmarshal(raw, &value); err != nil {
		return false
	}
	return acceptanceValueLooksCodeLike(value)
}

func acceptanceValueLooksCodeLike(value any) bool {
	switch typed := value.(type) {
	case map[string]any:
		for key, raw := range typed {
			lowerKey := strings.ToLower(strings.TrimSpace(key))
			switch lowerKey {
			case "code", "code_task", "writes_code", "worktree":
				if boolValue, ok := raw.(bool); ok && boolValue {
					return true
				}
			case "kind", "task_type", "mode", "type":
				if stringValue, ok := raw.(string); ok && isCodeLikeMarker(stringValue) {
					return true
				}
			}
			if acceptanceValueLooksCodeLike(raw) {
				return true
			}
		}
	case []any:
		for _, item := range typed {
			if acceptanceValueLooksCodeLike(item) {
				return true
			}
		}
	case string:
		return isCodeLikeMarker(typed)
	}
	return false
}

func roleLooksCodeLike(role string) bool {
	role = strings.ToLower(strings.TrimSpace(role))
	if role == "" {
		return false
	}

	for _, token := range splitIdentifierTokens(role) {
		switch token {
		case "backend", "frontend", "front", "admin", "ui", "fullstack", "foundation", "db", "database", "mobile", "ios", "android", "web", "platform", "infra", "api":
			return true
		}
	}
	return false
}

func isCodeLikeMarker(value string) bool {
	value = strings.ToLower(strings.TrimSpace(value))
	switch value {
	case "code", "code_task", "code-task", "code-change", "code_change", "implementation", "patch", "diff", "repo":
		return true
	default:
		return false
	}
}

func splitIdentifierTokens(value string) []string {
	return strings.FieldsFunc(value, func(r rune) bool {
		return !((r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'))
	})
}

func provisionDispatchWorkspace(ctx context.Context, opts dispatchOptions, task store.Task, attemptNo int) (store.DispatchWorkspace, func(), error) {
	repoRoot, err := resolveRepoRoot(ctx, opts.repoPath)
	if err != nil {
		return store.DispatchWorkspace{}, nil, err
	}

	workspaceRoot := resolveWorkspaceRoot(repoRoot, opts.workspaceRoot)
	if err := ensureWorkspaceRootIgnored(repoRoot, workspaceRoot); err != nil {
		return store.DispatchWorkspace{}, nil, err
	}

	baseRef, baseCommit, err := resolveDispatchBase(ctx, repoRoot, workspaceRoot, opts.baseRef, opts.strictWorktree)
	if err != nil {
		return store.DispatchWorkspace{}, nil, err
	}

	branchName := buildAttemptBranchName(task.RunID, task.TaskID, attemptNo)
	worktreePath := buildAttemptWorktreePath(workspaceRoot, task.RunID, task.TaskID, attemptNo)

	if err := os.MkdirAll(filepath.Dir(worktreePath), 0o755); err != nil {
		return store.DispatchWorkspace{}, nil, fmt.Errorf("create worktree parent dir: %w", err)
	}
	if _, err := os.Stat(worktreePath); err == nil {
		return store.DispatchWorkspace{}, nil, fmt.Errorf("%w: worktree path already exists: %s", store.ErrInvalidState, worktreePath)
	} else if err != nil && !os.IsNotExist(err) {
		return store.DispatchWorkspace{}, nil, fmt.Errorf("stat worktree path: %w", err)
	}

	if _, _, err := runGit(ctx, repoRoot, "worktree", "add", "-b", branchName, worktreePath, baseCommit); err != nil {
		return store.DispatchWorkspace{}, nil, err
	}

	cleanup := func() {
		_, _, _ = runGit(context.Background(), repoRoot, "worktree", "remove", "--force", worktreePath)
		_, _, _ = runGit(context.Background(), repoRoot, "branch", "-D", branchName)
		_ = os.RemoveAll(worktreePath)
	}

	return store.DispatchWorkspace{
		BaseRef:         baseRef,
		BaseCommit:      baseCommit,
		BranchName:      branchName,
		WorktreePath:    worktreePath,
		WorkspaceStatus: "created",
	}, cleanup, nil
}

func resolveRepoRoot(ctx context.Context, repoPath string) (string, error) {
	startPath := strings.TrimSpace(repoPath)
	if startPath == "" {
		var err error
		startPath, err = os.Getwd()
		if err != nil {
			return "", fmt.Errorf("get current working directory: %w", err)
		}
	}

	absPath, err := filepath.Abs(startPath)
	if err != nil {
		return "", fmt.Errorf("resolve repo path: %w", err)
	}

	if _, _, err := runGit(ctx, absPath, "rev-parse", "--show-toplevel"); err != nil {
		return "", protocol.InvalidInput("repo-path must point to a Git worktree", err)
	}

	commonDir, err := resolveCommonGitDir(ctx, absPath)
	if err != nil {
		return "", protocol.InvalidInput("repo-path must point to a Git worktree", err)
	}
	return filepath.Dir(commonDir), nil
}

func resolveDispatchBase(ctx context.Context, repoRoot, workspaceRoot, requestedBaseRef string, strict bool) (string, string, error) {
	baseRef := strings.TrimSpace(requestedBaseRef)
	if baseRef != "" {
		baseCommit, err := resolveCommit(ctx, repoRoot, baseRef)
		if err != nil {
			return "", "", protocol.InvalidInput("base-ref must resolve to a commit", err)
		}
		return baseRef, baseCommit, nil
	}

	if strict {
		dirty, err := repoHasUncommittedChanges(ctx, repoRoot, workspaceRoot)
		if err != nil {
			return "", "", err
		}
		if dirty {
			return "", "", fmt.Errorf("%w: repository has uncommitted changes; specify --base-ref or clean the repo", store.ErrInvalidState)
		}
	}

	baseCommit, err := resolveCommit(ctx, repoRoot, "HEAD")
	if err != nil {
		return "", "", protocol.InvalidInput("failed to resolve HEAD commit", err)
	}
	return "HEAD", baseCommit, nil
}

func resolveCommit(ctx context.Context, repoRoot, ref string) (string, error) {
	stdout, _, err := runGit(ctx, repoRoot, "rev-parse", "--verify", ref+"^{commit}")
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(stdout), nil
}

func repoHasUncommittedChanges(ctx context.Context, repoRoot, workspaceRoot string) (bool, error) {
	stdout, _, err := runGit(ctx, repoRoot, "status", "--porcelain")
	if err != nil {
		return false, fmt.Errorf("check repository status: %w", err)
	}

	for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") {
		line = strings.TrimSpace(line)
		if line == "" {
			continue
		}
		if len(line) >= 3 {
			path := strings.TrimSpace(line[3:])
			if shouldIgnoreStatusPath(repoRoot, workspaceRoot, path) {
				continue
			}
		}
		return true, nil
	}

	return false, nil
}

func resolveWorkspaceRoot(repoRoot, configuredRoot string) string {
	root := strings.TrimSpace(configuredRoot)
	if root == "" {
		return filepath.Join(repoRoot, ".orch", "worktrees")
	}
	if filepath.IsAbs(root) {
		return root
	}
	return filepath.Join(repoRoot, root)
}

func ensureWorkspaceRootIgnored(repoRoot, workspaceRoot string) error {
	relative, err := filepath.Rel(repoRoot, workspaceRoot)
	if err != nil {
		return fmt.Errorf("resolve workspace root exclude path: %w", err)
	}
	if relative == "." || strings.HasPrefix(relative, "..") {
		return nil
	}

	pattern := filepath.ToSlash(relative)
	if !strings.HasSuffix(pattern, "/") {
		pattern += "/"
	}

	excludePath := filepath.Join(repoRoot, ".git", "info", "exclude")
	if err := os.MkdirAll(filepath.Dir(excludePath), 0o755); err != nil {
		return fmt.Errorf("create git info dir: %w", err)
	}

	content, err := os.ReadFile(excludePath)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("read git exclude file: %w", err)
	}
	if strings.Contains(string(content), pattern) {
		return nil
	}

	appendContent := pattern + "\n"
	if len(content) > 0 && !strings.HasSuffix(string(content), "\n") {
		appendContent = "\n" + appendContent
	}
	file, err := os.OpenFile(excludePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
	if err != nil {
		return fmt.Errorf("open git exclude file: %w", err)
	}
	defer file.Close()

	if _, err := file.WriteString(appendContent); err != nil {
		return fmt.Errorf("append git exclude file: %w", err)
	}
	return nil
}

func shouldIgnoreStatusPath(repoRoot, workspaceRoot, statusPath string) bool {
	relative, err := filepath.Rel(repoRoot, workspaceRoot)
	if err != nil || relative == "." || strings.HasPrefix(relative, "..") {
		return false
	}

	relative = filepath.ToSlash(relative)
	statusPath = filepath.ToSlash(strings.Trim(statusPath, `"`))
	return statusPath == relative || strings.HasPrefix(statusPath, relative+"/")
}

func buildAttemptBranchName(runID, taskID string, attemptNo int) string {
	return fmt.Sprintf(
		"orch/%s/%s/attempt-%d",
		sanitizeGitSegment(runID),
		sanitizeGitSegment(taskID),
		attemptNo,
	)
}

func buildAttemptWorktreePath(workspaceRoot, runID, taskID string, attemptNo int) string {
	return filepath.Join(
		workspaceRoot,
		sanitizePathSegment(runID),
		sanitizePathSegment(taskID),
		fmt.Sprintf("attempt-%d", attemptNo),
	)
}

func deriveWorkspaceRootFromAttempt(runID, taskID, worktreePath string) (string, bool) {
	suffix := filepath.Join(
		sanitizePathSegment(runID),
		sanitizePathSegment(taskID),
		filepath.Base(worktreePath),
	)
	parent := filepath.Dir(worktreePath)
	if filepath.Base(parent) != sanitizePathSegment(taskID) {
		return "", false
	}
	runDir := filepath.Dir(parent)
	if filepath.Base(runDir) != sanitizePathSegment(runID) {
		return "", false
	}
	root := filepath.Dir(runDir)
	if filepath.Clean(filepath.Join(root, suffix)) != filepath.Clean(worktreePath) {
		return "", false
	}
	return root, true
}

func sanitizeGitSegment(value string) string {
	return sanitizeSegment(value)
}

func sanitizePathSegment(value string) string {
	return sanitizeSegment(value)
}

func sanitizeSegment(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return "item"
	}

	var b strings.Builder
	lastDash := false
	for _, r := range value {
		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
			b.WriteRune(r)
			lastDash = false
			continue
		}
		if r == '-' || r == '_' || r == '.' {
			if !lastDash {
				b.WriteByte('-')
				lastDash = true
			}
			continue
		}
		if !lastDash {
			b.WriteByte('-')
			lastDash = true
		}
	}

	result := strings.Trim(b.String(), "-.")
	if result == "" {
		return "item"
	}
	if strings.HasSuffix(result, ".lock") {
		result = strings.TrimSuffix(result, ".lock") + "-lock"
	}
	return result
}

func runGit(ctx context.Context, repoRoot string, args ...string) (string, string, error) {
	cmdArgs := append([]string{"-C", repoRoot}, args...)
	cmd := exec.CommandContext(ctx, "git", cmdArgs...)
	output, err := cmd.CombinedOutput()
	if err == nil {
		return string(output), "", nil
	}

	message := strings.TrimSpace(string(output))
	if message == "" {
		message = err.Error()
	}
	return "", message, fmt.Errorf("git %s: %s", strings.Join(args, " "), message)
}

func cleanupAttemptWorktree(ctx context.Context, attempt store.TaskAttempt, force bool) error {
	if strings.TrimSpace(attempt.WorktreePath) == "" {
		return nil
	}

	if _, err := os.Stat(attempt.WorktreePath); err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return fmt.Errorf("stat worktree path: %w", err)
	}

	repoRoot, err := resolveRepoRootFromExistingWorktree(ctx, attempt.WorktreePath)
	if err != nil {
		if force {
			return os.RemoveAll(attempt.WorktreePath)
		}
		return err
	}

	args := []string{"worktree", "remove"}
	if force {
		args = append(args, "--force")
	}
	args = append(args, attempt.WorktreePath)
	if _, _, err := runGit(ctx, repoRoot, args...); err != nil {
		if force {
			return os.RemoveAll(attempt.WorktreePath)
		}
		return err
	}

	return nil
}

func resolveRepoRootFromExistingWorktree(ctx context.Context, worktreePath string) (string, error) {
	commonDir, err := resolveCommonGitDir(ctx, worktreePath)
	if err != nil {
		return "", err
	}
	return filepath.Dir(commonDir), nil
}

func resolveCommonGitDir(ctx context.Context, repoPath string) (string, error) {
	stdout, _, err := runGit(ctx, repoPath, "rev-parse", "--path-format=absolute", "--git-common-dir")
	if err != nil {
		return "", err
	}

	commonDir := strings.TrimSpace(stdout)
	if !filepath.IsAbs(commonDir) {
		commonDir = filepath.Join(repoPath, commonDir)
	}
	return filepath.Clean(commonDir), nil
}
