package cache

import (
	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/db/queries"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/redis"
	"github.com/pkg/errors"
)

func NewDriversCache(redisClient redis.ClientPoolInterface, cars queries.CarsInterface) *DriversCache {
	return &DriversCache{
		redisClientPool: redisClient,
		cars:        cars,
	}
}

type DriversCache struct {
	redisClientPool redis.ClientPoolInterface
	cars        queries.CarsInterface
}

func (dc *DriversCache) RedisClientPool() redis.ClientPoolInterface {
	return dc.redisClientPool
}

func (dc *DriversCache) Cars() queries.CarsInterface {
	return dc.cars
}

func (dc *DriversCache) hasCachedNoDrivers(drivers []string) bool {
	// Redis will return []string{""} for no drivers
	return len(drivers) == 1 && len(drivers[0]) == 0
}

func (dc *DriversCache) cacheDrivers(key string, drivers []string) error {
	client := dc.redisClientPool.GetFromPool()
	defer client.Close()
	// cache driver IDs
	if len(drivers) > 0 {
		return client.NewSet(key, drivers, redisObjectExpire)
	}

	// Redis will not take an empty array as an arg
	return client.NewSet(key, nil, redisObjectExpire)
}

// RetrieveDriverIDs retrieves IDs from redis or from DB and proceeds to cache both the drivers and IDs
// redis keys:
//
//	car:<VIN>:drivers
func (dc *DriversCache) RetrieveDriverIDs(vin string) ([]string, error) {
	var driverIDs []string
	driverIDsKey := redis.CarToAllDriversKey(vin)

	// retrieve IDs from redis
	client := dc.redisClientPool.GetFromPool()
	err := client.GetSet(driverIDsKey, &driverIDs)
	if err != nil {
		logger.Warn().Err(err).Send()
	}
	client.Close()

	if dc.hasCachedNoDrivers(driverIDs) {
		return []string{}, nil
	}
	if len(driverIDs) > 0 {
		return driverIDs, nil
	}

	// if IDs not present in redis perform DB lookup
	var drivers []common.CarToDriver
	drivers, err = dc.cars.GetDrivers(vin)
	if err != nil {
		return nil, err
	}

	for _, driver := range drivers {
		driverIDs = append(driverIDs, driver.DriverID)
	}

	err = dc.cacheDrivers(driverIDsKey, driverIDs)
	if err != nil {
		return driverIDs, err
	}

	return driverIDs, nil
}

// RetrieveDriverIDsAsSet retrieves IDs from redis or from DB and proceeds to cache both the drivers and IDs
// redis keys:
//
//	car:<VIN>:drivers
func (dc *DriversCache) RetrieveDriverIDsAsSet(vin string) (map[string]struct{}, error) {
	driverIDs, err := dc.RetrieveDriverIDs(vin)
	if err != nil {
		return nil, err
	}

	var dIDsSet = make(map[string]struct{})
	for _, did := range driverIDs {
		dIDsSet[did] = struct{}{}
	}

	return dIDsSet, nil
}

func (dc *DriversCache) IsDriverOfVIN(vin string, driverid string) (bool, error) {
	ids, err := dc.RetrieveDriverIDs(vin)
	if err != nil {
		return false, err
	}

	for _, id := range ids {
		if id == driverid {
			return true, nil
		}
	}

	return false, dc.NotDriverError(vin, driverid)
}

// Add driver to database and cache
func (dc *DriversCache) AddDriver(car *common.Car, driver *common.Driver, role string) (*common.CarToDriver, error) {

	relation, err := dc.cars.AddDriver(car, driver, role)
	if err != nil {
		return nil, err
	}

	driverIDsKey := redis.CarToAllDriversKey(car.VIN)

	client := dc.redisClientPool.GetFromPool()
	defer client.Close()
	client.AddToSet(driverIDsKey, driver.ID, redisObjectExpire)
	return relation, nil
}

func (dc DriversCache) NotDriverError(vin string, driverid string) error {
	return errors.Errorf("id %s is not a driver for vin %v", driverid, vin)
}
