package release

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"time"

	"github.com/porter-dev/porter/internal/telemetry"

	"github.com/porter-dev/porter/api/server/authz"
	"github.com/porter-dev/porter/api/server/handlers"
	"github.com/porter-dev/porter/api/server/shared"
	"github.com/porter-dev/porter/api/server/shared/apierrors"
	"github.com/porter-dev/porter/api/server/shared/config"
	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/analytics"
	"github.com/porter-dev/porter/internal/auth/token"
	"github.com/porter-dev/porter/internal/encryption"
	"github.com/porter-dev/porter/internal/helm"
	"github.com/porter-dev/porter/internal/helm/loader"
	"github.com/porter-dev/porter/internal/helm/repo"
	"github.com/porter-dev/porter/internal/integrations/ci/actions"
	"github.com/porter-dev/porter/internal/integrations/ci/gitlab"
	"github.com/porter-dev/porter/internal/models"
	"github.com/porter-dev/porter/internal/oauth"
	"github.com/porter-dev/porter/internal/registry"
	"github.com/stefanmcshane/helm/pkg/release"
	"golang.org/x/crypto/bcrypt"
	"gopkg.in/yaml.v2"
	v1 "k8s.io/api/core/v1"
)

type CreateReleaseHandler struct {
	handlers.PorterHandlerReadWriter
	authz.KubernetesAgentGetter
}

func NewCreateReleaseHandler(
	config *config.Config,
	decoderValidator shared.RequestDecoderValidator,
	writer shared.ResultWriter,
) *CreateReleaseHandler {
	return &CreateReleaseHandler{
		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
	}
}

