/*
 * Copyright (c) 2024  The AdvantEDGE Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package server

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"sync"
	"time"

	sm "github.com/InterDigitalInc/AdvantEDGE/go-apps/meep-app-enablement/server/service-mgmt"
	apps "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-applications"
	dkm "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-key-mgr"
	log "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-logger"
	mq "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-mq"
	redis "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-redis"
	subs "github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-subscriptions"

	"github.com/gorilla/mux"
)

const moduleName = "meep-app-enablement"
const appSupportBasePath = "mec_app_support/v2/"
const appEnablementKey = "app-enablement"
const globalMepName = "global"
const APP_STATE_INITIALIZED = "INITIALIZED"
const APP_STATE_READY = "READY"
const APP_TERMINATION_NOTIF_SUB_TYPE = "AppTerminationNotificationSubscription"
const APP_TERMINATION_NOTIF_TYPE = "AppTerminationNotification"
const DEFAULT_GRACEFUL_TIMEOUT = 10

const serviceName = "App Enablement Service"

// App Info fields
const (
	fieldAppId   string = "id"
	fieldName    string = "name"
	fieldNode    string = "node"
	fieldType    string = "type"
	fieldPersist string = "persist"
	fieldState   string = "state"
)

// MQ payload fields
const (
	mqFieldAppId   string = "id"
	mqFieldPersist string = "persist"
)

var redisAddr string // = "meep-redis-master.default.svc.cluster.local:6379"
var APP_ENABLEMENT_DB = 0

var mutex *sync.Mutex
var rc *redis.Connector
var mqLocal *mq.MsgQueue
var hostUrl *url.URL
var sandboxName string
var mepName string
var basePath string
var baseKey string
var subMgr *subs.SubscriptionMgr
var appStore *apps.ApplicationStore
var appInfoMap map[string]map[string]string
var gracefulTerminateMap = map[string]chan bool{}

func notImplemented(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(http.StatusNotImplemented)
}

func Init(sandbox string, mep string, host *url.URL, msgQueue *mq.MsgQueue, redisAddr_ string, globalMutex *sync.Mutex) (err error) {
	redisAddr = redisAddr_
	sandboxName = sandbox
	hostUrl = host
	mqLocal = msgQueue
	mutex = globalMutex
	mepName = mep

	// Initialize app info cache
	appInfoMap = make(map[string]map[string]string)

	// Set base path & base storage key
	if mepName == globalMepName {
		basePath = "/" + sandboxName + "/" + appSupportBasePath
		baseKey = dkm.GetKeyRoot(sandboxName) + appEnablementKey + ":mep-global:"
	} else {
		basePath = "/" + sandboxName + "/" + mepName + "/" + appSupportBasePath
		baseKey = dkm.GetKeyRoot(sandboxName) + appEnablementKey + ":mep:" + mepName + ":"
	}

	// Connect to Redis DB
	rc, err = redis.NewConnector(redisAddr, APP_ENABLEMENT_DB)
	if err != nil {
		log.Error("Failed connection to Redis DB. Error: ", err)
		return err
	}
	log.Info("Connected to Redis DB")

	// Create Application Store
	cfg := &apps.ApplicationStoreCfg{
		Name:      moduleName,
		Namespace: sandboxName,
		RedisAddr: redisAddr,
	}
	appStore, err = apps.NewApplicationStore(cfg)
	if err != nil {
		log.Error("Failed to connect to Application Store. Error: ", err)
		return err
	}
	log.Info("Connected to Application Store")

	// Create Subscription Manager
	subMgrCfg := &subs.SubscriptionMgrCfg{
		Module:         moduleName,
		Sandbox:        sandboxName,
		Mep:            mepName,
		Service:        serviceName,
		Basekey:        baseKey,
		MetricsEnabled: true,
		ExpiredSubCb:   nil,
		PeriodicSubCb:  nil,
		TestNotifCb:    nil,
		NewWsCb:        nil,
	}
	subMgr, err = subs.NewSubscriptionMgr(subMgrCfg, redisAddr)
	if err != nil {
		log.Error("Failed to create Subscription Manager. Error: ", err)
		return err
	}
	log.Info("Created Subscription Manager")

	// TODO -- Initialize subscriptions from DB

	return nil
}

// Run - Start APP support
func Run() (err error) {

	// Register Message Queue handler
	handler := mq.MsgHandler{Handler: msgHandler, UserData: nil}
	_, err = mqLocal.RegisterHandler(handler)
	if err != nil {
		log.Error("Failed to listen for sandbox updates: ", err.Error())
		return err
	}

	// Update app info with latest apps from application store
	err = refreshApps()
	if err != nil {
		log.Error("Failed to sync & process apps with error: ", err.Error())
		return err
	}

	return nil
}

// Stop - Stop APP support
func Stop() (err error) {
	return nil
}

// Message Queue handler
func msgHandler(msg *mq.Msg, userData interface{}) {
	switch msg.Message {
	case mq.MsgAppUpdate:
		log.Debug("RX MSG: ", mq.PrintMsg(msg))
		appStore.Refresh()
		appId := msg.Payload[mqFieldAppId]
		_, _ = updateApp(appId)
	case mq.MsgAppRemove:
		log.Debug("RX MSG: ", mq.PrintMsg(msg))
		appStore.Refresh()
		appId := msg.Payload[mqFieldAppId]
		_ = terminateApp(appId)
	case mq.MsgAppFlush:
		log.Debug("RX MSG: ", mq.PrintMsg(msg))
		appStore.Refresh()
		persist, err := strconv.ParseBool(msg.Payload[mqFieldPersist])
		if err != nil {
			persist = false
		}
		_ = flushApps(persist)
	default:
	}
}

func applicationsConfirmReadyPOST(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> applicationsConfirmReadyPOST: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	appId := vars["appInstanceId"]

	mutex.Lock()
	defer mutex.Unlock()

	// Make sure App instance exists
	appInfo, err := getApp(appId)
	if err != nil {
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	// Retrieve App Ready information from request
	var confirmation AppReadyConfirmation
	decoder := json.NewDecoder(r.Body)
	err = decoder.Decode(&confirmation)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Validate App Ready params
	if confirmation.Indication == "" {
		log.Error("Mandatory Indication not present")
		errHandlerProblemDetails(w, "Mandatory Indication not present", http.StatusBadRequest)
		return
	}
	switch confirmation.Indication {
	case "READY":
	default:
		log.Error("Mandatory OperationAction value not valid")
		errHandlerProblemDetails(w, "Mandatory OperationAction value not valid", http.StatusBadRequest)
		return
	}

	// Set App state
	appInfo[fieldState] = APP_STATE_READY

	// Set App Info
	err = setAppInfo(appInfo)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Send response
	w.WriteHeader(http.StatusNoContent)
}

func applicationsConfirmTerminationPOST(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> applicationsConfirmTerminationPOST: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	appId := vars["appInstanceId"]

	mutex.Lock()
	defer mutex.Unlock()

	// Get App instance
	appInfo, err := getApp(appId)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	// Validate App info
	log.Info("applicationsConfirmTerminationPOST: appInfo: ", appInfo)
	code, problemDetails, err := validateAppInfo(appInfo)
	if err != nil {
		log.Error(err.Error())
		if problemDetails != "" {
			w.WriteHeader(code)
			fmt.Fprint(w, problemDetails)
		} else {
			errHandlerProblemDetails(w, err.Error(), code)
		}
		return
	}

	// Verify that confirmation is expected
	gracefulTerminateChannel, found := gracefulTerminateMap[appId]
	if !found {
		log.Error("Unexpected App Confirmation Termination Notification")
		errHandlerProblemDetails(w, "Unexpected App Confirmation Termination Notification", http.StatusBadRequest)
		return
	}

	// Retrieve Termination Confirmation data
	var confirmation AppTerminationConfirmation
	decoder := json.NewDecoder(r.Body)
	err = decoder.Decode(&confirmation)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
		return
	}
	log.Info("applicationsConfirmTerminationPOST: confirmation: ", confirmation)

	// Validate Termination Confirmation params
	if confirmation.OperationAction == nil {
		log.Error("Mandatory OperationAction not present")
		errHandlerProblemDetails(w, "Mandatory OperationAction not present", http.StatusBadRequest)
		return
	}
	switch *confirmation.OperationAction {
	case STOPPING_OperationActionType, TERMINATING_OperationActionType:
	default:
		log.Error("Mandatory OperationAction value not valid")
		errHandlerProblemDetails(w, "Mandatory OperationAction value not valid", http.StatusBadRequest)
		return
	}

	// Confirm termination
	gracefulTerminateChannel <- true

	// Send response
	w.WriteHeader(http.StatusNoContent)
}

func applicationsSubscriptionsPOST(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> applicationsSubscriptionsPOST: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	appId := vars["appInstanceId"]
	log.Info("applicationsSubscriptionsPOST: appInstanceId:", appId)

	mutex.Lock()
	defer mutex.Unlock()

	// Get App instance
	appInfo, err := getApp(appId)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}
	log.Info("applicationsSubscriptionsPOST: appInfo:", appInfo)

	// Validate App info
	code, problemDetails, err := validateAppInfo(appInfo)
	if err != nil {
		log.Error(err.Error())
		if problemDetails != "" {
			w.WriteHeader(code)
			fmt.Fprint(w, problemDetails)
		} else {
			errHandlerProblemDetails(w, err.Error(), code)
		}
		return
	}

	// Retrieve subscription request
	var appTermNotifSub AppTerminationNotificationSubscription
	decoder := json.NewDecoder(r.Body)
	err = decoder.Decode(&appTermNotifSub)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Verify mandatory properties
	if appTermNotifSub.CallbackReference == "" {
		log.Error("Mandatory CallbackReference parameter not present")
		errHandlerProblemDetails(w, "Mandatory CallbackReference parameter not present", http.StatusBadRequest)
		return
	}
	if appTermNotifSub.SubscriptionType != APP_TERMINATION_NOTIF_SUB_TYPE {
		log.Error("SubscriptionType shall be AppTerminationNotificationSubscription")
		errHandlerProblemDetails(w, "SubscriptionType shall be AppTerminationNotificationSubscription", http.StatusBadRequest)
		return
	}
	if appTermNotifSub.AppInstanceId == "" {
		log.Error("Mandatory AppInstanceId parameter not present")
		errHandlerProblemDetails(w, "Mandatory AppInstanceId parameter not present", http.StatusBadRequest)
		return
	}
	if appTermNotifSub.AppInstanceId != appId {
		log.Error("AppInstanceId in endpoint and in body not matching")
		errHandlerProblemDetails(w, "AppInstanceId in endpoint and in body not matching", http.StatusBadRequest)
		return
	}

	// Get a new subscription ID
	subId := subMgr.GenerateSubscriptionId()

	appTermNotifSub.Links = &Self{
		Self: &LinkType{
			Href: hostUrl.String() + basePath + "applications/" + appId + "/subscriptions/" + subId,
		},
	}

	// Create & store subscription
	subCfg := newAppTerminationNotifSubCfg(&appTermNotifSub, subId, appId)
	jsonSub := convertAppTerminationNotifSubToJson(&appTermNotifSub)
	_, err = subMgr.CreateSubscription(subCfg, jsonSub)
	if err != nil {
		log.Error("Failed to create subscription")
		errHandlerProblemDetails(w, "Failed to create subscription", http.StatusInternalServerError)
		return
	}

	// Send response
	w.Header().Set("Location", appTermNotifSub.Links.Self.Href)
	w.WriteHeader(http.StatusCreated)
	fmt.Fprint(w, jsonSub)
}

func applicationsSubscriptionGET(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> applicationsSubscriptionGET: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	subId := vars["subscriptionId"]
	appId := vars["appInstanceId"]
	log.Debug("applicationsSubscriptionGET: appId: ", appId)
	log.Debug("applicationsSubscriptionGET: subId: ", subId)

	mutex.Lock()
	defer mutex.Unlock()

	// Get App instance info
	appInfo, err := getApp(appId)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}
	log.Debug("applicationsSubscriptionGET: appInfo: ", appInfo)

	// Validate App info
	code, problemDetails, err := validateAppInfo(appInfo)
	if err != nil {
		log.Error(err.Error())
		if problemDetails != "" {
			w.WriteHeader(code)
			fmt.Fprint(w, problemDetails)
		} else {
			errHandlerProblemDetails(w, err.Error(), code)
		}
		return
	}

	// Find subscription by ID
	sub, err := subMgr.GetSubscription(subId)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	// Validate subscription
	if sub.Cfg.AppId != appId || sub.Cfg.Type != APP_TERMINATION_NOTIF_SUB_TYPE {
		err = errors.New("subscription not found")
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}
	log.Debug("applicationsSubscriptionGET: sub.JsonSubOrig: ", sub.JsonSubOrig)

	// Return original marshalled subscription
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, sub.JsonSubOrig)
}

func applicationsSubscriptionDELETE(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> applicationsSubscriptionDELETE: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	subId := vars["subscriptionId"]
	appId := vars["appInstanceId"]

	mutex.Lock()
	defer mutex.Unlock()

	// Get App instance info
	appInfo, err := getApp(appId)
	if err != nil {
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	// Validate App info
	code, problemDetails, err := validateAppInfo(appInfo)
	if err != nil {
		log.Error(err.Error())
		if problemDetails != "" {
			w.WriteHeader(code)
			fmt.Fprint(w, problemDetails)
		} else {
			errHandlerProblemDetails(w, err.Error(), code)
		}
		return
	}

	// Find subscription by ID
	sub, err := subMgr.GetSubscription(subId)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	// Validate subscription
	if sub.Cfg.AppId != appId || sub.Cfg.Type != APP_TERMINATION_NOTIF_SUB_TYPE {
		err = errors.New("subscription not found")
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	// Delete subscription
	err = subMgr.DeleteSubscription(sub)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Send response
	w.WriteHeader(http.StatusNoContent)
}

func applicationsSubscriptionsGET(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> applicationsSubscriptionsGET: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	appId := vars["appInstanceId"]
	log.Info("applicationsSubscriptionsGET: appInstanceId: ", appId)

	mutex.Lock()
	defer mutex.Unlock()

	// Get App instance info
	appInfo, err := getApp(appId)
	if err != nil {
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}
	log.Info("applicationsSubscriptionsGET: appInfo: ", appInfo)

	// Validate App info
	code, problemDetails, err := validateAppInfo(appInfo)
	if err != nil {
		log.Error(err.Error())
		if problemDetails != "" {
			w.WriteHeader(code)
			fmt.Fprint(w, problemDetails)
		} else {
			errHandlerProblemDetails(w, err.Error(), code)
		}
		return
	}

	// Get subscriptions for App instance
	subList, err := subMgr.GetFilteredSubscriptions(appId, APP_TERMINATION_NOTIF_SUB_TYPE)
	if err != nil {
		log.Error("Failed to get subscription list with err: ", err.Error())
		return
	}

	// Create subscription link list
	subscriptionLinkList := &MecAppSuptApiSubscriptionLinkList{
		Links: &MecAppSuptApiSubscriptionLinkListLinks{
			Self: &LinkType{
				Href: hostUrl.String() + basePath + "applications/" + appId + "/subscriptions",
			},
		},
	}

	for _, sub := range subList {
		// Create subscription reference & append it to link list
		subscription := MecAppSuptApiSubscriptionLinkListSubscription{
			SubscriptionType: APP_TERMINATION_NOTIF_SUB_TYPE,
			Href:             sub.Cfg.Self,
		}
		subscriptionLinkList.Links.Subscriptions = append(subscriptionLinkList.Links.Subscriptions, subscription)
	}

	// Send response
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, convertMecAppSuptApiSubscriptionLinkListToJson(subscriptionLinkList))
}

func timingCapsGET(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> timingCapsGET")

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	// Create timestamp
	seconds := time.Now().Unix()
	timingCaps := TimingCaps{
		TimeStamp: &TimingCapsTimeStamp{
			Seconds: int32(seconds),
		},
	}

	// Send response
	jsonResponse, err := json.Marshal(timingCaps)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, string(jsonResponse))
}

func timingCurrentTimeGET(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> timingCurrentTimeGET")

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	// Create timestamp
	seconds := time.Now().Unix()
	TimeSourceStatus := TRACEABLE_TimeSourceStatus
	currentTime := CurrentTime{
		Seconds:          int32(seconds),
		TimeSourceStatus: &TimeSourceStatus,
	}

	// Send response
	jsonResponse, err := json.Marshal(currentTime)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, string(jsonResponse))
}

/*
* appRegistrationPOST handles the registration of applications.
* It decodes the request body into an AppInfo struct, validates mandatory parameters, retrieves app instance information, validates the app info, and stores it in Redis.
* @param {string} keyName	App Info stored with this key
* @param {jsonResponse} contains APP Info response
* @return {error} error An error will return if occurs
 */
