package credentialgeneration

import (
	"context"
	"errors"
	"fmt"
	"math"
)

var (
	// ErrDispatchGateNotFound reports that an exact immutable generation has no
	// dispatch-eligibility authority. Missing never means enabled.
	ErrDispatchGateNotFound = errors.New("connector credential generation: dispatch gate not found")
	// ErrStoredDispatchGateInvalid reports a row that cannot be reconstructed as
	// one state reachable from enabled/fence-zero through the closed transitions.
	ErrStoredDispatchGateInvalid = errors.New("connector credential generation: stored dispatch gate invalid")
	// ErrInvalidDispatchTransition reports incomplete or non-canonical caller
	// input before any authorization or SQL is attempted.
	ErrInvalidDispatchTransition = errors.New("connector credential generation: invalid dispatch transition")
	// ErrDispatchFenceConflict reports that the caller did not observe the exact
	// current fence. Callers must reread; stale transitions never converge.
	ErrDispatchFenceConflict = errors.New("connector credential generation: dispatch fence conflict")
	// ErrDispatchModeConflict reports an otherwise current request whose source
	// mode is not the one required by the explicit operation.
	ErrDispatchModeConflict = errors.New("connector credential generation: dispatch mode conflict")
	// ErrDispatchFenceExhausted reports that the signed TiDB fence cannot advance
	// without overflow. A fence is never reset or reused.
	ErrDispatchFenceExhausted = errors.New("connector credential generation: dispatch fence exhausted")
	// ErrDispatchAuthorizationDenied reports that the required exact L2 decision
	// owner did not authorize the transition.
	ErrDispatchAuthorizationDenied = errors.New("connector credential generation: dispatch authorization denied")
	// ErrDispatchGenerationNotFound reports that authorization could not load the
	// exact immutable generation named by a transition.
	ErrDispatchGenerationNotFound = errors.New("connector credential generation: dispatch generation not found")
)

// MaxDispatchFenceToken is the greatest value representable by the signed
// BIGINT physical owner. It is readable but cannot be advanced.
const MaxDispatchFenceToken = uint64(math.MaxInt64)

// DispatchMode is the closed dispatch-eligibility state of one exact immutable
// credential generation.
type DispatchMode string

const (
	DispatchModeEnabled DispatchMode = "enabled"
	DispatchModeRevoked DispatchMode = "revoked"
)

// DispatchTransitionAction names one of the two explicit transition APIs.
type DispatchTransitionAction string

const (
	DispatchTransitionRevoke   DispatchTransitionAction = "revoke"
	DispatchTransitionReenable DispatchTransitionAction = "reenable"
)

// DispatchGate is the sole mutable dispatch-eligibility fact for one exact
// immutable generation. Private fields prevent callers from manufacturing a
// mode/fence pair that bypasses stored-state validation.
type DispatchGate struct {
	credentialGenerationID string
	mode                   DispatchMode
	fenceToken             uint64
}

func (gate DispatchGate) CredentialGenerationID() string { return gate.credentialGenerationID }
func (gate DispatchGate) Mode() DispatchMode             { return gate.mode }
func (gate DispatchGate) FenceToken() uint64             { return gate.fenceToken }

// DispatchTransitionAuthorization identifies the exact immutable L2 decision
// and actor whose owner must authorize a transition. These coordinates are
// authorization input, not duplicated gate state or audit authority.
type DispatchTransitionAuthorization struct {
	DecisionID            string
	ActorID               string
	L2PolicyGenerationRef string
}

// DispatchTransitionRequest binds one explicit operation to the exact gate
// fence observed by its caller.
type DispatchTransitionRequest struct {
	CredentialGenerationID string
	ExpectedFenceToken     uint64
	Authorization          DispatchTransitionAuthorization
}

// DispatchAuthorizationRequest is the canonical input passed to the required
// external decision owner. Generation and Gate have already been loaded and
// validated exactly, but are revalidated under lock after authorization.
type DispatchAuthorizationRequest struct {
	Action                DispatchTransitionAction
	DecisionID            string
	ActorID               string
	L2PolicyGenerationRef string
	ExpectedFenceToken    uint64
	Generation            Generation
	Gate                  DispatchGate
}

// DispatchTransitionAuthorizer fresh-reads the exact L2 decision owner. Both
// methods are mandatory so reenablement cannot accidentally reuse a weaker
// revocation policy, and there is no permissive nil/no-op production fallback.
// Implementations must be read-only and idempotent for an exact request: this
// check runs before the local transaction and may be repeated after a failed
// commit. It must not consume the decision or emit an irreversible side effect.
type DispatchTransitionAuthorizer interface {
	AuthorizeRevocation(context.Context, DispatchAuthorizationRequest) error
	AuthorizeReenablement(context.Context, DispatchAuthorizationRequest) error
}

func (gate DispatchGate) validateStored() error {
	if !validOpaqueIdentifier(gate.credentialGenerationID) {
		return storedDispatchGateInvalid("credential generation ID is not canonical")
	}
	switch gate.mode {
	case DispatchModeEnabled:
		if gate.fenceToken%2 != 0 {
			return storedDispatchGateInvalid("enabled mode requires an even fence")
		}
	case DispatchModeRevoked:
		if gate.fenceToken == 0 || gate.fenceToken%2 != 1 {
			return storedDispatchGateInvalid("revoked mode requires a positive odd fence")
		}
	default:
		return storedDispatchGateInvalid("mode %q is outside the closed dispatch contract", gate.mode)
	}
	if gate.fenceToken > MaxDispatchFenceToken {
		return storedDispatchGateInvalid("fence exceeds signed BIGINT")
	}
	return nil
}

func validateDispatchTransitionRequest(request DispatchTransitionRequest) error {
	for _, field := range []struct {
		name, value string
	}{
		{name: "credential generation ID", value: request.CredentialGenerationID},
		{name: "authorization decision ID", value: request.Authorization.DecisionID},
		{name: "authorization actor ID", value: request.Authorization.ActorID},
		{name: "L2 policy generation ref", value: request.Authorization.L2PolicyGenerationRef},
	} {
		if !validOpaqueIdentifier(field.value) {
			return invalidDispatchTransition("%s is not canonical", field.name)
		}
	}
	if request.ExpectedFenceToken > MaxDispatchFenceToken {
		return invalidDispatchTransition("expected fence exceeds signed BIGINT")
	}
	return nil
}

func invalidDispatchTransition(format string, args ...any) error {
	return fmt.Errorf("%w: %s", ErrInvalidDispatchTransition, fmt.Sprintf(format, args...))
}

func storedDispatchGateInvalid(format string, args ...any) error {
	return fmt.Errorf("%w: %s", ErrStoredDispatchGateInvalid, fmt.Sprintf(format, args...))
}
