package middleware

import (
	"net/http"

	"github.com/pingcap-inc/nexuscrm-api-host/dialect"
)

// CredentialLane is the one admission lane a request takes.
//
// A request never takes two lanes. The selector decides which single lane a
// route's declared credential policy permits for the credentials actually
// presented, so the lane is chosen before any credential is validated and cannot
// be influenced by validation results.
type CredentialLane int

const (
	// CredentialLaneRejected means no lane admits this request. It is the zero
	// value, so a selection that is never computed fails closed.
	CredentialLaneRejected CredentialLane = iota

	// CredentialLaneProgrammatic is the Authorization-credential lane. It carries
	// the full programmatic stack, including the effective-scope and ApiEnabled
	// requirements.
	CredentialLaneProgrammatic

	// CredentialLaneBrowserSession is the ambient first-party browser lane, keyed
	// by the session-control proof cookie. It never requires ApiEnabled and is
	// never interpreted as empty-scope API authority.
	CredentialLaneBrowserSession

	// CredentialLaneFirstPartyUnauthenticated admits an accepted first-party
	// request that carries no principal. Session establishment uses it, and
	// termination uses it when the presented proof grants nothing, so a stale
	// browser can still be cleaned up without an authentication oracle.
	CredentialLaneFirstPartyUnauthenticated
)

func (l CredentialLane) String() string {
	switch l {
	case CredentialLaneRejected:
		return "rejected"
	case CredentialLaneProgrammatic:
		return "programmatic"
	case CredentialLaneBrowserSession:
		return "browser_session"
	case CredentialLaneFirstPartyUnauthenticated:
		return "first_party_unauthenticated"
	default:
		return "unknown"
	}
}

// CredentialSelection is the selector's verdict.
type CredentialSelection struct {
	// Lane is the single admitted lane, or CredentialLaneRejected.
	Lane CredentialLane

	// Reason explains a rejection in non-secret terms. It never contains a
	// credential, a proof value, or any part of the Authorization header, because
	// a selector message is reachable by an unauthenticated caller.
	Reason string
}

// Rejected reports whether no lane admits the request.
func (s CredentialSelection) Rejected() bool { return s.Lane == CredentialLaneRejected }

// CredentialSelectorOptions opens the two compatibility windows the design names
// explicitly. Both zero values are the final state, so a caller that forgets to
// configure them gets the strict behavior rather than silently keeping a
// migration allowance forever.
type CredentialSelectorOptions struct {
	// AllowBearerAlongsideBrowserProof permits the pre-cutover pair, where the
	// browser still holds a Bearer and also sends the proof cookie. In that
	// window the Bearer remains the authoritative credential, so the pair takes
	// the programmatic lane and behavior does not change while the cookie is
	// merely being established. In the final state the pair fails closed.
	AllowBearerAlongsideBrowserProof bool

	// AllowBrowserProofOnProgrammaticOnlyRoutes permits a browser proof cookie to
	// ride along on a programmatic-only route, which conforming browsers will do
	// automatically for same-site requests before the UI migration completes. The
	// cookie grants nothing there in either case; the final state rejects the
	// request outright so a browser cannot reach a programmatic-only surface.
	AllowBrowserProofOnProgrammaticOnlyRoutes bool
}

