package controllers

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"oc-datacenter/conf"
	"oc-datacenter/infrastructure"
	"oc-datacenter/models"
	"slices"
	"time"

	oclib "cloud.o-forge.io/core/oc-lib"

	beego "github.com/beego/beego/v2/server/web"
	jwt "github.com/golang-jwt/jwt/v5"
	"gopkg.in/yaml.v2"
	v1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
)

type KubeInfo struct {
	Url      *string
	KubeCA   *string
	KubeCert *string
	KubeKey  *string
}

type RemoteKubeconfig struct {
	Data *string
}

type KubeUser struct {
	Name string
	User struct {
		Token string
	}
}

type KubeconfigToken struct {
	ApiVersion     string `yaml:"apiVersion"`
	Kind           string `yaml:"kind"`
	Preferences    string `yaml:"preferences"`
	CurrentContext string `yaml:"current-context"`
	Clusters       []struct {
		Cluster struct {
			CA     string `yaml:"certificate-authority-data"`
			Server string `yaml:"server"`
		} `yaml:"cluster"`
		Name string `yaml:"name"`
	} `yaml:"clusters"`
	Contexts []struct {
		Context struct {
			Cluster string `yaml:"cluster"`
			User    string `yaml:"user"`
		} `yaml:"context"`
		Name string `yaml:"name"`
	} `yaml:"contexts"`
	Users []struct {
		Name string `yaml:"name"`
		User struct {
			Token string `yaml:"token"`
		} `yaml:"user"`
	} `yaml:"users"`
}

// Operations about the admiralty objects of the datacenter
type AdmiraltyController struct {
	beego.Controller
}

// @Title GetAllTargets
// @Description find all Admiralty Target
// @Success 200
// @router /targets [get]
func (c *AdmiraltyController) GetAllTargets() {
	serv, err := infrastructure.NewService()
	if err != nil {
		// change code to 500
		HandleControllerErrors(c.Controller, 500, &err, nil)
		// c.Ctx.Output.SetStatus(500)
		// c.ServeJSON()
		// c.Data["json"] = map[string]string{"error": err.Error()}
		return
	}

	res, err := serv.GetTargets(c.Ctx.Request.Context())
	c.Data["json"] = res
	c.ServeJSON()
}

// @Title GetOneTarget
// @Description find one Admiralty Target
// @Param id path string true "the name of the target to get"
// @Success 200
// @router /targets/:execution [get]
func (c *AdmiraltyController) GetOneTarget() {
	id := c.Ctx.Input.Param(":execution")
	serv, err := infrastructure.NewService()
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
		c.Data["json"] = map[string]string{"error": err.Error()}
		return
	}

	res, err := serv.GetTargets(c.Ctx.Request.Context())
	if err != nil {
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
		c.Data["json"] = map[string]string{"error": err.Error()}
		return
	}
	id = "target-" + id
	found := slices.Contains(res, id)
	if !found {
		c.Ctx.Output.SetStatus(404)
		c.ServeJSON()
	}

	c.Data["json"] = id
	c.ServeJSON()
}

// @Title DeleteAdmiraltySession
// @Description find one Admiralty Target
// @Param execution path string true "the name of the target to get"
// @Success 200
// @router /targets/:execution [delete]
func (c *AdmiraltyController) DeleteAdmiraltySession() {
	id := c.Ctx.Input.Param(":execution")
	serv, err := infrastructure.NewService()
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
		c.Data["json"] = map[string]string{"error": err.Error()}
		return
	}

	err = serv.DeleteNamespace(c.Ctx.Request.Context(), id)
	if err != nil {
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
		c.Data["json"] = map[string]string{"error": err.Error()}
		return
	}

	c.Data["json"] = id
	c.ServeJSON()
}

// @Title CreateAdmiraltySource
// @Description Create an Admiralty Source on remote cluster
// @Param execution path 	string 		true	"execution id of the workflow"
// @Success 201
// @router /source/:execution [post]
func (c *AdmiraltyController) CreateAdmiraltySource() {

	execution := c.Ctx.Input.Param(":execution")
	fmt.Println("execution :: ", execution)
	fmt.Println("input :: ", c.Ctx.Input)
	serv, err := infrastructure.NewKubernetesService()
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	res, err := serv.CreateAdmiraltySource(c.Ctx.Request.Context(), execution)
	if err != nil {
		if apierrors.IsAlreadyExists(err) {
			c.Ctx.Output.SetStatus(409)
			c.Data["json"] = map[string]string{"info": "A source already exists for this namespace : " + execution}
			c.ServeJSON()
			return
		}
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	// TODO : Return a description of the created resource
	var respData map[string]interface{}
	err = json.Unmarshal(res, &respData)
	if err != nil {
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
		c.Data["json"] = map[string]string{"error": err.Error()}
		return
	}
	c.Ctx.Output.SetStatus(201)
	c.Data["json"] = respData
	c.ServeJSON()

}

// @Title CreateAdmiraltyTarget
// @Description Create an Admiralty Target in the namespace associated to the executionID
// @Param execution path 	string 		true	"execution id of the workflow"
// @Param peer	path	string	true	"peerId of the peer the target points to"
// @Success 201
// @router /target/:execution/:peer [post]
func (c *AdmiraltyController) CreateAdmiraltyTarget() {
	var data map[string]interface{}

	execution := c.Ctx.Input.Param(":execution")
	peerId := c.Ctx.Input.Param(":peer")

	if execution == "" || peerId == "" {
		c.Ctx.Output.SetStatus(400)
		c.Data["json"] = map[string]string{"error": "parameters can be empty " + "execution: " + execution + " peer: " + peerId}
		c.ServeJSON()
		return
	}

	serv, err := infrastructure.NewService()
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	resp, err := serv.CreateAdmiraltyTarget(c.Ctx.Request.Context(), execution, peerId)
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}
	if resp == nil {
		fmt.Println("Error while trying to create Admiralty target")
		fmt.Println(resp)
		fmt.Println(err)
		c.Ctx.Output.SetStatus(401)
		c.Data["json"] = map[string]string{"error": "Could not perform the action"}
		c.ServeJSON()
		return
	}

	err = json.Unmarshal(resp, &data)
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
		c.Data["json"] = map[string]string{"error": err.Error()}
		return
	}
	c.Ctx.Output.SetStatus(201)
	c.Data["json"] = data
	c.ServeJSON()

}

