package auth

import (
	"net/http"
	"time"

	. "kjol/basic"
	"kjol/l4g"
)

// Permissions are plain strings, aggregated per request by the app's
// PermissionResolver into a single map. The engine does not care where they come
// from — a user's direct grants, the groups they belong to, an organization
// role, a claim snapshot from an SSO login — only whether the map contains what
// a route requires.
//
// Two families of check live here, and the difference matters:
//
//   - Authorized / AuthorizedExact / AuthorizedAny are pure predicates, for
//     display logic. They are expected to fail routinely (most users are not
//     admins, and every "admin only" menu asks on every page), so they log
//     nothing.
//
//   - Require / RequireExact / RequireAny are middleware. A failure here means a
//     request actually reached a route the caller may not have, so it is logged.
//
// The pairing is deliberate: gate the link with Authorized so the request is
// never made, and gate the route with Require so it cannot be made anyway.

// Authorized reports whether the principal holds every listed permission.
// The super permission satisfies any requirement.
func (a *Authenticator) Authorized(p Principal, requirements ...string) bool {
	if a.hasSuper(p) {
		return true
	}
	return holdsAll(p, requirements)
}

// AuthorizedExact reports whether the principal holds every listed permission,
// with no super-permission bypass.
//
// Use it where the question is "does this user really have X" rather than "may
// this user do X" — e.g. showing an admin their own effective grants, where the
// wildcard would otherwise make every box look ticked.
func (a *Authenticator) AuthorizedExact(p Principal, requirements ...string) bool {
	return holdsAll(p, requirements)
}

// AuthorizedAny reports whether the principal holds at least one of the listed
// permissions. The super permission satisfies it.
//
// This is for resources read by several pages whose own permissions differ — a
// product-options list fetched by both the inventory editor and the sales
// terminal — where requiring all of them would lock out callers who legitimately
// hold only one.
func (a *Authenticator) AuthorizedAny(p Principal, anyOf ...string) bool {
	if a.hasSuper(p) {
		return true
	}
	for _, requirement := range anyOf {
		if p.Permissions[requirement] {
			return true
		}
	}
	return false
}

// Require gates a handler on the caller holding every listed permission,
// answering 403 otherwise. Mount it inside LoadContext.
func (a *Authenticator) Require(h http.HandlerFunc, requirements ...string) http.HandlerFunc {
	return a.gate(h, func(p Principal) bool { return a.Authorized(p, requirements...) }, requirements)
}

// RequireExact gates a handler on the caller holding every listed permission,
// with no super-permission bypass.
func (a *Authenticator) RequireExact(h http.HandlerFunc, requirements ...string) http.HandlerFunc {
	return a.gate(h, func(p Principal) bool { return a.AuthorizedExact(p, requirements...) }, requirements)
}

// RequireAny gates a handler on the caller holding at least one of the listed
// permissions.
func (a *Authenticator) RequireAny(h http.HandlerFunc, anyOf ...string) http.HandlerFunc {
	return a.gate(h, func(p Principal) bool { return a.AuthorizedAny(p, anyOf...) }, anyOf)
}

// gate is the shared middleware body: check, log-and-deny, or serve.
func (a *Authenticator) gate(h http.HandlerFunc, allow func(Principal) bool, requirements []string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		p := PrincipalFrom(r)

		if !allow(p) {
			a.logDenial(r, p, requirements)
			if isAPIRequest(r) {
				respondJSONError(w, http.StatusForbidden, "Forbidden: insufficient permissions")
				return
			}
			w.WriteHeader(http.StatusForbidden)
			return
		}

		h(w, r)
	}
}

// logDenial records a failed route authorization.
//
// Unlike a failed display check, this means a request was actually issued for a
// route the caller cannot have: either a user is missing access they need, or
// someone is probing. Both are worth seeing, so this is the one authorization
// path that logs.
func (a *Authenticator) logDenial(r *http.Request, p Principal, requirements []string) {
	entry := l4g.Entry{
		Category: l4g.CATEGORY_AUTH,
		LogType:  l4g.TYPE_WARN,
		Content:  MakePtr("Failed Endpoint Authorization"),
		StructuredContent: l4g.Serialize(struct {
			Route               string
			RequiredPermissions []string
			CurrentPermissions  map[string]bool
			Authenticated       bool
		}{
			Route:               r.URL.Path,
			RequiredPermissions: requirements,
			CurrentPermissions:  p.Permissions,
			Authenticated:       p.Authenticated,
		}),
		Timestamp: time.Now(),
	}

	// Anonymous denials carry no user to attribute the entry to.
	if p.Authenticated {
		entry.AppUserID = MakePtr(p.Session.UserID)
		entry.OrgID = p.Session.OrgID
		entry.IdentityID = MakePtr(p.Session.ID)
	}

	l4g.Write(entry)
}

// hasSuper reports whether the principal holds the app's wildcard permission.
func (a *Authenticator) hasSuper(p Principal) bool {
	return a.cfg.SuperPermission != "" && p.Permissions[a.cfg.SuperPermission]
}

// holdsAll reports whether the principal holds every listed permission.
func holdsAll(p Principal, requirements []string) bool {
	for _, requirement := range requirements {
		if !p.Permissions[requirement] {
			return false
		}
	}
	return true
}