func appRegistrationPOST(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> appRegistrationPOST: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")

	// Decode the request body into AppInfo struct
	var appInfo AppInfo
	err := json.NewDecoder(r.Body).Decode(&appInfo)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusBadRequest)
		return
	}
	log.Info("appRegistrationPOST: appInfo: ", appInfo)

	// Validation of mandatory parameters
	if appInfo.AppName == "" {
		log.Error("Mandatory AppName parameter not present")
		errHandlerProblemDetails(w, "Mandatory AppName parameter not present", http.StatusBadRequest)
		return
	}

	if appInfo.AppInstanceId == "" {
		log.Error("Mandatory appInstanceId parameter should be present")
		errHandlerProblemDetails(w, "Mandatory attribute appInstanceId is missing in the request body.", http.StatusBadRequest)
		return
	}

	if !appInfo.IsInsByMec && appInfo.Endpoint == nil {
		log.Error("Shall be present when IsInsByMec is FALSE")
		errHandlerProblemDetails(w, "Endpoint shall be present when IsInsByMec is FALSE.", http.StatusBadRequest)
		return
	}

	// Get App instance
	log.Info("appRegistrationPOST: appInfo.AppInstanceId: ", appInfo.AppInstanceId)
	appId, err := getAppInfo(appInfo.AppInstanceId)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}
	log.Info("appRegistrationPOST: appId: ", appId)

	// Validate App info
	code, problemDetails, err := validateAppInfo(appId)
	if err != nil {
		log.Error(err.Error())
		if problemDetails != "" {
			w.WriteHeader(code)
			fmt.Fprint(w, problemDetails)
		} else {
			errHandlerProblemDetails(w, err.Error(), code)
		}
		return
	}

	// Set the application info in Redis
	keyName := baseKey + "appInfo:" + appInfo.AppInstanceId
	log.Info("appRegistrationPOST: keyName: ", keyName)
	if err := rc.JSONSetEntry(keyName, ".", convertAppInfoToJson(&appInfo)); err != nil {
		log.Error("Unable to store new Registration in redis")
		errHandlerProblemDetails(w, "Unable to store new Registration in redis", http.StatusInternalServerError)
		return
	}

	// Create a unique link for every subscription and concatenate subscription to it
	resourceURI := hostUrl.String() + basePath + "registrations/" + appInfo.AppInstanceId
	log.Info("appRegistrationPOST: resourceUDI: ", resourceURI)
	w.Header().Set("Location", resourceURI)

	// Prepare & send response
	jsonResponse := convertAppInfoToJson(&appInfo)
	// On successful registration, return 201 Created with the response body
	w.WriteHeader(http.StatusCreated)
	fmt.Fprint(w, jsonResponse)
}

