package runnerconfiguration

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"encoding/binary"
	"encoding/xml"
	"fmt"
	"os"
	"runtime"
	"strings"
	"time"

	"github.com/google/uuid"

	"github.com/ChristopherHX/github-act-runner/common"
	"github.com/ChristopherHX/github-act-runner/protocol"
)

const (
	// RSA key generation constants
	rsaKeyBits         = 2048
	rsaExponentBytes   = 4
	defaultLabelsCount = 3
)

func containsEphemeralConfiguration(settings *RunnerSettings) bool {
	if settings == nil {
		return false
	}
	for _, instance := range settings.Instances {
		if instance.Agent != nil && instance.Agent.Ephemeral {
			return true
		}
	}
	return false
}

func (config *ConfigureRunner) Configure(
	settings *RunnerSettings,
	survey Survey,
	auth *protocol.GitHubAuthResult,
) (*RunnerSettings, error) {
	instance := &RunnerInstance{
		RunnerGuard: config.RunnerGuard,
		WorkFolder:  config.WorkFolder,
	}
	if config.Ephemeral && len(settings.Instances) > 0 || containsEphemeralConfiguration(settings) {
		return nil, fmt.Errorf("ephemeral is not supported for multi runners, runner already configured")
	}
	if config.URL == "" {
		if !config.Unattended {
			config.URL = survey.GetInput("Please enter your repository, organization or enterprise url:", "")
		} else {
			return nil, fmt.Errorf("no url provided")
		}
	}
	if config.URL == "" {
		return nil, fmt.Errorf("no url provided")
	}
	instance.RegistrationURL = config.URL
	c := config.GetHTTPClient()
	res := auth
	if res == nil {
		authres, err := config.Authenticate(c, survey)
		if err != nil {
			return nil, err
		}
		res = authres
	}

	instance.Auth = res
	vssConnection := &protocol.VssConnection{
		Client:    c,
		TenantURL: res.TenantURL,
		Token:     res.Token,
		Trace:     config.Trace,
	}
	if res.UseV2FLow {
		vssConnection = &protocol.VssConnection{
			AuthHeader: "Bearer " + res.Token,
			Trace:      config.Trace,
			Client:     c,
		}
	}

	getRunnerGroups := func() (*protocol.TaskAgentPools, error) {
		return vssConnection.GetAgentPools()
	}
	var apiBuilder *GithubApiUrlBuilder
	if res.UseV2FLow {
		var err error
		apiBuilder, err = NewGithubApiUrlBuilder(config.URL)
		if err != nil {
			return nil, fmt.Errorf("invalid Url: %v", config.URL)
		}
		getRunnerGroups = func() (*protocol.TaskAgentPools, error) {
			runnerGroupsURL, err := apiBuilder.ScopedApiUrl("actions/runner-groups")
			if err != nil {
				return nil, err
			}
			runnerGroups := &protocol.RunnerGroupList{}
			err = vssConnection.RequestWithContext2(context.Background(), "GET", runnerGroupsURL, "", nil, runnerGroups)
			if err != nil {
				return nil, err
			}
			poolList := &protocol.TaskAgentPools{}
			poolList.Count = int64(len(runnerGroups.RunnerGroups))
			for _, val := range runnerGroups.RunnerGroups {
				poolList.Value = append(poolList.Value, protocol.TaskAgentPool{
					TaskAgentPoolReference: protocol.TaskAgentPoolReference{
						ID:         int64(val.Id),
						Name:       val.Name,
						IsHosted:   val.IsHosted,
						IsInternal: val.IsDefault,
					},
				})
			}
			return poolList, nil
		}
	}
	{
		taskAgentPool := ""
		taskAgentPools := []string{}
		_taskAgentPools, err := getRunnerGroups()
		if err != nil {
			return nil, fmt.Errorf("failed to configure runner: %w", err)
		}
		for _, val := range _taskAgentPools.Value {
			if !val.IsHosted {
				taskAgentPools = append(taskAgentPools, val.Name)
			}
		}
		if len(taskAgentPools) == 0 {
			return nil, fmt.Errorf("failed to configure runner, no self-hosted runner group available")
		}
		if config.RunnerGroup != "" {
			taskAgentPool = config.RunnerGroup
		} else {
			taskAgentPool = taskAgentPools[0]
			if len(taskAgentPools) > 1 && !config.Unattended {
				taskAgentPool = survey.GetSelectInput("Choose a runner group:", taskAgentPools, taskAgentPool)
			}
		}
		vssConnection.PoolID = -1
		for _, val := range _taskAgentPools.Value {
			if !val.IsHosted && strings.EqualFold(val.Name, taskAgentPool) {
				vssConnection.PoolID = val.ID
			}
		}
		if vssConnection.PoolID < 0 {
			return nil, fmt.Errorf("runner Pool %v not found", taskAgentPool)
		}
	}
	key, _ := rsa.GenerateKey(rand.Reader, rsaKeyBits)
	instance.Key = base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PrivateKey(key))

	taskAgent := &protocol.TaskAgent{}
	bs := make([]byte, rsaExponentBytes)
	ui := uint32(key.E) //nolint:gosec
	binary.BigEndian.PutUint32(bs, ui)
	expof := 0
	for ; expof < 3 && bs[expof] == 0; expof++ {
		// Skip leading zero bytes
	}
	taskAgent.Authorization.PublicKey = protocol.TaskAgentPublicKey{
		Exponent: base64.StdEncoding.EncodeToString(bs[expof:]),
		Modulus:  base64.StdEncoding.EncodeToString(key.N.Bytes()),
	}
	taskAgent.Version = "3.0.0" // version, will not use fips crypto if set to 0.0.0 *
	taskAgent.OSDescription = "github-act-runner " + runtime.GOOS + "/" + runtime.GOARCH
	if config.Name != "" {
		taskAgent.Name = config.Name
	} else {
		taskAgent.Name = "golang_" + uuid.NewString()
		if !config.Unattended {
			taskAgent.Name = survey.GetInput("Please enter a name of your new runner:", taskAgent.Name)
		}
	}
	if !config.Unattended && len(config.Labels) == 0 {
		if res := survey.GetInput("Please enter custom labels of your new runner (case insensitive, separated by ','):", ""); res != "" {
			config.Labels = strings.Split(res, ",")
		}
	}
	systemLabels := make([]string, 0, defaultLabelsCount)
	if !config.NoDefaultLabels {
		systemLabels = append(systemLabels, "self-hosted", runtime.GOOS, runtime.GOARCH)
	}
	taskAgent.Labels = make([]protocol.AgentLabel, len(systemLabels)+len(config.SystemLabels)+len(config.Labels))
	for i := 0; i < len(systemLabels); i++ {
		taskAgent.Labels[i] = protocol.AgentLabel{Name: systemLabels[i], Type: "system"}
	}
	for i := 0; i < len(config.SystemLabels); i++ {
		taskAgent.Labels[i+len(systemLabels)] = protocol.AgentLabel{Name: config.SystemLabels[i], Type: "system"}
	}
	for i := 0; i < len(config.Labels); i++ {
		taskAgent.Labels[i+len(systemLabels)+len(config.SystemLabels)] = protocol.AgentLabel{Name: config.Labels[i], Type: "user"}
	}
	taskAgent.MaxParallelism = 1
	taskAgent.ProvisioningState = "Provisioned"
	taskAgent.CreatedOn = time.Now().UTC().Format(protocol.TimestampOutputFormat)
	taskAgent.Ephemeral = config.Ephemeral
	taskAgent.DisableUpdate = config.DisableUpdate
	{
		var err error
		if res.UseV2FLow {
			err = registerOrReplaceRunnerV2(taskAgent, config, vssConnection, apiBuilder, false)
		} else {
			err = vssConnection.Request("e298ef32-5878-4cab-993c-043836571f42", "6.0-preview.2", "POST", map[string]string{
				"poolId": fmt.Sprint(vssConnection.PoolID),
			}, map[string]string{}, taskAgent, taskAgent)
		}
		if err != nil {
			if !config.Replace {
				return nil, fmt.Errorf("failed to create taskAgent: %v", err.Error())
			}
			// Try replaceing runner if creation failed
			taskAgents := &protocol.TaskAgents{}
			if res.UseV2FLow {
				resv2 := &protocol.ListRunnersResponse{}
				runnersURL, _ := apiBuilder.ScopedApiUrl("actions/runners")
				err = vssConnection.RequestWithContext2(context.Background(), "GET", runnersURL, "", nil, resv2)
				if err == nil {
					for _, runner := range resv2.Runners {
						taskAgents.Value = append(taskAgents.Value, protocol.TaskAgent{
							ID:   runner.Id,
							Name: runner.Name,
						})
					}
					taskAgents.Count = int64(len(taskAgents.Value))
				}
			} else {
				err = vssConnection.Request("e298ef32-5878-4cab-993c-043836571f42", "6.0-preview.2", "GET", map[string]string{
					"poolId": fmt.Sprint(vssConnection.PoolID),
				}, map[string]string{}, nil, taskAgents)
			}
			if err != nil {
				return nil, fmt.Errorf("failed to update taskAgent: %v", err.Error())
			}
			invalid := true
			for i := 0; i < len(taskAgents.Value); i++ {
				if taskAgents.Value[i].Name == taskAgent.Name {
					taskAgent.ID = taskAgents.Value[i].ID
					invalid = false
					break
				}
			}
			if invalid {
				return nil, fmt.Errorf("failed to update taskAgent: failed to find agent")
			}
			if res.UseV2FLow {
				err = registerOrReplaceRunnerV2(taskAgent, config, vssConnection, apiBuilder, true)
			} else {
				err = vssConnection.Request("e298ef32-5878-4cab-993c-043836571f42", "6.0-preview.2", "PUT", map[string]string{
					"poolId":  fmt.Sprint(vssConnection.PoolID),
					"agentId": fmt.Sprint(taskAgent.ID),
				}, map[string]string{}, taskAgent, taskAgent)
			}
			if err != nil {
				return nil, fmt.Errorf("failed to update taskAgent: %v", err.Error())
			}
		}
	}
	instance.Agent = taskAgent
	instance.PoolID = vssConnection.PoolID
	settings.Instances = append(settings.Instances, instance)
	return settings, nil
}

