package zddc

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

// PolicyChain represents a chain of .zddc files from root to leaf.
//
// Embedded sits BELOW Levels[0] (the on-disk root .zddc): it's the
// baked-in defaults that ship with the binary, used as a baseline
// when an on-disk .zddc doesn't specify a rule. Consumers that want
// the full effective view should consult Levels then fall back to
// Embedded for unresolved lookups.
//
// Inherit:false on any level in Levels (or at deeper levels of a
// future paths: walker) zeroes out Embedded for that policy chain —
// the operator has taken full responsibility for spelling out every
// rule from scratch.
type PolicyChain struct {
	Levels     []ZddcFile // ordered root (index 0) → leaf (last index)
	HasAnyFile bool       // true if at least one .zddc file exists in the chain
	Embedded   ZddcFile   // baked-in defaults; zero ZddcFile{} if any level set inherit:false
}

// VisibleStart returns the lowest level index visible to evaluation at
// any level in [0, toIdx], honoring inherit:false fences. A level with
// `acl.inherit: false` is a fence: ancestors above it are invisible to
// descendants at-and-below the fence. The deepest fence in the prefix
// wins (nested fences are supported; the closer-to-leaf wins).
//
// toIdx is clamped to len(chain.Levels)-1.
func (chain PolicyChain) VisibleStart(toIdx int) int {
	if toIdx >= len(chain.Levels) {
		toIdx = len(chain.Levels) - 1
	}
	if toIdx < 0 {
		return 0
	}
	for i := toIdx; i >= 0; i-- {
		if !chain.Levels[i].ACL.InheritsAncestors() {
			return i
		}
	}
	return 0
}

// EffectiveHistory reports whether edit-history versioning is enabled
// for writes at this chain's directory. Unlike DropTarget (leaf-only),
// history is a subtree behavior: the closest-to-leaf explicit setting
// wins and applies to all descendants. It deliberately IGNORES
// inherit:false ACL fences — versioning is a write behavior, not a
// permission, so a fenced per-user home under a history-enabled
// working/ still records history. Falls back to the embedded defaults.
func (chain PolicyChain) EffectiveHistory() bool {
	for i := len(chain.Levels) - 1; i >= 0; i-- {
		if v := chain.Levels[i].History; v != nil {
			return *v
		}
	}
	if v := chain.Embedded.History; v != nil {
		return *v
	}
	return false
}

// EffectiveHistoryGlobs returns the basename globs selecting which files
// get text edit-history (deepest non-empty wins, then embedded defaults,
// then the built-in default ["*.md"]). Independent of EffectiveHistory:
// this says WHICH file types qualify; the bool gates whether snapshots are
// actually recorded.
func (chain PolicyChain) EffectiveHistoryGlobs() []string {
	for i := len(chain.Levels) - 1; i >= 0; i-- {
		if g := chain.Levels[i].HistoryGlobs; len(g) > 0 {
			return g
		}
	}
	if g := chain.Embedded.HistoryGlobs; len(g) > 0 {
		return g
	}
	return []string{"*.md"}
}

// policyCache caches effective policies keyed by dirPath.
// Values are PolicyChain.
var policyCache sync.Map

