package zddc

import (
	"sort"
	"strings"
)

// VerbSet is a bitmask over the five permission verbs r, w, c, d, a.
// Construct via ParseVerbSet (tolerant of any letter order, ignores
// duplicates and whitespace, rejects unknown letters as a deny). The
// canonical string form sorts to "rwcda" — see VerbSet.String.
type VerbSet uint8

const (
	VerbR VerbSet = 1 << iota // read file bytes / list directory
	VerbW                     // overwrite existing / rename existing
	VerbC                     // create new file or directory
	VerbD                     // delete file
	VerbA                     // modify ACL of this subtree

	VerbAll = VerbR | VerbW | VerbC | VerbD | VerbA

	// VerbsRWCD is the every-non-admin verb set — granted to a principal
	// that holds read+write+create+delete but not admin authority.
	VerbsRWCD = VerbR | VerbW | VerbC | VerbD

	// VerbsRC is the WORM-mask survivor: read + create only. Drop boxes
	// (doc controller filing into Issued/Received) and any other principal
	// with cascade-derived broader rights end up here once the mask runs.
	VerbsRC = VerbR | VerbC
)

// ParseVerbSet parses a verb-set string like "rwcd" or "cra". Empty
// string returns an explicit-deny (zero VerbSet). Any unknown letter
// returns ok=false; callers that round-trip operator-authored YAML
// should surface this as a parse error rather than silently dropping
// the entry.
func ParseVerbSet(s string) (VerbSet, bool) {
	var v VerbSet
	for _, r := range s {
		switch r {
		case 'r', 'R':
			v |= VerbR
		case 'w', 'W':
			v |= VerbW
		case 'c', 'C':
			v |= VerbC
		case 'd', 'D':
			v |= VerbD
		case 'a', 'A':
			v |= VerbA
		case ' ', '\t':
			// tolerate whitespace
		default:
			return 0, false
		}
	}
	return v, true
}

// String returns the canonical "rwcda" ordering with only the verbs
// present in the set. The empty set serializes to "" — round-trippable
// as the explicit-deny entry.
func (v VerbSet) String() string {
	var b strings.Builder
	if v&VerbR != 0 {
		b.WriteByte('r')
	}
	if v&VerbW != 0 {
		b.WriteByte('w')
	}
	if v&VerbC != 0 {
		b.WriteByte('c')
	}
	if v&VerbD != 0 {
		b.WriteByte('d')
	}
	if v&VerbA != 0 {
		b.WriteByte('a')
	}
	return b.String()
}

// Has reports whether the set contains every verb in mask.
func (v VerbSet) Has(mask VerbSet) bool { return v&mask == mask }

// Union returns the verb-wise union.
func (v VerbSet) Union(o VerbSet) VerbSet { return v | o }

// Intersect returns the verb-wise intersection.
func (v VerbSet) Intersect(o VerbSet) VerbSet { return v & o }

// IsPrincipalRole reports whether a Permissions key is a role
// reference (no "@") rather than a direct email pattern. This is the
// disambiguation rule: any principal containing "@" is treated as an
// email pattern matched via MatchesPattern; everything else is a role
// name looked up via Roles maps in the cascade.
func IsPrincipalRole(principal string) bool {
	return !strings.Contains(principal, "@")
}

// RoleMembers returns the effective member-pattern list for roleName
// as visible at chain.Levels[levelIdx] — the UNION of every level's
// definition in the visible chain, with a role.Reset=true level
// stopping the walk (its members plus anything deeper; ancestors
// above the reset excluded). The visible-chain lower bound is
// chain.VisibleStart(levelIdx) — an inherit:false fence at-or-below
// levelIdx hides definitions above it. Returns nil if no level in the
// visible chain defines the role.
//
// Levels are stored root (index 0) → leaf (last index), matching the
// EffectivePolicy convention.
func RoleMembers(chain PolicyChain, levelIdx int, roleName string) []string {
	members, _ := lookupRoleMembers(chain, levelIdx, roleName)
	return members
}

