package zddc

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

// DefaultToolAt returns the cascade-resolved default tool name for
// the directory at dirPath. Empty when no cascade level (on-disk or
// virtual via Paths) has declared a DefaultTool.
//
// Lookup walks chain.Levels from leaf toward root, returning the
// first non-empty value. This implements the "parent applies to
// descendants unless overridden" cascade rule: a working/ folder's
// default_tool=browse propagates to working/alice/notes/ even when
// no .zddc declares browse at the deeper levels.
//
// Used by the URL dispatcher to route no-slash directory URLs.
// Replaces apps.DefaultAppAt once consumers are migrated (Phase 3b).
func DefaultToolAt(fsRoot, dirPath string) string {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return ""
	}
	for i := len(chain.Levels) - 1; i >= 0; i-- {
		if dt := chain.Levels[i].DefaultTool; dt != "" {
			return dt
		}
	}
	return chain.Embedded.DefaultTool
}

// DisplayAt returns the cascade-resolved `display:` map for a directory —
// the human-friendly labels a client renders in place of on-disk child
// names (e.g. mdl → "Master Deliverables List"). Unlike a single tool
// name, display: is a MAP that merges across the cascade: the embedded
// baseline is the floor, then each on-disk level overrides per key
// (shallow→deep, so the deepest .zddc wins). Keys are lower-cased so the
// caller's lookup is case-insensitive on the on-disk basename. Returns nil
// when nothing declares a label. Mirrors how walker.go merges Display, but
// resolved on demand for one path (the listing reads it per directory).
func DisplayAt(fsRoot, dirPath string) map[string]string {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return nil
	}
	merged := map[string]string{}
	add := func(m map[string]string) {
		for k, v := range m {
			v = strings.TrimSpace(v)
			if v == "" {
				continue
			}
			merged[strings.ToLower(strings.TrimSpace(k))] = v
		}
	}
	add(chain.Embedded.Display) // embedded baseline (the defaults tree)
	for i := 0; i < len(chain.Levels); i++ {
		add(chain.Levels[i].Display) // on-disk overrides, shallow→deep
	}
	if len(merged) == 0 {
		return nil
	}
	return merged
}

// DirToolAt returns the cascade-resolved tool name served at the
// directory's TRAILING-SLASH URL form. Walks chain.Levels leaf→root
// (then the embedded defaults), returning the first non-empty
// DirTool. Floors at "browse": an undeclared directory serves the
// file-tree navigator, which is the site-wide convention. So callers
// never need to special-case the empty result.
//
// This is the slash half of the slash/no-slash routing convention;
// DefaultToolAt is the no-slash half.
func DirToolAt(fsRoot, dirPath string) string {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return "browse"
	}
	for i := len(chain.Levels) - 1; i >= 0; i-- {
		if dt := chain.Levels[i].DirTool; dt != "" {
			return dt
		}
	}
	if dt := chain.Embedded.DirTool; dt != "" {
		return dt
	}
	return "browse"
}

// ViewAt resolves the view for a URL shape ("dir", "dir_slash", "file") at
// dirPath. Walks the cascade leaf→root (then the embedded defaults): the first
// level whose Views declares the shape wins; default_tool / dir_tool are
// honored as sugar for the "dir" / "dir_slash" shapes. Returns
// (ViewSpec{}, false) when nothing declares the shape (the caller decides any
// floor, e.g. dir_slash → browse). Mirrors DefaultToolAt's first-match-wins
// cascade so default_tool/dir_tool semantics are unchanged.
func ViewAt(fsRoot, dirPath, shape string) (ViewSpec, bool) {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return ViewSpec{}, false
	}
	atLevel := func(lvl ZddcFile) (ViewSpec, bool) {
		if lvl.Views != nil {
			if v, ok := lvl.Views[shape]; ok && v.Tool != "" {
				return v, true
			}
		}
		switch shape {
		case "dir":
			if lvl.DefaultTool != "" {
				return ViewSpec{Tool: lvl.DefaultTool}, true
			}
		case "dir_slash":
			if lvl.DirTool != "" {
				return ViewSpec{Tool: lvl.DirTool}, true
			}
		}
		return ViewSpec{}, false
	}
	for i := len(chain.Levels) - 1; i >= 0; i-- {
		if v, ok := atLevel(chain.Levels[i]); ok {
			return v, true
		}
	}
	return atLevel(chain.Embedded)
}