// SelectRequestCredentialLane picks the single admission lane for a route's
// declared credential policy and the credentials a request actually presents.
//
// It reads headers only, performs no I/O, and validates no credential: it decides
// which lane is permitted, and rejects malformed or ambiguous input before any
// lane is taken. Deciding this first is what keeps a caller from selecting its
// own lane by presenting two credentials and letting whichever validates win.
//
// It answers "which credential lane", not "is this caller authorized". A request
// presenting no credential to a policy that requires one is reported as rejected
// with that reason; whether some surface separately permits anonymous access is a
// route-gate question this does not model.
//
// Nothing calls this yet.
func SelectRequestCredentialLane(header http.Header, policy dialect.CredentialPolicy, opts CredentialSelectorOptions) CredentialSelection {
	if !policy.Valid() {
		// An unrecognized policy is a programming error, and guessing a lane for
		// it would be the permissive failure. Reject instead.
		return CredentialSelection{Reason: "route declares an unknown credential policy"}
	}

	authorizationCount := len(header.Values(headerAuthorization))
	proofCount := sessionControlCookieOccurrenceCount(header)
	_, proofUsable := usableSessionControlProof(header)

	if authorizationCount > 1 {
		// Two Authorization headers make the presented credential ambiguous for
		// every policy, including the browser-only ones that reject the header
		// outright.
		return CredentialSelection{Reason: "request carries more than one Authorization header"}
	}

	switch policy {
	case dialect.CredentialProgrammaticOnly:
		if proofCount > 0 && !opts.AllowBrowserProofOnProgrammaticOnlyRoutes {
			return CredentialSelection{Reason: "programmatic-only route rejects a first-party browser session cookie"}
		}
		if authorizationCount != 1 {
			return CredentialSelection{Reason: "programmatic-only route requires an Authorization credential"}
		}
		return CredentialSelection{Lane: CredentialLaneProgrammatic}

	case dialect.CredentialBrowserOrProgrammatic:
		if authorizationCount == 1 && proofCount > 0 {
			if !opts.AllowBearerAlongsideBrowserProof {
				return CredentialSelection{Reason: "request carries both a browser session cookie and an Authorization credential"}
			}
			return CredentialSelection{Lane: CredentialLaneProgrammatic}
		}
		if authorizationCount == 1 {
			return CredentialSelection{Lane: CredentialLaneProgrammatic}
		}
		if proofCount > 1 {
			return CredentialSelection{Reason: "request carries more than one session-control cookie"}
		}
		if proofUsable {
			return CredentialSelection{Lane: CredentialLaneBrowserSession}
		}
		if proofCount == 1 {
			return CredentialSelection{Reason: "session-control cookie is not structurally valid"}
		}
		return CredentialSelection{Reason: "route requires either a browser session cookie or an Authorization credential"}

	case dialect.CredentialBrowserOnly:
		if authorizationCount > 0 {
			return CredentialSelection{Reason: "browser-only route rejects the Authorization header before authentication"}
		}
		if proofCount > 1 {
			return CredentialSelection{Reason: "request carries more than one session-control cookie"}
		}
		if !proofUsable {
			return CredentialSelection{Reason: "browser-only route requires exactly one structurally valid session-control cookie"}
		}
		return CredentialSelection{Lane: CredentialLaneBrowserSession}

	case dialect.CredentialBrowserEstablishment:
		if authorizationCount > 0 {
			return CredentialSelection{Reason: "session establishment rejects the Authorization header"}
		}
		// A stale cookie may still be attached by the browser. It is deliberately
		// not read as a principal and not rejected: establishment rotates any
		// prior session, so the caller cannot choose the credential it ends up
		// with by presenting one.
		return CredentialSelection{Lane: CredentialLaneFirstPartyUnauthenticated}

	case dialect.CredentialBrowserTermination:
		if authorizationCount > 0 {
			return CredentialSelection{Reason: "session termination rejects the Authorization header"}
		}
		if proofUsable {
			return CredentialSelection{Lane: CredentialLaneBrowserSession}
		}
		// Missing, malformed, or duplicated proof grants no principal, but the
		// request is still admitted so the cookie can be expired. Rejecting here
		// would turn logout into an oracle for whether a proof was well formed.
		return CredentialSelection{Lane: CredentialLaneFirstPartyUnauthenticated}
	}

	// Unreachable while Valid() covers every declared policy; kept so a newly
	// added value fails closed instead of falling through to a lane.
	return CredentialSelection{Reason: "route declares an unhandled credential policy"}
}
