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).
//
// In strict cascade mode, fences are ignored — returns 0 — because
// federal/AC-6 deployments require ancestor explicit-denies to be
// absolute, and the inherit directive would let a leaf widen access an
// ancestor refused.
//
// toIdx is clamped to len(chain.Levels)-1.
func (chain PolicyChain) VisibleStart(toIdx int, mode CascadeMode) int {
	if mode == ModeStrict {
		return 0
	}
	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
}

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

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