package environment

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"sync"

	"github.com/google/go-github/v41/github"
	"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/commonutils"
	"github.com/porter-dev/porter/api/server/shared/config"
	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/auth/token"
	"github.com/porter-dev/porter/internal/encryption"
	"github.com/porter-dev/porter/internal/integrations/ci/actions"
	"github.com/porter-dev/porter/internal/models"
	"github.com/porter-dev/porter/internal/models/integrations"
	"gorm.io/gorm"
)

type CreateEnvironmentHandler struct {
	handlers.PorterHandlerReadWriter
}

func NewCreateEnvironmentHandler(
	config *config.Config,
	decoderValidator shared.RequestDecoderValidator,
	writer shared.ResultWriter,
) *CreateEnvironmentHandler {
	return &CreateEnvironmentHandler{
		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
	}
}

func (c *CreateEnvironmentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ga, _ := r.Context().Value(types.GitInstallationScope).(*integrations.GithubAppInstallation)
	user, _ := r.Context().Value(types.UserScope).(*models.User)
	project, _ := r.Context().Value(types.ProjectScope).(*models.Project)
	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)

	owner, name, ok := commonutils.GetOwnerAndNameParams(c, w, r)

	if !ok {
		return
	}

	// create the environment
	request := &types.CreateEnvironmentRequest{}

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

	// create a random webhook id
	webhookUID, err := encryption.GenerateRandomBytes(32)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error generating webhook UID for new preview "+
			"environment: %w", err)))
		return
	}

	env := &models.Environment{
		ProjectID:           project.ID,
		ClusterID:           cluster.ID,
		GitInstallationID:   uint(ga.InstallationID),
		Name:                request.Name,
		GitRepoOwner:        owner,
		GitRepoName:         name,
		GitRepoBranches:     strings.Join(request.GitRepoBranches, ","),
		Mode:                request.Mode,
		WebhookID:           string(webhookUID),
		NewCommentsDisabled: request.DisableNewComments,
		GitDeployBranches:   strings.Join(request.GitDeployBranches, ","),
	}

	if len(request.NamespaceLabels) > 0 {
		var labels []string

		for k, v := range request.NamespaceLabels {
			labels = append(labels, fmt.Sprintf("%s=%s", k, v))
		}

		env.NamespaceLabels = []byte(strings.Join(labels, ","))
	}

	// write Github actions files to the repo
	client, err := getGithubClientFromEnvironment(c.Config(), env)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	webhookURL := getGithubWebhookURLFromUID(c.Config().ServerConf.ServerURL, string(webhookUID))

	// create incoming webhook
	hook, _, err := client.Repositories.CreateHook(
		context.Background(), owner, name, &github.Hook{
			Config: map[string]interface{}{
				"url":          webhookURL,
				"content_type": "json",
				"secret":       c.Config().ServerConf.GithubIncomingWebhookSecret,
			},
			Events: []string{"pull_request", "push"},
			Active: github.Bool(true),
		},
	)

	if err != nil && !strings.Contains(err.Error(), "already exists") {
		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, err),
			http.StatusConflict))
		return
	}

	env.GithubWebhookID = hook.GetID()

	env, err = c.Repo().Environment().CreateEnvironment(env)

	if err != nil {
		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())

		if deleteErr != nil {
			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
				http.StatusConflict, "error creating environment"))
			return
		}

		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error creating environment: %w", err)))
		return
	}

	// generate porter jwt token
	jwt, err := token.GetTokenForAPI(user.ID, project.ID)
	if err != nil {
		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())

		if deleteErr != nil {
			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
				http.StatusConflict, "error getting token for API while creating environment"))
			return
		}

		_, deleteErr = c.Repo().Environment().DeleteEnvironment(env)

		if deleteErr != nil {
			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting created preview environment: %w",
				deleteErr)))
			return
		}

		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting token for API: %w", err)))
		return
	}

	encoded, err := jwt.EncodeToken(c.Config().TokenConf)
	if err != nil {
		_, deleteErr := client.Repositories.DeleteHook(context.Background(), owner, name, hook.GetID())

		if deleteErr != nil {
			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(fmt.Errorf("%v: %w", errGithubAPI, deleteErr),
				http.StatusConflict, "error encoding token while creating environment"))
			return
		}

		_, deleteErr = c.Repo().Environment().DeleteEnvironment(env)

		if deleteErr != nil {
			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error deleting created preview environment: %w",
				deleteErr)))
			return
		}

		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error encoding API token: %w", err)))
		return
	}

	err = actions.SetupEnv(&actions.EnvOpts{
		Client:            client,
		ServerURL:         c.Config().ServerConf.ServerURL,
		PorterToken:       encoded,
		GitRepoOwner:      owner,
		GitRepoName:       name,
		ProjectID:         project.ID,
		ClusterID:         cluster.ID,
		GitInstallationID: uint(ga.InstallationID),
		EnvironmentName:   request.Name,
		InstanceName:      c.Config().ServerConf.InstanceName,
	})

	if err != nil {
		unwrappedErr := errors.Unwrap(err)

		if unwrappedErr != nil {
			if errors.Is(unwrappedErr, actions.ErrProtectedBranch) {
				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusConflict))
			} else if errors.Is(unwrappedErr, actions.ErrCreatePRForProtectedBranch) {
				c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusPreconditionFailed))
			}
		} else {
			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error setting up preview environment in the github "+
				"repo: %w", err)))
			return
		}
	}

	envType := env.ToEnvironmentType()

	if len(envType.GitDeployBranches) > 0 && c.Config().ServerConf.EnableAutoPreviewBranchDeploy {
		errs := autoDeployBranch(env, c.Config(), envType.GitDeployBranches, false)

		if len(errs) > 0 {
			errString := errs[0].Error()

			for _, e := range errs {
				errString += ": " + e.Error()
			}

			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
				fmt.Errorf("error auto deploying preview branches: %s", errString), http.StatusConflict),
			)
			return
		}
	}

	c.WriteResult(w, r, envType)
}

