package stack

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/porter-dev/porter/api/server/authz"
	"github.com/porter-dev/porter/api/server/handlers"
	"github.com/porter-dev/porter/api/server/handlers/release"
	"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/encryption"
	"github.com/porter-dev/porter/internal/kubernetes/envgroup"
	"github.com/porter-dev/porter/internal/models"

	helmrelease "github.com/stefanmcshane/helm/pkg/release"
)

type StackCreateHandler struct {
	handlers.PorterHandlerReadWriter
	authz.KubernetesAgentGetter
}

func NewStackCreateHandler(
	config *config.Config,
	reader shared.RequestDecoderValidator,
	writer shared.ResultWriter,
) *StackCreateHandler {
	return &StackCreateHandler{
		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, reader, writer),
		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
	}
}

func (p *StackCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
	namespace, _ := r.Context().Value(types.NamespaceScope).(string)

	req := &types.CreateStackRequest{}

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

	uid, err := encryption.GenerateRandomBytes(16)
	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	sourceConfigs, err := getSourceConfigModels(req.SourceConfigs)
	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	resources, err := getResourceModels(req.AppResources, sourceConfigs, p.Config().ServerConf.DefaultApplicationHelmRepoURL)
	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	nameValidator := make(map[string]bool)

	for _, res := range resources {
		if _, ok := nameValidator[res.Name]; ok {
			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("duplicate app resource name: %s", res.Name),
				http.StatusBadRequest))
			return
		}

		nameValidator[res.Name] = true
	}

	envGroups, err := getEnvGroupModels(req.EnvGroups, proj.ID, cluster.ID, namespace)
	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	nameValidator = make(map[string]bool)

	for _, eg := range envGroups {
		if _, ok := nameValidator[eg.Name]; ok {
			p.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("duplicate env group name: %s", eg.Name),
				http.StatusBadRequest))
			return
		}

		nameValidator[eg.Name] = true
	}

	// write stack to the database with creating status
	stack := &models.Stack{
		ProjectID: proj.ID,
		ClusterID: cluster.ID,
		Namespace: namespace,
		Name:      req.Name,
		UID:       uid,
		Revisions: []models.StackRevision{
			{
				RevisionNumber: 1,
				Status:         string(types.StackRevisionStatusDeploying),
				SourceConfigs:  sourceConfigs,
				Resources:      resources,
				EnvGroups:      envGroups,
			},
		},
	}

	stack, err = p.Repo().Stack().CreateStack(stack)

	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	// apply all env groups
	k8sAgent, err := p.GetAgent(r, cluster, "")
	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	envGroupDeployErrors := make([]string, 0)

	for _, envGroup := range req.EnvGroups {
		cm, err := envgroup.CreateEnvGroup(k8sAgent, types.ConfigMapInput{
			Name:            envGroup.Name,
			Namespace:       namespace,
			Variables:       envGroup.Variables,
			SecretVariables: envGroup.SecretVariables,
		})
		if err != nil {
			envGroupDeployErrors = append(envGroupDeployErrors, fmt.Sprintf("error creating env group %s", envGroup.Name))
		}

		// add each of the linked applications to the env group
		for _, appName := range envGroup.LinkedApplications {

			cm, err = k8sAgent.AddApplicationToVersionedConfigMap(cm, appName)

			if err != nil {
				envGroupDeployErrors = append(envGroupDeployErrors, fmt.Sprintf("error creating env group %s", envGroup.Name))
			}
		}
	}

	revision := &stack.Revisions[0]

	if len(envGroupDeployErrors) > 0 {
		revision.Status = string(types.StackRevisionStatusFailed)
		revision.Reason = "EnvGroupDeployErr"
		revision.Message = strings.Join(envGroupDeployErrors, " , ")

		revision, err = p.Repo().Stack().UpdateStackRevision(revision)

		if err != nil {
			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
			return
		}
	} else {
		// apply all app resources
		registries, err := p.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
		if err != nil {
			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
			return
		}

		helmAgent, err := p.GetHelmAgent(r.Context(), r, cluster, "")
		if err != nil {
			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
			return
		}

		helmReleaseMap := make(map[string]*helmrelease.Release)

		deployErrs := make([]string, 0)

		for _, appResource := range req.AppResources {
			rel, err := applyAppResource(&applyAppResourceOpts{
				config:        p.Config(),
				projectID:     proj.ID,
				namespace:     namespace,
				cluster:       cluster,
				registries:    registries,
				helmAgent:     helmAgent,
				request:       appResource,
				stackName:     stack.Name,
				stackRevision: stack.Revisions[0].RevisionNumber,
			})

			if err != nil {
				deployErrs = append(deployErrs, err.Error())
			} else {
				helmReleaseMap[fmt.Sprintf("%s/%s", namespace, appResource.Name)] = rel
			}
		}

		// update stack revision status
		if len(deployErrs) > 0 {
			revision.Status = string(types.StackRevisionStatusFailed)
			revision.Reason = "DeployError"
			revision.Message = strings.Join(deployErrs, " , ")
		} else {
			revision.Status = string(types.StackRevisionStatusDeployed)
		}

		revision, err = p.Repo().Stack().UpdateStackRevision(revision)

		if err != nil {
			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
			return
		}

		saveErrs := make([]string, 0)

		for _, resource := range revision.Resources {
			if rel, exists := helmReleaseMap[fmt.Sprintf("%s/%s", namespace, resource.Name)]; exists {
				_, err = release.CreateAppReleaseFromHelmRelease(r.Context(), p.Config(), proj.ID, cluster.ID, resource.ID, rel)

				if err != nil {
					saveErrs = append(saveErrs, fmt.Sprintf("the resource %s/%s could not be saved right now", namespace, resource.Name))
				}
			}
		}

		if len(saveErrs) > 0 {
			revision.Reason = "SaveError"
			revision.Message = strings.Join(saveErrs, " , ")

			revision, err = p.Repo().Stack().UpdateStackRevision(revision)

			if err != nil {
				p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
				return
			}
		}
	}

	if revision.Status != string(types.StackRevisionStatusFailed) && len(revision.Reason) == 0 {
		revision.Reason = "CreationSuccess"
		revision.Message = "Stack deployed successfully"

		revision, err = p.Repo().Stack().UpdateStackRevision(revision)

		if err != nil {
			p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
			return
		}
	}

	// read the stack again to get the latest revision info
	stack, err = p.Repo().Stack().ReadStackByStringID(proj.ID, stack.UID)

	if err != nil {
		p.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	w.WriteHeader(http.StatusCreated)
	p.WriteResult(w, r, stack.ToStackType())
}

