package featureflags

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"strconv"
	"strings"
	"time"

	"openreplay/backend/pkg/db/postgres/pool"

	"github.com/jackc/pgtype"
)

type FeatureFlagsRequest struct {
	ProjectID    string                 `json:"projectID"`
	UserOS       string                 `json:"os"`
	UserDevice   string                 `json:"device"`
	UserCountry  string                 `json:"country"`
	UserState    string                 `json:"state"`
	UserCity     string                 `json:"city"`
	UserBrowser  string                 `json:"browser"`
	Referrer     string                 `json:"referrer"`
	UserID       string                 `json:"userID"`
	Metadata     map[string]string      `json:"metadata"`
	PersistFlags map[string]interface{} `json:"persistFlags"` // bool or string
}

type FeatureFlagsResponse struct {
	Flags []interface{} `json:"flags"`
}

type FilterType string

const (
	UserCountry FilterType = "userCountry"
	UserCity    FilterType = "userCity"
	UserState   FilterType = "userState"
	UserOS      FilterType = "userOs"
	UserBrowser FilterType = "userBrowser"
	UserDevice  FilterType = "userDevice"
	UserID      FilterType = "userId"
	Referrer    FilterType = "referrer"
	Metadata    FilterType = "metadata"
)

type FilterOperator string

const (
	Is          FilterOperator = "is"
	IsNot       FilterOperator = "isNot"
	IsAny       FilterOperator = "isAny"
	Contains    FilterOperator = "contains"
	NotContains FilterOperator = "notContains"
	StartsWith  FilterOperator = "startsWith"
	EndsWith    FilterOperator = "endsWith"
	IsUndefined FilterOperator = "isUndefined"
)

type FeatureFlagFilter struct {
	Type     FilterType     `json:"type"`
	Operator FilterOperator `json:"operator"`
	Source   string         `json:"source"`
	Values   []string       `json:"value"`
}

type FeatureFlagCondition struct {
	Filters           []*FeatureFlagFilter
	RolloutPercentage int
}

type FeatureFlagVariant struct {
	Value             string
	Payload           string
	RolloutPercentage int
}

type FlagType string

const (
	Single FlagType = "single"
	Multi  FlagType = "multi"
)

type FeatureFlag struct {
	FlagID     uint32
	FlagKey    string
	FlagType   FlagType
	IsPersist  bool
	Payload    string
	Conditions []*FeatureFlagCondition
	Variants   []*FeatureFlagVariant
}

type FeatureFlagPG struct {
	FlagID             uint32
	FlagKey            string
	FlagType           string
	IsPersist          bool
	Payload            *string
	RolloutPercentages pgtype.EnumArray
	Filters            pgtype.TextArray
	Values             pgtype.TextArray
	Payloads           pgtype.TextArray
	VariantRollout     pgtype.EnumArray
}

type flagInfo struct {
	Key       string      `json:"key"`
	IsPersist bool        `json:"is_persist"`
	Value     interface{} `json:"value"`
	Payload   string      `json:"payload"`
}

func numArrayToIntSlice(arr *pgtype.EnumArray) []int {
	slice := make([]int, 0, len(arr.Elements))
	for i := range arr.Elements {
		num, err := strconv.Atoi(arr.Elements[i].String)
		if err != nil {
			slice = append(slice, 0)
		} else {
			slice = append(slice, num)
		}
	}
	return slice
}

func parseFlagConditions(conditions *pgtype.TextArray, rolloutPercentages *pgtype.EnumArray) ([]*FeatureFlagCondition, error) {
	percents := numArrayToIntSlice(rolloutPercentages)
	if len(conditions.Elements) != len(percents) {
		return nil, fmt.Errorf("error: len(conditions.Elements) != len(percents)")
	}
	conds := make([]*FeatureFlagCondition, 0, len(conditions.Elements))
	for i, currCond := range conditions.Elements {
		var filters []*FeatureFlagFilter
		cutInx := strings.Index(currCond.String, "[")
		if cutInx >= 0 {
			currCond.String = currCond.String[cutInx:]
		}
		err := json.Unmarshal([]byte(currCond.String), &filters)
		if err != nil {
			return nil, fmt.Errorf("filter unmarshal error: %v", err)
		}
		conds = append(conds, &FeatureFlagCondition{
			Filters:           filters,
			RolloutPercentage: percents[i],
		})
	}
	return conds, nil
}

