package zddc

import "path/filepath"

// Principal is the caller identity passed to admin-authority checks.
//
// Email is the authenticated user's email (or token-issued email for
// machine clients). Elevated reports whether the caller has opted into
// their admin powers for THIS request. The opt-in is sourced upstream
// (in handler.ACLMiddleware): machine clients with bearer tokens are
// implicitly elevated; browser clients elevate via the zddc-elevate
// cookie set by the UI's elevation toggle. The Principal is built once
// per request — every admin function takes it, so the type system
// enforces the gate at every call site.
//
// Sudo-style semantics: an admin who hasn't elevated is treated as a
// regular user. Their normal ACL grants still apply (those use the
// email directly, not this Principal); only admin escape hatches
// (WORM bypass, auto-own takeover, .zddc edit authority, profile
// admin scaffolds) are gated.
type Principal struct {
	Email    string
	Elevated bool
}

// gate is the common short-circuit for every admin-authority check:
// an empty email never matches, and an admin who hasn't elevated is
// treated as a non-admin regardless of what the .zddc files name.
func (p Principal) gate() bool {
	return p.Email != "" && p.Elevated
}

// AdminLevelInChain reports the chain-level index (0-based, root=0)
// at which the first admins: entry matching email occurs, or -1 if
// no level grants admin authority. Elevation-INDEPENDENT — the
// caller is responsible for gating on Principal.Elevated before
// treating the result as live authority.
//
// Role lookups inside this walk are bounded to the levels visible
// AT the matching level (via MatchesPrincipal's third/fourth args
// and the fence-aware VisibleStart helper).
//
// Exposed separately from IsAdminForChain so audit-logging callers
// can record WHICH level conferred admin authority — useful for
// forensics across nested delegation (root admin vs subtree admin
// at depth N).
func AdminLevelInChain(chain PolicyChain, email string) int {
	if email == "" {
		return -1
	}
	for i, level := range chain.Levels {
		for _, principal := range level.Admins {
			if MatchesPrincipal(principal, email, chain, i) {
				return i
			}
		}
	}
	return -1
}

// IsAdminForChain is the boolean shortcut over AdminLevelInChain.
// Returns true iff some level grants admin authority.
func IsAdminForChain(chain PolicyChain, email string) bool {
	return AdminLevelInChain(chain, email) >= 0
}

// IsConfigEditor reports STANDING authority to edit configuration at this
// chain — writing a .zddc / .zddc.zip / role definition (the mutations that
// map to VerbA). Authority comes from EITHER being a subtree admin (an
// admins: grant anywhere on the chain) OR holding the `a` verb in
// acl.permissions. Unlike IsSubtreeAdmin this is NOT elevation-gated:
// editing config you administer is a standing permission, not a sudo-style
// escape hatch. Elevation (IsActiveAdmin) only ADDS the WORM/destructive
// overrides on top — see policy.InternalDecider.Allow, which consults this
// for VerbA above the WORM clamp (config is not WORM-protected data, and
// VerbA never grants write/delete of records).
func IsConfigEditor(chain PolicyChain, email string) bool {
	if email == "" {
		return false
	}
	return IsAdminForChain(chain, email) || AllowedAction(chain, email, VerbA)
}

// HasAnyAdminGrant reports whether email is named as an admin somewhere
// in the cascade — either the root's admins: list (super-admin) or any
// subtree-admin grant via paths:.<dir>.admins. ELEVATION-INDEPENDENT:
// answers "could this user opt into admin powers if they wanted to?",
// which the header elevation toggle reads to decide whether to render
// itself. The elevation-AWARE checks (IsAdmin, IsSubtreeAdmin) take a
// Principal and short-circuit on !Elevated; this function just asks
// the cascade.
//
// Returns false for an empty email so anonymous callers can't probe.
func HasAnyAdminGrant(fsRoot, email string) bool {
	if email == "" {
		return false
	}
	// Root super-admin.
	if zf, err := ParseFile(filepath.Join(fsRoot, ".zddc")); err == nil {
		for _, pattern := range zf.Admins {
			if MatchesPattern(pattern, email) {
				return true
			}
		}
	}
	// Subtree-admin anywhere — walk every .zddc and check its
	// effective chain.
	dirs, _ := ScanZddcFiles(fsRoot)
	for _, d := range dirs {
		chain, err := EffectivePolicy(fsRoot, d)
		if err != nil {
			continue
		}
		for i, level := range chain.Levels {
			for _, principal := range level.Admins {
				if MatchesPrincipal(principal, email, chain, i) {
					return true
				}
			}
		}
	}
	return false
}

// IsAdmin reports whether p is listed in the admins entry of the ROOT
// .zddc file (<fsRoot>/.zddc). Subdirectory .zddc files' admins keys are
// deliberately ignored by this function — it gates the server-wide debug
// admin role (/.profile/{whoami,config,logs}) which only the bootstrap
// super-admin should reach.
//
// Subtree-scoped admin authority (the "fiefdom" model) is checked via
// IsSubtreeAdmin / CanEditZddc instead.
//
// Patterns use the same glob syntax as acl.permissions keys (see
// MatchesPattern). Returns false if the root file does not exist, has an
// empty Admins list, no entry matches, or the principal hasn't elevated.
func IsAdmin(fsRoot string, p Principal) bool {
	if !p.gate() {
		return false
	}
	zf, err := ParseFile(filepath.Join(fsRoot, ".zddc"))
	if err != nil {
		return false
	}
	for _, pattern := range zf.Admins {
		if MatchesPattern(pattern, p.Email) {
			return true
		}
	}
	return false
}

// IsSubtreeAdmin reports whether email administers the subtree rooted at
// dirPath. Authority cascades: a match against any Admins entry on the chain
// from fsRoot down to dirPath (inclusive) confers admin rights for dirPath.
// Subtree admins own their own .zddc — both reading admin tools and
// writing the file itself are gated by this same check (the file API's
// ActionAdmin path on .zddc edits).
//
// Admins entries may be email-glob patterns OR role references (a bare
// role name, or @role:<name>) — resolved the same way acl.permissions
// keys are, so `admins: [document_controller]` works once a deployment
// populates that role.
func IsSubtreeAdmin(fsRoot, dirPath string, p Principal) bool {
	if !p.gate() {
		return false
	}
	chain, err := EffectivePolicy(fsRoot, dirPath)
	if err != nil {
		return false
	}
	for i, level := range chain.Levels {
		for _, principal := range level.Admins {
			if MatchesPrincipal(principal, p.Email, chain, i) {
				return true
			}
		}
	}
	return false
}