// AutoOwnAt reports whether mkdir at THIS specific directory should
// write an auto-owned .zddc. Leaf-only lookup — auto-own does NOT
// propagate to descendants (creating working/alice/notes/sub/ does
// not auto-own sub/; only the explicitly-declared per-user home is
// auto-owned).
func AutoOwnAt(fsRoot, dirPath string) bool {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	leaf := leafLevel(chain)
	if leaf.AutoOwn != nil {
		return *leaf.AutoOwn
	}
	if v := chain.Embedded.AutoOwn; v != nil {
		return *v
	}
	return false
}

// AutoOwnRolesAt returns the role names that should be granted rwcda
// in the auto-own .zddc at this dir (alongside the creator's email).
// Leaf-only, same semantic as AutoOwnAt / AutoOwnFencedAt. Empty/nil
// when the cascade declares no role grants — the legacy creator-only
// behavior. Caller passes the result to WriteAutoOwnZddc / Fenced.
func AutoOwnRolesAt(fsRoot, dirPath string) []string {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return nil
	}
	leaf := leafLevel(chain)
	if leaf.AutoOwnRoles != nil {
		return leaf.AutoOwnRoles
	}
	return chain.Embedded.AutoOwnRoles
}

// AutoOwnFencedAt reports whether the auto-own .zddc at this dir
// should be written with `inherit: false` (private to creator).
// Leaf-only, same semantic as AutoOwnAt.
func AutoOwnFencedAt(fsRoot, dirPath string) bool {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	leaf := leafLevel(chain)
	if leaf.AutoOwnFenced != nil {
		return *leaf.AutoOwnFenced
	}
	if v := chain.Embedded.AutoOwnFenced; v != nil {
		return *v
	}
	return false
}

// DropTargetAt reports whether THIS specific directory accepts
// drag-drop uploads from a client (e.g. browse's drop-zone overlay).
// Leaf-only — the property describes a specific path, not a subtree.
func DropTargetAt(fsRoot, dirPath string) bool {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	leaf := leafLevel(chain)
	if leaf.DropTarget != nil {
		return *leaf.DropTarget
	}
	if v := chain.Embedded.DropTarget; v != nil {
		return *v
	}
	return false
}

// PartySourceAt returns the registry name that gates party-folder
// creation under THIS peer directory (e.g. "ssr"), or "" if this peer
// does no party gating. Leaf-only — the property describes the peer dir
// (working/, archive/, …), not its party-folder children.
func PartySourceAt(fsRoot, dirPath string) string {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return ""
	}
	leaf := leafLevel(chain)
	if leaf.PartySource != "" {
		return leaf.PartySource
	}
	return chain.Embedded.PartySource
}

// PartyRegistered reports whether party is registered in the named
// registry under projectAbs (e.g. source="ssr" → the registry is
// <projectAbs>/ssr/). A party exists iff its registry entry exists,
// checked as either a flat row file <registry>/<party>.yaml or a folder
// <registry>/<party>/ (so the key works for flat-file and folder-style
// registers). An empty source means "no gating" and returns true.
func PartyRegistered(projectAbs, source, party string) bool {
	if source == "" {
		return true
	}
	if party == "" {
		return false
	}
	reg := filepath.Join(projectAbs, source)
	if fi, err := os.Stat(filepath.Join(reg, party+".yaml")); err == nil && fi.Mode().IsRegular() {
		return true
	}
	if fi, err := os.Stat(filepath.Join(reg, party)); err == nil && fi.IsDir() {
		return true
	}
	return false
}

