package runnerconfiguration

import (
	"context"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"
	"time"

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

const (
	// HTTP client timeout
	httpClientTimeout = 100 * time.Second
	// Path segments for repository URL validation
	repositoryPathSegments = 2
)

type ConfigureRemoveRunner struct {
	Client     *http.Client
	URL        string
	Name       string
	Token      string
	Pat        string
	Unattended bool
	Trace      bool
}

func (c *ConfigureRemoveRunner) GetHTTPClient() *http.Client {
	if c.Client != nil {
		return c.Client
	}
	customTransport := http.DefaultTransport.(*http.Transport).Clone()
	if v, ok := common.LookupEnvBool("SKIP_TLS_CERT_VALIDATION"); ok && v {
		//nolint:gosec // Intentionally allows insecure TLS when explicitly configured
		customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	}
	c.Client = &http.Client{
		Timeout:   httpClientTimeout,
		Transport: customTransport,
	}
	return c.Client
}

type ConfigureRunner struct {
	ConfigureRemoveRunner
	Labels          []string
	NoDefaultLabels bool
	SystemLabels    []string
	RunnerGroup     string
	Ephemeral       bool
	RunnerGuard     string
	Replace         bool
	DisableUpdate   bool
	WorkFolder      string
}

type RemoveRunner struct {
	ConfigureRemoveRunner
	Force bool
}

type RunnerInstance struct {
	PoolID          int64
	RegistrationURL string
	Auth            *protocol.GitHubAuthResult
	Agent           *protocol.TaskAgent
	Key             string
	PKey            *rsa.PrivateKey `json:"-"`
	RunnerGuard     string
	WorkFolder      string // Currently unused for actions/runner compat
}

func (instance *RunnerInstance) EnsurePKey() error {
	if instance.PKey == nil {
		key, err := base64.StdEncoding.DecodeString(instance.Key)
		if err != nil {
			return err
		}
		pkey, err := x509.ParsePKCS1PrivateKey(key)
		if err != nil {
			return err
		}
		instance.PKey = pkey
	}
	return nil
}

type RunnerSettings struct {
	PoolID          int64
	RegistrationURL string
	Instances       []*RunnerInstance
}

type GithubApiUrlBuilder struct {
	URL      *url.URL
	ApiScope string
}

func NewGithubApiUrlBuilder(baseURLString string) (*GithubApiUrlBuilder, error) {
	baseUrl, err := url.Parse(baseURLString)
	if err != nil {
		return nil, err
	}
	apiBuilder := &GithubApiUrlBuilder{
		URL: baseUrl,
	}
	if strings.EqualFold(apiBuilder.URL.Host, "github.com") || strings.HasSuffix(strings.ToLower(apiBuilder.URL.Host), ".ghe.com") {
		apiBuilder.URL.Host = "api." + apiBuilder.URL.Host
	} else {
		apiBuilder.ApiScope = "/api/v3"
	}
	return apiBuilder, nil
}

func (apiBuilder *GithubApiUrlBuilder) AbsoluteApiUrl(p string) string {
	apiURL := *apiBuilder.URL
	apiURL.Path = path.Join(apiBuilder.ApiScope, p)
	return apiURL.String()
}

func (apiBuilder *GithubApiUrlBuilder) ScopedApiUrl(p string) (string, error) {
	apiURL := *apiBuilder.URL
	paths := strings.Split(strings.TrimPrefix(apiURL.Path, "/"), "/")
	if len(paths) == 1 {
		apiURL.Path = path.Join(apiBuilder.ApiScope, "orgs", paths[0], p)
	} else if len(paths) == repositoryPathSegments {
		scope := "repos"
		if strings.EqualFold(paths[0], "enterprises") {
			scope = ""
		}
		apiURL.Path = path.Join(apiBuilder.ApiScope, scope, paths[0], paths[1], p)
	} else {
		return "", fmt.Errorf("unsupported registration url")
	}
	return apiURL.String(), nil
}

func gitHubAuth(config *ConfigureRemoveRunner, c *http.Client, runnerEvent, apiEndpoint string, survey Survey,
) (*protocol.GitHubAuthResult, error) {
	if config.URL == "" && !config.Unattended {
		config.URL = survey.GetInput("Which GitHub Url is associated with this runner (Normally this isn't missing):", "")
	}
	apiBuilder, err := NewGithubApiUrlBuilder(config.URL)
	if err != nil {
		return nil, fmt.Errorf("invalid Url: %v", config.URL)
	}
	if config.Token == "" {
		if config.Pat != "" {
			apiURL, serr := apiBuilder.ScopedApiUrl(path.Join("actions/runners", apiEndpoint))
			if serr != nil {
				return nil, serr
			}
			client := &protocol.VssConnection{
				AuthHeader: fmt.Sprintf("Basic %v", base64.StdEncoding.EncodeToString([]byte("GitHub:"+config.Pat))),
				Trace:      config.Trace,
				Client:     c,
			}
			tokenresp := &protocol.GitHubRunnerRegisterToken{}
			err = client.RequestWithContext2(context.Background(), "POST", apiURL, "", nil, tokenresp)
			if err != nil {
				return nil, fmt.Errorf("failed to retrieve %v token via pat: %v", apiEndpoint, err.Error())
			}
			config.Token = tokenresp.Token
		} else if !config.Unattended {
			config.Token = survey.GetInput("Please enter your runner registration token:", "")
		}
	}
	if config.Token == "" {
		return nil, fmt.Errorf("no runner registration token provided")
	}

	finalregisterURL := apiBuilder.AbsoluteApiUrl("actions/runner-registration")

	client := &protocol.VssConnection{
		AuthHeader: "RemoteAuth " + config.Token,
		Trace:      config.Trace,
		Client:     c,
	}
	res := &protocol.GitHubAuthResult{}
	err = client.RequestWithContext2(context.Background(), "POST", finalregisterURL, "", &protocol.RunnerAddRemove{
		URL:         config.URL,
		RunnerEvent: runnerEvent,
	}, res)
	if err != nil {
		return nil, fmt.Errorf("failed to authenticate as Runner Admin: %w", err)
	}
	return res, nil
}

func (config *ConfigureRunner) Authenticate(c *http.Client, survey Survey) (*protocol.GitHubAuthResult, error) {
	return gitHubAuth(&config.ConfigureRemoveRunner, c, "register", "registration-token", survey)
}

func (config *RemoveRunner) Authenticate(c *http.Client, survey Survey) (*protocol.GitHubAuthResult, error) {
	return gitHubAuth(&config.ConfigureRemoveRunner, c, "remove", "remove-token", survey)
}

// Authenicate is deprecated: Use the Authenticate method.
func (config *ConfigureRunner) Authenicate(c *http.Client, survey Survey) (*protocol.GitHubAuthResult, error) {
	return config.Authenticate(c, survey)
}

// Authenicate is deprecated: Use the Authenticate method.
func (config *RemoveRunner) Authenicate(c *http.Client, survey Survey) (*protocol.GitHubAuthResult, error) {
	return config.Authenticate(c, survey)
}

func (c *ConfigureRemoveRunner) ReadFromEnvironment() {
	if c.Pat == "" {
		if v, ok := os.LookupEnv("ACTIONS_RUNNER_INPUT_PAT"); ok {
			c.Pat = v
		}
	}
	if c.Token == "" {
		if v, ok := os.LookupEnv("ACTIONS_RUNNER_INPUT_TOKEN"); ok {
			c.Token = v
		}
	}
	if !c.Unattended {
		if v, ok := common.LookupEnvBool("ACTIONS_RUNNER_INPUT_UNATTENDED"); ok {
			c.Unattended = v
		}
	}
	if c.URL == "" {
		if v, ok := os.LookupEnv("ACTIONS_RUNNER_INPUT_URL"); ok {
			c.URL = v
		}
	}
}