/*
* appRegistrationGET handles retrieving the registration information of an application.
* It fetches the appInstanceId from the URL variables, retrieves the associated application info from Redis, and sends it back in the response. If the appInstanceId is not found or an error occurs, it responds with the appropriate status and error message.
* @param {http.ResponseWriter} w   HTTP response writer
* @param {string} jsonAppInfo Application information
* @return {error} error An error will return if occurs
 */
func appRegistrationGET(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> appRegistrationGET: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	appInstanceId := vars["appInstanceId"]
	log.Info("appRegistrationGET: appInstanceId: ", appInstanceId)

	mutex.Lock()
	defer mutex.Unlock()

	keyName := baseKey + "appInfo:" + appInstanceId
	log.Info("appRegistrationGET: keyName: ", keyName)
	jsonAppInfo, err := rc.JSONGetEntry(keyName, ".")
	if err != nil {
		err = errors.New("appInfo not found against the provided appInstanceId")
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	var appInfo AppInfo
	err = json.Unmarshal([]byte(jsonAppInfo), &appInfo)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusInternalServerError)
		return
	}
	log.Info("appRegistrationGET: appInfo: ", appInfo)

	// Marshal the AppInfo struct to JSON format
	jsonResponse := convertAppInfoToJson(&appInfo)

	// write the JSON response
	w.WriteHeader(http.StatusOK)
	fmt.Fprint(w, jsonResponse)
}