func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user, _ := r.Context().Value(types.UserScope).(*models.User)
	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
	namespace := r.Context().Value(types.NamespaceScope).(string)
	operationID := oauth.CreateRandomState()

	ctx, span := telemetry.NewSpan(r.Context(), "serve-create-release")
	defer span.End()

	telemetry.WithAttributes(span,
		telemetry.AttributeKV{Key: "project-id", Value: cluster.ProjectID},
		telemetry.AttributeKV{Key: "cluster-id", Value: cluster.ID},
		telemetry.AttributeKV{Key: "user-email", Value: user.Email},
		telemetry.AttributeKV{Key: "namespace", Value: namespace},
	)

	c.Config().AnalyticsClient.Track(analytics.ApplicationLaunchStartTrack(
		&analytics.ApplicationLaunchStartTrackOpts{
			ClusterScopedTrackOpts: analytics.GetClusterScopedTrackOpts(user.ID, cluster.ProjectID, cluster.ID),
			FlowID:                 operationID,
		},
	))

	helmAgent, err := c.GetHelmAgent(ctx, r, cluster, "")
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error getting helm agent")))
		return
	}

	request := &types.CreateReleaseRequest{}

	if ok := c.DecodeAndValidate(w, r, request); !ok {
		return
	}

	if request.RepoURL == "" {
		request.RepoURL = c.Config().ServerConf.DefaultApplicationHelmRepoURL
	}

	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "repo-url", Value: request.RepoURL})

	// if the repo url is not an addon or application url, validate against the helm repos
	if request.RepoURL != c.Config().ServerConf.DefaultAddonHelmRepoURL && request.RepoURL != c.Config().ServerConf.DefaultApplicationHelmRepoURL {
		// load the helm repos in the project
		hrs, err := c.Repo().HelmRepo().ListHelmReposByProjectID(cluster.ProjectID)
		if err != nil {
			c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error listing helm repos for project")))
			return
		}

		isValid := repo.ValidateRepoURL(c.Config().ServerConf.DefaultAddonHelmRepoURL, c.Config().ServerConf.DefaultApplicationHelmRepoURL, hrs, request.RepoURL)

		if !isValid {
			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
				telemetry.Error(ctx, span, err, "invalid repo_url parameter"),
				http.StatusBadRequest,
			))

			return
		}
	}

	if request.TemplateVersion == "latest" {
		request.TemplateVersion = ""
	}

	chart, err := loader.LoadChartPublic(ctx, request.RepoURL, request.TemplateName, request.TemplateVersion)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error loading public chart")))
		return
	}

	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error listing registries")))
		return
	}

	k8sAgent, err := c.GetAgent(r, cluster, "")
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error getting k8s agent")))
		return
	}

	// create the namespace if it does not exist already
	_, err = k8sAgent.CreateNamespace(namespace, nil)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error creating namespace")))
		return
	}

	conf := &helm.InstallChartConfig{
		Chart:      chart,
		Name:       request.Name,
		Namespace:  namespace,
		Values:     request.Values,
		Cluster:    cluster,
		Repo:       c.Repo(),
		Registries: registries,
	}

	helmRelease, err := helmAgent.InstallChart(ctx, conf, c.Config().DOConf, c.Config().ServerConf.DisablePullSecretsInjection)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
			telemetry.Error(ctx, span, err, "error installing a new chart"),
			http.StatusBadRequest,
		))

		return
	}

	configMaps := make([]*v1.ConfigMap, 0)

	if request.SyncedEnvGroups != nil && len(request.SyncedEnvGroups) > 0 {
		for _, envGroupName := range request.SyncedEnvGroups {
			// read the attached configmap
			cm, _, err := k8sAgent.GetLatestVersionedConfigMap(envGroupName, namespace)
			if err != nil {
				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(telemetry.Error(ctx, span, err, "Couldn't find the env group"), http.StatusNotFound))
				return
			}

			configMaps = append(configMaps, cm)
		}
	}

	release, err := CreateAppReleaseFromHelmRelease(ctx, c.Config(), cluster.ProjectID, cluster.ID, 0, helmRelease)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error creating app release from helm release")))
		return
	}

	if len(configMaps) > 0 {
		for _, cm := range configMaps {

			_, err = k8sAgent.AddApplicationToVersionedConfigMap(cm, release.Name)

			if err != nil {
				telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "release-name", Value: release.Name})
				telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "config-map-name", Value: cm.Name})
				c.HandleAPIErrorNoWrite(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "Couldn't add release to the config map")))
			}
		}
	}

	if request.Tags != nil {
		tags, err := c.Repo().Tag().LinkTagsToRelease(request.Tags, release)

		if err == nil {
			release.Tags = append(release.Tags, tags...)
		}
	}

	if request.BuildConfig != nil {
		_, err = createBuildConfig(ctx, c.Config(), release, request.BuildConfig)
	}

	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error building config")))
		return
	}

	if request.GitActionConfig != nil {
		_, _, err := createGitAction(
			ctx,
			c.Config(),
			user.ID,
			cluster.ProjectID,
			cluster.ID,
			request.GitActionConfig,
			request.Name,
			namespace,
			release,
		)
		if err != nil {
			unwrappedErr := errors.Unwrap(err)

			if unwrappedErr != nil {
				if errors.Is(unwrappedErr, actions.ErrProtectedBranch) {
					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(telemetry.Error(ctx, span, err, "error creating git action on protected branch"), http.StatusConflict))
				} else if errors.Is(unwrappedErr, actions.ErrCreatePRForProtectedBranch) {
					c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(telemetry.Error(ctx, span, err, "error creating PR on protected branch"), http.StatusPreconditionFailed))
				}
			} else {
				c.HandleAPIError(w, r, apierrors.NewErrInternal(telemetry.Error(ctx, span, err, "error creating git action")))
				return
			}
		}
	}

	c.Config().AnalyticsClient.Track(analytics.ApplicationLaunchSuccessTrack(
		&analytics.ApplicationLaunchSuccessTrackOpts{
			ApplicationScopedTrackOpts: analytics.GetApplicationScopedTrackOpts(
				user.ID,
				cluster.ProjectID,
				cluster.ID,
				release.Name,
				release.Namespace,
				chart.Metadata.Name,
			),
			FlowID: operationID,
		},
	))

	w.WriteHeader(http.StatusCreated)
}

func CreateAppReleaseFromHelmRelease(
	ctx context.Context,
	config *config.Config,
	projectID, clusterID, stackResourceID uint,
	helmRelease *release.Release,
) (*models.Release, error) {
	ctx, span := telemetry.NewSpan(ctx, "create-app-release-from-helm-release")
	defer span.End()

	telemetry.WithAttributes(span,
		telemetry.AttributeKV{Key: "project-id", Value: projectID},
		telemetry.AttributeKV{Key: "cluster-id", Value: clusterID},
		telemetry.AttributeKV{Key: "stack-resource-id", Value: stackResourceID},
		telemetry.AttributeKV{Key: "helm-release-name", Value: helmRelease.Name},
	)

	token, err := encryption.GenerateRandomBytes(16)
	if err != nil {
		return nil, err
	}

	// create release with webhook token in db
	image, ok := helmRelease.Config["image"].(map[string]interface{})

	if !ok {
		return nil, telemetry.Error(ctx, span, nil, "Could not find field image in config")
	}

	repository := image["repository"]
	repoStr, ok := repository.(string)

	if !ok {
		return nil, telemetry.Error(ctx, span, nil, "Could not find field repository in config")
	}

	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "repo-uri", Value: repoStr})

	release := &models.Release{
		ClusterID:       clusterID,
		ProjectID:       projectID,
		Namespace:       helmRelease.Namespace,
		Name:            helmRelease.Name,
		WebhookToken:    token,
		ImageRepoURI:    repoStr,
		StackResourceID: stackResourceID,
	}

	return config.Repo.Release().CreateRelease(release)
}

