package query

import (
	"errors"
	"fmt"
	"time"

	"github.com/RafayLabs/rcloud-base/internal/random"
	commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
	"github.com/uptrace/bun"
)

const (
	// DefaultLimit is the default limit if no limit is set in query options
	DefaultLimit = 10
	// MaxLimit is the max limit for page size
	MaxLimit = 50
	// MaxOffset is the max offset
	MaxOffset = 100000
	// DefaultOrderBy is the default column used to order results
	DefaultOrderBy = "created_at"
	orderASC       = "ASC"
	orderDESC      = "DESC"
	// DefaultOrder is the order of the results
	DefaultOrder = orderASC
)

const (
	nameWithAliasQ           = "?TableAlias.name = ?"
	idWithAliasQ             = "?TableAlias.id = ?"
	partnerIDWithAliasQ      = "?TableAlias.partner_id = ?"
	organizationIDWithAliasQ = "?TableAlias.organization_id = ?"
	projectIDwithAliasQ      = "?TableAlias.project_id = ?"

	nameQ        = "name = ?"
	deletedAtQ   = "deleted_at = ?"
	modifiedAtQ  = "modified_at = ?"
	displayNameQ = "display_name = ?"
	labelsQ      = "labels = ?"
	annotationsQ = "annotations = ?"
	idQ          = "id = ?"
)

const (
	scopeOrganization = "organization"
	scopePartner      = "partner"
	scopeProject      = "project"
	scopeProjects     = "projects"
	scopeUser         = "user"
	scopeCluster      = "cluster"
	scopeSSOUser      = "ssouser"
)

var (
	// ErrNoName is returned when name is not set in query option
	// trying to build query for get/update/delete
	ErrNoName     = errors.New("name not set in options")
	ErrNoNameOrID = errors.New("neither name nor id is set in options")
)

// Option is the functional query option signature
type Option func(*commonv3.QueryOptions)

type setOption func(q *bun.SelectQuery, opts *commonv3.QueryOptions) (*bun.SelectQuery, error)

// WithMeta sets meta in query options
func WithMeta(o *commonv3.Metadata) Option {
	return func(opts *commonv3.QueryOptions) {
		opts.Name = o.GetName()
		opts.ID = o.GetId()
		opts.Partner = o.GetPartner()
		opts.Organization = o.GetOrganization()
		opts.Project = o.GetProject()
		opts.DisplayName = o.GetName()
		opts.Labels = o.GetLabels()
		opts.Annotations = o.GetAnnotations()
	}
}

// WithOptions copies options to query options
func WithOptions(in *commonv3.QueryOptions) Option {
	return func(opts *commonv3.QueryOptions) {
		*opts = *in
		opts.Limit = getLimit(opts)
		opts.Offset = getOffset(opts)
	}
}

// WithIgnoreScopeDefault ignores default values for scope when building queries
func WithIgnoreScopeDefault() Option {
	return func(opts *commonv3.QueryOptions) {
		opts.IgnoreScopeDefault = true
	}
}

// WithExtended sets extended in query options
func WithExtended() Option {
	return func(opts *commonv3.QueryOptions) {
		opts.Extended = true
	}
}

// WithGlobalScope should be used to query resources in global scope
// partnerID, orgID, projectID = 0, 0, 0
func WithGlobalScope() Option {
	return func(opts *commonv3.QueryOptions) {
		opts.GlobalScope = true
	}
}

func getLimit(opts *commonv3.QueryOptions) int64 {
	// if limit is < 0 send all records
	if opts.Limit == 0 {
		return DefaultLimit
	}

	if opts.Limit > MaxLimit {
		return MaxLimit
	}
	return opts.Limit
}

func getOffset(opts *commonv3.QueryOptions) int64 {
	if opts.Offset > MaxOffset {
		return MaxOffset
	}
	return opts.Offset
}

func Paginate(q *bun.SelectQuery, opts *commonv3.QueryOptions) *bun.SelectQuery {
	limit := getLimit(opts)

	if limit > 0 {
		offset := getOffset(opts)
		q = q.Limit(int(limit)).Offset(int(offset))
	}

	return q
}

// Select builds query for selecting resources
func Select(q *bun.SelectQuery, opts *commonv3.QueryOptions) (*bun.SelectQuery, error) {
	var err error
	q, err = setRequestMeta(q, opts)
	if err != nil {
		return nil, err
	}

	q, err = FilterLabels(q, opts)
	if err != nil {
		return nil, err
	}

	//q = Order(q, opts)

	return q, nil
}