func parseFlagVariants(values *pgtype.TextArray, payloads *pgtype.TextArray, variantRollout *pgtype.EnumArray) ([]*FeatureFlagVariant, error) {
	percents := numArrayToIntSlice(variantRollout)
	variants := make([]*FeatureFlagVariant, 0, len(values.Elements))
	if len(values.Elements) != len(payloads.Elements) || len(values.Elements) != len(percents) {
		return nil, fmt.Errorf("wrong number of variant elements")
	}
	for i := range values.Elements {
		variants = append(variants, &FeatureFlagVariant{
			Value:             values.Elements[i].String,
			Payload:           payloads.Elements[i].String,
			RolloutPercentage: percents[i],
		})
	}
	return variants, nil
}

func ParseFeatureFlag(rawFlag *FeatureFlagPG) (*FeatureFlag, error) {
	flag := &FeatureFlag{
		FlagID:    rawFlag.FlagID,
		FlagKey:   rawFlag.FlagKey,
		FlagType:  FlagType(rawFlag.FlagType),
		IsPersist: rawFlag.IsPersist,
		Payload: func() string {
			if rawFlag.Payload != nil {
				return *rawFlag.Payload
			}
			return ""
		}(),
	}
	// Parse conditions
	conditions, err := parseFlagConditions(&rawFlag.Filters, &rawFlag.RolloutPercentages)
	if err != nil {
		return nil, fmt.Errorf("error: parseFlagConditions: %v", err)
	}
	flag.Conditions = conditions

	if flag.FlagType == Single {
		flag.Variants = []*FeatureFlagVariant{}
		return flag, nil
	}

	// Parse variants
	variants, err := parseFlagVariants(&rawFlag.Values, &rawFlag.Payloads, &rawFlag.VariantRollout)
	if err != nil {
		return nil, fmt.Errorf("error: parseFlagVariants: %v", err)
	}
	flag.Variants = variants
	return flag, nil
}

func checkCondition(varValue string, exprValues []string, operator FilterOperator) bool {
	switch operator {
	case Is:
		for _, value := range exprValues {
			if varValue == value {
				return true
			}
		}
	case IsNot:
		for _, value := range exprValues {
			if varValue == value {
				return false
			}
		}
		return true
	case IsAny:
		if varValue != "" {
			return true
		}
	case Contains:
		for _, value := range exprValues {
			if strings.Contains(varValue, value) {
				return true
			}
		}
	case NotContains:
		for _, value := range exprValues {
			if strings.Contains(varValue, value) {
				return false
			}
		}
		return true
	case StartsWith:
		for _, value := range exprValues {
			if strings.HasPrefix(varValue, value) {
				return true
			}
		}
	case EndsWith:
		for _, value := range exprValues {
			if strings.HasSuffix(varValue, value) {
				return true
			}
		}
	case IsUndefined:
		return varValue == ""
	}
	return false
}

func ComputeFlagValue(flag *FeatureFlag, sessInfo *FeatureFlagsRequest) interface{} {
	for _, cond := range flag.Conditions {
		conditionValue := true
		for _, filter := range cond.Filters {
			filterValue := false
			switch filter.Type {
			case UserCountry:
				filterValue = checkCondition(sessInfo.UserCountry, filter.Values, filter.Operator)
			case UserCity:
				filterValue = checkCondition(sessInfo.UserCity, filter.Values, filter.Operator)
			case UserState:
				filterValue = checkCondition(sessInfo.UserState, filter.Values, filter.Operator)
			case UserOS:
				filterValue = checkCondition(sessInfo.UserOS, filter.Values, filter.Operator)
			case UserBrowser:
				filterValue = checkCondition(sessInfo.UserBrowser, filter.Values, filter.Operator)
			case UserDevice:
				filterValue = checkCondition(sessInfo.UserDevice, filter.Values, filter.Operator)
			case UserID:
				filterValue = checkCondition(sessInfo.UserID, filter.Values, filter.Operator)
			case Referrer:
				filterValue = checkCondition(sessInfo.Referrer, filter.Values, filter.Operator)
			case Metadata:
				filterValue = checkCondition(sessInfo.Metadata[filter.Source], filter.Values, filter.Operator)
			default:
				filterValue = false
			}
			// If any filter is false, the condition is false, so we can check the next condition
			if !filterValue {
				conditionValue = false
				break
			}
		}
		// If any condition is true, we can return the flag value
		if conditionValue {
			if cond.RolloutPercentage == 0 {
				return nil
			}
			rand.Seed(time.Now().UnixNano())
			randNum := rand.Intn(100)
			if randNum > cond.RolloutPercentage {
				return nil
			}
			if flag.FlagType == Single {
				return flagInfo{
					Key:       flag.FlagKey,
					IsPersist: flag.IsPersist,
					Value:     true,
					Payload:   flag.Payload,
				}
			}
			// Multi variant flag
			randNum = rand.Intn(100)
			prev, curr := 0, 0
			for _, variant := range flag.Variants {
				curr += variant.RolloutPercentage
				if randNum >= prev && randNum <= curr {
					return flagInfo{
						Key:       flag.FlagKey,
						IsPersist: flag.IsPersist,
						Value:     variant.Value,
						Payload:   variant.Payload,
					}
				}
				prev = curr
			}
		}
	}
	return nil
}

