package zddc

import (
	"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=mdedit propagates to working/alice/notes/ even when
// no .zddc declares mdedit 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
}

// AutoOwnAt reports whether mkdir at this directory should write an
// auto-owned .zddc. Returns the deepest explicit value found in the
// cascade (true OR false), falling back to false when nothing set
// anywhere. *bool semantics let descendants explicitly disable an
// ancestor's auto_own: true.
//
// Replaces AutoOwnCanonicalNames once the file API's mkdir hook is
// migrated.
func AutoOwnAt(fsRoot, dirPath string) bool {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	for i := len(chain.Levels) - 1; i >= 0; i-- {
		if v := chain.Levels[i].AutoOwn; v != nil {
			return *v
		}
	}
	if v := chain.Embedded.AutoOwn; v != nil {
		return *v
	}
	return false
}

// VirtualAt reports whether the directory at dirPath is declared as
// purely virtual. Walks the cascade like DefaultToolAt; deepest
// explicit value wins.
//
// Replaces VirtualOnlyCanonicalNames once consumers are migrated.
func VirtualAt(fsRoot, dirPath string) bool {
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	for i := len(chain.Levels) - 1; i >= 0; i-- {
		if v := chain.Levels[i].Virtual; v != nil {
			return *v
		}
	}
	if v := chain.Embedded.Virtual; v != nil {
		return *v
	}
	return false
}

// 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).
//
// Replaces IsProjectRootFolder + IsArchivePartyFolder once
// consumers are migrated.
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)
}

// 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
}

// 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 != "" {
		return false
	}
	if zf.AutoOwn != nil || zf.Virtual != nil || zf.Inherit != nil {
		return false
	}
	if zf.AppsPubKey != "" || zf.CreatedBy != "" {
		return false
	}
	if len(zf.Admins) > 0 {
		return false
	}
	if len(zf.ACL.Permissions) > 0 || len(zf.ACL.Allow) > 0 || len(zf.ACL.Deny) > 0 {
		return false
	}
	if zf.ACL.Inherit != nil {
		return false
	}
	if len(zf.Apps) > 0 || len(zf.Tables) > 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))
}