// GetAccountID returns account ID from QueryOptions
func GetAccountID(opts *commonv3.QueryOptions) (string, error) {
	switch {
	case opts.GlobalScope:
		return "", nil
	default:
		return opts.Account, nil

	}
}

// GetOrganizationID returns organization id from QueryOptions
func GetOrganizationID(opts *commonv3.QueryOptions) (string, error) {
	switch {
	case opts.GlobalScope:
		return "", nil
	default:
		return opts.Organization, nil
	}
}

func setRequestMeta(q *bun.SelectQuery, opts *commonv3.QueryOptions) (*bun.SelectQuery, error) {
	var err error
	for _, f := range []setOption{
		setPartner,
		setOrganization,
		setProject,
	} {
		q, err = f(q, opts)
		if err != nil {
			return nil, err
		}
	}

	return q, nil

}

func setProject(q *bun.SelectQuery, opts *commonv3.QueryOptions) (*bun.SelectQuery, error) {
	id := opts.Project

	if !opts.GlobalScope && id != "" {
		q = q.Where(projectIDwithAliasQ, id)
	}

	return q, nil
}

func setOrganization(q *bun.SelectQuery, opts *commonv3.QueryOptions) (*bun.SelectQuery, error) {

	id := opts.Organization

	if !opts.GlobalScope && id != "" {
		q = q.Where(organizationIDWithAliasQ, id)
	}

	return q, nil

}

func setPartner(q *bun.SelectQuery, opts *commonv3.QueryOptions) (*bun.SelectQuery, error) {
	id := opts.Partner

	// global scope takes precedence over ignore scope default
	if !opts.GlobalScope && id != "" {
		q = q.Where(partnerIDWithAliasQ, id)
	}

	return q, nil
}

// WithName sets name in query options
func WithName(name string) Option {
	return func(opts *commonv3.QueryOptions) {
		opts.Name = name
	}
}

// WithSelector sets selector in query options
func WithSelector(selector string) Option {
	return func(opts *commonv3.QueryOptions) {
		opts.Selector = selector
	}
}

// WithDeleted sets deleted in query options
func WithDeleted() Option {
	return func(opts *commonv3.QueryOptions) {
		opts.Deleted = true
	}
}

// WithPartnerID sets partner id in query options
func WithPartnerID(partnerID string) Option {
	return func(opts *commonv3.QueryOptions) {
		opts.Partner = partnerID
	}
}

// WithOrganizationID sets organization id in query options
func WithOrganizationID(organizationID string) Option {
	return func(opts *commonv3.QueryOptions) {
		opts.Organization = organizationID
	}
}

// WithProjectID sets project id in query options
func WithProjectID(projectID string) Option {
	return func(opts *commonv3.QueryOptions) {
		opts.Project = projectID
	}
}

// GetClusterID returns cluster ID from QueryOptions
func GetClusterID(opts *commonv3.QueryOptions) (string, error) {
	switch {
	case opts.GlobalScope:
		return "", nil
	default:
		return opts.ClusterID, nil

	}
}

// Get builds query for getting resource
func Get(q *bun.SelectQuery, opts *commonv3.QueryOptions) (*bun.SelectQuery, error) {

	var err error
	if !opts.GlobalScope {
		q, err = setRequestMeta(q, opts)
	}

	if err != nil {
		return nil, err
	}

	if opts.Name == "" && opts.ID == "" {
		return nil, ErrNoNameOrID
	}

	if opts.Name != "" {
		q = q.Where(nameWithAliasQ, opts.Name)
	} else if opts.ID != "" {
		q = q.Where(idWithAliasQ, opts.ID)
	}

	return q, nil
}

// Update builds query for updating resource
func Update(uq *bun.UpdateQuery, opts *commonv3.QueryOptions) (*bun.UpdateQuery, error) {

	uq = uq.Set(modifiedAtQ, time.Now())
	if opts.DisplayName != "" {
		uq = uq.Set(displayNameQ, opts.DisplayName)
	}
	if opts.Labels != nil {
		uq = uq.Set(labelsQ, opts.Labels)
	}
	if opts.Annotations != nil {
		uq = uq.Set(annotationsQ, opts.Annotations)
	}

	return uq, nil
}

// Delete builds query for deleting resource
func Delete(q *bun.SelectQuery, opts *commonv3.QueryOptions) (*bun.UpdateQuery, error) {
	var err error
	q, err = Get(q, opts)
	if err != nil {
		return nil, err
	}

	uq := q.DB().NewUpdate()
	if opts.Deleted {
		uq = uq.Set(nameQ, fmt.Sprintf("%s-%s", opts.Name, random.NewRandomString(10)))
	}
	now := time.Now()

	uq = uq.Set(deletedAtQ, &now)

	return uq, nil
}
