package webhook

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

	"github.com/bradleyfalzon/ghinstallation/v2"
	"github.com/google/go-github/v41/github"
	"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/server/shared/requestutils"
	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/models"
	"gorm.io/gorm"
)

type GithubIncomingWebhookHandler struct {
	handlers.PorterHandlerReadWriter
	authz.KubernetesAgentGetter
}

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

func (c *GithubIncomingWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	payload, err := github.ValidatePayload(r, []byte(c.Config().ServerConf.GithubIncomingWebhookSecret))
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error validating webhook payload: %w", err)))
		return
	}

	event, err := github.ParseWebHook(github.WebHookType(r), payload)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error parsing webhook: %w", err)))
		return
	}

	switch event := event.(type) {
	case *github.PullRequestEvent:
		err = c.processPullRequestEvent(event, r)

		if err != nil {
			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error processing pull request webhook event: %w", err)))
			return
		}
	case *github.PushEvent:
		err = c.processPushEvent(event, r)

		if err != nil {
			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error processing push webhook event: %w", err)))
			return
		}
	}
}

func (c *GithubIncomingWebhookHandler) processPullRequestEvent(event *github.PullRequestEvent, r *http.Request) error {
	// get the webhook id from the request
	webhookID, reqErr := requestutils.GetURLParamString(r, types.URLParamIncomingWebhookID)

	if reqErr != nil {
		return fmt.Errorf(reqErr.Error())
	}

	owner := event.GetRepo().GetOwner().GetLogin()
	repo := event.GetRepo().GetName()

	env, err := c.Repo().Environment().ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}

		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s] error reading environment: %w", webhookID, owner, repo, err)
	}

	if event.GetPullRequest() == nil {
		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s] incoming webhook does not have pull request information: %w",
			webhookID, owner, repo, err)
	}

	envType := env.ToEnvironmentType()

	if len(envType.GitRepoBranches) > 0 {
		found := false

		for _, br := range envType.GitRepoBranches {
			if br == event.GetPullRequest().GetHead().GetRef() {
				found = true
				break
			}
		}

		if !found {
			return nil
		}
	} else if len(envType.GitDeployBranches) > 0 {
		// if the pull request's head branch is in the list of deploy branches
		// then we ignore it to avoid a double deploy
		found := false

		for _, br := range envType.GitDeployBranches {
			if br == event.GetPullRequest().GetHead().GetRef() {
				found = true
				break
			}
		}

		if found {
			return nil
		}
	}

	// create deployment on GitHub API
	client, err := getGithubClientFromEnvironment(c.Config(), env)
	if err != nil {
		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, prNumber: %d] "+
			"error getting github client: %w", webhookID, owner, repo, env.ID, event.GetPullRequest().GetNumber(), err)
	}

	if env.Mode == "auto" && event.GetAction() == "opened" {
		depl := &models.Deployment{
			EnvironmentID: env.ID,
			Namespace:     "",
			Status:        types.DeploymentStatusCreating,
			PullRequestID: uint(event.GetPullRequest().GetNumber()),
			PRName:        event.GetPullRequest().GetTitle(),
			RepoName:      repo,
			RepoOwner:     owner,
			CommitSHA:     event.GetPullRequest().GetHead().GetSHA()[:7],
			PRBranchFrom:  event.GetPullRequest().GetHead().GetRef(),
			PRBranchInto:  event.GetPullRequest().GetBase().GetRef(),
		}

		_, err = c.Repo().Environment().CreateDeployment(depl)

		if err != nil {
			return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, prNumber: %d] "+
				"error creating new deployment: %w", webhookID, owner, repo, env.ID, event.GetPullRequest().GetNumber(), err)
		}

		_, err := client.Actions.CreateWorkflowDispatchEventByFileName(
			r.Context(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
			github.CreateWorkflowDispatchEventRequest{
				Ref: event.GetPullRequest().GetHead().GetRef(),
				Inputs: map[string]interface{}{
					"pr_number":      strconv.FormatUint(uint64(event.GetPullRequest().GetNumber()), 10),
					"pr_title":       event.GetPullRequest().GetTitle(),
					"pr_branch_from": event.GetPullRequest().GetHead().GetRef(),
					"pr_branch_into": event.GetPullRequest().GetBase().GetRef(),
				},
			},
		)
		if err != nil {
			return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, prNumber: %d] "+
				"error creating workflow dispatch event: %w", webhookID, owner, repo, env.ID, event.GetPullRequest().GetNumber(), err)
		}
	} else if event.GetAction() == "synchronize" || event.GetAction() == "closed" || event.GetAction() == "edited" {
		depl, err := c.Repo().Environment().ReadDeploymentByGitDetails(
			env.ID, owner, repo, uint(event.GetPullRequest().GetNumber()),
		)
		if err != nil {
			return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, prNumber: %d] "+
				"error reading deployment: %w", webhookID, owner, repo, env.ID, event.GetPullRequest().GetNumber(), err)
		}

		if depl.Status == types.DeploymentStatusInactive {
			return nil
		}

		if event.GetAction() == "synchronize" {
			_, err := client.Actions.CreateWorkflowDispatchEventByFileName(
				r.Context(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
				github.CreateWorkflowDispatchEventRequest{
					Ref: event.GetPullRequest().GetHead().GetRef(),
					Inputs: map[string]interface{}{
						"pr_number":      strconv.FormatUint(uint64(event.GetPullRequest().GetNumber()), 10),
						"pr_title":       event.GetPullRequest().GetTitle(),
						"pr_branch_from": event.GetPullRequest().GetHead().GetRef(),
						"pr_branch_into": event.GetPullRequest().GetBase().GetRef(),
					},
				},
			)
			if err != nil {
				return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, deploymentID: %d, prNumber: %d] "+
					"error creating workflow dispatch event: %w", webhookID, owner, repo, env.ID, depl.ID,
					event.GetPullRequest().GetNumber(), err)
			}
		} else if event.GetAction() == "closed" {
			// check for already running workflows we should be cancelling
			var wg sync.WaitGroup
			statuses := []string{"in_progress", "queued", "requested", "waiting"}
			chanErr := fmt.Errorf("")

			wg.Add(len(statuses))

			for _, status := range statuses {
				go func(status string) {
					defer wg.Done()

					runs, _, err := client.Actions.ListWorkflowRunsByFileName(
						context.Background(), owner, repo, fmt.Sprintf("porter_%s_env.yml", env.Name),
						&github.ListWorkflowRunsOptions{
							Branch: event.GetPullRequest().GetHead().GetRef(),
							Status: status,
						},
					)

					if err == nil {
						for _, run := range runs.WorkflowRuns {
							resp, err := client.Actions.CancelWorkflowRunByID(context.Background(), owner, repo, run.GetID())

							if err != nil && resp.StatusCode != http.StatusAccepted {
								// the go library we are using returns a 202 Accepted status as an error
								// in this case, we should rule this out as an error
								chanErr = fmt.Errorf("%s: error cancelling %s: %w", chanErr.Error(), run.GetHTMLURL(), err)
							}
						}
					} else {
						chanErr = fmt.Errorf("%s: error listing workflows for status %s: %w", chanErr.Error(), status, err)
					}
				}(status)
			}

			wg.Wait()

			err = c.deleteDeployment(r, depl, env, client)

			if err != nil {
				deleteErr := fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, deploymentID: %d, prNumber: %d] "+
					"error deleting deployment: %w", webhookID, owner, repo, env.ID, depl.ID, event.GetPullRequest().GetNumber(), err)

				if chanErr.Error() != "" {
					deleteErr = fmt.Errorf("%s. errors found while trying to cancel active workflow runs %w", deleteErr.Error(), chanErr)
				}

				return deleteErr
			} else if chanErr.Error() != "" {
				return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, deploymentID: %d, prNumber: %d] "+
					"deployment deleted but errors found while trying to cancel active workflow runs %w", webhookID, owner, repo, env.ID, depl.ID,
					event.GetPullRequest().GetNumber(), chanErr)
			}
		} else if event.GetChanges() != nil {
			shouldUpdate := false

			if event.GetChanges().GetTitle() != nil && event.GetPullRequest().GetTitle() != depl.PRName {
				depl.PRName = event.GetPullRequest().GetTitle()
				shouldUpdate = true
			}

			if event.GetChanges().GetBase() != nil && event.GetChanges().GetBase().GetRef() != nil && event.GetPullRequest().GetBase().GetRef() != depl.PRBranchInto {
				depl.PRBranchInto = event.GetPullRequest().GetBase().GetRef()
				shouldUpdate = true
			}

			if shouldUpdate {
				_, err := c.Repo().Environment().UpdateDeployment(depl)
				if err != nil {
					return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, deploymentID: %d, prNumber: %d] "+
						"error updating deployment to reflect changes in the pull request %w", webhookID, owner, repo, env.ID, depl.ID,
						event.GetPullRequest().GetNumber(), err)
				}
			}
		}
	}

	return nil
}

