package tekton

import (
	"context"
	"fmt"
	"strings"

	"github.com/google/go-containerregistry/pkg/authn"
	"github.com/google/go-containerregistry/pkg/name"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"

	"knative.dev/func/pkg/docker"
	fn "knative.dev/func/pkg/functions"
	"knative.dev/func/pkg/git"
	"knative.dev/func/pkg/k8s"
	"knative.dev/func/pkg/pipelines"
	"knative.dev/func/pkg/pipelines/tekton/pac"
	"knative.dev/func/pkg/random"
)

// ConfigurePAC cofigures Pipelines as Code resources based on the input:
// - locally (create .tekton directory)
// - on cluster (create Repository, Secret, PVC...)
// - on remote git repo (webhook)
// Parameter `metadata` is type `any` to not bring `pkg/pipelines` package dependency to `pkg/functions`,
// this specific implementation expects the parameter to be a type `pipelines.PacMetada`.
func (pp *PipelinesProvider) ConfigurePAC(ctx context.Context, f fn.Function, metadata any) error {
	// Use the new target namespace if specified, otherwise use the
	// function's currently deployed namespace (if any)
	namespace := f.Namespace
	if namespace == "" {
		namespace = f.Deploy.Namespace
	}

	data, ok := metadata.(pipelines.PacMetadata)
	if !ok {
		return fmt.Errorf("incorrect type of pipelines metadata: %T", metadata)
	}

	var err error
	if err = validatePipeline(f); err != nil {
		return err
	}

	if data.ConfigureLocalResources {
		if err := pp.createLocalPACResources(ctx, f); err != nil {
			return err
		}
	}

	if data.ConfigureClusterResources || data.ConfigureRemoteResources {
		if data.WebhookSecret == "" {
			data.WebhookSecret = random.AlphaString(10)

			// try to reuse existing Webhook Secret stored in the cluster
			secret, err := k8s.GetSecret(ctx, getPipelineSecretName(f), namespace)
			if err != nil {
				if !k8serrors.IsNotFound(err) {
					return err
				}
			} else {
				val, ok := secret.StringData["webhook.secret"]
				if ok {
					data.WebhookSecret = val
				}
			}
		}
	}

	if data.ConfigureClusterResources {
		if err := pp.createClusterPACResources(ctx, f, data); err != nil {
			return err
		}
	}

	if data.ConfigureRemoteResources {
		if err := pp.createRemotePACResources(ctx, f, data); err != nil {
			return err
		}
	}

	return nil
}

// RemovePAC tries to remove all local and remote resources that were created for PAC.
// Resources on the remote GitHub repo are not removed, we would need to store webhook id somewhere locally.
func (pp *PipelinesProvider) RemovePAC(ctx context.Context, f fn.Function, metadata any) error {
	data, ok := metadata.(pipelines.PacMetadata)
	if !ok {
		return fmt.Errorf("incorrect type of pipelines metadata: %T", metadata)
	}

	compoundErrMsg := ""

	if data.ConfigureLocalResources {
		errMsg := deleteAllPipelineTemplates(f)
		compoundErrMsg += errMsg
	}

	if data.ConfigureClusterResources {
		errMsg := pp.removeClusterResources(ctx, f)
		if errMsg != nil {
			compoundErrMsg += errMsg.Error()
		}

	}

	if compoundErrMsg != "" {
		return fmt.Errorf("%s", compoundErrMsg)
	}

	return nil
}

// createLocalPACResources creates necessary local resources in .tekton directory:
// Pipeline and PipelineRun templates
func (pp *PipelinesProvider) createLocalPACResources(ctx context.Context, f fn.Function) error {
	// let's specify labels that will be applied to every resource that is created for a Pipeline
	labels, err := f.LabelsMap()
	if err != nil {
		return err
	}
	if pp.decorator != nil {
		labels = pp.decorator.UpdateLabels(f, labels)
	}

	err = createPipelineTemplatePAC(f, labels)
	if err != nil {
		return err
	}

	err = createPipelineRunTemplatePAC(f, labels)
	if err != nil {
		return err
	}

	return nil
}

