package services

import (
	"context"
	"fmt"
	"github.com/fiskerinc/cloud-services/services/jetfire/models"
	"github.com/fiskerinc/cloud-services/services/jetfire/utils"
	"reflect"
	"strings"
	"sync"
	"time"

	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/pkg/errors"
)

var (
	//singleton cache definitions for vehicle states
	vehicleCache *models.VehicleCache

	// lists of variables needed by destination tables
	featureVars []string

	// set of all variables needing to be tracked
	featureSet map[string]bool
	varsSet    map[string]bool

	SchemaLock sync.Mutex
)

// Initializes feature variables by pulling the table schema for destination tables
func ResetCacheVars() {
	SchemaLock.Lock()
	defer SchemaLock.Unlock()

	conn, err := GetClickhouseConnection()
	if err != nil {
		logger.Error().Err(err).Send()
		return
	}
	featureVars = GetVarList(conn, FEATURE_TABLE, utils.FeatureVarsDefaults)
	logger.Debug().Msgf("FEATURE VARS %d", len(featureVars))

	featureSet = make(map[string]bool)
	varsSet = make(map[string]bool)

	for _, key := range featureVars {
		varsSet[key] = true
		featureSet[key] = true
	}
	logger.Debug().Msgf("VARSSET %d", len(varsSet))

	vCache := GetVehicleCache()
	vCache.SignalsSet = &varsSet

	//insert and flush the insertion buffers
	insertClient := GetShardClient()

	//guarantee initialization of singleton buffer
	//GetVehicleSignalBatch()

	//only insert signals in the intersection of insert buffer columns and updated sink table columns

	fBuffer := GetFeatureBatch()
	flBuffer := GetFeatureLastBatch()

	if insertClient != nil {
		ctx := context.Background()
		models.InsertAndFlushHead(fBuffer, ctx, insertClient.GetClient(), insertClient.GetBreaker(), featureSet)
		models.InsertAndFlushHead(flBuffer, ctx, insertClient.GetClient(), insertClient.GetBreaker(), featureSet)
	}
	//now resize the buffers and reset the column names
	fBuffer.Resize(featureVars)

	//now resize the buffers and reset the column names
	flBuffer.Resize(featureVars)
}

// flushes the head node for all buffers.
func InsertFlushAllBuffers(client *ChGoClient) {
	ctx := context.Background()

	//models.InsertAndFlushHead(GetVehicleSignalBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet)
	models.InsertAndFlushHead(GetFeatureBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet)
	models.InsertAndFlushHead(GetFeatureLastBatch(), ctx, client.GetClient(), client.GetBreaker(), featureSet)
}

func GetFeatureVars() []string {
	return featureVars
}

// Initializes the VIN cache from clickhouse feature table. This is used during Kafka rebalance events.
func InitCacheFromClickhouse() {
	SchemaLock.Lock()
	defer SchemaLock.Unlock()

	logger.Debug().Msgf("InitCacheFromClickhouse")
	//query feature table

	featureColumns := append([]string{"VIN", "Timestamp", "TripStart", "TripID"}, featureVars...)
	logger.Debug().Msgf("Querying feature table, %d columns", len(featureColumns))

	queryString := fmt.Sprintf("SELECT * FROM %s LIMIT 1 BY VIN", FEATURE_LAST_TABLE)
	populateCacheFromTable(GetVehicleCache(), queryString, &featureColumns)

	logger.Debug().Msgf("Pulled cache from clickhouse! %d vehicles!", len(GetVehicleCache().Cache))
}