func (c *GithubIncomingWebhookHandler) deleteDeployment(
	r *http.Request,
	depl *models.Deployment,
	env *models.Environment,
	client *github.Client,
) error {
	cluster, err := c.Repo().Cluster().ReadCluster(env.ProjectID, env.ClusterID)
	if err != nil {
		return fmt.Errorf("[projectID: %d, clusterID: %d] error reading cluster when deleting existing deployment: %w",
			env.ProjectID, env.ClusterID, err)
	}

	agent, err := c.GetAgent(r, cluster, "")
	if err != nil {
		return err
	}

	// make sure we do not delete any kubernetes "system" namespaces
	if !isSystemNamespace(depl.Namespace) {
		err = agent.DeleteNamespace(depl.Namespace)

		if err != nil {
			return fmt.Errorf("[owner: %s, repo: %s, environmentID: %d, deploymentID: %d] error deleting namespace '%s': %w",
				env.GitRepoOwner, env.GitRepoName, env.ID, depl.ID, depl.Namespace, err)
		}
	}

	// Create new deployment status to indicate deployment is ready
	state := "inactive"

	deploymentStatusRequest := github.DeploymentStatusRequest{
		State: &state,
	}

	client.Repositories.CreateDeploymentStatus(
		context.Background(),
		env.GitRepoOwner,
		env.GitRepoName,
		depl.GHDeploymentID,
		&deploymentStatusRequest,
	)

	_, err = c.Repo().Environment().DeleteDeployment(depl)

	if err != nil {
		return fmt.Errorf("[owner: %s, repo: %s, environmentID: %d, deploymentID: %d] error updating deployment: %w",
			env.GitRepoOwner, env.GitRepoName, env.ID, depl.ID, err)
	}

	return nil
}

