package graph

import (
	"errors"
	"maps"
	"sync/atomic"
	"time"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/structpb"

	openfgav1 "github.com/openfga/api/proto/openfga/v1"

	"github.com/openfga/openfga/pkg/storage"
	"github.com/openfga/openfga/pkg/tuple"
)

type ResolveCheckRequest struct {
	StoreID                   string
	AuthorizationModelID      string // TODO replace with typesystem
	TupleKey                  *openfgav1.TupleKey
	ContextualTuples          []*openfgav1.TupleKey
	Context                   *structpb.Struct
	RequestMetadata           *ResolveCheckRequestMetadata
	VisitedPaths              map[string]struct{}
	Consistency               openfgav1.ConsistencyPreference
	LastCacheInvalidationTime time.Time

	// SelectedStrategy is the strategy selected by the planner for this request chain.
	// When set, child requests dispatched within that strategy should continue using
	// the same strategy instead of calling the planner again. This prevents re-planning
	// during recursive dispatch calls (e.g., when default strategy calls checkTTU multiple times).
	SelectedStrategy string

	// invariantCacheKey encodes the request parts that are constant across
	// sub-problems (storeID, modelID, context, contextual tuples). Computed
	// once and propagated to clones.
	invariantCacheKey uint64

	objectType string
	userType   string
}

type ResolveCheckRequestMetadata struct {
	// Thinking of a Check as a tree of evaluations,
	// Depth is the current level in the tree in the current path that we are exploring.
	// When we jump one level, we increment it by 1. If it hits maxResolutionDepth (resolveNodeLimit), we throw ErrResolutionDepthExceeded.
	Depth uint32

	// DispatchCounter is the address to a shared counter that keeps track of how many calls to ResolveCheck we had to do
	// to solve the root/parent problem.
	// The contents of this counter will be written by concurrent goroutines.
	// After the root problem has been solved, this value can be read.
	DispatchCounter *atomic.Uint32

	// DispatchThrottled indicates whether the request was throttled by the dispatch throttling check resolver
	DispatchThrottled *atomic.Bool

	// DatastoreThrottled indicates whether this request was throttled at the datastore level.
	DatastoreThrottled *atomic.Bool
}

type ResolveCheckRequestParams struct {
	StoreID                   string
	TupleKey                  *openfgav1.TupleKey
	ContextualTuples          []*openfgav1.TupleKey
	Context                   *structpb.Struct
	Consistency               openfgav1.ConsistencyPreference
	LastCacheInvalidationTime time.Time
	AuthorizationModelID      string
}

func NewCheckRequestMetadata() *ResolveCheckRequestMetadata {
	return &ResolveCheckRequestMetadata{
		DispatchCounter:    new(atomic.Uint32),
		DispatchThrottled:  new(atomic.Bool),
		DatastoreThrottled: new(atomic.Bool),
	}
}

func NewResolveCheckRequest(
	params ResolveCheckRequestParams,
) (*ResolveCheckRequest, error) {
	if params.AuthorizationModelID == "" {
		return nil, errors.New("missing authorization_model_id")
	}

	if params.StoreID == "" {
		return nil, errors.New("missing store_id")
	}

	userType := tuple.GetType(params.TupleKey.GetUser())
	if tuple.IsObjectRelation(params.TupleKey.GetUser()) {
		objectRelation := tuple.GetRelation(params.TupleKey.GetUser())
		userType = tuple.ToObjectRelationString(userType, objectRelation)
	}

	r := &ResolveCheckRequest{
		StoreID:              params.StoreID,
		AuthorizationModelID: params.AuthorizationModelID,
		TupleKey:             params.TupleKey,
		ContextualTuples:     params.ContextualTuples,
		Context:              params.Context,
		VisitedPaths:         make(map[string]struct{}),
		RequestMetadata:      NewCheckRequestMetadata(),
		Consistency:          params.Consistency,
		// avoid having to read from cache consistently by propagating it
		LastCacheInvalidationTime: params.LastCacheInvalidationTime,

		objectType: tuple.GetType(params.TupleKey.GetObject()),
		userType:   userType,
	}

	r.invariantCacheKey = storage.InvariantCacheKey(params.StoreID, params.AuthorizationModelID, params.Context, params.ContextualTuples...)

	return r, nil
}