// @Title GetKubeSecret
// @Description Retrieve the secret created from a Kubeconfig that will be associated to an Admiralty Target
// @Param execution path 	string 		true	"execution id of the workflow"
// @Param peer		path 	string 		true	"UUID of the peer to which the resource is linked"
// @Success 200
// @router /secret/:execution/:peer [get]
func (c *AdmiraltyController) GetKubeSecret() {
	var data map[string]interface{}

	execution := c.Ctx.Input.Param(":execution")
	peerId := c.Ctx.Input.Param(":peer")

	serv, err := infrastructure.NewService()
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	resp, err := serv.GetKubeconfigSecret(c.Ctx.Request.Context(), execution, peerId)
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}
	if resp == nil {
		c.Ctx.Output.SetStatus(404)
		c.ServeJSON()
		return
	}

	err = json.Unmarshal(resp, &data)
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.ServeJSON()
		c.Data["json"] = map[string]string{"error": err.Error()}
		return
	}

	c.Data["json"] = data
	c.ServeJSON()
}

// @Title CreateKubeSecret
// @Description Creat a secret from a Kubeconfig that will be associated to an Admiralty Target

// @Param execution path 	string 		true	"execution id of the workflow"
// @Param peer		path 	string 		true	"UUID of the peer to which the resource is linked"
// @Param kubeconfig 	body	controllers.RemoteKubeconfig 		true 	"Kubeconfig to use when creating secret"
// @Success 201
// @router /secret/:execution/:peer [post]
func (c *AdmiraltyController) CreateKubeSecret() {
	var kubeconfig RemoteKubeconfig
	var respData map[string]interface{}

	data := c.Ctx.Input.CopyBody(100000)

	err := json.Unmarshal(data, &kubeconfig)
	if err != nil {
		fmt.Println("Error when retrieving the data for kubeconfig from request")
		fmt.Println(err)
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	execution := c.Ctx.Input.Param(":execution")
	peerId := c.Ctx.Input.Param(":peer")

	serv, err := infrastructure.NewService()
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	resp, err := serv.CreateKubeconfigSecret(c.Ctx.Request.Context(), *kubeconfig.Data, execution, peerId)
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	err = json.Unmarshal(resp, &respData)
	c.Ctx.Output.SetStatus(201)
	c.Data["json"] = respData
	c.ServeJSON()

}

// @name GetAdmiraltyNodes
// @description	Allows user to test if an admiralty connection has already been established : Target and valid Secret set up on the local host  and Source set up on remote host
// @Param execution path 	string 		true	"execution id of the workflow"
// @Param peer		path 	string 		true	"UUID of the peer to which the resource is linked"
// @Success 200
// @router /node/:execution/:peer [get]
func (c *AdmiraltyController) GetNodeReady() {
	var secret v1.Secret
	execution := c.Ctx.Input.Param(":execution")
	peerId := c.Ctx.Input.Param(":peer")

	serv, err := infrastructure.NewService()
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	node, err := serv.GetOneNode(c.Ctx.Request.Context(), execution, peerId)
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}
	if node == nil {
		c.Ctx.Output.SetStatus(404)
		c.Data["json"] = map[string]string{
			"node": "the node for " + execution + " can't be found, make sure both target and source resources are set up on local and remote hosts",
		}
		c.ServeJSON()
		return
	}

	resp, err := serv.GetKubeconfigSecret(c.Ctx.Request.Context(), execution, peerId)
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}
	if resp == nil {
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": "Nodes was up but the secret can't be found"}
		c.ServeJSON()
		return
	}

	// Extract JWT token RS265 encoded
	var editedKubeconfig map[string]interface{}
	json.Unmarshal(resp, &secret)
	byteEditedKubeconfig := secret.Data["config"]
	err = yaml.Unmarshal(byteEditedKubeconfig, &editedKubeconfig)
	// err = json.Unmarshal(byteEditedKubeconfig,&editedKubeconfig)
	if err != nil {
		fmt.Println("Error while retrieving the kubeconfig from secret-", execution)
		fmt.Println(err)
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = err
		c.ServeJSON()
		return
	}

	token, err := retrieveTokenFromKonfig(editedKubeconfig)
	if err != nil {
		fmt.Println("Error while trying to retrieve token for kubeconfing")
		fmt.Println(err)
		HandleControllerErrors(c.Controller, 500, &err, nil)
	}

	// Decode token
	isExpired, err := isTokenExpired(token)
	if err != nil {
		fmt.Println("Error veryfing token's expiration")
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = err
		c.ServeJSON()
	}

	if *isExpired {
		c.Data["json"] = map[string]interface{}{
			"token": "token in the secret is expired and must be regenerated",
			"node":  node,
		}
		c.Ctx.Output.SetStatus(410)
		c.ServeJSON()
	}

	c.Data["json"] = map[string]interface{}{"node": node, "token": true}
	c.ServeJSON()

}

