package flashpackversion

import (
	"sort"
	"strconv"

	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/db/queries"
	"github.com/pkg/errors"
	"golang.org/x/exp/maps"
)

// Any time we insert new CarECUs, we want to see if the flashpack number for the car has changed
// If so, we save it on the car, send it to SAP, and update the Version Log.
func InsertCarECUsAndUpdateFlashpackVersion(cars queries.CarsInterface, cvl queries.CarVersionsLogInterface,  vin string, ecus []common.CarECU) error {
	// Convert the ecu names to cloud compatible ecus
	for _, e := range ecus {
		e.TransformECUName()
	}

	// Insert the new CarECUs
	err := cars.InsertCarECUs(ecus)
	if err != nil {
		return err
	}

	var previousFlashpackNumber, currentFlashpackNumber string

	// Find the previous flashpack version of the car
	car, err := cars.SelectByVIN(vin)
	if err != nil {
		return err
	}
	previousFlashpackNumber = car.Flashpack

	// Find the current flashpack number of the car
	currentFlashpackNumber, err = FindCurrentFlashpackVersionForCar(cars, *car)
	if err != nil {
		return err
	}

	// If the flashpack has changed, update the car and SAP
	return updateFlashpackNumber(cars, cvl, vin, currentFlashpackNumber, previousFlashpackNumber)
}

// Given the latest ECUs for a car, find out what the flashpack number for the car is
func FindCurrentFlashpackVersionForCar(cars queries.CarsInterface, car common.Car) (currentFlashpackNumber string, err error) {
	currentFlashpackNumber = ""

	// Get a map of key-value pairs, latest updated ECUs for the car, ECU names as keys and ECU Supplier SW Versions as values
	latestCarECUsToVersionsMap, err := getLatestCarECUsMap(cars, car.VIN)
	if err != nil {
		return currentFlashpackNumber, err
	}

	// Get a map of all flashpack version mappings, and an array of the flashpack numbers sorted in descending numerical order
	flashpackMappings, flashpackNumbers, err := getAllFlashpackMappingsByModelTrimMap(cars, car.Model, car.Trim)
	if err != nil {
		return currentFlashpackNumber, err
	}

	// Here is the logic to calculate the current flashpack number for the car
	// More than one version for an ECU may map to the same flashpack number
	// A car may be several versions ahead on one ECU, and several versions behind on another ECU
	// Hence the following ugly code

	// For each flashpack number in descending order
	var ecusUpdated []string
	for _, flashpackNumber := range flashpackNumbers {

		var flashpackNumberComplete = true

	outer: // For each ECU version mapping for the flashpack number
		for _, flashpackMapping := range flashpackMappings[flashpackNumber] {
			var ecuUpdated = false

		inner: // Determine whether this ECU is updated on the car to this flashpack number or a higher one
			for _, e := range ecusUpdated {
				if e == flashpackMapping.ECU {
					ecuUpdated = true
					break inner
				}
			}

			if !ecuUpdated {
				ecuVersion, ok := latestCarECUsToVersionsMap[flashpackMapping.ECU]

				// If the ECU has not been updated at all, or it is not at this version, then the flashpack number is incomplete and the car has not achieved it
				if !ok || ecuVersion != flashpackMapping.SupplierSWVersion {
					flashpackNumberComplete = false
					break outer
				} else {
					// We have proved that this ECU is updated on the car to this flashpack number
					ecusUpdated = append(ecusUpdated, flashpackMapping.ECU)
				}
			}
		}

		if flashpackNumberComplete && flashpackNumber > currentFlashpackNumber {
			currentFlashpackNumber = flashpackNumber
		}
	}

	return currentFlashpackNumber, err
}

