package handlers

import (
	"encoding/json"
	"io"
	"net/http"
	"strings"

	"github.com/fiskerinc/cloud-services/services/depot/services"

	"github.com/fiskerinc/cloud-services/pkg/cache"
	"github.com/fiskerinc/cloud-services/pkg/carcommand"
	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/db/queries"
	"github.com/fiskerinc/cloud-services/pkg/dbc/state"
	"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/userconsent"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"

	"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
	"github.com/fiskerinc/cloud-services/pkg/utils"
	"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
	"github.com/go-pg/pg/v10"
	redigo "github.com/gomodule/redigo/redis"
	"github.com/pkg/errors"
	mon "go.mongodb.org/mongo-driver/mongo"
)

var errInvalidICCID = errors.New("invalid iccid submitted")
var errBlocked = errors.New("unable to connect: car is blocked")
var logLevel = envtool.GetEnv("TREX_LOG_LEVEL", common.CriticalLabel)
var wakeUpVINS = envtool.GetEnv("WAKE_UP_VINS", "VCF1EBU2XPG001140")
var defaultFleet = envtool.GetEnv("DEFAULT_FLEET", "Default-Ocean")

// id is the vehicles VIN
func TRexInit(id string, vMap map[string]string) error {
	carsDB := services.GetDB().GetCars()
	blocked, err := insertIfNotExist(id, carsDB)
	if err != nil {
		return err
	}
	if blocked {
		return errBlocked
	}

	if strings.Contains(wakeUpVINS, id) {
		go wakeup(id)
	}

	client := services.RedisClientPool().GetFromPool()
	defer client.Close()
	err = addTRexSession(client, id)
	if err != nil {
		logger.Warn().Str("id", id).Err(err).Send()
	}

	m, err := services.GetMongoClient()
	if err != nil {
		logger.Warn().Str("id", id).Err(err).Send()
	} else {
		msg, err := retrieveTRexSettings(client, m, id)
		if err != nil {
			return err
		}

		// Log the config that will be sent to TREX
		config, err := json.Marshal(msg)
		if err != nil {
			return err
		}
		logger.Info().Msgf("TREX Config sent for vin %s: \n%s", id, config)

		err = sendTRexSettings(client, id, msg)
		if err != nil {
			return err
		}
	}

	err = setTRexVersion(client, id, vMap["version"])
	if err != nil {
		return err
	}

	err = setDBCVersion(client, id, vMap["dbc_version"])
	if err != nil {
		return err
	}

	err = setTRexIP(client, id, vMap["ip"])
	if err != nil {
		return err
	}

	err = addICCID(carsDB, id, vMap["iccid"])
	if err != nil {
		logger.Warn().Err(err).Str("VIN", id).Str("ICCID", vMap["iccid"]).Msg("failed to add iccid to car")
	}

	uMsg, err := getUserConsentData(id)
	if err != nil {
		return err
	}

	err = sendTRexUserConsentData(client, id, uMsg)
	if err != nil {
		return err
	}

	err = checkAndRenewCertificate(id)
	if err != nil {
		return err
	}

	return nil
}

func checkAndRenewCertificate(id string) error {
	logger.Debug().Msg("on trex init: checking TBOX cert for vin " + id)

	cs := services.GetCertService()
	certNeedsRenewal, err := cs.CheckCertificateNeedsRenewal(id, common.CertTBOX)
	if err != nil {
		return err
	} else if certNeedsRenewal {
		logger.Debug().Msg("TBOX cert for vin " + id + " is out of date")

		cert, err := cs.RenewCertificate(id, common.CertTBOX)
		if err != nil {
			return err
		}

		client := services.RedisClientPool().GetFromPool()
		defer client.Close()

		err = client.SafeQueueMessage(
			common.TRex.Key(id),
			common.Message{
				Handler: "update_cert",
				Data: common.UpdateCert{
					SSLCertBase64: cert.PublicKey,
				},
			},
		)
		if err != nil {
			return err
		}
	}

	logger.Debug().Msg("TBOX cert for vin " + id + " is not out of date")

	return nil
}

func getUserConsentData(id string) ([]common.UserConsentDataTrexMsg, error) {
	var ucdToSend = []common.UserConsentDataTrexMsg{}

	resp, err := userconsent.GetUserConsentService().UserConsentByVehicleNumber(id)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New("call to user-consent/byVehicleNumber endpoint returned " + resp.Status)
	}

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	ucdResult := []common.UserConsentDataResponse{}
	err = json.Unmarshal(respBody, &ucdResult)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if len(ucdResult) < 1 {
		return ucdToSend, nil
	}

	// only send to TREX the consents that have the userID of the first consent returned
	var userId = ucdResult[0].UserID
	for _, ucd := range ucdResult {
		if ucd.UserID == userId {
			ucdToSend = append(ucdToSend, common.BuildUserConsentTrexMessage(ucd))
		}
	}

	return ucdToSend, nil
}

