package exec

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os/exec"
	"path"
	"runtime"
	"strings"
	"time"

	"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
	"github.com/portainer/portainer/api/http/security"
	"github.com/portainer/portainer/api/kubernetes/cli"

	portainer "github.com/portainer/portainer/api"
	"github.com/portainer/portainer/api/crypto"
)

// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment.
type KubernetesDeployer struct {
	binaryPath                  string
	dataStore                   portainer.DataStore
	reverseTunnelService        portainer.ReverseTunnelService
	signatureService            portainer.DigitalSignatureService
	kubernetesClientFactory     *cli.ClientFactory
	kubernetesTokenCacheManager *kubernetes.TokenCacheManager
}

// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
	return &KubernetesDeployer{
		binaryPath:                  binaryPath,
		dataStore:                   datastore,
		reverseTunnelService:        reverseTunnelService,
		signatureService:            signatureService,
		kubernetesClientFactory:     kubernetesClientFactory,
		kubernetesTokenCacheManager: kubernetesTokenCacheManager,
	}
}

func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
	tokenData, err := security.RetrieveTokenData(request)
	if err != nil {
		return "", err
	}

	kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
	if err != nil {
		return "", err
	}

	tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))

	tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
	if err != nil {
		return "", err
	}

	if tokenData.Role == portainer.AdministratorRole {
		return tokenManager.GetAdminServiceAccountToken(), nil
	}

	token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID), endpoint.ID)
	if err != nil {
		return "", err
	}

	if token == "" {
		return "", fmt.Errorf("can not get a valid user service account token")
	}
	return token, nil
}

// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint.
// Otherwise it will use kubectl to deploy the manifest.
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
	if endpoint.Type == portainer.KubernetesLocalEnvironment {
		token, err := deployer.getToken(request, endpoint, true)
		if err != nil {
			return "", err
		}

		command := path.Join(deployer.binaryPath, "kubectl")
		if runtime.GOOS == "windows" {
			command = path.Join(deployer.binaryPath, "kubectl.exe")
		}

		args := make([]string, 0)
		args = append(args, "--server", endpoint.URL)
		args = append(args, "--insecure-skip-tls-verify")
		args = append(args, "--token", token)
		args = append(args, "--namespace", namespace)
		args = append(args, "apply", "-f", "-")

		var stderr bytes.Buffer
		cmd := exec.Command(command, args...)
		cmd.Stderr = &stderr
		cmd.Stdin = strings.NewReader(stackConfig)

		output, err := cmd.Output()
		if err != nil {
			return "", errors.New(stderr.String())
		}

		return string(output), nil
	}

	// agent

	endpointURL := endpoint.URL
	if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
		tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
		if tunnel.Status == portainer.EdgeAgentIdle {

			err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
			if err != nil {
				return "", err
			}

			settings, err := deployer.dataStore.Settings().Settings()
			if err != nil {
				return "", err
			}

			waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
			time.Sleep(waitForAgentToConnect * 2)
		}

		endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
	}

	transport := &http.Transport{}

	if endpoint.TLSConfig.TLS {
		tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
		if err != nil {
			return "", err
		}
		transport.TLSClientConfig = tlsConfig
	}

	httpCli := &http.Client{
		Transport: transport,
	}

	if !strings.HasPrefix(endpointURL, "http") {
		endpointURL = fmt.Sprintf("https://%s", endpointURL)
	}

	url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
	if err != nil {
		return "", err
	}

	reqPayload, err := json.Marshal(
		struct {
			StackConfig string
			Namespace   string
		}{
			StackConfig: stackConfig,
			Namespace:   namespace,
		})
	if err != nil {
		return "", err
	}

	req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
	if err != nil {
		return "", err
	}

	signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
	if err != nil {
		return "", err
	}

	token, err := deployer.getToken(request, endpoint, false)
	if err != nil {
		return "", err
	}

	req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
	req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
	req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)

	resp, err := httpCli.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		var errorResponseData struct {
			Message string
			Details string
		}
		err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
		if err != nil {
			output, parseStringErr := ioutil.ReadAll(resp.Body)
			if parseStringErr != nil {
				return "", parseStringErr
			}

			return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)

		}

		return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
	}

	var responseData struct{ Output string }
	err = json.NewDecoder(resp.Body).Decode(&responseData)
	if err != nil {
		parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
		if parseStringErr != nil {
			return "", parseStringErr
		}

		return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
	}

	return responseData.Output, nil

}

// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error) {
	command := path.Join(deployer.binaryPath, "kompose")
	if runtime.GOOS == "windows" {
		command = path.Join(deployer.binaryPath, "kompose.exe")
	}

	args := make([]string, 0)
	args = append(args, "convert", "-f", "-", "--stdout")

	var stderr bytes.Buffer
	cmd := exec.Command(command, args...)
	cmd.Stderr = &stderr
	cmd.Stdin = bytes.NewReader(data)

	output, err := cmd.Output()
	if err != nil {
		return nil, errors.New(stderr.String())
	}

	return output, nil
}