// Performs a query, expecting a dynamic schema from the table. Query results are used to populate cache.
// Columns describes the columns expected in the table
func populateCacheFromTable(cache *models.VehicleCache, query string, columns *[]string) {
	ctx := context.Background()
	logger.Debug().Msgf("query: %s", query)
	conn, err := GetClickhouseConnection()

	if err != nil {
		logger.Error().Err(err)
		return
	}

	if !HasClickhouseParams() {
		logger.Error().Msgf("Could not open clickhouse connection. skipping initialization of vehicle caches")
		return
	}

	rows, err := QueryWithBreaker(ctx, conn, &query)

	if err != nil {
		logger.Error().Err(err).Send()
		panic(errors.WithStack(err))
	}

	if rows == nil {
		return
	}

	columnTypes := rows.ColumnTypes()

	row := make([]interface{}, len(columnTypes))

	for i, cType := range columnTypes {
		kind := cType.ScanType().Kind()
		scanName := cType.ScanType().Name()

		if kind == reflect.String || strings.Contains(scanName, "string") {
			row[i] = new(string)
		} else if kind == reflect.Float64 || kind == reflect.Float32 || strings.Contains(scanName, "float") || strings.Contains(scanName, "decimal") {
			row[i] = new(float64)
		} else if kind == reflect.Int64 || kind == reflect.Int || strings.Contains(scanName, "int") {
			row[i] = new(int64)
		} else if strings.Contains(scanName, "Time") {
			row[i] = new(time.Time)
		} else {
			row[i] = new(string)
		}
	}

	defer rows.Close()
	for rows.Next() {
		err := rows.Scan(row...)
		if err != nil {
			panic(errors.WithStack(err))
		}

		vin := *row[0].(*string)
		timestamp := *row[1].(*time.Time)

		_, contains := cache.Cache[vin]
		// initialize new state cache as needed
		if !contains {
			cache.Cache[vin] = models.NewVehicleState(vin, len(varsSet))
		}
		state := cache.Cache[vin]

		if timestamp.After(state.Timestamp) {
			for i, valPtr := range row {
				key := (*columns)[i]
				if key == "VIN" {
					continue
				}
				if key == "Timestamp" {
					continue
				}
				if key == "TripStart" {
					state.TripStart = *valPtr.(*time.Time)
					state.TripID = fmt.Sprintf("%s_%d", state.VIN, state.TripStart.Unix())
					continue
				}
				if key == "TripID" {
					state.TripID = *valPtr.(*string)
					continue
				}
				myValue, hasValue := state.StateValues[key]
				if !hasValue || myValue == 0 || timestamp.After(state.Timestamp) {
					state.StateValues[key] = *valPtr.(*float64)
				}
			}
			state.Timestamp = timestamp
		}
	}
	// append states to right
	for _, state := range cache.Cache {
		cache.ReinsertRight(state)
	}
}

// Queries clickhouse and reads into the dest struct
func LoadChState(VIN string, dest *models.VehicleState, columns *[]string) error {
	ctx := context.Background()
	queryString := fmt.Sprintf("SELECT * FROM %s WHERE VIN='%s' LIMIT 1", FEATURE_LAST_TABLE, VIN)
	logger.Debug().Msgf("query: %s", queryString)
	conn, err := GetClickhouseConnection()

	if !HasClickhouseParams() || err != nil {
		return errors.Errorf("Could not open clickhouse connection. Failed to load VIN state %s", VIN)
	}

	rows, err := QueryWithBreaker(ctx, conn, &queryString)

	if err != nil {
		logger.Error().Err(err).Send()
		panic(errors.WithStack(err))
	}

	if rows == nil {
		return nil
	}

	columnTypes := rows.ColumnTypes()

	row := make([]interface{}, len(columnTypes))

	for i, cType := range columnTypes {
		kind := cType.ScanType().Kind()
		scanName := cType.ScanType().Name()

		if kind == reflect.String || strings.Contains(scanName, "string") {
			row[i] = new(string)
		} else if kind == reflect.Float64 || kind == reflect.Float32 || strings.Contains(scanName, "float") || strings.Contains(scanName, "decimal") {
			row[i] = new(float64)
		} else if kind == reflect.Int64 || kind == reflect.Int || strings.Contains(scanName, "int") {
			row[i] = new(int64)
		} else if strings.Contains(scanName, "Time") {
			row[i] = new(time.Time)
		} else {
			row[i] = new(string)
		}
	}

	defer rows.Close()

	for rows.Next() {
		err := rows.Scan(row...)
		if err != nil {
			return err
		}

		timestamp := *row[1].(*time.Time)

		for i, valPtr := range row {
			key := (*columns)[i]
			if key == "VIN" {
				dest.VIN = *row[0].(*string)
				continue
			}
			if key == "Timestamp" {
				dest.Timestamp = timestamp
				continue
			}
			if key == "TripStart" {
				dest.TripStart = *valPtr.(*time.Time)
				dest.TripID = fmt.Sprintf("%s_%d", dest.VIN, dest.TripStart.Unix())
				continue
			}
			if key == "TripID" {
				dest.TripID = *valPtr.(*string)
				continue
			}
			_, ok := dest.StateValues[key]
			if ok {
				dest.StateValues[key] = *valPtr.(*float64)
			}
		}
	}

	GetVehicleCache().ReinsertRight(dest)

	return nil
}

// Returns Singleton Vehicle Cache; only one is needed for Jetfire service
func GetVehicleCache() *models.VehicleCache {
	if vehicleCache != nil {
		return vehicleCache
	}
	vehicleCache = new(models.VehicleCache)

	vehicleCache.SignalsSet = &varsSet
	vehicleCache.Cache = make(map[string]*models.VehicleState)
	vehicleCache.LastTimestamp = utils.FloatToTime(0.0)

	return vehicleCache
}
