package services

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

	fClickhouse "github.com/fiskerinc/cloud-services/pkg/clickhouse"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
	"github.com/ClickHouse/clickhouse-go/v2"
	"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
	"github.com/pkg/errors"
	"github.com/sony/gobreaker"
	"gopkg.in/retry.v1"
)

var (
	conn                 fClickhouse.ConnInterface
	clickLock            sync.Mutex
	FEATURE_TABLE        = envtool.GetEnv("CLICKHOUSE_FEATURE_TABLE", "feature_table_shard")
	FEATURE_LAST_TABLE   = envtool.GetEnv("CLICKHOUSE_FEATURE_LAST_TABLE", "feature_table_last_shard")
	VEHICLE_SIGNAL_TABLE = envtool.GetEnv("CLICKHOUSE_VEHICLE_SIGNAL_TABLE", "vehicle_signal_shard")

	clickhouseBreaker = gobreaker.NewCircuitBreaker(gobreaker.Settings{
		Name:          "clickhouse",
		MaxRequests:   1,
		Interval:      time.Minute * 1,
		Timeout:       time.Second * 60,
		OnStateChange: utils.BreakerStateChange,
		ReadyToTrip:   func(counts gobreaker.Counts) bool { return counts.Requests > 0 },
	})

	featureBuffer       *models.InsertBuffer
	featureLastBuffer   *models.InsertBuffer
	vehicleSignalBuffer *models.InsertBuffer

	retryStrategy = retry.LimitTime(
		120*time.Second,
		retry.Exponential{
			Initial: 100 * time.Millisecond,
		},
	)
)

func HasClickhouseParams() bool {
	return len(envtool.GetEnv("CLICKHOUSE_USER", "")) > 0
}

// Returns singleton instance of clickhouse connection
func GetClickhouseConnection() (fClickhouse.ConnInterface, error) {

	clickLock.Lock()
	defer clickLock.Unlock()
	if conn != nil {
		return conn, nil
	}

	executeWrapper := func() (interface{}, error) {
		return fClickhouse.NewConn()
	}

	var err error
	if conn == nil {
		//instantiate singleton
		newConn, err := clickhouseBreaker.Execute(executeWrapper)
		if err != nil {
			panic(errors.WithStack(err))
		}
		conn = newConn.(clickhouse.Conn)
	}

	return conn, err
}

func SetClickhouseConn(newConn fClickhouse.ConnInterface) {
	clickLock.Lock()
	defer clickLock.Unlock()

	conn = newConn
}

// Returns the buffer struct for batched inserts into Vehicle Signal table
func GetVehicleSignalBatch() *models.InsertBuffer {
	if vehicleSignalBuffer != nil {
		return vehicleSignalBuffer
	}

	vehicleSignalBuffer = models.NewInsertBuffer(
		false,
		nil,
		VEHICLE_SIGNAL_TABLE,
		envtool.GetEnvDuration("JETFIRE_VEHICLE_SIGNAL_BATCH_PERIOD_MS", 10000)*time.Millisecond,
		models.SignalBlockType,
	)
	return vehicleSignalBuffer
}

// Returns the buffer struct for batched inserts into Feature table
func GetFeatureBatch() *models.InsertBuffer {
	if featureBuffer != nil {
		return featureBuffer
	}

	featureBuffer = models.NewInsertBuffer(
		true,
		featureVars,
		FEATURE_TABLE,
		envtool.GetEnvDuration("JETFIRE_FEATURE_BATCH_PERIOD_MS", 10000)*time.Millisecond,
		models.PivotBlockType,
	)
	return featureBuffer
}

func GetFeatureLastBatch() *models.InsertBuffer {
	if featureLastBuffer != nil {
		return featureLastBuffer
	}

	featureLastBuffer = models.NewInsertBuffer(
		true,
		featureVars,
		FEATURE_LAST_TABLE,
		envtool.GetEnvDuration("JETFIRE_FEATURE_BATCH_PERIOD_MS", 10000)*time.Millisecond,
		models.VinLastBlockType,
	)
	return featureLastBuffer
}

// Queries table schema to get ordered list of can signals
func GetVarList(conn fClickhouse.ConnInterface, table string, defaultVarsFile string) []string {
	var result []string
	ctx := context.Background()
	if !HasClickhouseParams() {
		logger.Warn().Msgf("No clickhouse params found, reading default vars instead.")
		return utils.ReadVarListFromFile(defaultVarsFile)
	}

	query := fmt.Sprintf("DESCRIBE %s", table)

	logger.Debug().Msgf("%s", query)

	var rows []descRow
	err := SelectWithBreaker(ctx, conn, &query, &rows)
	if err != nil {
		logger.Warn().Err(err).Msgf("Failed to select data from clickhouse, reading default vars instead...")
		return utils.ReadVarListFromFile(defaultVarsFile)
	}

	for _, row := range rows {
		if row.Name == "VIN" || row.Name == "Timestamp" {
			continue
		}
		if row.Name == "TripID" || row.Name == "TripStart" {
			continue
		}
		result = append(result, row.Name)
	}
	return result
}

func SelectWithBreaker(ctx context.Context, conn fClickhouse.ConnInterface, query *string, buffer interface{}) error {
	var err error

	selectWrapper := func() (interface{}, error) {
		return nil, conn.Select(ctx, buffer, *query)
	}

	for a := retry.Start(retryStrategy, nil); a.Next(); {
		_, err = clickhouseBreaker.Execute(selectWrapper)
		if err == nil {
			break
		}
		logger.Error().Err(err).Send()
	}
	return err
}

func QueryWithBreaker(ctx context.Context, conn fClickhouse.ConnInterface, query *string) (driver.Rows, error) {
	var rows interface{}
	var err error

	queryWrapper := func() (interface{}, error) {
		return conn.Query(ctx, *query)
	}

	for a := retry.Start(retryStrategy, nil); a.Next(); {
		rows, err = clickhouseBreaker.Execute(queryWrapper)
		if err == nil && rows != nil {
			break
		}
	}

	//exit if rows is nil; do not attempt to convert to driver.Rows
	if rows == nil {
		return nil, err
	}

	return rows.(driver.Rows), err
}

type descRow struct {
	Name               string `ch:"name"`
	Type               string `ch:"type"`
	Default_type       string `ch:"default_type"`
	Default_expression string `ch:"default_expression"`
	Comment            string `ch:"comment"`
	Codec_expression   string `ch:"codec_expression"`
	Ttl_expression     string `ch:"ttl_expression"`
}