func ComputeFeatureFlags(flags []*FeatureFlag, sessInfo *FeatureFlagsRequest) ([]interface{}, error) {
	result := make([]interface{}, 0, len(flags))

	for _, flag := range flags {
		if val, ok := sessInfo.PersistFlags[flag.FlagKey]; ok && flag.IsPersist {
			if flag.FlagType == Single {
				result = append(result, flagInfo{
					Key:       flag.FlagKey,
					IsPersist: flag.IsPersist,
					Value:     val,
					Payload:   flag.Payload,
				})
				continue
			} else {
				found := false
				for _, variant := range flag.Variants {
					if variant.Value == val {
						found = true
						result = append(result, flagInfo{
							Key:       flag.FlagKey,
							IsPersist: flag.IsPersist,
							Value:     val,
							Payload:   variant.Payload,
						})
						break
					}
				}
				if found {
					continue
				}
			}
		}
		if computedFlag := ComputeFlagValue(flag, sessInfo); computedFlag != nil {
			result = append(result, computedFlag)
		}
	}
	return result, nil
}

//---------------------------------//

func (f *featureFlagsImpl) GetFeatureFlags(projectID uint32) ([]*FeatureFlag, error) {
	rows, err := f.db.Query(`
		SELECT ff.flag_id, ff.flag_key, ff.flag_type, ff.is_persist, ff.payload, ff.rollout_percentages, ff.filters,
       		ARRAY_AGG(fv.value) as values,
       		ARRAY_AGG(fv.payload) as payloads,
       		ARRAY_AGG(fv.rollout_percentage) AS variants_percentages
		FROM (
			SELECT ff.feature_flag_id AS flag_id, ff.flag_key AS flag_key, ff.flag_type, ff.is_persist, ff.payload,
				ARRAY_AGG(fc.rollout_percentage) AS rollout_percentages,
				ARRAY_AGG(fc.filters) AS filters
			FROM public.feature_flags ff
				  LEFT JOIN public.feature_flags_conditions fc ON ff.feature_flag_id = fc.feature_flag_id
			WHERE ff.project_id = $1 AND ff.is_active = TRUE
			GROUP BY ff.feature_flag_id
		) AS ff
		LEFT JOIN public.feature_flags_variants fv ON ff.flag_type = 'multi' AND ff.flag_id = fv.feature_flag_id
		GROUP BY ff.flag_id, ff.flag_key, ff.flag_type, ff.is_persist, ff.payload, ff.filters, ff.rollout_percentages;
	`, projectID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var flags []*FeatureFlag

	for rows.Next() {
		var flag FeatureFlagPG
		if err := rows.Scan(&flag.FlagID, &flag.FlagKey, &flag.FlagType, &flag.IsPersist, &flag.Payload, &flag.RolloutPercentages,
			&flag.Filters, &flag.Values, &flag.Payloads, &flag.VariantRollout); err != nil {
			return nil, err
		}
		parsedFlag, err := ParseFeatureFlag(&flag)
		if err != nil {
			return nil, err
		}
		flags = append(flags, parsedFlag)
	}
	return flags, nil
}

//---------------------------------//

type FeatureFlags interface {
	ComputeFlagsForSession(req *FeatureFlagsRequest) ([]interface{}, error)
}

type featureFlagsImpl struct {
	db pool.Pool
}

func New(db pool.Pool) FeatureFlags {
	return &featureFlagsImpl{
		db: db,
	}
}

func (f *featureFlagsImpl) ComputeFlagsForSession(req *FeatureFlagsRequest) ([]interface{}, error) {
	// Grab flags and conditions for project
	projectID, err := strconv.ParseUint(req.ProjectID, 10, 32)
	if err != nil {
		return nil, err
	}

	flags, err := f.GetFeatureFlags(uint32(projectID))
	if err != nil {
		return nil, err
	}
	return ComputeFeatureFlags(flags, req)
}