func (c *GithubIncomingWebhookHandler) processPushEvent(event *github.PushEvent, r *http.Request) error {
	if !strings.HasPrefix(event.GetRef(), "refs/heads/") {
		return nil
	}

	// get the webhook id from the request
	webhookID, reqErr := requestutils.GetURLParamString(r, types.URLParamIncomingWebhookID)

	if reqErr != nil {
		return fmt.Errorf(reqErr.Error())
	}

	owner := event.GetRepo().GetOwner().GetLogin()
	repo := event.GetRepo().GetName()

	env, err := c.Repo().Environment().ReadEnvironmentByWebhookIDOwnerRepoName(webhookID, owner, repo)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil
		}

		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s] error reading environment: %w", webhookID, owner, repo, err)
	}

	envType := env.ToEnvironmentType()

	if len(envType.GitDeployBranches) == 0 {
		return nil
	}

	branch := strings.TrimPrefix(event.GetRef(), "refs/heads/")

	found := false

	for _, br := range envType.GitDeployBranches {
		if br == branch {
			found = true
			break
		}
	}

	if !found {
		return nil
	}

	client, err := getGithubClientFromEnvironment(c.Config(), env)
	if err != nil {
		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s] error creating github client: %w", webhookID, owner, repo, err)
	}

	var deplID uint

	depl, err := c.Repo().Environment().ReadDeploymentForBranch(env.ID, owner, repo, branch)

	if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
		depl, err := c.Repo().Environment().CreateDeployment(&models.Deployment{
			EnvironmentID: env.ID,
			Status:        types.DeploymentStatusCreating,
			PRName:        fmt.Sprintf("Deployment for branch %s", branch),
			RepoName:      repo,
			RepoOwner:     owner,
			CommitSHA:     event.GetAfter()[:7],
			PRBranchFrom:  branch,
			PRBranchInto:  branch,
		})
		if err != nil {
			return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, branch: %s] "+
				"error creating new deployment: %w", webhookID, owner, repo, env.ID, branch, err)
		}

		deplID = depl.ID
	} else if err != nil {
		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, branch: %s] "+
			"error reading deployment: %w", webhookID, owner, repo, env.ID, branch, err)
	} else {
		deplID = depl.ID
	}

	// FIXME: we should case on if env mode is auto or manual
	_, err = client.Actions.CreateWorkflowDispatchEventByFileName(
		r.Context(), owner, repo, 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 {
		return fmt.Errorf("[webhookID: %s, owner: %s, repo: %s, environmentID: %d, branch: %s] "+
			"error creating workflow dispatch event: %w", webhookID, owner, repo, env.ID, branch, err)
	}

	return nil
}

func isSystemNamespace(namespace string) bool {
	return namespace == "cert-manager" || namespace == "ingress-nginx" ||
		namespace == "kube-node-lease" || namespace == "kube-public" ||
		namespace == "kube-system" || namespace == "monitoring" ||
		namespace == "porter-agent-system" || namespace == "default" ||
		namespace == "ingress-nginx-private"
}

func getGithubClientFromEnvironment(config *config.Config, env *models.Environment) (*github.Client, error) {
	// get the github app client
	ghAppId, err := strconv.Atoi(config.ServerConf.GithubAppID)
	if err != nil {
		return nil, err
	}

	// authenticate as github app installation
	itr, err := ghinstallation.NewKeyFromFile(
		http.DefaultTransport,
		int64(ghAppId),
		int64(env.GitInstallationID),
		config.ServerConf.GithubAppSecretPath,
	)
	if err != nil {
		return nil, err
	}

	return github.NewClient(&http.Client{Transport: itr}), nil
}