// EffectivePolicy returns the ACL policy chain for dirPath by reading
// all .zddc files from fsRoot down to dirPath.
//
// Child directory rules take precedence over parent rules.
// A deny at any level cannot be overridden by a parent allow.
func EffectivePolicy(fsRoot, dirPath string) (PolicyChain, error) {
	// Normalize: ensure fsRoot and dirPath use the same separator
	fsRoot = filepath.Clean(fsRoot)
	dirPath = filepath.Clean(dirPath)

	// Build list of directories from root to dirPath (inclusive)
	var dirs []string
	if !strings.HasPrefix(dirPath, fsRoot) {
		// dirPath must be under fsRoot; if not, treat it as an empty chain
		return PolicyChain{}, nil
	}

	rel, err := filepath.Rel(fsRoot, dirPath)
	if err != nil {
		return PolicyChain{}, err
	}

	// Walk from root down: root, root/a, root/a/b, ...
	dirs = append(dirs, fsRoot)
	if rel != "." {
		parts := strings.Split(rel, string(filepath.Separator))
		current := fsRoot
		for _, part := range parts {
			current = filepath.Join(current, part)
			dirs = append(dirs, current)
		}
	}

	// Check cache for the most specific (deepest) cached entry we can reuse
	cacheKey := dirPath
	if cached, ok := policyCache.Load(cacheKey); ok {
		return cached.(PolicyChain), nil
	}

	// Build policy chain: read each on-disk .zddc file.
	onDisk := make([]ZddcFile, 0, len(dirs))
	hasAny := false
	for _, dir := range dirs {
		zddcPath := filepath.Join(dir, ".zddc")
		_, err := os.Stat(zddcPath)
		if err == nil {
			hasAny = true
			parsed, perr := ParseFile(zddcPath)
			if perr != nil {
				onDisk = append(onDisk, ZddcFile{})
			} else {
				onDisk = append(onDisk, parsed)
			}
		} else {
			onDisk = append(onDisk, ZddcFile{})
		}
	}

	// Walk ancestor paths: trees alongside the on-disk chain. Each
	// virtual source is a paths-map seeded by an ancestor's Paths
	// (embedded or an on-disk level). At each segment we try to
	// match the source's glob; on hit, the matched ZddcFile becomes
	// a virtual contribution at THIS level, and its own Paths map
	// becomes a new virtual source descending into deeper segments.
	embedded := ZddcFile{}
	if e, err := EmbeddedDefaults(); err == nil {
		embedded = e
	}

	// segments[0] is the first child segment under fsRoot. dirs[0]
	// is fsRoot itself, dirs[i+1] is the directory at segments[i].
	var segments []string
	if rel != "." {
		segments = strings.Split(rel, string(filepath.Separator))
	}

	// Active virtual sources (paths-maps actively descending into the
	// target). Seeded with the embedded defaults' Paths and the
	// fsRoot/.zddc's Paths (both apply to fsRoot's children).
	var virtualSources []map[string]ZddcFile
	if embedded.Paths != nil {
		virtualSources = append(virtualSources, embedded.Paths)
	}
	if onDisk[0].Paths != nil {
		virtualSources = append(virtualSources, onDisk[0].Paths)
	}

	// inherit:false at any level along the way zeroes the embedded
	// layer and drops accumulated ancestor contributions when first
	// encountered. Track once.
	embeddedActive := true
	if onDisk[0].Inherit != nil && !*onDisk[0].Inherit {
		embeddedActive = false
		virtualSources = nil
		if onDisk[0].Paths != nil {
			// Re-seed only THIS level's paths; embedded.Paths drops.
			virtualSources = append(virtualSources, onDisk[0].Paths)
		}
	}

	// Assemble chain.Levels with virtual contributions merged in.
	chain := PolicyChain{
		Levels:     make([]ZddcFile, len(dirs)),
		HasAnyFile: hasAny,
	}
	chain.Levels[0] = onDisk[0] // root level has no ancestor virtual paths

	for i, seg := range segments {
		depth := i + 1
		// Match each active virtual source against this segment.
		var contributions []ZddcFile
		var newSources []map[string]ZddcFile
		for _, src := range virtualSources {
			if match := matchGlob(src, seg); match != nil {
				contributions = append(contributions, *match)
				if match.Paths != nil {
					newSources = append(newSources, match.Paths)
				}
			}
		}
		virtualSources = newSources

		// Honor inherit:false on the on-disk level at this depth.
		// Dropping ancestor sources also means dropping this level's
		// inherited contributions; the level stands alone (plus its
		// own Paths for descendants).
		if onDisk[depth].Inherit != nil && !*onDisk[depth].Inherit {
			embeddedActive = false
			contributions = nil
			virtualSources = nil
		}

		// Seed this level's own Paths as a virtual source for deeper
		// segments. It applies BELOW this level, not at this level.
		if onDisk[depth].Paths != nil {
			virtualSources = append(virtualSources, onDisk[depth].Paths)
		}

		// Compose: ancestor contributions first (lowest specificity),
		// then on-disk at this level (most specific) on top.
		merged := ZddcFile{}
		for _, c := range contributions {
			merged = mergeOverlay(merged, c)
		}
		merged = mergeOverlay(merged, onDisk[depth])
		chain.Levels[depth] = merged
	}

	// Layer in the embedded defaults as the bottom of the cascade
	// (consulted as fallback by lookups that don't find a value in
	// chain.Levels). The Paths-walking above has already threaded
	// embedded.Paths through to the appropriate levels; this is the
	// non-paths baseline for level-0-style lookups.
	if embeddedActive {
		chain.Embedded = embedded
		for _, lvl := range onDisk {
			if lvl.Inherit != nil && !*lvl.Inherit {
				chain.Embedded = ZddcFile{}
				break
			}
		}
	}

	policyCache.Store(cacheKey, chain)
	return chain, nil
}