func CreateAddonReleaseFromHelmRelease(
	config *config.Config,
	projectID, clusterID, stackResourceID uint,
	helmRelease *release.Release,
) (*models.Release, error) {
	release := &models.Release{
		ClusterID:       clusterID,
		ProjectID:       projectID,
		Namespace:       helmRelease.Namespace,
		Name:            helmRelease.Name,
		StackResourceID: stackResourceID,
	}

	return config.Repo.Release().CreateRelease(release)
}

func createGitAction(
	ctx context.Context,
	config *config.Config,
	userID, projectID, clusterID uint,
	request *types.CreateGitActionConfigRequest,
	name, namespace string,
	release *models.Release,
) (*types.GitActionConfig, []byte, error) {
	ctx, span := telemetry.NewSpan(ctx, "create-git-action")
	defer span.End()

	telemetry.WithAttributes(span,
		telemetry.AttributeKV{Key: "project-id", Value: projectID},
		telemetry.AttributeKV{Key: "cluster-id", Value: clusterID},
		telemetry.AttributeKV{Key: "user-id", Value: userID},
		telemetry.AttributeKV{Key: "name", Value: name},
		telemetry.AttributeKV{Key: "namespace", Value: namespace},
	)

	// if the registry was provisioned through Porter, create a repository if necessary
	if release != nil && request.RegistryID != 0 {
		// read the registry
		reg, err := config.Repo.Registry().ReadRegistry(projectID, request.RegistryID)
		if err != nil {
			return nil, nil, telemetry.Error(ctx, span, err, "could not read repo registry")
		}

		_reg := registry.Registry(*reg)
		regAPI := &_reg

		// parse the name from the registry
		nameSpl := strings.Split(request.ImageRepoURI, "/")
		repoName := nameSpl[len(nameSpl)-1]

		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "repo-name", Value: repoName})

		err = regAPI.CreateRepository(ctx, config, repoName)

		if err != nil {
			return nil, nil, telemetry.Error(ctx, span, err, "could not create repo")
		}
	}

	isDryRun := release == nil

	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "is-dry-run", Value: isDryRun})

	encoded := ""
	var err error

	// if this isn't a dry run, generate the token
	if !isDryRun {
		encoded, err = getToken(ctx, config, userID, projectID, clusterID, request)

		if err != nil {
			return nil, nil, telemetry.Error(ctx, span, err, "error getting token")
		}
	}

	var workflowYAML []byte
	var gitErr error

	if request.GitlabIntegrationID != 0 {
		giRunner := &gitlab.GitlabCI{
			ServerURL:        config.ServerConf.ServerURL,
			GitRepoPath:      request.GitRepo,
			GitBranch:        request.GitBranch,
			Repo:             config.Repo,
			ProjectID:        projectID,
			ClusterID:        clusterID,
			UserID:           userID,
			IntegrationID:    request.GitlabIntegrationID,
			PorterConf:       config,
			ReleaseName:      name,
			ReleaseNamespace: namespace,
			FolderPath:       request.FolderPath,
			PorterToken:      encoded,
		}

		gitErr = giRunner.Setup()
	} else {
		repoSplit := strings.Split(request.GitRepo, "/")

		if len(repoSplit) != 2 {
			return nil, nil, fmt.Errorf("invalid formatting of repo name")
		}

		// create the commit in the git repo
		gaRunner := &actions.GithubActions{
			InstanceName:           config.ServerConf.InstanceName,
			ServerURL:              config.ServerConf.ServerURL,
			GithubOAuthIntegration: nil,
			GithubAppID:            config.GithubAppConf.AppID,
			GithubAppSecretPath:    config.GithubAppConf.SecretPath,
			GithubInstallationID:   request.GitRepoID,
			GitRepoName:            repoSplit[1],
			GitRepoOwner:           repoSplit[0],
			Repo:                   config.Repo,
			ProjectID:              projectID,
			ClusterID:              clusterID,
			ReleaseName:            name,
			ReleaseNamespace:       namespace,
			GitBranch:              request.GitBranch,
			DockerFilePath:         request.DockerfilePath,
			FolderPath:             request.FolderPath,
			ImageRepoURL:           request.ImageRepoURI,
			PorterToken:            encoded,
			Version:                "v0.1.0",
			ShouldCreateWorkflow:   request.ShouldCreateWorkflow,
			DryRun:                 release == nil,
		}

		// Save the github err for after creating the git action config. However, we
		// need to call Setup() in order to get the workflow file before writing the
		// action config, in the case of a dry run, since the dry run does not create
		// a git action config.
		workflowYAML, gitErr = gaRunner.Setup()

		if gaRunner.DryRun {
			if gitErr != nil {
				return nil, nil, telemetry.Error(ctx, span, gitErr, "error setting up git")
			}

			return nil, workflowYAML, nil
		}
	}

	// handle write to the database
	ga, err := config.Repo.GitActionConfig().CreateGitActionConfig(&models.GitActionConfig{
		ReleaseID:           release.ID,
		GitRepo:             request.GitRepo,
		GitBranch:           request.GitBranch,
		ImageRepoURI:        request.ImageRepoURI,
		GitRepoID:           request.GitRepoID,
		GitlabIntegrationID: request.GitlabIntegrationID,
		DockerfilePath:      request.DockerfilePath,
		FolderPath:          request.FolderPath,
		IsInstallation:      true,
		Version:             "v0.1.0",
	})
	if err != nil {
		return nil, nil, telemetry.Error(ctx, span, err, "error creating git action config")
	}

	// update the release in the db with the image repo uri
	release.ImageRepoURI = ga.ImageRepoURI

	_, err = config.Repo.Release().UpdateRelease(release)
	if err != nil {
		return nil, nil, telemetry.Error(ctx, span, err, "error updating release")
	}

	return ga.ToGitActionConfigType(), workflowYAML, gitErr
}

