package zddc

import (
	"io/fs"
	"os"
	"path"
	"strings"
)

// PolicyTree is a set of .zddc documents addressed by directory path relative
// to a mount point, with "*" as the any-segment wildcard. It is the in-memory
// form of a per-depth default tree or an operator .zddc.zip dropped at a
// directory: mounting the tree at directory D means key "working" governs
// D/working/, "*/mdl" governs D/<anyproject>/mdl/, and "" is D's own .zddc.
//
// Resolution mirrors the paths: cascade — a literal segment beats "*" — so a
// .zddc.zip and a paths: block compose identically. A .zddc.zip can therefore
// be dropped at ANY level to contribute a whole policy subtree; combined with
// inherit:false in its resolved .zddc it becomes a self-contained island.
type PolicyTree map[string]ZddcFile

// AnyPlaceholder is the on-disk directory name standing in for the "*" wildcard
// in the embedded default-tree source (internal/zddc/defaults/), so the repo
// holds no shell-/go:embed-hostile literal "*" directories. Operator .zddc.zip
// bundles use "*" directly.
const AnyPlaceholder = "_any_"

// segsOf splits a "/"-joined member-dir key into segments ("" → no segments).
func segsOf(key string) []string {
	if key == "" {
		return nil
	}
	return strings.Split(key, "/")
}

// resolveTreeDir returns the member-dir key governing segs: same length, each
// key segment literal-equal or "*", most-literal wins (a literal beats "*" at
// the earliest differing position — matching the paths: literal-first rule).
func (t PolicyTree) resolveTreeDir(segs []string) (string, bool) {
	bestKey := ""
	var bestSegs []string
	found := false
	for key := range t {
		ks := segsOf(key)
		if len(ks) != len(segs) {
			continue
		}
		match := true
		for i := range ks {
			if ks[i] != "*" && ks[i] != segs[i] {
				match = false
				break
			}
		}
		if !match {
			continue
		}
		if !found || moreLiteral(ks, bestSegs) {
			bestKey, bestSegs, found = key, ks, true
		}
	}
	return bestKey, found
}

// moreLiteral reports whether a is more specific than b: at the earliest
// position where one is literal and the other "*", the literal wins.
func moreLiteral(a, b []string) bool {
	for i := range a {
		al, bl := a[i] != "*", b[i] != "*"
		if al != bl {
			return al
		}
	}
	return false
}

// Along returns the .zddc documents this tree contributes along relSegs — one
// per cascade level from the mount root (the empty prefix) down to the full
// path, in root→leaf order (matching PolicyChain.Levels indexing). Levels with
// no governing member contribute nothing.
func (t PolicyTree) Along(relSegs []string) []ZddcFile {
	var out []ZddcFile
	for k := 0; k <= len(relSegs); k++ {
		if key, ok := t.resolveTreeDir(relSegs[:k]); ok {
			out = append(out, t[key])
		}
	}
	return out
}

// Assemble folds a flat per-depth tree into a single nested ZddcFile whose
// Paths map mirrors the tree — the inverse of authoring policy as per-depth
// files. The result is exactly what a single nested-paths: .zddc would parse
// to, so the cascade walker consumes it unchanged: the embedded defaults and
// any operator .zddc.zip both become ordinary paths:-bearing ZddcFiles.
func (t PolicyTree) Assemble() ZddcFile {
	return assembleTree(map[string]ZddcFile(t))
}

// assembleTree recursively builds the nested node for a set of members keyed by
// path relative to this node ("" = this node's own content; "head/rest" = a
// descendant). Children are grouped by first segment and recursed into Paths.
func assembleTree(members map[string]ZddcFile) ZddcFile {
	node := members[""]
	groups := map[string]map[string]ZddcFile{}
	for key, zf := range members {
		if key == "" {
			continue
		}
		head, rest, _ := strings.Cut(key, "/")
		if groups[head] == nil {
			groups[head] = map[string]ZddcFile{}
		}
		groups[head][rest] = zf
	}
	if len(groups) > 0 {
		node.Paths = make(map[string]ZddcFile, len(groups))
		for head, sub := range groups {
			node.Paths[head] = assembleTree(sub)
		}
	}
	return node
}

// LoadPolicyTreeFromFS loads a per-depth .zddc tree rooted at `root` within
// fsys, mapping the AnyPlaceholder directory to the "*" wildcard. Keys are
// member dirs relative to root ("" for root/.zddc). Works on an embed.FS, an
// os.DirFS, or a zip's fs.FS.
func LoadPolicyTreeFromFS(fsys fs.FS, root string) (PolicyTree, error) {
	out := PolicyTree{}
	err := fs.WalkDir(fsys, root, func(p string, d fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if d.IsDir() || d.Name() != ".zddc" {
			return nil
		}
		rel := strings.TrimPrefix(path.Dir(p), root)
		rel = strings.Trim(rel, "/")
		key := ""
		if rel != "" {
			parts := strings.Split(rel, "/")
			for i, s := range parts {
				if s == AnyPlaceholder {
					parts[i] = "*"
				}
			}
			key = strings.Join(parts, "/")
		}
		data, err := fs.ReadFile(fsys, p)
		if err != nil {
			return err
		}
		zf, err := parseBytes(data)
		if err != nil {
			return err
		}
		out[key] = zf
		return nil
	})
	if err != nil {
		return nil, err
	}
	return out, nil
}

// LoadPolicyTreeFromDir is the on-disk convenience over LoadPolicyTreeFromFS.
func LoadPolicyTreeFromDir(fsDir string) (PolicyTree, error) {
	return LoadPolicyTreeFromFS(os.DirFS(fsDir), ".")
}