func getSourceConfigModels(sourceConfigs []*types.CreateStackSourceConfigRequest) ([]models.StackSourceConfig, error) {
	res := make([]models.StackSourceConfig, 0)

	// for now, only write source configs which are deployed as a docker image
	// TODO: add parsing/writes for git-based sources
	for _, sourceConfig := range sourceConfigs {
		if sourceConfig.StackSourceConfigBuild == nil {
			uid, err := encryption.GenerateRandomBytes(16)
			if err != nil {
				return nil, err
			}

			res = append(res, models.StackSourceConfig{
				UID:          uid,
				DisplayName:  sourceConfig.DisplayName,
				Name:         sourceConfig.Name,
				ImageRepoURI: sourceConfig.ImageRepoURI,
				ImageTag:     sourceConfig.ImageTag,
			})
		}
	}

	return res, nil
}

func getResourceModels(appResources []*types.CreateStackAppResourceRequest, sourceConfigs []models.StackSourceConfig, defaultRepoURL string) ([]models.StackResource, error) {
	res := make([]models.StackResource, 0)

	for _, appResource := range appResources {
		if appResource.TemplateRepoURL == "" {
			appResource.TemplateRepoURL = defaultRepoURL
		}

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

		var linkedSourceConfigUID string

		for _, sourceConfig := range sourceConfigs {
			if sourceConfig.Name == appResource.SourceConfigName {
				linkedSourceConfigUID = sourceConfig.UID
			}
		}

		if linkedSourceConfigUID == "" {
			return nil, fmt.Errorf("source config %s does not exist in source config list", appResource.SourceConfigName)
		}

		res = append(res, models.StackResource{
			Name:                 appResource.Name,
			UID:                  uid,
			StackSourceConfigUID: linkedSourceConfigUID,
			TemplateRepoURL:      appResource.TemplateRepoURL,
			TemplateName:         appResource.TemplateName,
			TemplateVersion:      appResource.TemplateVersion,
			HelmRevisionID:       1,
		})
	}

	return res, nil
}

func getEnvGroupModels(envGroups []*types.CreateStackEnvGroupRequest, projID, clusterID uint, namespace string) ([]models.StackEnvGroup, error) {
	res := make([]models.StackEnvGroup, 0)

	for _, envGroup := range envGroups {
		uid, err := encryption.GenerateRandomBytes(16)
		if err != nil {
			return nil, err
		}

		res = append(res, models.StackEnvGroup{
			Name:            envGroup.Name,
			UID:             uid,
			EnvGroupVersion: 1,
			ProjectID:       projID,
			ClusterID:       clusterID,
			Namespace:       namespace,
		})
	}

	return res, nil
}