func sendTRexUserConsentData(r redis.Client, id string, msg []common.UserConsentDataTrexMsg) error {

	if len(msg) > 0 {
		return r.SafePublishMessage(
			common.TRex.Key(id),
			common.Message{
				Handler: "consent",
				Data:    msg,
			},
		)
	}
	return nil
}

func insertIfNotExist(id string, carsDB queries.CarsInterface) (bool, error) {
	car, err := carsDB.SelectByVIN(id)
	if err == nil && car != nil {
		return car.Blocked, nil
	}
	if errors.Is(err, pg.ErrNoRows) {
		err = nil

		c, err := utils.ParseVIN(id)
		if err != nil {
			return false, errors.WithStack(err)
		}

		_, err = carsDB.Insert(c)
		if err != nil {
			return false, errors.WithStack(err)
		}

		client, err := services.GetMongoClient()
		if err != nil {
			return false, errors.WithStack(err)
		}

		v := &mongo.Vehicle{
			VIN:      id,
			CANBus:   common.CANBus{Enabled: true, DataLogger: true, DTCEnabled: elptr.ElPtr(false)},
			LogLevel: common.UnmarshalLogLevelString(logLevel),
		}
		err = client.GetVehicles().AddVehicle(v)
		if mon.IsDuplicateKeyError(err) {
			err = client.GetVehicles().UpdateVehicle(v)
		}
		if err != nil {
			return false, errors.WithStack(err)
		}

		err = client.GetFleets().AddVehiclesToFleet(defaultFleet, []string{id})
		if err != nil {
			return false, errors.WithStack(err)
		}

		return false, nil
	}

	return false, errors.WithStack(err)
}

func addICCID(db queries.CarsInterface, id, iccid string) error {
	// Going to only update where an iccid is valid
	if len(iccid) != 20 {
		return errInvalidICCID
	}

	car := &common.Car{ICCID: iccid, VIN: id}

	if ct, err := db.UpdateICCID(car); err != nil {
		return errors.Wrapf(err, "failed to update car's iccid %s", id)
	} else if ct.RowsAffected() == 1 {
		logger.Info().Msgf("car %s has been updated with iccid %s", id, iccid)
	}

	return nil
}

func addTRexSession(client redis.Client, id string) error {
	_, err := client.Execute("SADD", redis.CarSessionsKey(), id)
	return err
}

func retrieveTRexSettings(r redis.Client, m mongo.Client, id string) (*common.TRexConfigResponse, error) {
	return cache.RetrieveVehicleConfig(r, m, id)
}

func sendTRexSettings(r redis.Client, id string, msg *common.TRexConfigResponse) error {
	err := r.SafePublishMessage(
		common.TRex.Key(id),
		common.Message{
			Handler: "config",
			Data:    msg,
		},
	)
	return err
}

func setTRexIP(r redis.Client, vin, ip string) error {
	elms := strings.Split(ip, ":")

	return r.SetObjectField(redis.CarStateHashKey(vin), state.TREX_IP, elms[0])
}

func setDBCVersion(r redis.Client, vin, version string) error {
	return setVersion(r, vin, version, state.DBC_VERSION, common.DBCVersionSource)
}

func setTRexVersion(r redis.Client, vin, version string) error {
	return setVersion(r, vin, version, state.TREX_VERSION, common.TREXVersionSource)
}

func setVersion(r redis.Client, vin, version, redisField string, versionSource common.VersionSource) error {
	v, err := r.GetObjectField(redis.CarStateHashKey(vin), redisField)
	if err != nil && !errors.Is(err, redigo.ErrNil) {
		return errors.WithStack(err)
	}

	if v == version {
		return nil
	}

	db := services.GetDB().GetCarVersionsLog()
	err = r.SetObjectField(redis.CarStateHashKey(vin), redisField, version)
	if err != nil {
		return errors.WithStack(err)
	}

	_, err = db.LogVersionChange(&common.CarVersionLogs{
		VIN:           vin,
		VersionSource: versionSource,
		Version:       version,
	})
	if err != nil {
		return errors.WithStack(err)
	}

	return nil
}

func wakeup(vin string) {
	wake := carcommand.NewCarWakeUp(services.GetDB().GetCars(), services.GetSMSClient())
	err := wake.WakeUp(vin, false)
	loggerdataresp.BadDataError(err)
}