func retrieveTokenFromKonfig(editedKubeconfig map[string]interface{}) (string, error) {
	var kubeUsers []KubeUser
	b, err := yaml.Marshal(editedKubeconfig["users"])
	if err != nil {
		fmt.Println("Error while retrieving the users attribute from the Kubeconfig")
		fmt.Println(err)
		return "", err
	}
	err = yaml.Unmarshal(b, &kubeUsers)
	if err != nil {
		fmt.Println("Error while unmarshalling users attribute from kubeconfig")
		fmt.Println(err)
		return "", nil
	}
	fmt.Println(kubeUsers)
	token := kubeUsers[0].User.Token

	return token, nil
}

func isTokenExpired(token string) (*bool, error) {
	logger := oclib.GetLogger()

	t, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
	if err != nil {
		fmt.Println("couldn't decode token")
		return nil, err
	}

	expiration, err := t.Claims.GetExpirationTime()
	if err != nil {
		fmt.Println("Error while checking token's expiration time")
		return nil, err
	}

	logger.Debug().Msg("Expiration date : " + expiration.UTC().Format("2006-01-02T15:04:05"))
	logger.Debug().Msg(fmt.Sprint("Now : ", time.Now().Unix()))
	logger.Debug().Msg(fmt.Sprint("Token : ", expiration.Unix()))

	expired := expiration.Unix() < time.Now().Unix()

	return &expired, nil
}

// @name Get Admiralty Kubeconfig
// @description	Retrieve a kubeconfig from the host with the token to authenticate as the SA from the namespace identified with execution id

// @Param execution path 	string 		true	"execution id of the workflow"
// @Success 200
// @router /kubeconfig/:execution [get]
func (c *AdmiraltyController) GetAdmiraltyKubeconfig() {

	execution := c.Ctx.Input.Param(":execution")

	serv, err := infrastructure.NewService()
	if err != nil {
		// change code to 500
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	generatedToken, err := serv.GenerateToken(c.Ctx.Request.Context(), execution, 3600)
	if err != nil {
		fmt.Println("Couldn't generate a token for ns-", execution)
		fmt.Println(err)
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	kubeconfig, err := NewHostKubeWithToken(generatedToken)
	if err != nil {
		fmt.Println("Could not retrieve the Kubeconfig edited with token")
		fmt.Println(err)
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
		return
	}

	b, err := json.Marshal(kubeconfig)
	if err != nil {
		fmt.Println("Error while marshalling kubeconfig")
		c.Ctx.Output.SetStatus(500)
		c.Data["json"] = map[string]string{"error": err.Error()}
		c.ServeJSON()
	}

	encodedKubeconfig := base64.StdEncoding.EncodeToString(b)
	c.Data["json"] = map[string]string{
		"data": encodedKubeconfig,
	}
	json.NewEncoder(c.Ctx.ResponseWriter)
	c.ServeJSON()
}

func NewHostKubeWithToken(token string) (*models.KubeConfigValue, error) {
	if len(token) == 0 {
		return nil, fmt.Errorf("you didn't provide a token to be inserted in the Kubeconfig")
	}

	encodedCA := base64.StdEncoding.EncodeToString([]byte(conf.GetConfig().KubeCA))

	hostKube := models.KubeConfigValue{
		APIVersion:     "v1",
		CurrentContext: "default",
		Kind:           "Config",
		Preferences:    struct{}{},
		Clusters: []models.KubeconfigNamedCluster{
			{
				Name: "default",
				Cluster: models.KubeconfigCluster{
					Server:                   "https://" + conf.GetConfig().KubeHost + ":6443",
					CertificateAuthorityData: encodedCA,
				},
			},
		},
		Contexts: []models.KubeconfigNamedContext{
			{
				Name: "default",
				Context: models.KubeconfigContext{
					Cluster: "default",
					User:    "default",
				},
			},
		},
		Users: []models.KubeconfigUser{
			{
				Name: "default",
				User: models.KubeconfigUserKeyPair{
					Token: token,
				},
			},
		},
	}

	return &hostKube, nil
}