// VirtualAt reports whether THIS specific directory is declared as
// purely virtual (never materialise on disk). Leaf-only: the virtual
// property describes a particular path, not a subtree. A child of a
// virtual directory is not automatically virtual itself.
func VirtualAt(fsRoot, dirPath string) bool {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	leaf := leafLevel(chain)
	if leaf.Virtual != nil {
		return *leaf.Virtual
	}
	if v := chain.Embedded.Virtual; v != nil {
		return *v
	}
	return false
}

// HistoryAt reports whether edit-history versioning is enabled for
// writes in dirPath. Subtree-inheriting (see
// PolicyChain.EffectiveHistory) — a `history: true` at an ancestor
// applies here even through inherit:false fences.
func HistoryAt(fsRoot, dirPath string) bool {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	return chain.EffectiveHistory()
}

// HistoryGlobsAt returns the effective history file-type globs at dirPath
// (default ["*.md"]). See PolicyChain.EffectiveHistoryGlobs.
func HistoryGlobsAt(fsRoot, dirPath string) []string {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return []string{"*.md"}
	}
	return chain.EffectiveHistoryGlobs()
}

// IsDeclaredPath reports whether dirPath is mentioned in the
// cascade — either by an on-disk .zddc at that level OR by any
// ancestor's paths: tree (including the embedded defaults).
//
// A declared path is one the cascade has *something to say about*
// even if the directory doesn't exist on disk. Used by listing
// fallbacks to decide whether a missing directory should return an
// empty listing (treat as virtual) vs 404 (truly unknown).
func IsDeclaredPath(fsRoot, dirPath string) bool {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	if len(chain.Levels) == 0 {
		return false
	}
	leaf := leafLevel(chain)
	// A non-empty merged level at the leaf means at least one
	// contribution reached here — either an on-disk file, or an
	// ancestor's paths: glob matched.
	return !isZeroZddcFile(leaf)
}

// AvailableToolsAt returns the cascade-unioned list of tool names
// the server may auto-serve at this directory. Built by walking
// chain.Levels from leaf to root, then the embedded defaults, and
// concat-deduping each level's AvailableTools.
//
// Empty result means no auto-routed tools are allowed at this path.
// The dispatcher gates auto-route fallbacks against this list;
// explicit static <dir>/tool.html files bypass the gate.
func AvailableToolsAt(fsRoot, dirPath string) []string {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return nil
	}
	seen := make(map[string]struct{})
	var out []string
	add := func(list []string) {
		for _, t := range list {
			if _, ok := seen[t]; ok {
				continue
			}
			seen[t] = struct{}{}
			out = append(out, t)
		}
	}
	for i := len(chain.Levels) - 1; i >= 0; i-- {
		add(chain.Levels[i].AvailableTools)
	}
	add(chain.Embedded.AvailableTools)
	return out
}

// IsToolAvailableAt reports whether tool is in the cascade's
// available-tools union at dirPath. The dispatcher gates apps-
// subsystem auto-route on this; explicit on-disk <tool>.html files
// always serve regardless.
func IsToolAvailableAt(fsRoot, dirPath, tool string) bool {
	for _, t := range AvailableToolsAt(fsRoot, dirPath) {
		if t == tool {
			return true
		}
	}
	return false
}

// ChildrenDeclaredAt returns the set of child directory names that
// the cascade declares should exist under dirPath. Includes
// wildcard "*" specs (caller decides how to expose those) and
// literal names. Used by fs.ListDirectory to inject virtual
// canonical-folder entries at a project root.
//
// Returns the literal names; "*" wildcards are NOT included
// (callers can't synthesise a meaningful name for a wildcard).
func ChildrenDeclaredAt(fsRoot, dirPath string) []string {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return nil
	}
	leaf := leafLevel(chain)
	if len(leaf.Paths) == 0 {
		return nil
	}
	var out []string
	for k := range leaf.Paths {
		if k == "*" {
			continue
		}
		out = append(out, k)
	}
	return out
}