func getGithubWebhookURLFromUID(serverURL, webhookUID string) string {
	return fmt.Sprintf("%s/api/github/incoming_webhook/%s", serverURL, string(webhookUID))
}

func autoDeployBranch(
	env *models.Environment,
	config *config.Config,
	branches []string,
	onlyNewDeployments bool,
) []error {
	var (
		errs []error
		wg   sync.WaitGroup
	)

	for _, branch := range branches {
		wg.Add(1)

		go func(errs []error, branch string) {
			defer wg.Done()
			errs = append(errs, createWorkflowDispatchForBranch(env, config, onlyNewDeployments, branch)...)
		}(errs, branch)
	}

	wg.Wait()

	return errs
}

func createWorkflowDispatchForBranch(
	env *models.Environment,
	config *config.Config,
	onlyNewDeployments bool,
	branch string,
) []error {
	var errs []error

	client, err := getGithubClientFromEnvironment(config, env)
	if err != nil {
		errs = append(errs, err)
		return errs
	}

	var deplID uint

	depl, err := config.Repo.Environment().ReadDeploymentForBranch(env.ID, env.GitRepoOwner, env.GitRepoName, branch)

	if err == nil {
		if onlyNewDeployments {
			return errs
		}

		deplID = depl.ID
	} else {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			depl, err := config.Repo.Environment().CreateDeployment(&models.Deployment{
				EnvironmentID: env.ID,
				Status:        types.DeploymentStatusCreating,
				PRName:        fmt.Sprintf("Deployment for branch %s", branch),
				RepoName:      env.GitRepoName,
				RepoOwner:     env.GitRepoOwner,
				PRBranchFrom:  branch,
				PRBranchInto:  branch,
			})
			if err != nil {
				errs = append(errs, fmt.Errorf("error creating deployment for branch %s: %w", branch, err))
				return errs
			}

			deplID = depl.ID
		} else {
			errs = append(errs, fmt.Errorf("error reading deployment for branch %s: %w", branch, err))
			return errs
		}
	}

	if deplID == 0 {
		errs = append(errs, fmt.Errorf("deployment id is 0 for branch %s", branch))
		return errs
	}

	_, err = client.Actions.CreateWorkflowDispatchEventByFileName(
		context.Background(), env.GitRepoOwner, env.GitRepoName, fmt.Sprintf("porter_%s_env.yml", env.Name),
		github.CreateWorkflowDispatchEventRequest{
			Ref: branch,
			Inputs: map[string]interface{}{
				"pr_number":      fmt.Sprintf("%d", deplID),
				"pr_title":       fmt.Sprintf("Deployment for branch %s", branch),
				"pr_branch_from": branch,
				"pr_branch_into": branch,
			},
		},
	)

	if err != nil {
		errs = append(errs, err)
	}

	return errs
}