func getToken(
	ctx context.Context,
	config *config.Config,
	userID, projectID, clusterID uint,
	request *types.CreateGitActionConfigRequest,
) (string, error) {
	ctx, span := telemetry.NewSpan(ctx, "get-git-action-token")
	defer span.End()

	telemetry.WithAttributes(span,
		telemetry.AttributeKV{Key: "project-id", Value: projectID},
		telemetry.AttributeKV{Key: "cluster-id", Value: clusterID},
		telemetry.AttributeKV{Key: "user-id", Value: userID},
	)

	// create a policy for the token
	policy := []*types.PolicyDocument{
		{
			Scope: types.ProjectScope,
			Verbs: types.ReadWriteVerbGroup(),
			Children: map[types.PermissionScope]*types.PolicyDocument{
				types.ClusterScope: {
					Scope: types.ClusterScope,
					Verbs: types.ReadWriteVerbGroup(),
				},
				types.RegistryScope: {
					Scope: types.RegistryScope,
					Verbs: types.ReadVerbGroup(),
				},
				types.HelmRepoScope: {
					Scope: types.HelmRepoScope,
					Verbs: types.ReadVerbGroup(),
				},
			},
		},
	}

	uid, err := encryption.GenerateRandomBytes(16)
	if err != nil {
		return "", telemetry.Error(ctx, span, err, "error generating uid")
	}

	policyBytes, err := json.Marshal(policy)
	if err != nil {
		return "", telemetry.Error(ctx, span, err, "error marshalling policy into json")
	}

	policyModel := &models.Policy{
		ProjectID:       projectID,
		UniqueID:        uid,
		CreatedByUserID: userID,
		Name:            strings.ToLower(fmt.Sprintf("repo-%s-token-policy", request.GitRepo)),
		PolicyBytes:     policyBytes,
	}

	policyModel, err = config.Repo.Policy().CreatePolicy(policyModel)

	if err != nil {
		return "", telemetry.Error(ctx, span, err, "error creating policy")
	}

	// create the token in the database
	tokenUID, err := encryption.GenerateRandomBytes(16)
	if err != nil {
		return "", telemetry.Error(ctx, span, err, "error generating tokenUID")
	}

	secretKey, err := encryption.GenerateRandomBytes(16)
	if err != nil {
		return "", telemetry.Error(ctx, span, err, "error generating secret key")
	}

	// hash the secret key for storage in the db
	hashedToken, err := bcrypt.GenerateFromPassword([]byte(secretKey), 8)
	if err != nil {
		return "", telemetry.Error(ctx, span, err, "error generating hashedToken")
	}

	expiresAt := time.Now().Add(time.Hour * 24 * 365)

	apiToken := &models.APIToken{
		UniqueID:        tokenUID,
		ProjectID:       projectID,
		CreatedByUserID: userID,
		Expiry:          &expiresAt,
		Revoked:         false,
		PolicyUID:       policyModel.UniqueID,
		PolicyName:      policyModel.Name,
		Name:            strings.ToLower(fmt.Sprintf("repo-%s-token", request.GitRepo)),
		SecretKey:       hashedToken,
	}

	apiToken, err = config.Repo.APIToken().CreateAPIToken(apiToken)

	if err != nil {
		return "", telemetry.Error(ctx, span, err, "error creating api token")
	}

	// generate porter jwt token
	jwt, err := token.GetStoredTokenForAPI(userID, projectID, apiToken.UniqueID, secretKey)
	if err != nil {
		return "", telemetry.Error(ctx, span, err, "error getting stored token for api")
	}

	return jwt.EncodeToken(config.TokenConf)
}

