package e2e

import (
	"context"
	"encoding/base64"
	"fmt"
	"log"
	"os"
	"regexp"
	"strings"
	"time"

	"github.com/go-logr/logr"

	"github.com/aws/eks-anywhere/internal/pkg/ssm"
	"github.com/aws/eks-anywhere/pkg/api/v1alpha1"
	"github.com/aws/eks-anywhere/pkg/config"
	"github.com/aws/eks-anywhere/pkg/git"
	"github.com/aws/eks-anywhere/pkg/retrier"
	e2etests "github.com/aws/eks-anywhere/test/framework"
)

type s3Files struct {
	key, dstPath string
	permission   int
}

type fileFromBytes struct {
	dstPath    string
	permission int
	content    []byte
}

func (f *fileFromBytes) contentString() string {
	return string(f.content)
}

func (e *E2ESession) setupFluxGitEnv(testRegex string) error {
	re := regexp.MustCompile(`^.*GitFlux.*$`)
	if !re.MatchString(testRegex) {
		return nil
	}

	requiredEnvVars := e2etests.RequiredFluxGitCreateRepoEnvVars()
	for _, eVar := range requiredEnvVars {
		if val, ok := os.LookupEnv(eVar); ok {
			e.testEnvVars[eVar] = val
		}
	}

	repo, err := e.setupGithubRepo()
	if err != nil {
		return fmt.Errorf("setting up github repo for test: %v", err)
	}
	// add the newly generated repository to the test
	e.testEnvVars[e2etests.GitRepoSshUrl] = gitRepoSshUrl(repo.Name, e.testEnvVars[e2etests.GithubUserVar])

	for _, file := range buildFluxGitFiles(e.testEnvVars) {
		if err := e.downloadFileInInstance(file); err != nil {
			return fmt.Errorf("downloading flux-git file to instance: %v", err)
		}
	}

	err = e.setUpSshAgent(e.testEnvVars[config.EksaGitPrivateKeyTokenEnv])
	if err != nil {
		return fmt.Errorf("setting up ssh agent on remote instance: %v", err)
	}

	return nil
}

func buildFluxGitFiles(envVars map[string]string) []s3Files {
	return []s3Files{
		{
			key:        "git-flux/known_hosts",
			dstPath:    envVars[config.EksaGitKnownHostsFileEnv],
			permission: 644,
		},
	}
}

func (e *E2ESession) decodeAndWriteFileToInstance(file fileFromBytes) error {
	e.logger.V(1).Info("Writing bytes to file in instance", "file", file.dstPath)

	command := fmt.Sprintf("echo '%s' | base64 -d >> %s && chmod %d %[2]s", file.contentString(), file.dstPath, file.permission)
	if err := ssm.Run(e.session, logr.Discard(), e.instanceId, command, ssmTimeout); err != nil {
		return fmt.Errorf("writing file in instance: %v", err)
	}
	e.logger.V(1).Info("Successfully decoded and wrote file", "file", file.dstPath)

	return nil
}

func (e *E2ESession) downloadFileInInstance(file s3Files) error {
	e.logger.V(1).Info("Downloading from s3 in instance", "file", file.key)

	command := fmt.Sprintf("aws s3 cp s3://%s/%s %s && chmod %d %[3]s", e.storageBucket, file.key, file.dstPath, file.permission)
	if err := ssm.Run(e.session, logr.Discard(), e.instanceId, command, ssmTimeout); err != nil {
		return fmt.Errorf("downloading file in instance: %v", err)
	}
	e.logger.V(1).Info("Successfully downloaded file", "file", file.key)

	return nil
}

func (e *E2ESession) setUpSshAgent(privateKeyFile string) error {
	command := fmt.Sprintf("eval $(ssh-agent -s) ssh-add %s", privateKeyFile)

	if err := ssm.Run(e.session, logr.Discard(), e.instanceId, command, ssmTimeout); err != nil {
		return fmt.Errorf("starting SSH agent on instance: %v", err)
	}
	e.logger.V(1).Info("Successfully started SSH agent on instance")

	return nil
}

func (e *E2ESession) setupGithubRepo() (*git.Repository, error) {
	e.logger.V(1).Info("setting up Github repo for test")
	owner := e.testEnvVars[e2etests.GithubUserVar]
	repo := strings.ReplaceAll(e.jobId, ":", "-") // Github API urls get funky if you use ":" in the repo name

	c := &v1alpha1.GithubProviderConfig{
		Owner:      owner,
		Repository: repo,
		Personal:   true,
	}

	ctx := context.Background()
	g, err := e.TestGithubClient(ctx, e.testEnvVars[e2etests.GithubTokenVar], c.Owner, c.Repository, c.Personal)
	if err != nil {
		return nil, fmt.Errorf("couldn't create Github client for test setup: %v", err)
	}

	// Create a new github repository for the tests to run on
	o := git.CreateRepoOpts{
		Name:        repo,
		Owner:       owner,
		Description: fmt.Sprintf("repository for use with E2E test job %v", e.jobId),
		Personal:    true,
		AutoInit:    true,
	}

	r, err := g.CreateRepo(ctx, o)
	if err != nil {
		return nil, fmt.Errorf("creating repository in Github for test: %v", err)
	}

	pk, pub, err := e.generateKeyPairForGitTest()
	if err != nil {
		return nil, fmt.Errorf("generating key pair for git tests: %v", err)
	}

	e.logger.Info("Create Deploy Key Configuration for Git Flux tests", "owner", owner, "repo", repo)
	// Add the newly generated public key to the newly created repository as a deploy key
	ko := git.AddDeployKeyOpts{
		Owner:      owner,
		Repository: repo,
		Key:        string(pub),
		Title:      fmt.Sprintf("Test key created for job %v", e.jobId),
		ReadOnly:   false,
	}

	// Newly generated repositories may take some time to show up in the GitHub API; retry a few times to get around this
	err = retrier.Retry(10, time.Second*10, func() error {
		err = g.AddDeployKeyToRepo(ctx, ko)
		if err != nil {
			return fmt.Errorf("couldn't add deploy key to repo: %v", err)
		}
		return nil
	})
	if err != nil {
		return r, err
	}

	encodedPK := encodePrivateKey(pk)
	// Generate a PEM file from the private key and write it instance at the user-provided path
	pkFile := fileFromBytes{
		dstPath:    e.testEnvVars[config.EksaGitPrivateKeyTokenEnv],
		permission: 600,
		content:    encodedPK,
	}

	err = e.decodeAndWriteFileToInstance(pkFile)
	if err != nil {
		return nil, fmt.Errorf("writing private key file to instance: %v", err)
	}

	return r, err
}

func encodePrivateKey(privateKey []byte) []byte {
	b64EncodedPK := make([]byte, base64.StdEncoding.EncodedLen(len(privateKey)))
	base64.StdEncoding.Encode(b64EncodedPK, privateKey)
	return b64EncodedPK
}

func (e *E2ESession) generateKeyPairForGitTest() (privateKeyBytes, publicKeyBytes []byte, err error) {
	k, err := generateKeyPairEcdsa()
	if err != nil {
		return nil, nil, err
	}

	privateKeyBytes, err = pemFromPrivateKeyEcdsa(k)
	if err != nil {
		return nil, nil, err
	}

	publicKeyBytes, err = pubFromPrivateKeyEcdsa(k)
	if err != nil {
		return nil, nil, err
	}

	log.Println("Public key generated")
	return privateKeyBytes, publicKeyBytes, nil
}

func gitRepoSshUrl(repo, owner string) string {
	t := "ssh://git@github.com/%s/%s.git"
	return fmt.Sprintf(t, owner, repo)
}