package cache

import (
	"encoding/json"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/mongo"
	"github.com/fiskerinc/cloud-services/pkg/redis"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"

	"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
	redigo "github.com/gomodule/redigo/redis"
	"github.com/pkg/errors"
)

const (
	ENABLE_DBG_MASK_EV_NAME     = "ENABLE_DEBUGMASK"
	ENABLE_DBG_MASK_VAL_FALSE   = "0"
	ENABLE_DBG_MASK_VAL_TRUE    = "1"
	ENABLE_DBG_MASK_VAL_DEFAULT = ENABLE_DBG_MASK_VAL_FALSE
)

// This flag is to decide whether retrieved value of DebugMask is to be passed to TrexCfg or not.
// When the flag is true, the retrieved value is passed; else no value is passed.
// The value of flag is fetched from the specific environmental variable. If that environmental
// variable is not present / not defined, we assume the flag itself to be FALSE. That is the
// default value (FALSE) of the environmental variable. When user/developer has set this evironmental
// variable correctly, the flag can become TRUE in which case the value is passed to TrexCfg.
var ENABLE_DEBUG_MASK = DbgMaskEnabled()

// method introduced so as unit testing is easier otherwise not necessary since environment variables
// can't be changed so easily subsequent to a process start (meaning revaluation at runtime of no much use).
func DbgMaskEnabled() bool {
	return envtool.GetEnv(ENABLE_DBG_MASK_EV_NAME, ENABLE_DBG_MASK_VAL_DEFAULT) == ENABLE_DBG_MASK_VAL_TRUE
}

func RetrieveVehicleConfig(r redis.Client, m mongo.Client, id string) (*common.TRexConfigResponse, error) {
	config := &common.TRexConfigResponse{}

	reply, err := checkCacheForVehicleConfig(r, id)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if reply != nil {
		err = json.Unmarshal(reply, config)
		if err != nil {
			return nil, errors.WithStack(err)
		}

		if config.CANBus.DTCEnabled == nil {
			config.CANBus.DTCEnabled = elptr.ElPtr(false)
		}
		return config, nil
	}

	config.LogLevel = common.Critical
	config.Log = &common.LogConfig{
		Matches: []common.LogConfigChannel{
			{
				Channel: common.ChannelCMD,
				Level:   common.Trace,
			},
		},
	}

	config.CANBus.Enabled = true
	config.CANBus.DataLogger = true

	filters := make(FiltersMap)

	f, err := checkFleetsDBForVehicleConfig(m, id)
	if err != nil {
		logger.Warn().Err(err).Send()
	}
	if f != nil {
		config.CANBus = f.CANBus
		config.LogLevel = f.LogLevel
		filters.AppendFilters(f.CANBus.Filters)
	}

	v, err := checkVehiclesDBForVehicleConfig(m, id)
	if err != nil {
		logger.Warn().Err(err).Send()
	}
	if v != nil {
		config.CANBus = v.CANBus
		config.LogLevel = v.LogLevel
		config.DLTEnabled = v.DLTEnabled
		config.DLTLevel = v.DLTLevel
		// we should evaluate at run-time, not just at start-up time
		if ENABLE_DEBUG_MASK {
			config.DebugMask = v.DebugMask
		}
		config.IDPSEnabled = v.IDPSEnabled

		filters.AppendFilters(v.CANBus.Filters)
	}

	config.CANBus.Filters = filters.ToSlice()
	if config.CANBus.DTCEnabled == nil {
		config.CANBus.DTCEnabled = elptr.ElPtr(false)
	}
	err = setCacheForVehicleConfig(r, id, config)
	return config, err
}

func checkCacheForVehicleConfig(r redis.Client, id string) ([]byte, error) {
	key := redis.CarConfigKey(id)

	reply, err := redigo.Bytes(r.Execute("GET", key))
	if err != nil {
		if errors.Is(err, redigo.ErrNil) {
			return nil, nil
		}
		return nil, err
	}

	return reply, nil
}

func checkVehiclesDBForVehicleConfig(m mongo.Client, id string) (*mongo.Vehicle, error) {
	return m.GetVehicles().FindVehicle(&mongo.Vehicle{VIN: id})
}

func checkFleetsDBForVehicleConfig(m mongo.Client, id string) (*mongo.Fleet, error) {
	return m.GetFleets().GetCANBusForVehicle(id)
}

func setCacheForVehicleConfig(r redis.Client, id string, config *common.TRexConfigResponse) error {
	key := redis.CarConfigKey(id)

	data, err := json.Marshal(config)
	if err != nil {
		return errors.WithStack(err)
	}

	batch := redis.NewRedisBatchCommands()
	batch.Add("SET", key, data)
	batch.Add("EXPIRE", key, redisObjectExpire)

	_, err = r.ExecuteBatch(batch)
	if err != nil {
		return errors.WithStack(err)
	}

	return nil
}

func RemoveCacheConfigForVehicles(r redis.Client, vins []string) error {
	batch := redis.NewRedisBatchCommands()
	for _, vin := range vins {
		batch.Add("DEL", redis.CarConfigKey(vin))
	}

	_, err := r.ExecuteBatch(batch)
	if err != nil {
		return errors.WithStack(err)
	}

	return nil
}

type IntervalEdgeMask struct {
	Interval *int
	EdgeMask *common.BinaryHex
}

type FiltersMap map[string]IntervalEdgeMask

func (f FiltersMap) AppendFilters(filters []common.CANFilter) {
	for _, filter := range filters {
		if filter.EdgeMask != nil && filter.EdgeMask.String() != "" {
			f[filter.CANID] = IntervalEdgeMask{
				EdgeMask: filter.EdgeMask,
			}
		} else if filter.Interval != nil {
			f[filter.CANID] = IntervalEdgeMask{
				Interval: filter.Interval,
			}
		}
	}
}

func (f FiltersMap) ToSlice() []common.CANFilter {
	filters := make([]common.CANFilter, 0, len(f))

	for k, v := range f {
		filters = append(filters, common.CANFilter{
			CANID:    k,
			Interval: v.Interval,
			EdgeMask: v.EdgeMask,
		})
	}

	return filters
}