func createBuildConfig(
	ctx context.Context,
	config *config.Config,
	release *models.Release,
	bcRequest *types.CreateBuildConfigRequest,
) (*types.BuildConfig, error) {
	ctx, span := telemetry.NewSpan(ctx, "create-build-config")
	defer span.End()

	data, err := json.Marshal(bcRequest.Config)
	if err != nil {
		return nil, telemetry.Error(ctx, span, err, "error marshalling build config request")
	}

	// handle write to the database
	bc, err := config.Repo.BuildConfig().CreateBuildConfig(&models.BuildConfig{
		Builder:    bcRequest.Builder,
		Buildpacks: strings.Join(bcRequest.Buildpacks, ","),
		Config:     data,
	})
	if err != nil {
		return nil, telemetry.Error(ctx, span, err, "error creating build config")
	}

	release.BuildConfig = bc.ID

	_, err = config.Repo.Release().UpdateRelease(release)
	if err != nil {
		return nil, telemetry.Error(ctx, span, err, "error updating release")
	}

	return bc.ToBuildConfigType(), nil
}

type containerEnvConfig struct {
	Container struct {
		Env struct {
			Normal map[string]string `yaml:"normal"`
		} `yaml:"env"`
	} `yaml:"container"`
}

func GetGARunner(
	ctx context.Context,
	config *config.Config,
	userID, projectID, clusterID uint,
	ga *models.GitActionConfig,
	name, namespace string,
	release *models.Release,
	helmRelease *release.Release,
) (*actions.GithubActions, error) {
	ctx, span := telemetry.NewSpan(ctx, "get-ga-runner")
	defer span.End()

	cEnv := &containerEnvConfig{}

	rawValues, err := yaml.Marshal(helmRelease.Config)
	if err == nil {
		err = yaml.Unmarshal(rawValues, cEnv)

		// if unmarshal error, just set to empty map
		if err != nil {
			cEnv.Container.Env.Normal = make(map[string]string)
		}
	}

	repoSplit := strings.Split(ga.GitRepo, "/")

	if len(repoSplit) != 2 {
		return nil, telemetry.Error(ctx, span, nil, "invalid formatting of repo name")
	}

	// create the commit in the git repo
	return &actions.GithubActions{
		ServerURL:              config.ServerConf.ServerURL,
		GithubOAuthIntegration: nil,
		BuildEnv:               cEnv.Container.Env.Normal,
		GithubAppID:            config.GithubAppConf.AppID,
		GithubAppSecretPath:    config.GithubAppConf.SecretPath,
		GithubInstallationID:   ga.GitRepoID,
		GitRepoName:            repoSplit[1],
		GitRepoOwner:           repoSplit[0],
		Repo:                   config.Repo,
		ProjectID:              projectID,
		ClusterID:              clusterID,
		ReleaseName:            name,
		GitBranch:              ga.GitBranch,
		DockerFilePath:         ga.DockerfilePath,
		FolderPath:             ga.FolderPath,
		ImageRepoURL:           ga.ImageRepoURI,
		Version:                "v0.1.0",
	}, nil
}