func FindCarECUsToUpdateForNextFlashpackNumber(cars queries.CarsInterface, car common.Car, nextFlashpackNumber string) ([]common.CarECUVersion, error) {
	ecusToUpdateForNextFlashpackNumber := []common.CarECUVersion{}

	// Get a map of key-value pairs, latest updated ECUs for the car, ECU names as keys and ECU Supplier SW Versions as values
	latestCarECUsToVersionsMap, err := getLatestCarECUsMap(cars, car.VIN)
	if err != nil {
		return nil, err
	}

	// Get a map of all flashpack version mappings, and an array of the flashpack numbers sorted in descending numerical order
	allFlashpackMappings, allFlashpackNumbers, err := getAllFlashpackMappingsByModelTrimMap(cars, car.Model, car.Trim)
	if err != nil {
		return nil, err
	}

	// Get the mappings specifically for the next flashpack version
	nextFlashpackVersionMappings := allFlashpackMappings[nextFlashpackNumber]

	// Here is the logic to find which ECUs on the car need to be updated in order to achieve nextFlashpackNumber
	// A car may be several versions ahead on one ECU, and several versions behind on another ECU
	// Hence the following ugly code

	// For each mapping for the next flashpack version
	for _, nextFlashpackVersionMapping := range nextFlashpackVersionMappings {
		ecuVersion, ok := latestCarECUsToVersionsMap[nextFlashpackVersionMapping.ECU]

		if !ok {
			// If the ECU has not been updated at all on this car, we know it needs to be updated to achieve the next flashpack version
			ecusToUpdateForNextFlashpackNumber = append(ecusToUpdateForNextFlashpackNumber, common.CarECUVersion{
				CarECUName:    nextFlashpackVersionMapping.ECU,
				CarECUVersion: nextFlashpackVersionMapping.SupplierSWVersion,
			})
		} else {
			// If the ECU has been updated, we need to determine whether its version applies to this flashpack number or a higher one
			// That means we need to check all the mappings for the next flashpack number and all higher ones as well

			found := false

		outer: // For each flashpack number in descending order
			for _, flashpackNumber := range allFlashpackNumbers {

			inner: // For each ECU mapping for this flashpack number
				for _, carECU := range allFlashpackMappings[flashpackNumber] {
					if ecuVersion == carECU.SupplierSWVersion {
						found = true
						break inner
					}
				}

				// Don't check flashpack numbers lower than the next flashpack number
				if flashpackNumber == nextFlashpackNumber {
					break outer
				}
			}

			if !found {
				// We have proved that the ECU has not been updated for the next flashpack number
				ecusToUpdateForNextFlashpackNumber = append(ecusToUpdateForNextFlashpackNumber, common.CarECUVersion{
					CarECUName:    nextFlashpackVersionMapping.ECU,
					CarECUVersion: nextFlashpackVersionMapping.SupplierSWVersion,
				})
			}
		}
	}

	return ecusToUpdateForNextFlashpackNumber, err
}

// Save the flashpack number in the database, display it in the version log, and send it to SAP
func updateFlashpackNumber(cars queries.CarsInterface, cvl queries.CarVersionsLogInterface, vin, currentFlashpack string, previousFlashpack string) error {
	if currentFlashpack != previousFlashpack {
		_, err := cars.UpdateCarFlashpackVersion(vin, currentFlashpack)
		if err != nil {
			return errors.WithStack(err)
		}

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

	return nil
}

func getLatestCarECUsMap(cars queries.CarsInterface, vin string) (map[string]string, error) {
	allLatestCarECUs, err := cars.GetCarECUs(common.CarECUFilter{VIN: vin, Unique: true}, nil)
	if err != nil {
		return nil, err
	}

	// Put into a map by ECU name
	var latestCarECUsToVersionsMap = make(map[string]string)
	for _, e := range allLatestCarECUs {
		latestCarECUsToVersionsMap[e.ECU] = e.SupplierSWVersion
	}

	// Sometimes the TBOX version is applied to MCU, or the PDU version is applied to OBC, etc.
	for ecuName, ecuReplacementName := range common.FlashpackCalculationECUReplacement {
		_, hasECUName := latestCarECUsToVersionsMap[ecuName]
		_, hasECUReplacementName := latestCarECUsToVersionsMap[ecuReplacementName]
		if hasECUName && !hasECUReplacementName {
			latestCarECUsToVersionsMap[ecuReplacementName] = latestCarECUsToVersionsMap[ecuName]
		}
	}

	return latestCarECUsToVersionsMap, nil
}

func getAllFlashpackMappingsByModelTrimMap(cars queries.CarsInterface, model string, trim string) (map[string][]common.CarECU, []string, error) {
	allFlashpackMappingsByModelTrim, err := cars.GetCarFlashpackVersionMappingsByModelTrim(model, trim, &queries.PageQueryOptions{Order: "flashpack DESC"})
	if err != nil {
		return nil, nil, err
	}

	// Map flashpack numbers to an array of ECU names and versions
	var flashpackMappings = make(map[string][]common.CarECU)
	for _, f := range allFlashpackMappingsByModelTrim {
		flashpackMappings[f.Flashpack] =
			append(flashpackMappings[f.Flashpack],
				common.CarECU{
					ECU:               f.CarECUName,
					SupplierSWVersion: f.CarECUVersion,
				})
	}

	// Sort the flashpack numbers in descending order
	var flashpackNumbers = maps.Keys(flashpackMappings)
	sort.Slice(flashpackNumbers, func(i, j int) bool {
		iFp, _ := strconv.ParseFloat(flashpackNumbers[i], 64) // guaranteed numeric
		jFp, _ := strconv.ParseFloat(flashpackNumbers[j], 64) // guaranteed numeric

		return iFp > jFp
	})

	return flashpackMappings, flashpackNumbers, nil
}
