package handlers

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"otaupdate/services"
	"sort"
	"strconv"
	"strings"

	"github.com/fiskerinc/cloud-services/pkg/cache"
	"github.com/fiskerinc/cloud-services/pkg/common"
	"github.com/fiskerinc/cloud-services/pkg/logger"
	"github.com/fiskerinc/cloud-services/pkg/utils"
	"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
	"github.com/fiskerinc/cloud-services/pkg/validator"
	"github.com/julienschmidt/httprouter"
	"github.com/fiskerinc/cloud-services/pkg/loggerdataresp"
)

type ManifestMigrateVersion struct {
	Version int
}

var ERR_MANIFEST_MIGRATE_BAD_VERSION = errors.New("received version not recognized")

// TargetURLS is multiple as stage pushes to Prod and Euro Prod
var apiToken string = envtool.GetEnv("MANIFEST_MIGRATE_TOKEN", "MISSING API TOKEN")
var targetURLS []string = strings.Split(envtool.GetEnv("MANIFEST_MIGRATE_URLS", "MISSING_TARGET_URL"), ",")

// So we are going to take the information from our lower env and push it into a higher one.
// Need to decide how we want to get the information from the lower level to this higher level
// HandleUpdateManifestMigrate godoc
// @Summary Push update manifest from this environment to a higher one
// @Accept json
// @Produce json
// @Param Authorization header string false "Bearer <ID token>"
// @Param Api-Key header string false "<API token>"
// @Param manifest_id path string true "Manifest ID"
// @Success 200 {object} common.JSONMessage
// @Failure 400 {object} common.JSONError "Bad Request"
// @Failure 401 {object} common.JSONError "Unauthorized"
// @Failure 503 {object} common.JSONError "Service unavailable"
// @Router /manifestmigrate/{manifest_id} [post]
func HandleUpdateManifestMigrate(w http.ResponseWriter, r *http.Request) {
	params := httprouter.ParamsFromContext(r.Context())
	manifestIdString := params.ByName("manifest_id")

	// Gather the right information
	manifestId, err := strconv.Atoi(manifestIdString)
	if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
		return
	}
	var manifest common.UpdateManifest
	manifest.ID = int64(manifestId)

	err = validator.ValidateIDField(manifest.ID)
	if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
		return
	}

	um := services.GetDB().GetUpdateManifests()
	err = um.Load(&manifest)
	if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
		return
	}

	manifest.SUMS = ""
	manifest.Env = ""

	manifest.MigratePrepareHardwareVersion()

	fileKeys, err := getFileKeys(manifest)
	if loggerdataresp.BadDataErrorResp(w, err, http.StatusServiceUnavailable, loggerdataresp.PostgresNoRowsErrorCheck) {
		return
	}

	sendStruct := ManifestMigrateBody{}
	sendStruct.MigratedManifest = manifest
	sendStruct.FileKeys = fileKeys

	// Send message to the other API's
	sendBody, err := PrepareMigrateBodyForSending(&sendStruct)
	if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
		return
	}

	for index, targetURL := range targetURLS {
		turl, err := url.JoinPath(targetURL, "manifestmigrate")
		if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
			return
		}
		req, err := http.NewRequest("POST", turl, bytes.NewBuffer(sendBody))
		if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
			return
		}
		req.Header.Set("Content-Type", "application/json")
		req.Header.Add("Api-Key", apiToken)
		resp, err := http.DefaultClient.Do(req)
		if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
			deleteBecauseFailedMigrate(targetURLS[:index], int(manifest.ID))
			return
		}
		defer resp.Body.Close()
		b, _ := io.ReadAll(resp.Body)
		// Forgot to check the status code
		if resp.StatusCode >= 300 {
			logger.Error().Msgf("Failed to send manifest migrate: %s \n Status: %s\n Resp Body: %s\n", string(sendBody), resp.Status, string(b))
			var response common.JSONError
			err := json.Unmarshal(b, &response)
			if loggerdataresp.BadDataErrorResp(w, err, http.StatusBadRequest) {
				return
			}
			utils.RespJSON(w, http.StatusBadRequest, response)
			return
		}
	}
	utils.RespJSON(w, http.StatusOK, common.JSONMessage{
		Message: "Migration Sent",
	})
}

