package zddc

import "strings"

// AllowedAtLevel is a thin shim over GrantedVerbsAtLevel preserved for
// callers that only need the legacy boolean read decision on a single
// ZddcFile (no cascade chain).
//
// Hardcodes ModeDelegated — safe because the synthetic chain has only
// one level and no ancestors to fence — but callers that operate on a
// real PolicyChain must call GrantedVerbsAtLevel directly with the
// active mode.
//
// Deprecated: prefer GrantedVerbsAtLevel for any code path that may
// later need fence-aware or strict-mode evaluation.
func AllowedAtLevel(level ZddcFile, email string) (decision bool, matched bool) {
	chain := PolicyChain{Levels: []ZddcFile{level}, HasAnyFile: true}
	v, m := GrantedVerbsAtLevel(chain, 0, email, ModeDelegated)
	if !m {
		return false, false
	}
	return v.Has(VerbR), true
}

// GrantedVerbsAtLevel computes the verb set granted to email at
// chain.Levels[levelIdx]. Returns (set, matched):
//   - matched=false → no entry in this level matches the user; cascade walks on
//   - matched=true, set={} → an entry matched with the empty verb set; explicit deny
//   - matched=true, set!={} → union of verb sets from every matching entry
//
// Role lookups for principal keys without "@" use RoleMembers, which
// walks levelIdx → fence-or-root for the closest definition. mode
// controls whether inherit:false fences are honored — see VisibleStart.
//
// Legacy acl.allow / acl.deny entries are folded in here (rather than at
// parse time) so this function works correctly on test-constructed
// ZddcFile literals as well as parser output.
func GrantedVerbsAtLevel(chain PolicyChain, levelIdx int, email string, mode CascadeMode) (VerbSet, bool) {
	if levelIdx < 0 || levelIdx >= len(chain.Levels) {
		return 0, false
	}
	level := chain.Levels[levelIdx]
	perms := effectivePermissions(level.ACL)
	if len(perms) == 0 {
		return 0, false
	}

	matched := false
	deniedExplicit := false
	var grant VerbSet
	for principal, verbStr := range perms {
		if !MatchesPrincipal(principal, email, chain, levelIdx, mode) {
			continue
		}
		matched = true
		v, _ := ParseVerbSet(verbStr) // unknown letters silently dropped
		if verbStr == "" {
			deniedExplicit = true
			continue
		}
		grant = grant.Union(v)
	}
	if !matched {
		return 0, false
	}
	if deniedExplicit {
		// Empty-set match wins over any grant entries at the same level —
		// explicit deny is always more specific than a permissive role
		// membership at the same scope.
		return 0, true
	}
	return grant, true
}

// effectivePermissions returns the union of acl.permissions and the
// legacy acl.allow / acl.deny fields, with permissions winning on
// collision. Returns nil if all three are empty. Does not mutate rules.
func effectivePermissions(rules ACLRules) map[string]string {
	if len(rules.Permissions) == 0 && len(rules.Allow) == 0 && len(rules.Deny) == 0 {
		return nil
	}
	out := make(map[string]string, len(rules.Permissions)+len(rules.Allow)+len(rules.Deny))
	for _, pat := range rules.Allow {
		out[pat] = "rwcd"
	}
	for _, pat := range rules.Deny {
		out[pat] = ""
	}
	for k, v := range rules.Permissions {
		out[k] = v
	}
	return out
}

// AllowedWithChain evaluates a PolicyChain leaf→root (deepest level first)
// for the read action. Preserved for legacy callers and existing read paths
// that haven't migrated to AllowedAction yet.
func AllowedWithChain(chain PolicyChain, email string) bool {
	return AllowedAction(chain, email, VerbR, ModeDelegated)
}

// AllowedAction evaluates a PolicyChain for a specific verb and cascade mode.
// Thin wrapper around EffectiveVerbs that surfaces the boolean answer.
func AllowedAction(chain PolicyChain, email string, verb VerbSet, mode CascadeMode) bool {
	return EffectiveVerbs(chain, email, mode).Has(verb)
}

// EffectiveVerbs computes the verb set granted to email by the cascade.
// Walks the full chain and applies the default-allow rule (no .zddc
// anywhere → public access).
func EffectiveVerbs(chain PolicyChain, email string, mode CascadeMode) VerbSet {
	v := EffectiveVerbsRange(chain, 0, len(chain.Levels), email, mode)
	if v == 0 && !chain.HasAnyFile {
		// Public-tree default: empty chain with no .zddc files anywhere
		// → grant everything. EffectiveVerbsRange returns 0 in this
		// case because it has no opinion on default semantics outside
		// a sub-range walk; the full-chain wrapper applies the rule.
		return VerbAll
	}
	return v
}

