package multiclient

import (
	"context"
	"crypto/sha256"
	"crypto/x509"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"sync"

	"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1/appconfig"
	"github.com/actions/actions-runner-controller/build"
	"github.com/actions/actions-runner-controller/github/actions"
	"github.com/actions/scaleset"
	"github.com/google/uuid"
)

type MultiClient interface {
	GetClientFor(ctx context.Context, opts *ClientForOptions) (Client, error)
}

type Scaleset struct {
	mu      sync.Mutex
	clients map[string]*multiClientEntry
}

type multiClientEntry struct {
	client  *scaleset.Client
	rootCAs *x509.CertPool
}

func NewScaleset() *Scaleset {
	return &Scaleset{
		clients: make(map[string]*multiClientEntry),
	}
}

type Client interface {
	SetSystemInfo(info scaleset.SystemInfo)
	SystemInfo() scaleset.SystemInfo

	MessageSessionClient(ctx context.Context, runnerScaleSetID int, owner string, options ...scaleset.HTTPOption) (*scaleset.MessageSessionClient, error)

	GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *scaleset.RunnerScaleSetJitRunnerSetting, scaleSetID int) (*scaleset.RunnerScaleSetJitRunnerConfig, error)

	GetRunner(ctx context.Context, runnerID int) (*scaleset.RunnerReference, error)
	GetRunnerByName(ctx context.Context, runnerName string) (*scaleset.RunnerReference, error)
	RemoveRunner(ctx context.Context, runnerID int64) error

	GetRunnerGroupByName(ctx context.Context, runnerGroup string) (*scaleset.RunnerGroup, error)

	GetRunnerScaleSet(ctx context.Context, runnerGroupID int, runnerScaleSetName string) (*scaleset.RunnerScaleSet, error)
	GetRunnerScaleSetByID(ctx context.Context, runnerScaleSetID int) (*scaleset.RunnerScaleSet, error)

	CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error)
	UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetID int, runnerScaleSet *scaleset.RunnerScaleSet) (*scaleset.RunnerScaleSet, error)
	DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetID int) error
}

func (m *Scaleset) GetClientFor(ctx context.Context, opts *ClientForOptions) (Client, error) {
	identifier, err := opts.identifier()
	if err != nil {
		return nil, fmt.Errorf("failed to generate client identifier: %w", err)
	}

	m.mu.Lock()
	defer m.mu.Unlock()

	entry, ok := m.clients[identifier]
	if ok && entry.rootCAs.Equal(opts.RootCAs) {
		return entry.client, nil
	}

	client, err := opts.newClient()
	if err != nil {
		return nil, fmt.Errorf("failed to create new client: %w", err)
	}

	m.clients[identifier] = &multiClientEntry{
		client:  client,
		rootCAs: opts.RootCAs,
	}

	return client, nil
}

type ClientForOptions struct {
	GithubConfigURL string
	AppConfig       appconfig.AppConfig
	Namespace       string
	RootCAs         *x509.CertPool
	ProxyFunc       func(*http.Request) (*url.URL, error)
}

func (o *ClientForOptions) identifier() (string, error) {
	if err := o.AppConfig.Validate(); err != nil {
		return "", fmt.Errorf("failed to validate app config: %w", err)
	}
	if _, err := actions.ParseGitHubConfigFromURL(o.GithubConfigURL); err != nil {
		return "", fmt.Errorf("failed to parse GitHub config URL: %w", err)
	}
	if o.Namespace == "" {
		return "", fmt.Errorf("namespace is required to generate client identifier")
	}
	identifier := fmt.Sprintf("configURL:%q,namespace:%q,proxy:%t", o.GithubConfigURL, o.Namespace, o.ProxyFunc != nil)

	if o.AppConfig.Token != "" {
		identifier += fmt.Sprintf(",token:%q,", o.AppConfig.Token)
	} else {
		identifier += fmt.Sprintf(
			",appID:%q,installationID:%q,key:%q",
			o.AppConfig.AppID,
			strconv.FormatInt(o.AppConfig.AppInstallationID, 10),
			o.AppConfig.AppPrivateKey,
		)
	}

	if o.RootCAs != nil {
		// ignoring because this cert pool is intended not to come from SystemCertPool
		// nolint:staticcheck
		identifier += fmt.Sprintf(",rootCAs:%q", o.RootCAs.Subjects())
	}

	return uuid.NewHash(sha256.New(), uuid.NameSpaceOID, []byte(identifier), 6).String(), nil
}

func (o *ClientForOptions) newClient() (*scaleset.Client, error) {
	systemInfo := scaleset.SystemInfo{
		System:     "actions-runner-controller",
		Version:    build.Version,
		CommitSHA:  build.CommitSHA,
		ScaleSetID: 0, // by default, scale set is 0 (not created yet)
		Subsystem:  "gha-scale-set-controller",
	}

	var options []scaleset.HTTPOption
	if o.RootCAs != nil {
		options = append(options, scaleset.WithRootCAs(o.RootCAs))
	}
	if o.ProxyFunc != nil {
		options = append(options, scaleset.WithProxy(o.ProxyFunc))
	}

	if o.AppConfig.Token != "" {
		c, err := scaleset.NewClientWithPersonalAccessToken(
			scaleset.NewClientWithPersonalAccessTokenConfig{
				GitHubConfigURL:     o.GithubConfigURL,
				PersonalAccessToken: o.AppConfig.Token,
				SystemInfo:          systemInfo,
			},
			options...,
		)
		if err != nil {
			return nil, fmt.Errorf("failed to instantiate client with personal access token auth: %w", err)
		}
		return c, nil
	}

	c, err := scaleset.NewClientWithGitHubApp(
		scaleset.ClientWithGitHubAppConfig{
			GitHubConfigURL: o.GithubConfigURL,
			GitHubAppAuth: scaleset.GitHubAppAuth{
				ClientID:       o.AppConfig.AppID,
				InstallationID: o.AppConfig.AppInstallationID,
				PrivateKey:     o.AppConfig.AppPrivateKey,
			},
			SystemInfo: systemInfo,
		},
		options...,
	)
	if err != nil {
		return nil, fmt.Errorf("failed to instantiate client with GitHub App auth: %w", err)
	}
	return c, nil
}
