// Package parallel implements deterministic parallel-worktree control ops:
// path-disjoint checks, advisory leases, and git worktree create/integrate/cleanup.
package parallel

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
)

var winAbsRE = regexp.MustCompile(`(?i)^[A-Za-z]:[/\\]`)

// SlicePaths is one path-disjoint eligibility unit.
type SlicePaths struct {
	ID    string   `json:"id"`
	Paths []string `json:"paths"`
}

// NormalizePath mirrors scripts/check-path-disjoint.py: slash-normalize, reject
// empty/absolute/.. paths, and drop empty/"." segments.
func NormalizePath(raw string) (string, error) {
	path := strings.TrimSpace(strings.ReplaceAll(raw, `\`, "/"))
	if path == "" {
		return "", fmt.Errorf("empty path is not allowed")
	}
	if strings.HasPrefix(path, "/") || winAbsRE.MatchString(path) {
		return "", fmt.Errorf("path must be project-relative, not absolute: %q", raw)
	}
	parts := make([]string, 0, strings.Count(path, "/")+1)
	for part := range strings.SplitSeq(path, "/") {
		// Edge whitespace on a segment would survive segment filtering and
		// make normalization non-idempotent ("0\n." collapsing to "0\n"),
		// so reject it outright like the other malformed shapes.
		if trimmed := strings.TrimSpace(part); trimmed != part {
			return "", fmt.Errorf("path segment must not have leading or trailing whitespace: %q", raw)
		}
		if part == "" || part == "." {
			continue
		}
		if part == ".." {
			return "", fmt.Errorf("path must not contain '..': %q", raw)
		}
		parts = append(parts, part)
	}
	if len(parts) == 0 {
		return "", fmt.Errorf("empty path is not allowed")
	}
	return strings.Join(parts, "/"), nil
}

func validateSlicePaths(paths []string, label, root string) ([]string, error) {
	if paths == nil {
		return nil, fmt.Errorf("%s: paths must be a list", label)
	}
	normalized := make([]string, 0, len(paths))
	seen := make(map[string]struct{}, len(paths))
	for _, raw := range paths {
		path, err := NormalizePath(raw)
		if err != nil {
			return nil, fmt.Errorf("%s: %w", label, err)
		}
		// Parallel eligibility must reject workspace control metadata so slices
		// cannot claim control-tree / SSOT files (docs: ".devrites/**").
		if path == ".devrites" || strings.HasPrefix(path, ".devrites/") {
			return nil, fmt.Errorf("%s: path must not include .devrites: %q", label, path)
		}
		// Same for git internals: a transfer commit can never carry .git/**
		// anyway, and inside a worktree .git is a file, so claiming it is
		// pure footgun.
		if path == ".git" || strings.HasPrefix(path, ".git/") {
			return nil, fmt.Errorf("%s: path must not include .git: %q", label, path)
		}
		if _, ok := seen[path]; ok {
			return nil, fmt.Errorf("%s: duplicate path %q", label, path)
		}
		seen[path] = struct{}{}
		normalized = append(normalized, path)
		if root != "" {
			// Check every component, not just the leaf: a symlinked directory
			// in the chain would let a worktree write outside itself.
			cur := root
			for _, seg := range strings.Split(path, "/") {
				cur = filepath.Join(cur, seg)
				info, err := os.Lstat(cur)
				if err != nil {
					break // missing components are fine; git creates them
				}
				if info.Mode()&os.ModeSymlink != 0 {
					return nil, fmt.Errorf("%s: symlink path is not allowed: %q", label, path)
				}
			}
		}
	}
	return normalized, nil
}

// CheckPathDisjoint returns slice ids when every pair of path sets is disjoint.
func CheckPathDisjoint(slices []SlicePaths, root string) ([]string, error) {
	if len(slices) < 2 {
		return nil, fmt.Errorf("need at least two slices to check path-disjoint eligibility")
	}
	owners := make(map[string][]string)
	ids := make([]string, 0, len(slices))
	for index, item := range slices {
		label := fmt.Sprintf("slice %d", index)
		if item.ID != "" {
			label = fmt.Sprintf("slice %q", item.ID)
			ids = append(ids, item.ID)
		} else {
			ids = append(ids, fmt.Sprintf("%d", index))
		}
		paths, err := validateSlicePaths(item.Paths, label, root)
		if err != nil {
			return nil, err
		}
		for _, path := range paths {
			owners[path] = append(owners[path], label)
		}
	}
	var overlaps []string
	for path, labels := range owners {
		if len(labels) > 1 {
			overlaps = append(overlaps, fmt.Sprintf("%q shared by %s", path, strings.Join(labels, ", ")))
		}
	}
	if len(overlaps) > 0 {
		sort.Strings(overlaps)
		return nil, fmt.Errorf("path sets overlap: %s", strings.Join(overlaps, "; "))
	}
	return ids, nil
}

// ParseSlicesJSON accepts {"slices":[...]} or a top-level slices array.
func ParseSlicesJSON(data []byte) ([]SlicePaths, error) {
	var asObject struct {
		Slices []SlicePaths `json:"slices"`
	}
	if err := json.Unmarshal(data, &asObject); err == nil && asObject.Slices != nil {
		return asObject.Slices, nil
	}
	var asList []SlicePaths
	if err := json.Unmarshal(data, &asList); err == nil {
		return asList, nil
	}
	return nil, fmt.Errorf(`input must be {"slices": [...]} or a top-level slices array`)
}
