package sdkresolvers

import (
	"bytes"
	"fmt"
	"reflect"
	"strings"

	"github.com/yandex-cloud/go-sdk/v2/pkg/sdkerrors"
)

const DefaultResolverPageSize int64 = 100

func CreateResolverFilter(nameField string, value string) string {
	// TODO(novikoff): should we add escaping or value validation?
	return fmt.Sprintf(`%s = "%s"`, nameField, value)
}

type resolveOptions struct {
	out                    *string
	folderID               string
	cloudID                string
	organizationID         string
	organizationIDFallback string
	clusterID              string
	federationID           string
	communityID            string
}

type ResolveOption func(*resolveOptions)

func Out(out *string) ResolveOption {
	return func(o *resolveOptions) {
		o.out = out
	}
}

// FolderID specifies folder id for resolvers that need it (most of the resolvers).
func FolderID(folderID string) ResolveOption {
	return func(o *resolveOptions) {
		o.folderID = folderID
	}
}

// CloudID specifies cloud id for resolvers that need it, e.g. FolderResolver
func CloudID(cloudID string) ResolveOption {
	return func(o *resolveOptions) {
		o.cloudID = cloudID
	}
}

// OrganizationID specifies organization id for resolvers that need it, e.g. OrganizationSamlFederationResolver
func OrganizationID(organizationID string) ResolveOption {
	return func(o *resolveOptions) {
		o.organizationID = organizationID
	}
}

// OrganizationIDFallback supplies a default organization id used by name
// resolvers whose underlying List request marks organization_id required. The
// fallback is consulted only when OrganizationID() is empty. sdk-v2 carries no
// opinion about the value — the caller (e.g. cli-v2) chooses it.
func OrganizationIDFallback(organizationID string) ResolveOption {
	return func(o *resolveOptions) {
		o.organizationIDFallback = organizationID
	}
}

// ClusterID specifies cluster id for resolvers that need it, e.g. DataprocSubclusterResolver
func ClusterID(clusterID string) ResolveOption {
	return func(o *resolveOptions) {
		o.clusterID = clusterID
	}
}

// FederationID specifies federation id for resolvers that need it, e.g. CertificateResolver
func FederationID(federationID string) ResolveOption {
	return func(o *resolveOptions) {
		o.federationID = federationID
	}
}

func combineOpts(opts ...ResolveOption) *resolveOptions {
	o := &resolveOptions{}
	for _, opt := range opts {
		opt(o)
	}
	return o
}

type BaseResolver struct {
	Name string
	id   string
	err  error
	opts *resolveOptions
}

func NewBaseResolver(name string, opts ...ResolveOption) BaseResolver {
	return BaseResolver{
		Name: name,
		opts: combineOpts(opts...),
	}
}

type BaseNameResolver struct {
	BaseResolver

	resolvingObjectType string
}

func NewBaseNameResolver(name string, resolvingObjectType string, opts ...ResolveOption) BaseNameResolver {
	return BaseNameResolver{
		BaseResolver:        NewBaseResolver(name, opts...),
		resolvingObjectType: resolvingObjectType,
	}
}

func (r *BaseResolver) ID() string {
	return r.id
}
func (r *BaseResolver) Err() error {
	return r.err
}

func (r *BaseResolver) SetErr(err error) error {
	if r.err != nil {
		panic(fmt.Sprintf("Trying to change error. Old: %v; New: %v", r.err, err))
	}
	r.err = err
	return r.err
}

func (r *BaseResolver) SetID(id string) {
	r.id = id
	r.writeOut()
}

func (r *BaseResolver) Set(entity Entity, err error) error {
	if err != nil {
		return r.SetErr(err)
	}
	r.SetID(entity.GetId())
	return nil
}

type Entity interface {
	GetId() string
}

func (r *BaseResolver) FolderID() string {
	return r.opts.folderID
}

func (r *BaseResolver) CloudID() string {
	return r.opts.cloudID
}

func (r *BaseResolver) OrganizationID() string {
	return r.opts.organizationID
}

// OrganizationIDFallback returns the caller-supplied default organization id.
// Generated resolvers for List requests that mark organization_id required
// use this value when OrganizationID() is empty.
func (r *BaseResolver) OrganizationIDFallback() string {
	return r.opts.organizationIDFallback
}