// EffectiveFieldCodes returns the merged field-code vocabulary
// visible at the leaf of this chain. Walks root → leaf, applying
// map-merge per top-level key (a leaf entry for the same code
// replaces the root entry, mirroring mergeOverlay).
//
// Embedded defaults are layered in below the on-disk root unless
// inherit:false on any level dropped them (chain.Embedded is zeroed
// in that case, so reading it as a baseline is safe either way).
func (chain PolicyChain) EffectiveFieldCodes() map[string]FieldCode {
	out := map[string]FieldCode{}
	for k, v := range chain.Embedded.FieldCodes {
		out[k] = v
	}
	for _, lvl := range chain.Levels {
		for k, v := range lvl.FieldCodes {
			out[k] = v
		}
	}
	return out
}

// EffectiveRecordRule returns the merged RecordRule for files whose
// basename matches a pattern in any level's Records map. Walks root
// → leaf, mergeRecordRule-combining successive matches so a
// per-folder .zddc can refine an ancestor's rule (add a lock, set a
// default) without restating everything.
//
// pattern is the most-specific pattern that matched (deepest level's
// chosen key); rule is the merged result; ok is false when no level
// declared a matching pattern.
//
// Matching at each level prefers literal-key over glob; see
// matchRecordRule.
func (chain PolicyChain) EffectiveRecordRule(basename string) (string, RecordRule, bool) {
	merged := RecordRule{}
	any := false
	pattern := ""
	consider := func(rules map[string]RecordRule) {
		if pat, rule, hit := matchRecordRule(rules, basename); hit {
			merged = mergeRecordRule(merged, rule)
			pattern = pat
			any = true
		}
	}
	consider(chain.Embedded.Records)
	for _, lvl := range chain.Levels {
		consider(lvl.Records)
	}
	if !any {
		return "", RecordRule{}, false
	}
	return pattern, merged, true
}

// SourceEntry names one cascade contribution to an EffectiveZddc
// composition. Level -1 is the embedded defaults baseline (chain.
// Embedded); levels 0+ index into chain.Levels (root→leaf). Contributed
// lists the top-level ZddcFile field names this level supplied a non-
// zero value for — used by inspection clients to answer "where does
// this value come from?" without re-walking the cascade.
type SourceEntry struct {
	Level       int      `json:"level"`
	Contributed []string `json:"contributed,omitempty"`
}

// EffectiveZddc composes the cascade into a single ZddcFile by walking
// chain.Embedded then chain.Levels[VisibleStart..] through mergeOverlay,
// and folding the cross-level Roles union (via lookupRoleMembers) into
// merged.Roles so the result reflects the same role membership the
// runtime ACL evaluator sees.
//
// Returned alongside is a per-source list of which top-level fields
// each contributing level declared. Caller maps SourceEntry.Level to a
// URL (-1 = embedded baseline; 0..len(chain.Levels)-1 = dirs along the
// walk from fsRoot to the requested directory).
//
// Returns the zero ZddcFile + nil sources when the chain is empty.
// Used by the ?effective=1 query on /.zddc — distinct from the .zddc
// file itself, which serves only what's defined at the leaf level.
func EffectiveZddc(chain PolicyChain) (ZddcFile, []SourceEntry) {
	if len(chain.Levels) == 0 {
		return ZddcFile{}, nil
	}
	sources := make([]SourceEntry, 0, len(chain.Levels)+1)
	var merged ZddcFile

	// Embedded baseline (skipped when an inherit:false fence dropped
	// it; cascade.go zeroes chain.Embedded in that case).
	if c := nonZeroZddcFields(chain.Embedded); len(c) > 0 {
		merged = mergeOverlay(merged, chain.Embedded)
		sources = append(sources, SourceEntry{Level: -1, Contributed: c})
	}

	leafIdx := len(chain.Levels) - 1
	floor := chain.VisibleStart(leafIdx)
	for i := floor; i <= leafIdx; i++ {
		lvl := chain.Levels[i]
		if c := nonZeroZddcFields(lvl); len(c) > 0 {
			merged = mergeOverlay(merged, lvl)
			sources = append(sources, SourceEntry{Level: i, Contributed: c})
		}
	}

	// Roles: mergeOverlay does per-level name-keyed replacement, but
	// the runtime evaluator unions members across levels via
	// lookupRoleMembers (handling reset:true and the embedded
	// baseline). Re-resolve every role name reachable in the visible
	// chain so merged.Roles matches what ACL evaluation sees.
	roleNames := collectRoleNames(chain, floor, leafIdx)
	if len(roleNames) > 0 {
		out := make(map[string]Role, len(roleNames))
		for _, name := range roleNames {
			members, defined := lookupRoleMembers(chain, leafIdx, name)
			if !defined {
				continue
			}
			out[name] = Role{Members: members}
		}
		merged.Roles = out
	} else {
		merged.Roles = nil
	}

	return merged, sources
}