// If we fail to upload a manifest to one of the urls, delete from the other environments we are uploading to
func deleteBecauseFailedMigrate(urls []string, manifestID int) {
	for _, targetURL := range urls {
		turl, err := url.JoinPath(targetURL, "manifestraw")
		if err != nil {
			logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload", manifestID, targetURL)
			continue
		}
		turl += fmt.Sprintf("?id=%d", manifestID)
		req, err := http.NewRequest("DELETE", turl, nil)
		if err != nil {
			logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload", manifestID, targetURL)
			continue
		}
		req.Header.Set("Content-Type", "application/json")
		req.Header.Add("Api-Key", apiToken)
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload %s", manifestID, targetURL, resp.Status)
			continue
		}
		defer resp.Body.Close()
		if resp.StatusCode >= 300 {
			if err != nil {
				logger.Err(err).Msgf("Failed to delete manifest %d from %s after failing an upload %s", manifestID, targetURL, resp.Status)
				continue
			}
		}
	}
}

func getFileKeys(manifest common.UpdateManifest) ([]common.FileKeyResponse, error) {
	conn := services.RedisClientPool().GetFromPool()
	dbFK := services.GetDB().GetFileKeys()
	defer conn.Close()
	fileIDs := make([]string, 0)
	for _, ecu := range manifest.ECUs {
		for _, file := range ecu.Files {
			fileIDs = append(fileIDs, file.FileID)
		}
	}
	return cache.RetrieveFileEncryptionParams(conn, dbFK, fileIDs)
}

// In case we need a completely different struct, we will just return the bytes
func PrepareMigrateBodyForSending(manifestMigrate *ManifestMigrateBody) (jsonBody []byte, err error) {
	v, err := getVersionToSend()
	if err != nil {
		return
	}
	err = transformMigrateBody(manifestMigrate, v)
	if err != nil {
		return
	}
	return json.Marshal(manifestMigrate)
}

// Get the version that the env we are pushing to is going to run, then we can transform is needed or not send at all
// If we do not get a version, it defaults to 0
func getVersionToSend() (version int, err error) {
	version = -1
	if len(targetURLS) == 0 {
		return version, errors.New("missing target url")
	}
	targetURL := targetURLS[0]
	// The eu prod and normal prod should always be the same version, so just going to act like they are
	turl, err := url.JoinPath(targetURL, "manifestmigrate-version")
	if err != nil {
		return
	}
	req, err := http.NewRequest("GET", turl, nil)
	if err != nil {
		return
	}
	req.Header.Add("Api-Key", apiToken)

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()
	// If they don't have this route, we can assume they are on version 0
	if resp.StatusCode >= 300 {
		logger.Warn().Msgf("manifest migrate version returned status code %s", resp.Status)
		version = 0
		return
	}
	var mmv ManifestMigrateVersion
	err = json.NewDecoder(resp.Body).Decode(&mmv)
	if err != nil {
		return
	}
	version = mmv.Version

	return
}

const MIGRATION_VERSION = 1

// If we do not get a version, it defaults to 0
// If we cannot send the manifest due to some major change, then return an error
func transformMigrateBody(manifestMigrate *ManifestMigrateBody, version int) (err error) {
	// We have to go from our version down to their version
	// EX we are on version 5, they are on version 3
	// So do the conversion of 5 -> 4, then 4 -> 3

	// For each version that is added on, add to this switch statement
	// This switch statement is more of a way to organize the flow, can probably be improved
	switch MIGRATION_VERSION {
	case 1: // The conversion needed to go from VERSION 1 to VERSION 0
		if version == 1 { // If we are at the desired version, then we can break
			break
		}
		swapECUInstallPriority(manifestMigrate)
		fallthrough
	case 0:
		// THis is just an example row to show the fallthrough from case of 1 -> 0
		if version == 0 {
			break
		}
	default:
		err = ERR_MANIFEST_MIGRATE_BAD_VERSION
	}

	return
}

func swapECUInstallPriority(manifestMigrate *ManifestMigrateBody) {
	ecuList := manifestMigrate.MigratedManifest.ECUs
	// Order the array so we can begin to swap easily
	sort.Slice(ecuList, func(i, j int) bool { return ecuList[i].InstallPriority < ecuList[j].InstallPriority })
	rightIndex := len(ecuList) - 1
	for leftIndex := 0; leftIndex < rightIndex; leftIndex++ {
		ecuList[leftIndex].InstallPriority, ecuList[rightIndex].InstallPriority = ecuList[rightIndex].InstallPriority, ecuList[leftIndex].InstallPriority
		rightIndex--
	}
}