// EffectiveVerbsRange computes the verb set granted by walking only
// chain.Levels[fromIdx:toIdx] for matching permission entries. Role
// definitions are still looked up over the FULL chain via
// GrantedVerbsAtLevel → MatchesPrincipal → lookupRoleMembers, so an
// ancestor's role definition remains visible to a sub-range walk.
//
// Used by the WORM split: above-the-WORM-folder and at-or-below-the-
// WORM-folder are evaluated as separate ranges, then their grants are
// masked and unioned.
//
// Cascade mode controls whether ancestor explicit-denies are absolute
// (Strict) or can be overridden by a leaf grant (Delegated). The
// strict-mode pass is restricted to the same range — splitting the
// chain implies splitting the strict-mode walk too.
//
// This function does NOT consult the admins:/IsAdmin escape hatch and
// does NOT apply the Issued/Received WORM mask.
func EffectiveVerbsRange(chain PolicyChain, fromIdx, toIdx int, email string, mode CascadeMode) VerbSet {
	if fromIdx < 0 {
		fromIdx = 0
	}
	if toIdx > len(chain.Levels) {
		toIdx = len(chain.Levels)
	}
	if fromIdx >= toIdx {
		// Empty range — no levels to consult. Caller is responsible
		// for the default-deny semantics in this case (typically the
		// caller has another range to combine with).
		return 0
	}
	// Honor inherit:false fences — clamp fromIdx upward to the deepest
	// fence visible from the leaf end of the range. In strict mode the
	// fence helper returns 0 unconditionally, so this is a no-op.
	if fence := chain.VisibleStart(toIdx-1, mode); fence > fromIdx {
		fromIdx = fence
	}
	if mode == ModeStrict {
		for i := fromIdx; i < toIdx; i++ {
			grant, matched := GrantedVerbsAtLevel(chain, i, email, mode)
			if matched && grant == 0 {
				return 0
			}
		}
	}
	for i := toIdx - 1; i >= fromIdx; i-- {
		grant, matched := GrantedVerbsAtLevel(chain, i, email, mode)
		if !matched {
			continue
		}
		return grant
	}
	// No match in range. The "no .zddc anywhere → public" default is
	// applied by the EffectiveVerbs wrapper, not here, because callers
	// using sub-ranges (e.g. WORM split) want a sub-range with no match
	// to contribute nothing rather than implicitly granting everything.
	return 0
}

// MatchesPattern checks if email matches a glob pattern.
//
// The pattern may use * as a wildcard within the local part or domain part,
// but * does not cross the @ boundary. Examples:
//   - "*@mycompany.com" matches any user at mycompany.com
//   - "alice@*" matches alice at any domain
//   - "alice@example.com" matches exactly
//   - "*" matches any non-empty email (the @ boundary rule means * must stay in one segment)
//
// Exported so handlers can reuse it — for example, to verify that the
// writer of a root .zddc remains in the Admins list after the edit, the
// editor's POST handler calls MatchesPattern directly rather than going
// through AllowedAtLevel/IsAdmin/etc.
func MatchesPattern(pattern, email string) bool {
	// Exact match (fast path)
	if pattern == email {
		return true
	}

	// Wildcard-free check already handled above; split on @
	patternParts := strings.SplitN(pattern, "@", 2)
	emailParts := strings.SplitN(email, "@", 2)

	if len(patternParts) == 2 && len(emailParts) == 2 {
		// Both have @: match local and domain separately
		return globMatch(patternParts[0], emailParts[0]) &&
			globMatch(patternParts[1], emailParts[1])
	}

	if len(patternParts) == 1 {
		// Pattern has no @ — match against the full email string
		return globMatch(pattern, email)
	}

	return false
}

// globMatch performs simple glob matching where * matches any sequence of chars.
func globMatch(pattern, s string) bool {
	// Two-pointer approach: handle multiple * segments
	if pattern == "*" {
		return true
	}
	if !strings.Contains(pattern, "*") {
		return pattern == s
	}

	// Split pattern on * and match segments in order
	parts := strings.Split(pattern, "*")
	remaining := s
	for i, part := range parts {
		if part == "" {
			continue
		}
		idx := strings.Index(remaining, part)
		if idx < 0 {
			return false
		}
		// First segment must match at start
		if i == 0 && idx != 0 {
			return false
		}
		remaining = remaining[idx+len(part):]
	}
	// Last segment must match at end (no trailing *)
	if parts[len(parts)-1] != "" {
		// The remaining string must be empty because the last part already consumed it
		// Actually we need to check: if the last part is non-empty, remaining must be empty
		// because we consumed up through that part above. If there's a trailing *, remaining
		// can be anything.
		// Re-check: split("a*b", "*") → ["a", "b"]. After matching "a" at start and "b"
		// somewhere after, remaining should be "" if "b" is last segment.
		// The loop above leaves remaining = s[after last part matched]. If last part is
		// non-empty and pattern doesn't end with *, remaining must be "".
		// Actually, the last segment check needs to verify that remaining is empty after
		// the last part was matched. If there's a trailing *, remaining can be anything.
		// The last part in parts is what remains after the last *. If it's non-empty,
		// we need remaining to be empty (pattern didn't end with *).
		// Since we're processing left-to-right and the last part must match at the end,
		// remaining must be empty after consuming the last part.
		if !strings.HasSuffix(pattern, "*") && remaining != "" {
			return false
		}
	}
	return true
}
