package dialect

import "fmt"

// CredentialPolicy declares which credential class a route may admit. It is the
// one neutral policy vocabulary for every authenticated or browser-session-
// establishing HTTP route in this host, whether the route is declared by a
// product dialect manifest, by a product runtime surface, or by an app-blind
// host route this module authors.
//
// The policy names a credential class, never a product route. Enforcement reads
// this declaration and never inspects a path prefix, so no product route grammar
// enters this module.
type CredentialPolicy uint8

const (
	// CredentialProgrammaticOnly is the zero value, so a route that declares
	// nothing keeps the programmatic admission it has today and never gains
	// ambient browser-cookie access.
	CredentialProgrammaticOnly CredentialPolicy = iota

	// CredentialBrowserOrProgrammatic admits either an established browser
	// session or a scoped programmatic credential. Exactly one lane runs per
	// request; admitting the browser lane waives no permission gate, record
	// authorization, write admission, audit, or CSRF control.
	CredentialBrowserOrProgrammatic

	// CredentialBrowserOnly requires an established browser session and rejects
	// a caller-supplied Authorization credential even when a valid session is
	// also present.
	CredentialBrowserOnly

	// CredentialBrowserEstablishment requires first-party browser admission but
	// no existing session, and accepts no caller-supplied authentication
	// credential. Only an establishment handler may issue the session cookie.
	CredentialBrowserEstablishment

	// CredentialBrowserTermination requires first-party browser admission,
	// rejects a caller-supplied Authorization credential, and never derives a
	// principal from an invalid proof.
	CredentialBrowserTermination
)

// Valid reports whether p is a declared policy. Callers fail closed on false:
// an unrecognized value must never be treated as the zero value, because that
// would silently downgrade an intended browser policy to programmatic-only, or
// mount a route whose intended admission this host does not implement.
func (p CredentialPolicy) Valid() bool {
	switch p {
	case CredentialProgrammaticOnly,
		CredentialBrowserOrProgrammatic,
		CredentialBrowserOnly,
		CredentialBrowserEstablishment,
		CredentialBrowserTermination:
		return true
	default:
		return false
	}
}

// A policy is described by three orthogonal admission dimensions. Widening means
// gaining any one of them, so they have to be independent: collapsing "no session
// is required because a programmatic credential may be used" together with "no
// session is required at all" hides a real widening.
//
//	policy                   programmatic  established session  unauthenticated first-party
//	programmatic_only        yes           no                   no
//	browser_or_programmatic  yes           yes                  no
//	browser_only             no            yes                  no
//	browser_establishment    no            no                   yes
//	browser_termination      no            yes                  yes

// AdmitsProgrammaticCredential reports whether a caller-supplied Authorization
// credential may be admitted. The three browser policies reject one outright, so
// a browser route cannot be driven by a bearer token that happens to be present.
func (p CredentialPolicy) AdmitsProgrammaticCredential() bool {
	switch p {
	case CredentialProgrammaticOnly, CredentialBrowserOrProgrammatic:
		return true
	default:
		return false
	}
}

// AdmitsEstablishedBrowserSession reports whether an existing valid browser
// session may be the request's principal. Termination admits one because it
// resolves a valid proof in order to revoke it; establishment does not, because it
// runs before a session exists.
func (p CredentialPolicy) AdmitsEstablishedBrowserSession() bool {
	switch p {
	case CredentialBrowserOrProgrammatic, CredentialBrowserOnly, CredentialBrowserTermination:
		return true
	default:
		return false
	}
}

// AdmitsUnauthenticatedFirstParty reports whether an admitted first-party request
// carrying no valid credential or session may still be served. Only establishment
// and termination may: establishment authenticates and issues a session, and
// termination must still answer a stale browser with the cookie-clearing response
// without granting it a principal.
//
// This is the widest admission in the vocabulary, which is why it is tracked
// separately from "does not require an established session".
func (p CredentialPolicy) AdmitsUnauthenticatedFirstParty() bool {
	switch p {
	case CredentialBrowserEstablishment, CredentialBrowserTermination:
		return true
	default:
		return false
	}
}

// AdmitsBrowserAdmission reports whether the browser lane may be entered at all.
// It is false for the zero value, which is what keeps an undeclared route
// unreachable by an ambient browser credential.
func (p CredentialPolicy) AdmitsBrowserAdmission() bool {
	return p.AdmitsEstablishedBrowserSession() || p.AdmitsUnauthenticatedFirstParty()
}

// RequiresEstablishedBrowserSession reports whether admission requires a session
// that already exists: the policy admits neither a programmatic credential nor an
// unauthenticated first-party request. Only CredentialBrowserOnly does.
//
// Deprecated: this predicate cannot compare two policies. "Does not require an
// established session" does not distinguish a policy that accepts a programmatic
// credential from one that accepts no credential at all, and treating those as the
// same hid two real widenings. Use AdmitsProgrammaticCredential,
// AdmitsEstablishedBrowserSession, and AdmitsUnauthenticatedFirstParty, or Widens,
// which is derived from all three.
func (p CredentialPolicy) RequiresEstablishedBrowserSession() bool {
	return p.AdmitsEstablishedBrowserSession() &&
		!p.AdmitsProgrammaticCredential() &&
		!p.AdmitsUnauthenticatedFirstParty()
}

// Widens reports whether p admits a request that base would reject. It is the
// predicate a composition step uses to refuse a route override that broadens
// admission: a replacement may narrow a policy or keep it, never widen it.
//
// Two policies can widen each other. CredentialProgrammaticOnly and
// CredentialBrowserOnly each admit a class the other rejects, so neither may
// replace the other in either direction. Reporting both as widening is the
// fail-closed answer for policies that are not comparable.
func (p CredentialPolicy) Widens(base CredentialPolicy) bool {
	if p.AdmitsProgrammaticCredential() && !base.AdmitsProgrammaticCredential() {
		return true
	}
	if p.AdmitsEstablishedBrowserSession() && !base.AdmitsEstablishedBrowserSession() {
		return true
	}
	if p.AdmitsUnauthenticatedFirstParty() && !base.AdmitsUnauthenticatedFirstParty() {
		return true
	}
	return false
}

func (p CredentialPolicy) String() string {
	switch p {
	case CredentialProgrammaticOnly:
		return "programmatic_only"
	case CredentialBrowserOrProgrammatic:
		return "browser_or_programmatic"
	case CredentialBrowserOnly:
		return "browser_only"
	case CredentialBrowserEstablishment:
		return "browser_establishment"
	case CredentialBrowserTermination:
		return "browser_termination"
	default:
		return fmt.Sprintf("CredentialPolicy(%d)", uint8(p))
	}
}