// nonZeroZddcFields returns the names of top-level ZddcFile fields zf
// has populated. Field names match the yaml tags (so "acl" not "ACL").
// Used to populate SourceEntry.Contributed.
func nonZeroZddcFields(zf ZddcFile) []string {
	var out []string
	add := func(name string, cond bool) {
		if cond {
			out = append(out, name)
		}
	}
	add("title", zf.Title != "")
	add("acl", len(zf.ACL.Permissions) > 0 || zf.ACL.Inherit != nil)
	add("admins", len(zf.Admins) > 0)
	add("tables", len(zf.Tables) > 0)
	add("views", len(zf.Views) > 0)
	add("display", len(zf.Display) > 0)
	add("convert", zf.Convert != nil)
	add("roles", len(zf.Roles) > 0)
	add("created_by", zf.CreatedBy != "")
	add("default_tool", zf.DefaultTool != "")
	add("dir_tool", zf.DirTool != "")
	add("auto_own", zf.AutoOwn != nil)
	add("auto_own_fenced", zf.AutoOwnFenced != nil)
	add("virtual", zf.Virtual != nil)
	add("drop_target", zf.DropTarget != nil)
	add("party_source", zf.PartySource != "")
	add("history", zf.History != nil)
	add("history_globs", len(zf.HistoryGlobs) > 0)
	add("worm", zf.Worm != nil)
	add("available_tools", len(zf.AvailableTools) > 0)
	add("received_path", zf.ReceivedPath != "")
	add("planned_review_date", zf.PlannedReviewDate != "")
	add("planned_response_date", zf.PlannedResponseDate != "")
	add("field_codes", len(zf.FieldCodes) > 0)
	add("records", len(zf.Records) > 0)
	add("paths", len(zf.Paths) > 0)
	return out
}

// collectRoleNames returns every role name that has a definition in
// any visible level (or the embedded baseline). Used by EffectiveZddc
// to know which roles to resolve via lookupRoleMembers — without it
// we'd miss roles declared only at an ancestor not directly merged at
// the leaf level (since per-level mergeOverlay replaces Roles by key,
// not by union).
func collectRoleNames(chain PolicyChain, floor, leafIdx int) []string {
	seen := make(map[string]struct{})
	for i := floor; i <= leafIdx; i++ {
		for name := range chain.Levels[i].Roles {
			seen[name] = struct{}{}
		}
	}
	for name := range chain.Embedded.Roles {
		seen[name] = struct{}{}
	}
	if len(seen) == 0 {
		return nil
	}
	out := make([]string, 0, len(seen))
	for name := range seen {
		out = append(out, name)
	}
	return out
}

// InvalidateCache removes the cached policy for dirPath and all descendants.
func InvalidateCache(dirPath string) {
	dirPath = filepath.Clean(dirPath)
	policyCache.Range(func(key, _ any) bool {
		k := key.(string)
		if k == dirPath || strings.HasPrefix(k, dirPath+string(filepath.Separator)) {
			policyCache.Delete(key)
		}
		return true
	})
}
