package fs

import (
	"os"
	"path/filepath"
	"sort"
	"strings"
)

// ResolveCanonical translates a URL path to its canonical on-disk form
// under fsRoot, treating segment matches as case-insensitive. When a
// directory contains multiple case variants of the same name, the
// all-lowercase variant wins; lowercase is the project-wide canonical
// case for folders.
//
// The walk stops at the first segment that does not exist on disk:
// the remainder of the URL path is appended verbatim. This lets virtual
// prefixes (.profile, .archive, .tokens) and 404 paths flow through
// unchanged.
//
// Returns the resolved absolute path and the canonical URL form
// (leading "/", "/"-separated, no trailing "/"). ok is false only when
// the input would escape fsRoot.
//
// The fast path is one Lstat for the lowercase form of the segment;
// readdir+scan only runs when neither lowercase nor exact-case exists.
func ResolveCanonical(fsRoot, urlPath string) (absPath, canonicalURL string, ok bool) {
	rel := strings.TrimPrefix(urlPath, "/")
	rel = strings.TrimSuffix(rel, "/")
	if rel == "" {
		return fsRoot, "/", true
	}

	cur := fsRoot
	canonParts := make([]string, 0, 8)
	segments := strings.Split(rel, "/")
	stopped := false
	for _, seg := range segments {
		if seg == "" || seg == "." || seg == ".." {
			// Defensive — Clean strips these before this is called in
			// practice, but if a caller passes a raw URL preserve it
			// verbatim and let downstream safety checks reject it.
			canonParts = append(canonParts, seg)
			stopped = true
			continue
		}
		if stopped {
			canonParts = append(canonParts, seg)
			continue
		}
		resolved, found := resolveSegment(cur, seg)
		canonParts = append(canonParts, resolved)
		if !found {
			// Tail passes through verbatim — virtual prefixes
			// (.archive, .tokens, .profile) and 404 paths land here.
			stopped = true
			continue
		}
		cur = filepath.Join(cur, resolved)
	}

	canonicalURL = "/" + strings.Join(canonParts, "/")
	abs := filepath.Join(fsRoot, filepath.FromSlash(strings.Join(canonParts, "/")))
	abs = filepath.Clean(abs)
	if abs != fsRoot && !strings.HasPrefix(abs, fsRoot+string(filepath.Separator)) {
		return "", "", false
	}
	return abs, canonicalURL, true
}

// resolveSegment finds the on-disk name for a URL segment under parent.
// Order: (1) lowercase variant (canonical), (2) exact-case as given,
// (3) readdir scan picking the lowercase tiebreaker. Returns
// (segment-as-given, false) if nothing matches.
func resolveSegment(parent, segment string) (string, bool) {
	lower := strings.ToLower(segment)

	if _, err := os.Lstat(filepath.Join(parent, lower)); err == nil {
		return lower, true
	}
	if segment != lower {
		if _, err := os.Lstat(filepath.Join(parent, segment)); err == nil {
			return segment, true
		}
	}

	entries, err := os.ReadDir(parent)
	if err != nil {
		return segment, false
	}
	var matches []string
	for _, e := range entries {
		if strings.EqualFold(e.Name(), segment) {
			matches = append(matches, e.Name())
		}
	}
	switch len(matches) {
	case 0:
		return segment, false
	case 1:
		return matches[0], true
	default:
		// Multiple case variants exist on disk. Prefer the all-lowercase
		// variant; otherwise pick the first in sort order so the choice
		// is deterministic across requests.
		for _, m := range matches {
			if m == strings.ToLower(m) {
				return m, true
			}
		}
		sort.Strings(matches)
		return matches[0], true
	}
}
