package ports

import (
	"context"
	"strings"
	"time"
)

// AuthorizationGrant is an immutable authorization era for one subject and
// one authorization client. A credential binds to ID, rather than to a mutable
// policy version, so revoking an era can never be undone by later policy edits.
//
// The public contract deliberately uses subject/client vocabulary. Product
// distributions map their own identity and client records to this primitive.
type AuthorizationGrant struct {
	ID            string
	SubjectID     string
	ClientID      string
	Scope         string
	PolicyVersion int
}

// AuthorizationGrantRequest is the policy-approved authorization fact to
// persist. Scope is opaque to L0: the product that owns the client policy must
// normalize and authorize it before this call.
type AuthorizationGrantRequest struct {
	SubjectID     string
	ClientID      string
	Scope         string
	PolicyVersion int
}

// AuthorizationGrantConnection is the self-service projection of one live
// authorization-client relationship. A live grant is the only authority for
// showing a connection; credentials are deliberately not consulted.
type AuthorizationGrantConnection struct {
	ClientID         string
	ClientName       string
	Scope            string
	FirstConnectedAt time.Time
	LastUsedAt       *time.Time
	PolicyVersion    int
}

// AuthorizationGrantRepository persists and revokes authorization grants.
//
// Every mutation requires a caller-owned pessimistic transaction that has
// already acquired the authorization-client lock and marked the context with
// WithAuthorizationClientLock. The repository then locks the subject row before
// reading or changing the grant. This makes the lock order explicit and
// product-neutral: client -> subject -> grant. L0 never decides how a product
// stores or locks its client record.
type AuthorizationGrantRepository interface {
	EnsureAuthorizationGrant(ctx context.Context, request AuthorizationGrantRequest) (AuthorizationGrant, error)
	AuthorizationGrantActive(ctx context.Context, grantID, subjectID, clientID string, policyVersion int) (bool, error)
	RevokeAuthorizationGrantsForSubjectAndClient(ctx context.Context, subjectID, clientID, revokedBy string) (int, error)
	// ConsumeUnboundOAuthCodesForSubject consumes the still-redeemable
	// pre-grant authorization codes for one subject. lockingClientID identifies
	// the caller-owned client lock that serializes this cross-client cleanup; it
	// is not a filter, so every unbound code for the subject is consumed.
	ConsumeUnboundOAuthCodesForSubject(ctx context.Context, subjectID, lockingClientID string) (int, error)
	RevokeAuthorizationGrantsForClient(ctx context.Context, clientID, revokedBy string) (int, error)
	ListLiveAuthorizationGrantConnections(ctx context.Context, subjectID string) ([]AuthorizationGrantConnection, error)
}

type authorizationClientLockContextKey struct{}

// authorizationGrantTransactionScope is a per-transaction identity. It is
// deliberately opaque: transaction managers issue a fresh scope for every
// transaction they inject, so a client-lock marker inherited through DetachTx
// cannot authorize work in a later transaction.
type authorizationGrantTransactionScope struct {
	// Keep this token non-zero-sized. Go may give distinct zero-sized
	// allocations the same address, which would make a fresh transaction look
	// like the one that was detached.
	identity byte
}

type authorizationGrantTransactionScopeContextKey struct{}

type authorizationClientLock struct {
	clientID string
	scope    *authorizationGrantTransactionScope
}

// WithAuthorizationGrantTransactionScope installs a fresh opaque transaction
// identity. Transaction-manager implementations must call it when they inject
// a transaction into a context and clear it when detaching that transaction.
func WithAuthorizationGrantTransactionScope(ctx context.Context) context.Context {
	return context.WithValue(ctx, authorizationGrantTransactionScopeContextKey{}, &authorizationGrantTransactionScope{identity: 1})
}

// WithoutAuthorizationGrantTransactionScope clears the current transaction
// identity. This makes any inherited client-lock marker fail closed until the
// caller re-acquires its client lock in a newly injected transaction.
func WithoutAuthorizationGrantTransactionScope(ctx context.Context) context.Context {
	return context.WithValue(ctx, authorizationGrantTransactionScopeContextKey{}, (*authorizationGrantTransactionScope)(nil))
}

// WithAuthorizationClientLock marks a context whose caller has already locked
// clientID in its pessimistic transaction. It is a contract marker, not a lock
// implementation: L1 owns the concrete client record and must acquire that
// record lock before applying this marker.
func WithAuthorizationClientLock(ctx context.Context, clientID string) context.Context {
	scope, _ := ctx.Value(authorizationGrantTransactionScopeContextKey{}).(*authorizationGrantTransactionScope)
	return context.WithValue(ctx, authorizationClientLockContextKey{}, authorizationClientLock{
		clientID: strings.TrimSpace(clientID),
		scope:    scope,
	})
}

// AuthorizationClientLockHeld reports whether ctx carries the lock marker for
// this exact client. Repositories use it to fail closed rather than accidentally
// introduce a subject -> client lock inversion.
func AuthorizationClientLockHeld(ctx context.Context, clientID string) bool {
	locked, ok := ctx.Value(authorizationClientLockContextKey{}).(authorizationClientLock)
	scope, scopeOK := ctx.Value(authorizationGrantTransactionScopeContextKey{}).(*authorizationGrantTransactionScope)
	return ok && scopeOK && scope != nil &&
		locked.scope == scope &&
		locked.clientID != "" &&
		locked.clientID == strings.TrimSpace(clientID)
}