/*
* appRegistrationPUT modifies the registration information of an application.
* It fetches the appInstanceId from the URL variables, decodes the request body into an AppInfo struct, validates the parameters, checks for the existence of the app info in Redis, updates the Redis entry if validation passes,and responds with the appropriate status.
* @param {string} jsonAppInfo Application information
* @param {http.ResponseWriter} w   HTTP response writer
* @param {http.Request} r          HTTP request
* @return {error} error An error will return if occurs
 */
func appRegistrationPUT(w http.ResponseWriter, r *http.Request) {
	log.Debug(">>> appRegistrationPUT: ", r)

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	appInstanceId := vars["appInstanceId"]
	log.Info("appRegistrationPUT: appInstanceId: ", appInstanceId)

	// Read JSON input stream provided in the Request, and stores it in the buffer of a Decoder object
	decoder := json.NewDecoder(r.Body)
	// Decode function return strings containing the text provided in the request body
	var appInfoPut AppInfo
	err := decoder.Decode(&appInfoPut)
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusBadRequest)
		return
	}
	log.Info("appRegistrationPUT: appInfoPut: ", appInfoPut)

	keyName := baseKey + "appInfo:" + appInstanceId
	log.Info("appRegistrationPUT: keyName: ", keyName)
	jsonAppInfo, _ := rc.JSONGetEntry(keyName, ".")
	if jsonAppInfo == "" {
		log.Error("appInfo not found against the provided appInstanceId")
		errHandlerProblemDetails(w, "appInfo not found against the provided appInstanceId", http.StatusNotFound)
		return
	}

	if appInfoPut.AppInstanceId != "" {
		if appInstanceId != appInfoPut.AppInstanceId {
			log.Error("appInstnaceId provided in endpoint and in request body not matching")
			errHandlerProblemDetails(w, "appInstanceId provided in endpoint and in request body not matching", http.StatusNotFound)
			return
		}
	}

	if appInfoPut.AppName == "" {
		log.Error("Mandatory AppName parameter not present")
		errHandlerProblemDetails(w, "Mandatory AppName parameter not present", http.StatusBadRequest)
		return
	}

	if !appInfoPut.IsInsByMec && appInfoPut.Endpoint == nil {
		log.Error("Shall be present when IsInsByMec is FALSE")
		errHandlerProblemDetails(w, "Shall be present when IsInsByMec is FALSE.", http.StatusBadRequest)
		return
	}

	appInfoPut.AppInstanceId = appInstanceId

	// Store appInfo key in redis
	err = rc.JSONSetEntry(baseKey+"appInfo:"+appInstanceId, ".", convertAppInfoToJson(&appInfoPut))
	if err != nil {
		log.Error("Failed to store AppInfo in the redis DB: ", err)
	}

	w.WriteHeader(http.StatusNoContent)
}