// CanonicalFolderAt returns the canonical-folder name for THIS specific
// directory — one of the top-level peers (archive, incoming, working,
// staging, reviewing, mdl, rsk, ssr) or the WORM record slots
// (received, issued) — or "" if the path is not at a canonical slot.
//
// Detection is structural against the flat-peer layout declared in
// internal/zddc/defaults/:
//
//   - second-level <project>/<peer> for any top-level peer.
//   - third-level <project>/<peer>/<party> reports its peer (slot) for
//     the workspace/register peers (not archive), so the SPA can scope
//     party-level context-menu actions.
//   - fourth-level <project>/archive/<party>/{received,issued} are the
//     WORM record slots.
//
// Operators don't rename these slots (the cascade keys them by literal
// name). Used by the browse SPA to scope-gate context-menu actions
// (Accept, Stage/Unstage, Create Transmittal) without re-implementing
// the cascade client-side. Surfaced via the X-ZDDC-Canonical-Folder
// header.
func CanonicalFolderAt(fsRoot, dirPath string) string {
	segs := resolvePathSegments(fsRoot, dirPath)
	// <project>/<peer> — all top-level peers are physical canonical slots.
	if len(segs) == 2 && IsProjectPeer(segs[1]) {
		return segs[1]
	}
	// <project>/<peer>/<party> — the party folder under a workspace/
	// register peer reports its peer; archive's party folder is not a slot.
	if len(segs) == 3 && IsProjectPeer(segs[1]) && segs[1] != "archive" {
		return segs[1]
	}
	// <project>/archive/<party>/<slot> — the WORM record slots.
	if len(segs) == 4 && segs[1] == "archive" && IsPerPartySlot(segs[3]) {
		return segs[3]
	}
	return ""
}

// leafLevel returns the deepest (most-specific) ZddcFile in chain.
// Caller's responsibility to check len(chain.Levels) > 0 — but
// returns ZddcFile{} on empty for ergonomic chaining.
func leafLevel(chain PolicyChain) ZddcFile {
	if len(chain.Levels) == 0 {
		return ZddcFile{}
	}
	return chain.Levels[len(chain.Levels)-1]
}

// isZeroZddcFile reports whether zf carries no meaningful content.
// Used by IsDeclaredPath to distinguish "ancestor paths: matched
// and stamped something here" from "no contribution at all".
func isZeroZddcFile(zf ZddcFile) bool {
	if zf.Title != "" {
		return false
	}
	if zf.DefaultTool != "" || zf.DirTool != "" {
		return false
	}
	if zf.AutoOwn != nil || zf.AutoOwnFenced != nil || zf.Virtual != nil ||
		zf.DropTarget != nil || zf.Inherit != nil {
		return false
	}
	if zf.PartySource != "" {
		return false
	}
	if zf.ReceivedPath != "" || zf.PlannedReviewDate != "" ||
		zf.PlannedResponseDate != "" {
		return false
	}
	if len(zf.AvailableTools) > 0 {
		return false
	}
	if zf.CreatedBy != "" {
		return false
	}
	if zf.Worm != nil { // non-nil even when empty — marks a WORM zone
		return false
	}
	if len(zf.Admins) > 0 {
		return false
	}
	if len(zf.ACL.Permissions) > 0 {
		return false
	}
	if zf.ACL.Inherit != nil {
		return false
	}
	if len(zf.Tables) > 0 || len(zf.Views) > 0 || len(zf.Display) > 0 || len(zf.Paths) > 0 {
		return false
	}
	if len(zf.Roles) > 0 {
		return false
	}
	return true
}

// resolvePathSegments turns dirPath (absolute, under fsRoot) into a
// slice of segments relative to fsRoot. Used by helpers that walk
// the cascade by segment. Returns nil for dirPath == fsRoot or for
// any path outside fsRoot.
func resolvePathSegments(fsRoot, dirPath string) []string {
	fsRoot = filepath.Clean(fsRoot)
	dirPath = filepath.Clean(dirPath)
	if dirPath == fsRoot {
		return nil
	}
	rel, err := filepath.Rel(fsRoot, dirPath)
	if err != nil || strings.HasPrefix(rel, "..") {
		return nil
	}
	return strings.Split(rel, string(filepath.Separator))
}