type RSAKeyValue struct {
	Modulus  string
	Exponent string
}

func registerOrReplaceRunnerV2(taskAgent *protocol.TaskAgent, config *ConfigureRunner,
	vssConnection *protocol.VssConnection, apiBuilder *GithubApiUrlBuilder, replace bool,
) error {
	runnerResp := &protocol.Runner{}
	pubKeyXml, err := xml.Marshal(&RSAKeyValue{
		Modulus:  taskAgent.Authorization.PublicKey.Modulus,
		Exponent: taskAgent.Authorization.PublicKey.Exponent,
	})
	if err != nil {
		return err
	}
	v2Register := map[string]interface{}{
		"url":              config.URL,
		"group_id":         vssConnection.PoolID,
		"name":             config.Name,
		"version":          taskAgent.Version,
		"updates_disabled": taskAgent.DisableUpdate,
		"ephemeral":        taskAgent.Ephemeral,
		"labels":           taskAgent.Labels,
		"public_key":       string(pubKeyXml),
	}
	if replace {
		v2Register["runner_id"] = taskAgent.ID
		v2Register["replace"] = true
	}
	err = vssConnection.RequestWithContext2(context.Background(), "POST",
		apiBuilder.AbsoluteApiUrl("actions/runners/register"), "", v2Register, runnerResp)
	if err != nil {
		return err
	}
	taskAgent.ID = runnerResp.Id
	taskAgent.Name = runnerResp.Name
	taskAgent.Authorization.AuthorizationURL = runnerResp.Authorization.AuthorizationURL
	taskAgent.Authorization.ClientID = runnerResp.Authorization.ClientId
	return nil
}

func (config *ConfigureRunner) ReadFromEnvironment() {
	config.ConfigureRemoveRunner.ReadFromEnvironment()
	if !config.Ephemeral {
		if v, ok := common.LookupEnvBool("ACTIONS_RUNNER_INPUT_EPHEMERAL"); ok {
			config.Ephemeral = v
		}
	}
	if !config.DisableUpdate {
		if v, ok := common.LookupEnvBool("ACTIONS_RUNNER_INPUT_DISABLEUPDATE"); ok {
			config.DisableUpdate = v
		}
	}
	if config.Name == "" {
		if v, ok := os.LookupEnv("ACTIONS_RUNNER_INPUT_NAME"); ok {
			config.Name = v
		}
	}
	if len(config.Labels) == 0 {
		if v, ok := os.LookupEnv("ACTIONS_RUNNER_INPUT_LABELS"); ok {
			config.Labels = strings.Split(v, ",")
		}
	}
	if !config.Replace {
		if v, ok := common.LookupEnvBool("ACTIONS_RUNNER_INPUT_REPLACE"); ok {
			config.Replace = v
		}
	}
}