func (r *ResolveCheckRequest) clone() *ResolveCheckRequest {
	var requestMetadata *ResolveCheckRequestMetadata
	origRequestMetadata := r.GetRequestMetadata()
	if origRequestMetadata != nil {
		requestMetadata = &ResolveCheckRequestMetadata{
			DispatchCounter:    origRequestMetadata.DispatchCounter,
			Depth:              origRequestMetadata.Depth,
			DispatchThrottled:  origRequestMetadata.DispatchThrottled,
			DatastoreThrottled: origRequestMetadata.DatastoreThrottled,
		}
	}

	var tupleKey *openfgav1.TupleKey
	if origTupleKey := r.GetTupleKey(); origTupleKey != nil {
		tupleKey = proto.Clone(origTupleKey).(*openfgav1.TupleKey)
	}

	cloned := &ResolveCheckRequest{
		StoreID:                   r.GetStoreID(),
		AuthorizationModelID:      r.GetAuthorizationModelID(),
		TupleKey:                  tupleKey,
		ContextualTuples:          r.GetContextualTuples(),
		Context:                   r.GetContext(),
		RequestMetadata:           requestMetadata,
		VisitedPaths:              maps.Clone(r.GetVisitedPaths()),
		Consistency:               r.GetConsistency(),
		LastCacheInvalidationTime: r.GetLastCacheInvalidationTime(),
		invariantCacheKey:         r.GetInvariantCacheKey(),
		SelectedStrategy:          r.GetSelectedStrategy(),
		objectType:                r.GetObjectType(),
		userType:                  r.GetUserType(),
	}
	return cloned
}

func (r *ResolveCheckRequest) GetStoreID() string {
	if r == nil {
		return ""
	}
	return r.StoreID
}

func (r *ResolveCheckRequest) GetAuthorizationModelID() string {
	if r == nil {
		return ""
	}
	return r.AuthorizationModelID
}

func (r *ResolveCheckRequest) GetTupleKey() *openfgav1.TupleKey {
	if r == nil {
		return nil
	}
	return r.TupleKey
}

func (r *ResolveCheckRequest) GetContextualTuples() []*openfgav1.TupleKey {
	if r == nil {
		return nil
	}
	return r.ContextualTuples
}

func (r *ResolveCheckRequest) GetRequestMetadata() *ResolveCheckRequestMetadata {
	if r == nil {
		return nil
	}
	return r.RequestMetadata
}

func (r *ResolveCheckRequest) GetContext() *structpb.Struct {
	if r == nil {
		return nil
	}
	return r.Context
}

func (r *ResolveCheckRequest) GetConsistency() openfgav1.ConsistencyPreference {
	if r == nil {
		return openfgav1.ConsistencyPreference_UNSPECIFIED
	}
	return r.Consistency
}

func (r *ResolveCheckRequest) GetVisitedPaths() map[string]struct{} {
	if r == nil {
		return map[string]struct{}{}
	}
	return r.VisitedPaths
}

func (r *ResolveCheckRequest) GetLastCacheInvalidationTime() time.Time {
	if r == nil {
		return time.Time{}
	}
	return r.LastCacheInvalidationTime
}

func (r *ResolveCheckRequest) GetInvariantCacheKey() uint64 {
	if r == nil {
		return 0
	}
	return r.invariantCacheKey
}

func (r *ResolveCheckRequest) GetSelectedStrategy() string {
	if r == nil {
		return ""
	}
	return r.SelectedStrategy
}

func (r *ResolveCheckRequest) GetObjectType() string {
	if r == nil {
		return ""
	}
	return r.objectType
}

func (r *ResolveCheckRequest) GetUserType() string {
	if r == nil {
		return ""
	}
	return r.userType
}
