// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package operatorapi

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/go-openapi/runtime/middleware"
	"github.com/golang-jwt/jwt/v4"
	"github.com/minio/console/cluster"
	"github.com/minio/console/models"
	"github.com/minio/console/operatorapi/operations"
	"github.com/minio/console/operatorapi/operations/operator_api"
	"github.com/minio/console/pkg"
	errors "github.com/minio/console/restapi"
	"github.com/minio/pkg/env"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var (
	mpConfigMapDefault = "mp-config"
	mpConfigMapKey     = "MP_CONFIG_KEY"
	mpHostEnvVar       = "MP_HOST"
	defaultMPHost      = "https://marketplace.apps.min.dev"
	mpEUHostEnvVar     = "MP_EU_HOST"
	defaultEUMPHost    = "https://marketplace-eu.apps.min.dev"
	isMPEmailSet       = "isEmailSet"
	emailNotSetMsg     = "Email was not sent in request"
)

func registerMarketplaceHandlers(api *operations.OperatorAPI) {
	api.OperatorAPIGetMPIntegrationHandler = operator_api.GetMPIntegrationHandlerFunc(func(params operator_api.GetMPIntegrationParams, session *models.Principal) middleware.Responder {
		payload, err := getMPIntegrationResponse(session, params)
		if err != nil {
			return operator_api.NewGetMPIntegrationDefault(int(err.Code)).WithPayload(err)
		}
		return operator_api.NewGetMPIntegrationOK().WithPayload(payload)
	})

	api.OperatorAPIPostMPIntegrationHandler = operator_api.PostMPIntegrationHandlerFunc(func(params operator_api.PostMPIntegrationParams, session *models.Principal) middleware.Responder {
		err := postMPIntegrationResponse(session, params)
		if err != nil {
			return operator_api.NewPostMPIntegrationDefault(int(err.Code)).WithPayload(err)
		}
		return operator_api.NewPostMPIntegrationCreated()
	})
}

func getMPIntegrationResponse(session *models.Principal, params operator_api.GetMPIntegrationParams) (*operator_api.GetMPIntegrationOKBody, *models.Error) {
	clientSet, err := cluster.K8sClient(session.STSSessionToken)
	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
	defer cancel()
	if err != nil {
		return nil, errors.ErrorWithContext(ctx, err)
	}
	isMPEmailSet, err := getMPEmail(ctx, &k8sClient{client: clientSet})
	if err != nil {
		return nil, errors.ErrorWithContext(ctx, errors.ErrNotFound)
	}
	return &operator_api.GetMPIntegrationOKBody{
		IsEmailSet: isMPEmailSet,
	}, nil
}

func getMPEmail(ctx context.Context, clientSet K8sClientI) (bool, error) {
	cm, err := clientSet.getConfigMap(ctx, "default", getMPConfigMapKey(mpConfigMapKey), metav1.GetOptions{})
	if err != nil {
		return false, err
	}
	return cm.Data[isMPEmailSet] == "true", nil
}

func postMPIntegrationResponse(session *models.Principal, params operator_api.PostMPIntegrationParams) *models.Error {
	clientSet, err := cluster.K8sClient(session.STSSessionToken)
	ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
	defer cancel()
	if err != nil {
		return errors.ErrorWithContext(ctx, err)
	}
	return setMPIntegration(ctx, params.Body.Email, params.Body.IsInEU, &k8sClient{client: clientSet})
}

func setMPIntegration(ctx context.Context, email string, isInEU bool, clientSet K8sClientI) *models.Error {
	if email == "" {
		return errors.ErrorWithContext(ctx, errors.ErrBadRequest, fmt.Errorf(emailNotSetMsg))
	}
	if _, err := setMPEmail(ctx, email, isInEU, clientSet); err != nil {
		return errors.ErrorWithContext(ctx, err)
	}
	return nil
}

func setMPEmail(ctx context.Context, email string, isInEU bool, clientSet K8sClientI) (*corev1.ConfigMap, error) {
	if err := postEmailToMP(email, isInEU); err != nil {
		return nil, err
	}
	cm := createCM()
	return clientSet.createConfigMap(ctx, "default", cm, metav1.CreateOptions{})
}

func postEmailToMP(email string, isInEU bool) error {
	mpURL, err := getMPURL(isInEU)
	if err != nil {
		return err
	}
	return makePostRequestToMP(mpURL, email)
}

func getMPURL(isInEU bool) (string, error) {
	mpHost := getMPHost(isInEU)
	if mpHost == "" {
		return "", fmt.Errorf("mp host not set")
	}
	return fmt.Sprintf("%s/mp-email", mpHost), nil
}

func getMPHost(isInEU bool) string {
	if isInEU {
		return env.Get(mpEUHostEnvVar, defaultEUMPHost)
	}
	return env.Get(mpHostEnvVar, defaultMPHost)
}

func makePostRequestToMP(url, email string) error {
	request, err := createMPRequest(url, email)
	if err != nil {
		return err
	}
	client := &http.Client{Timeout: 3 * time.Second}
	if res, err := client.Do(request); err != nil {
		return err
	} else if res.StatusCode >= http.StatusBadRequest {
		b, _ := io.ReadAll(res.Body)
		return fmt.Errorf("request to %s failed with status code %d and error %s", url, res.StatusCode, string(b))
	}
	return nil
}

func createMPRequest(url, email string) (*http.Request, error) {
	request, err := http.NewRequest("POST", url, strings.NewReader(fmt.Sprintf("{\"email\":\"%s\"}", email)))
	if err != nil {
		return nil, err
	}
	jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{})
	jwtTokenString, err := jwtToken.SignedString([]byte(pkg.MPSecret))
	if err != nil {
		return nil, err
	}
	request.Header.Add("Cookie", fmt.Sprintf("jwtToken=%s", jwtTokenString))
	request.Header.Add("Content-Type", "application/json")
	return request, nil
}

func createCM() *corev1.ConfigMap {
	return &corev1.ConfigMap{
		TypeMeta: metav1.TypeMeta{
			Kind:       "ConfigMap",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      getMPConfigMapKey(mpConfigMapKey),
			Namespace: "default",
		},
		Data: map[string]string{isMPEmailSet: "true"},
	}
}

func getMPConfigMapKey(envVar string) string {
	if mp := os.Getenv(envVar); mp != "" {
		return mp
	}
	return mpConfigMapDefault
}