// createClusterPACResources create resources on cluster, it tries to detect PAC installation,
// creates necessary secret with image registry credentials and git credentials (access tokens, webhook secrets),
// also creates PVC for the function source code
func (pp *PipelinesProvider) createClusterPACResources(ctx context.Context, f fn.Function, metadata pipelines.PacMetadata) error {
	namespace := f.Namespace
	if namespace == "" {
		namespace = f.Deploy.Namespace
	}

	// figure out pac installation namespace
	installed, _, err := pac.DetectPACInstallation(ctx)
	if !installed {
		errMsg := ""
		if err != nil {
			errMsg = fmt.Sprintf(", %v", err)
		}
		return fmt.Errorf("pipelines as code not installed%s", errMsg)
	}
	if installed && err != nil {
		return err
	}

	// let's specify labels that will be applied to every resource that is created for a Pipeline
	labels, err := f.LabelsMap()
	if err != nil {
		return err
	}
	if pp.decorator != nil {
		labels = pp.decorator.UpdateLabels(f, labels)
	}

	img := f.Deploy.Image
	if img == "" {
		img = f.Image
	}

	registry, err := docker.GetRegistry(img)
	if err != nil {
		return fmt.Errorf("problem in resolving image registry name: %w", err)
	}

	if registry == name.DefaultRegistry {
		registry = authn.DefaultAuthKey
	}

	creds, err := pp.credentialsProvider(ctx, img)
	if err != nil {
		return err
	}

	metadata.RegistryUsername = creds.Username
	metadata.RegistryPassword = creds.Password
	metadata.RegistryServer = registry

	err = createPipelinePersistentVolumeClaim(ctx, f, namespace, labels)
	if err != nil {
		return err
	}
	fmt.Printf(" ✅ Persistent Volume is present on the cluster with name %q\n", getPipelinePvcName(f))

	err = ensurePACSecretExists(ctx, f, namespace, metadata, labels)
	if err != nil {
		return err
	}
	fmt.Printf(" ✅ Credentials are present on the cluster in secret %q\n", getPipelineSecretName(f))

	err = ensurePACRepositoryExists(ctx, f, namespace, metadata, labels)
	if err != nil {
		return err
	}
	fmt.Printf(" ✅ Webhook with payload validation secret %q is present on the cluster in repository %q\n", metadata.WebhookSecret, getPipelineRepositoryName(f))

	return nil
}

// createRemotePACResources creates resources on the remote git repository
// set up a webhook with secrets, access tokens and it tries to detec PAC installation
// together with PAC controller route url - needed for webhook payload trigger
func (pp *PipelinesProvider) createRemotePACResources(ctx context.Context, f fn.Function, metadata pipelines.PacMetadata) error {

	// figure out pac installation namespace
	installed, installationNS, err := pac.DetectPACInstallation(ctx)
	if !installed {
		errMsg := ""
		if err != nil {
			errMsg = fmt.Sprintf(", %v", err)
		}
		return fmt.Errorf("pipelines as code not installed%s", errMsg)
	}
	if installed && err != nil {
		return err
	}

	// fetch configmap to get controller url
	controllerURL, err := pac.GetPACInfo(ctx, installationNS)
	if err != nil {
		return err
	}

	// check if info configmap has url then use that otherwise try to detect
	if controllerURL == "" {
		controllerURL, _ = pac.DetectPACOpenShiftRoute(ctx, installationNS)
	}

	// we haven't been able to detect PAC controller public route, let's prompt:
	if controllerURL == "" {
		if controllerURL, err = pp.getPacURL(); err != nil {
			return err
		}
	}

	if err := git.CreateWebHook(ctx, f.Build.Git.URL, controllerURL, metadata.WebhookSecret, metadata.PersonalAccessToken); err != nil {
		// Error: POST https://api.github.com/repos/foobar/test-function/hooks: 422 Validation Failed [{Resource:Hook Field: Code:custom Message:Hook already exists on this repository}]
		if !strings.Contains(err.Error(), "Hook already exists") {
			return err
		}
		fmt.Printf(" ✅ Webhook already exists on repository %v\n", f.Build.Git.URL)
	} else {
		fmt.Printf(" ✅ Webhook is created on repository %v\n", f.Build.Git.URL)
	}

	return nil
}