// MatchesPrincipal reports whether email satisfies the given Permissions
// key at chain.Levels[levelIdx].
//
// Resolution order:
//
//  1. Principals containing "@" are always email patterns; dispatch to
//     MatchesPattern.
//  2. Principals without "@" are role-or-pattern. Look up the name in
//     the cascade's roles, honoring fences. If a role definition is
//     found in the visible chain, match the user against the role's
//     members. If no role definition exists in the visible chain, fall
//     back to MatchesPattern so bare wildcards like "*" still match.
func MatchesPrincipal(principal, email string, chain PolicyChain, levelIdx int) bool {
	if !IsPrincipalRole(principal) {
		return MatchesPattern(principal, email)
	}
	members, defined := lookupRoleMembers(chain, levelIdx, principal)
	if !defined {
		// Bare wildcards / unqualified strings still match via the
		// email-pattern matcher when no role of that name exists.
		return MatchesPattern(principal, email)
	}
	for _, m := range members {
		if MatchesPattern(m, email) {
			return true
		}
	}
	return false
}

// lookupRoleMembers returns the member list and whether the role was
// defined anywhere in the visible chain. Distinguishes "role exists
// but is empty" (defined=true, empty members) from "role not defined"
// (defined=false), which the principal-fallback logic depends on. The
// visible-chain bound is determined by chain.VisibleStart(levelIdx).
//
// Members UNION across every level that defines the role. Walking
// deep→shallow, a level with role.Reset=true stops the walk: its
// members (plus anything deeper that already accumulated) are the
// final set; ancestor definitions above the reset are excluded.
func lookupRoleMembers(chain PolicyChain, levelIdx int, roleName string) ([]string, bool) {
	if levelIdx < 0 || levelIdx >= len(chain.Levels) {
		return nil, false
	}
	floor := chain.VisibleStart(levelIdx)
	var members []string
	seen := make(map[string]struct{})
	defined := false
	addAll := func(ms []string) {
		for _, m := range ms {
			if _, dup := seen[m]; dup {
				continue
			}
			seen[m] = struct{}{}
			members = append(members, m)
		}
	}
	for i := levelIdx; i >= floor; i-- {
		role, ok := chain.Levels[i].Roles[roleName]
		if !ok {
			continue
		}
		defined = true
		addAll(role.Members)
		if role.Reset {
			return members, true // authoritative; ignore ancestors + embedded
		}
	}
	// The embedded defaults sit below chain.Levels[0] in the cascade.
	// Fold its role definitions in as the baseline (so a role declared
	// only in defaults.zddc.yaml is "defined", and a deployment's
	// on-disk redefinition unions on top). Skipped above only if a
	// reset:true level already returned.
	if role, ok := chain.Embedded.Roles[roleName]; ok {
		defined = true
		addAll(role.Members)
	}
	return members, defined
}

// MatchingPrincipals returns the keys of level.ACL.Permissions whose
// principal matches email at chain.Levels[levelIdx]. Output is sorted
// for stable iteration in tests and audit logs.
func MatchingPrincipals(chain PolicyChain, levelIdx int, email string) []string {
	if levelIdx < 0 || levelIdx >= len(chain.Levels) {
		return nil
	}
	level := chain.Levels[levelIdx]
	if len(level.ACL.Permissions) == 0 {
		return nil
	}
	var out []string
	for principal := range level.ACL.Permissions {
		if MatchesPrincipal(principal, email, chain, levelIdx) {
			out = append(out, principal)
		}
	}
	sort.Strings(out)
	return out
}

// RolesForPrincipalInChain returns the sorted, de-duplicated role names
// that email is a member of, as roles resolve at the chain's leaf level —
// honouring inherit:false fences and role resets via MatchesPrincipal.
// Role names declared anywhere in the visible chain OR in the embedded
// defaults are considered (so a standard role like document_controller
// that ships empty but gains members from an on-disk .zddc is reported).
// Returns nil for an empty email or empty chain.
//
// This is "which roles do I hold HERE" — roles are cascade-scoped, so the
// answer can differ between locations. The file handler surfaces it via
// /.profile/access?path=… (AccessView.PathRoles).
func RolesForPrincipalInChain(chain PolicyChain, email string) []string {
	if email == "" || len(chain.Levels) == 0 {
		return nil
	}
	leaf := len(chain.Levels) - 1
	seen := make(map[string]struct{})
	var out []string
	consider := func(name string) {
		if _, dup := seen[name]; dup {
			return
		}
		seen[name] = struct{}{}
		if MatchesPrincipal(name, email, chain, leaf) {
			out = append(out, name)
		}
	}
	floor := chain.VisibleStart(leaf)
	for i := leaf; i >= floor; i-- {
		for name := range chain.Levels[i].Roles {
			consider(name)
		}
	}
	for name := range chain.Embedded.Roles {
		consider(name)
	}
	sort.Strings(out)
	return out
}