func (r *BaseResolver) CommunityID() string {
	return r.opts.communityID
}

func (r *BaseResolver) ClusterID() string {
	return r.opts.clusterID
}

func (r *BaseResolver) FederationID() string {
	return r.opts.federationID
}

func (r *BaseResolver) coordinates() string {
	buf := bytes.Buffer{}
	if r.FederationID() != "" {
		buf.WriteString(fmt.Sprintf("in the federation \"%s\" ", r.FederationID()))
	}
	if r.ClusterID() != "" {
		buf.WriteString(fmt.Sprintf("in the cluster \"%s\" ", r.ClusterID()))
	}
	if r.CloudID() != "" {
		buf.WriteString(fmt.Sprintf("in the cloud \"%s\" ", r.CloudID()))
	}
	if r.FolderID() != "" {
		buf.WriteString(fmt.Sprintf("in the folder \"%s\" ", r.FolderID()))
	}
	return strings.TrimSpace(buf.String())
}

func (r *BaseResolver) writeOut() {
	if r.opts.out != nil {
		*r.opts.out = r.id
	}
}

func (r *BaseNameResolver) FindName(slice interface{}, err error) error {
	return r.SetErr(r.findNameImpl(slice, err))
}

func (r *BaseNameResolver) EnsureFolderID() error {
	if r.FolderID() == "" {
		err := &ErrNotFound{error: fmt.Sprintf("can't resolve %v without folder id specified", r.resolvingObjectType)}
		return r.SetErr(err)
	}

	return nil
}

func (r *BaseNameResolver) EnsureCloudID() error {
	if r.CloudID() == "" {
		err := &ErrNotFound{error: fmt.Sprintf("can't resolve %v without cloud id specified", r.resolvingObjectType)}
		return r.SetErr(err)
	}

	return nil
}

func (r *BaseNameResolver) EnsureOrganizationID() error {
	if r.OrganizationID() == "" {
		err := &ErrNotFound{error: fmt.Sprintf("can't resolve %v without organization id specified", r.resolvingObjectType)}
		return r.SetErr(err)
	}

	return nil
}

func (r *BaseNameResolver) EnsureFederationID() error {
	if r.OrganizationID() == "" {
		err := &ErrNotFound{error: fmt.Sprintf("can't resolve %v without federation id specified", r.resolvingObjectType)}
		return r.SetErr(err)
	}

	return nil
}

func (r *BaseNameResolver) EnsureClusterID() error {
	if r.OrganizationID() == "" {
		err := &ErrNotFound{error: fmt.Sprintf("can't resolve %v without cluster id specified", r.resolvingObjectType)}
		return r.SetErr(err)
	}

	return nil
}

func (r *BaseNameResolver) EnsureCommunityID() error {
	if r.CommunityID() == "" {
		err := &ErrNotFound{error: fmt.Sprintf("can't resolve %v without community id specified", r.resolvingObjectType)}

		return r.SetErr(err)
	}

	return nil
}

func NewErrNotFound(err string) error {
	return &ErrNotFound{error: err}
}

type ErrNotFound struct {
	error string
}

func (e *ErrNotFound) Error() string {
	return e.error
}

func errNotFound(caption, name string) error {
	return &ErrNotFound{error: fmt.Sprintf("%v with name \"%v\" not found", caption, name)}
}

func (r *BaseNameResolver) findNameImpl(slice interface{}, err error) error {
	if err != nil {
		return sdkerrors.WithMessagef(err, "failed to find %v with name \"%v\" %v", r.resolvingObjectType, r.Name, r.coordinates())
	}
	rv := reflect.ValueOf(slice)
	var found nameAndID
	for i := 0; i < rv.Len(); i++ {
		v := rv.Index(i).Interface().(nameAndID)
		if v.GetName() == r.Name {
			if found != nil {
				return fmt.Errorf("multiple %v items with name \"%v\" found %v", r.resolvingObjectType, r.Name, r.coordinates())
			}
			found = v
		}
	}
	if found == nil {
		return errNotFound(r.resolvingObjectType, r.Name)
	}
	r.SetID(found.GetId())
	return nil
}

type nameAndID interface {
	GetId() string
	GetName() string
}
