package handler

import (
	"errors"
	"path/filepath"
	"strings"
)

// URL ↔ filesystem path math used by several handler files. Pure
// string manipulation — no I/O, no policy decisions — so it lives
// in its own file rather than being attached to any one feature.

// resolvePath translates a URL `path=` query (relative to fsRoot, with
// '/' separator and leading '/') into an absolute filesystem path. It
// rejects path traversal and the reserved .zddc.d/ bookkeeping sidecar so
// the token store et al. cannot be addressed through admin APIs (admins
// manage tokens via /.tokens, not the generic file path). All other
// dot-/underscore-prefixed paths are ordinary content. Returns the cleaned
// absolute path or an error suitable for a 404.
func resolvePath(fsRoot, urlPath string) (string, error) {
	urlPath = strings.TrimSpace(urlPath)
	if urlPath == "" {
		urlPath = "/"
	}
	if !strings.HasPrefix(urlPath, "/") {
		return "", errors.New("path must be absolute (start with /)")
	}
	cleanURL := filepath.ToSlash(filepath.Clean(urlPath))

	// Reject the one reserved namespace (.zddc.d/) so admin APIs cannot
	// address the token store / history / caches through a generic path.
	for _, seg := range strings.Split(strings.Trim(cleanURL, "/"), "/") {
		if seg == ReservedSidecar {
			return "", errors.New("reserved path segment")
		}
	}

	rel := strings.TrimPrefix(cleanURL, "/")
	abs := filepath.Join(fsRoot, filepath.FromSlash(rel))
	abs = filepath.Clean(abs)

	// Path containment.
	if abs != fsRoot && !strings.HasPrefix(abs, fsRoot+string(filepath.Separator)) {
		return "", errors.New("path escapes root")
	}
	return abs, nil
}

// urlPathOf produces the URL form of an absolute filesystem path under
// fsRoot. Returns "/" for fsRoot itself, otherwise "/<rel>".
func urlPathOf(fsRoot, abs string) string {
	if abs == fsRoot {
		return "/"
	}
	rel, err := filepath.Rel(fsRoot, abs)
	if err != nil {
		return "/"
	}
	return "/" + filepath.ToSlash(rel)
}

// chainDirs reproduces EffectivePolicy's directory walk so callers can
// label each policy-chain level with the directory it came from. Used
// by the virtual-.zddc body to annotate which ancestor contributed
// which rule.
func chainDirs(fsRoot, dirPath string) []string {
	fsRoot = filepath.Clean(fsRoot)
	dirPath = filepath.Clean(dirPath)
	dirs := []string{fsRoot}
	if dirPath == fsRoot {
		return dirs
	}
	rel, err := filepath.Rel(fsRoot, dirPath)
	if err != nil || rel == "." {
		return dirs
	}
	current := fsRoot
	for _, part := range strings.Split(rel, string(filepath.Separator)) {
		current = filepath.Join(current, part)
		dirs = append(dirs, current)
	}
	return dirs
}