/*
* appRegistrationDELETE handles the deletion of an application's registration information.
* It fetches the appInstanceId from the URL variables, checks if the app info exists in Redis, deletes the Redis entry if it exists, and responds with the appropriate status.
* @param {http.ResponseWriter} w   HTTP response writer
* @param {http.Request} r          HTTP request
* @return {error} error An error will return if occurs
 */
func appRegistrationDELETE(w http.ResponseWriter, r *http.Request) {
	log.Info("Delete appInfo by appInstanceId")

	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	vars := mux.Vars(r)
	appInstanceId := vars["appInstanceId"]

	keyName := baseKey + "appInfo:" + appInstanceId

	// Find appInfo entry in redis DB
	_, err := rc.JSONGetEntry(keyName, ".")
	if err != nil {
		err = errors.New("appInfo not found against the provided appInstanceId")
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	// Delete appInfo entry from redis DB
	err = rc.JSONDelEntry(keyName, ".")
	if err != nil {
		log.Error(err.Error())
		errHandlerProblemDetails(w, err.Error(), http.StatusNotFound)
		return
	}

	// Send response on successful deletion of registration
	w.WriteHeader(http.StatusNoContent)
}

func deleteAppInstance(appId string) {
	log.Debug(">>> deleteAppInstance: ", appId)

	// Delete app support subscriptions
	err := subMgr.DeleteFilteredSubscriptions(appId, APP_TERMINATION_NOTIF_SUB_TYPE)
	if err != nil {
		log.Error(err.Error())
	}

	// Clear App instance service subscriptions
	_ = sm.DeleteServiceSubscriptions(appId)

	// Clear App services
	_ = sm.DeleteServices(appId)

	// Flush App instance data
	keyName := baseKey + "app:" + appId
	log.Info("deleteAppInstance: keyName: ", keyName)
	_ = rc.DBFlush(keyName)

	// Confirm App removal
	sendAppRemoveCnf(appId)
}

func getAppList() ([]map[string]string, error) {
	log.Debug(">>> getAppList")

	var appInfoList []map[string]string

	// Get all applications from DB
	keyMatchStr := baseKey + "app:*:info"
	err := rc.ForEachEntry(keyMatchStr, populateAppInfo, &appInfoList)
	if err != nil {
		log.Error("Failed to get app info list with error: ", err.Error())
		return nil, err
	}
	return appInfoList, nil
}

func populateAppInfo(key string, entry map[string]string, userData interface{}) error {
	appInfoList := userData.(*[]map[string]string)

	// Copy entry
	appInfo := make(map[string]string, len(entry))
	for k, v := range entry {
		appInfo[k] = v
	}

	// Add app info to list
	*appInfoList = append(*appInfoList, appInfo)
	return nil
}

func validateAppInfo(appInfo map[string]string) (int, string, error) {
	// Make sure App is in ready state
	if appInfo[fieldState] != APP_STATE_READY {
		var problemDetails ProblemDetails
		problemDetails.Status = http.StatusForbidden
		problemDetails.Detail = "App Instance not ready. Waiting for AppReadyConfirmation."
		return http.StatusForbidden, convertProblemDetailsToJson(&problemDetails), errors.New("App Instance not ready")
	}
	return http.StatusOK, "", nil
}

func newAppInfo(app *apps.Application) (map[string]string, error) {
	log.Debug(">>> newAppInfo: ", app)

	// Validate app
	if app == nil {
		return nil, errors.New("nil application")
	}

	// Create App Info
	appInfo := make(map[string]string)
	appInfo[fieldAppId] = app.Id
	appInfo[fieldName] = app.Name
	appInfo[fieldNode] = app.Node
	appInfo[fieldType] = app.Type
	appInfo[fieldPersist] = strconv.FormatBool(app.Persist)
	appInfo[fieldState] = APP_STATE_INITIALIZED
	return appInfo, nil
}

func setAppInfo(appInfo map[string]string) error {
	log.Debug(">>> setAppInfo: ", appInfo)

	appId, found := appInfo[fieldAppId]
	if !found || appId == "" {
		return errors.New("missing app instance id")
	}

	// Convert value type to interface{} before storing app info
	entry := make(map[string]interface{}, len(appInfo))
	for k, v := range appInfo {
		entry[k] = v
	}

	// Store entry
	keyName := baseKey + "app:" + appId + ":info"
	log.Info("setAppInfo: keyName: ", keyName)
	err := rc.SetEntry(keyName, entry)
	if err != nil {
		return err
	}

	// Cache entry
	appInfoMap[appId] = appInfo

	return nil
}

func delAppInfo(appInfo map[string]string) error {
	log.Debug(">>> delAppInfo: ", appInfo)

	appId := appInfo[fieldAppId]

	// Clear graceful termination
	delete(gracefulTerminateMap, appId)

	// Remove from cache
	delete(appInfoMap, appId)

	// Delete app instance
	deleteAppInstance(appId)

	return nil
}

func refreshApps() error {
	// Refresh app store
	appStore.Refresh()

	// Get full App store app list
	fullAppList, err := appStore.GetAll()
	if err != nil {
		log.Error(err.Error())
		return err
	}

	// If MEP instance, ignore non-local apps
	appList := make([]*apps.Application, 0)
	for _, app := range fullAppList {
		if mepName != globalMepName && app.Node != mepName {
			log.Debug("Ignoring update on non-local MEP for app: ", app.Id)
			continue
		}
		appList = append(appList, app)
	}

	mutex.Lock()
	defer mutex.Unlock()

	// Retrieve app info list from DB
	appInfoList, err := getAppList()
	if err != nil {
		log.Error(err.Error())
		return err
	}

	// Update app info
	for _, app := range appList {
		found := false
		for _, appInfo := range appInfoList {
			if appInfo[fieldAppId] == app.Id {
				found = true
				// Set existing app info to make sure cache is updated
				err = setAppInfo(appInfo)
				if err != nil {
					log.Error(err.Error())
				}
				break
			}
		}
		// Create & set app info for new apps
		if !found {
			appInfo, err := newAppInfo(app)
			if err != nil {
				log.Error(err.Error())
				continue
			}
			err = setAppInfo(appInfo)
			if err != nil {
				log.Error(err.Error())
				continue
			}
		}
	}

	// Remove deleted app info
	for _, appInfo := range appInfoList {
		found := false
		for _, app := range appList {
			if app.Id == appInfo[fieldAppId] {
				found = true
				break
			}
		}
		if !found {
			err := delAppInfo(appInfo)
			if err != nil {
				log.Error(err.Error())
			}
		}
	}
	return nil
}

func getApp(appId string) (map[string]string, error) {
	log.Debug(">>> getApp: ", appId)

	appInfo, found := appInfoMap[appId]
	if !found {
		return nil, errors.New("app instance not found")
	}
	return appInfo, nil
}

func updateApp(appId string) (map[string]string, error) {
	log.Debug(">>> updateApp: ", appId)

	// Get App information from app store
	app, err := appStore.Get(appId)
	if err != nil {
		log.Error(err.Error())
		return nil, err
	}
	log.Info("updateApp: app: ", app)

	// If MEP instance, ignore non-local apps
	if mepName != globalMepName && app.Node != mepName {
		return nil, errors.New("ignoring app update on other MEP")
	}

	mutex.Lock()
	defer mutex.Unlock()

	// Store App Info
	appInfo, err := newAppInfo(app)
	if err != nil {
		log.Error(err.Error())
		return nil, err
	}
	err = setAppInfo(appInfo)
	if err != nil {
		log.Error(err.Error())
		return nil, err
	}
	log.Info("updateApp: appInfo: ", appInfo)

	// Store App Info
	return appInfo, nil
}

func terminateApp(appId string) error {
	log.Debug(">>> terminateApp: ", appId)

	mutex.Lock()
	defer mutex.Unlock()

	// Get App info
	appInfo, found := appInfoMap[appId]
	if !found {
		return errors.New("App info not found for: " + appId)
	}

	// Get subscriptions for App instance
	subList, err := subMgr.GetFilteredSubscriptions(appId, APP_TERMINATION_NOTIF_SUB_TYPE)
	if err != nil {
		log.Error("Failed to get subscription list with err: ", err.Error())
		return err
	}

	// Process graceful termination
	gracefulTermination := false
	for _, sub := range subList {
		gracefulTermination = true

		// Create notification payload
		operationAction := TERMINATING_OperationActionType
		notif := &AppTerminationNotification{
			NotificationType:   APP_TERMINATION_NOTIF_TYPE,
			OperationAction:    &operationAction,
			MaxGracefulTimeout: DEFAULT_GRACEFUL_TIMEOUT,
			Links: &AppTerminationNotificationLinks{
				Subscription: &LinkType{
					Href: sub.Cfg.Self,
				},
				ConfirmTermination: &LinkTypeConfirmTermination{
					Href: hostUrl.String() + basePath + "confirm_termination",
				},
			},
		}

		// Start graceful timeout timer prior to sending the app termination notification
		gracefulTerminateChannel := make(chan bool)
		gracefulTerminateMap[appId] = gracefulTerminateChannel

		go func(sub *subs.Subscription) {
			log.Info("Sending App Termination notification (" + sub.Cfg.Id + ") for " + appId)
			err := subMgr.SendNotification(sub, []byte(convertAppTerminationNotifToJson(notif)))
			if err != nil {
				log.Error("Failed to send App termination notif with err: ", err.Error())
			}

			// Wait for app termination confirmation or timeout
			select {
			case <-gracefulTerminateChannel:
				mutex.Lock()
				defer mutex.Unlock()
				log.Debug("Termination confirmation received for: ", appId)

			case <-time.After(time.Duration(DEFAULT_GRACEFUL_TIMEOUT) * time.Second):
				mutex.Lock()
				defer mutex.Unlock()
				delete(gracefulTerminateMap, appId)
			}

			// Delete App
			err = delAppInfo(appInfo)
			if err != nil {
				log.Error(err.Error())
			}
		}(sub)
	}

	// Delete App instance immediately if no graceful termination subscription
	if !gracefulTermination {
		err := delAppInfo(appInfo)
		if err != nil {
			log.Error(err.Error())
		}
	}

	return nil
}

func flushApps(persist bool) error {
	mutex.Lock()
	defer mutex.Unlock()

	// Delete App instances
	for appId, appInfo := range appInfoMap {
		// Ignore persistent apps unless required
		if !persist {
			appPersist, err := strconv.ParseBool(appInfo[fieldPersist])
			if err != nil {
				appPersist = false
			}
			if appPersist {
				continue
			}
		}

		// No need for graceful termination when flushing apps
		delete(gracefulTerminateMap, appId)

		// Delete app info
		err := delAppInfo(appInfo)
		if err != nil {
			log.Error(err.Error())
		}
	}
	return nil
}

func newAppTerminationNotifSubCfg(sub *AppTerminationNotificationSubscription, subId string, appId string) *subs.SubscriptionCfg {
	subCfg := &subs.SubscriptionCfg{
		Id:                  subId,
		AppId:               appId,
		Type:                APP_TERMINATION_NOTIF_SUB_TYPE,
		NotifType:           APP_TERMINATION_NOTIF_TYPE,
		Self:                sub.Links.Self.Href,
		NotifyUrl:           sub.CallbackReference,
		ExpiryTime:          nil,
		PeriodicInterval:    0,
		RequestTestNotif:    false,
		RequestWebsocketUri: false,
	}
	return subCfg
}

func sendAppRemoveCnf(id string) {
	// Create message to send on MQ
	msg := mqLocal.CreateMsg(mq.MsgAppRemoveCnf, mq.TargetAll, sandboxName)
	msg.Payload[mqFieldAppId] = id

	// Send message to inform other modules of app removal
	log.Debug("TX MSG: ", mq.PrintMsg(msg))
	err := mqLocal.SendMsg(msg)
	if err != nil {
		log.Error("Failed to send message. Error: ", err.Error())
		return
	}
}

/*
* getAppInfo gets application information using its application Instance Id stored in redis
* @param {string} appId application Instance Id used to retrive its information from redis DB
*	@return {map[string]string} appInfo application information
* @return {error} err It returns error if there is no information associated with this appId
 */
func getAppInfo(appId string) (map[string]string, error) {
	log.Debug(">>> getAppInfo: ", appId)

	var appInfo map[string]string

	// Get app instance from local MEP only
	keyName := baseKey + "app:" + appId + ":info"
	log.Info("getAppInfo: keyName: ", keyName)
	appInfo, err := rc.GetEntry(keyName)
	if err != nil {
		log.Error(err.Error())
		return nil, err
	} else if len(appInfo) == 0 {
		return nil, errors.New("app instance not found")
	}
	log.Info("getAppInfo: appInfo: ", appInfo)

	return appInfo, nil
}

func errHandlerProblemDetails(w http.ResponseWriter, error string, code int) {
	var pd ProblemDetails
	pd.Detail = error
	pd.Status = int32(code)

	jsonResponse := convertProblemDetailstoJson(&pd)

	w.WriteHeader(code)
	fmt.Fprint(w, jsonResponse)
}
