package actions

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

	"github.com/google/go-github/v41/github"

	"gopkg.in/yaml.v2"
)

type EnvOpts struct {
	Client                                  *github.Client
	ServerURL                               string
	PorterToken                             string
	GitRepoOwner, GitRepoName               string
	EnvironmentName                         string
	InstanceName                            string
	ProjectID, ClusterID, GitInstallationID uint
}

func SetupEnv(opts *EnvOpts) error {
	// make a best-effort to create a Github environment. this is a non-fatal operation,
	// as the environments API is not enabled for private repositories that don't have
	// github enterprise.
	_, resp, err := opts.Client.Repositories.GetEnvironment(
		context.Background(),
		opts.GitRepoOwner,
		opts.GitRepoName,
		opts.EnvironmentName,
	)

	if resp != nil && resp.StatusCode == http.StatusNotFound {
		opts.Client.Repositories.CreateUpdateEnvironment(
			context.Background(),
			opts.GitRepoOwner,
			opts.GitRepoName,
			opts.EnvironmentName,
			nil,
		)
	}

	// create porter token secret
	err = CreateGithubSecret(
		opts.Client,
		getPreviewEnvSecretName(opts.ProjectID, opts.ClusterID, opts.InstanceName),
		opts.PorterToken,
		opts.GitRepoOwner,
		opts.GitRepoName,
	)

	if err != nil {
		return err
	}

	// get the repository to find the default branch
	repo, _, err := opts.Client.Repositories.Get(
		context.TODO(),
		opts.GitRepoOwner,
		opts.GitRepoName,
	)
	if err != nil {
		return err
	}

	defaultBranch := repo.GetDefaultBranch()

	applyWorkflowYAML, err := getPreviewApplyActionYAML(opts)
	if err != nil {
		return err
	}

	githubBranch, _, err := opts.Client.Repositories.GetBranch(
		context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, true,
	)
	if err != nil {
		return err
	}

	if githubBranch.GetProtected() {
		err = createNewBranch(opts.Client, opts.GitRepoOwner, opts.GitRepoName, defaultBranch, "porter-preview")

		if err != nil {
			return fmt.Errorf(
				"Unable to create PR to merge workflow files into protected branch: %s.\n"+
					"To enable Porter Preview Environment deployments, please create Github workflow "+
					"files in this branch with the following contents:\n"+
					"--------\n%s--------\nERROR: %w",
				defaultBranch, string(applyWorkflowYAML), ErrCreatePRForProtectedBranch,
			)
		}

		_, err = commitWorkflowFile(
			opts.Client,
			fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
			applyWorkflowYAML, opts.GitRepoOwner,
			opts.GitRepoName, "porter-preview", false,
		)

		if err != nil {
			return fmt.Errorf(
				"Unable to create PR to merge workflow files into protected branch: %s.\n"+
					"To enable Porter Preview Environment deployments, please create Github workflow "+
					"files in this branch with the following contents:\n"+
					"--------\n%s--------\nERROR: %w",
				defaultBranch, string(applyWorkflowYAML), ErrCreatePRForProtectedBranch,
			)
		}

		pr, _, err := opts.Client.PullRequests.Create(
			context.Background(), opts.GitRepoOwner, opts.GitRepoName, &github.NewPullRequest{
				Title: github.String("Enable Porter Preview Environment deployments"),
				Base:  github.String(defaultBranch),
				Head:  github.String("porter-preview"),
			},
		)
		if err != nil {
			return err
		}

		return fmt.Errorf("Please merge %s to enable Porter Preview Environment deployments.\nERROR: %w",
			pr.GetHTMLURL(), ErrProtectedBranch)
	}

	_, err = commitWorkflowFile(
		opts.Client,
		fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
		applyWorkflowYAML,
		opts.GitRepoOwner,
		opts.GitRepoName,
		defaultBranch,
		false,
	)

	return err
}

func DeleteEnv(opts *EnvOpts) error {
	// get the repository to find the default branch
	repo, _, err := opts.Client.Repositories.Get(
		context.TODO(),
		opts.GitRepoOwner,
		opts.GitRepoName,
	)
	if err != nil {
		return err
	}

	defaultBranch := repo.GetDefaultBranch()

	// delete GitHub Environment: check that environment exists before deletion

	_, resp, err := opts.Client.Repositories.GetEnvironment(
		context.Background(),
		opts.GitRepoOwner,
		opts.GitRepoName,
		opts.EnvironmentName,
	)

	if err == nil && resp != nil && resp.StatusCode == http.StatusOK {
		_, err = opts.Client.Repositories.DeleteEnvironment(
			context.Background(),
			opts.GitRepoOwner,
			opts.GitRepoName,
			opts.EnvironmentName,
		)

		if err != nil {
			return err
		}
	}

	githubBranch, _, err := opts.Client.Repositories.GetBranch(
		context.Background(), opts.GitRepoOwner, opts.GitRepoName, defaultBranch, true,
	)
	if err != nil {
		return err
	}

	if githubBranch.GetProtected() {
		return ErrProtectedBranch
	}

	err = deleteGithubFile(
		opts.Client,
		fmt.Sprintf("porter_%s_env.yml", strings.ToLower(opts.EnvironmentName)),
		opts.GitRepoOwner,
		opts.GitRepoName,
		defaultBranch,
		false,
	)

	if err != nil {
		return err
	}

	return deleteGithubFile(
		opts.Client,
		fmt.Sprintf("porter_%s_delete_env.yml", strings.ToLower(opts.EnvironmentName)),
		opts.GitRepoOwner,
		opts.GitRepoName,
		defaultBranch,
		false,
	)
}

func getPreviewEnvSecretName(projectID, clusterID uint, instanceName string) string {
	if instanceName != "" {
		return fmt.Sprintf("PORTER_PREVIEW_%s_%d_%d", strings.ToUpper(instanceName), projectID, clusterID)
	}

	return fmt.Sprintf("PORTER_PREVIEW_%d_%d", projectID, clusterID)
}

func getPreviewApplyActionYAML(opts *EnvOpts) ([]byte, error) {
	gaSteps := []GithubActionYAMLStep{
		getCheckoutCodeStep(),
		getCreatePreviewEnvStep(
			opts.ServerURL,
			getPreviewEnvSecretName(opts.ProjectID, opts.ClusterID, opts.InstanceName),
			opts.ProjectID,
			opts.ClusterID,
			opts.GitInstallationID,
			opts.GitRepoOwner,
			opts.GitRepoName,
			"v0.2.1",
		),
	}

	actionYAML := GithubActionYAML{
		On: map[string]interface{}{
			"workflow_dispatch": map[string]interface{}{
				"inputs": map[string]interface{}{
					"pr_number": map[string]interface{}{
						"description": "Pull request number",
						"type":        "string",
						"required":    true,
					},
					"pr_title": map[string]interface{}{
						"description": "Pull request title",
						"type":        "string",
						"required":    true,
					},
					"pr_branch_from": map[string]interface{}{
						"description": "Pull request head branch",
						"type":        "string",
						"required":    true,
					},
					"pr_branch_into": map[string]interface{}{
						"description": "Pull request base branch",
						"type":        "string",
						"required":    true,
					},
				},
			},
		},
		Name: "Porter Preview Environment",
		Jobs: map[string]GithubActionYAMLJob{
			"porter-preview": {
				RunsOn: "ubuntu-latest",
				Concurrency: map[string]string{
					"group": "${{ github.workflow }}-${{ github.event.inputs.pr_number }}",
				},
				Steps: gaSteps,
			},
		},
	}

	return yaml.Marshal(actionYAML)
}

func createNewBranch(
	client *github.Client,
	gitRepoOwner, gitRepoName, baseBranch, headBranch string,
) error {
	_, resp, err := client.Repositories.GetBranch(
		context.Background(), gitRepoOwner, gitRepoName, headBranch, true,
	)

	headBranchRef := fmt.Sprintf("refs/heads/%s", headBranch)

	if err == nil {
		// delete the stale branch
		_, err := client.Git.DeleteRef(
			context.Background(), gitRepoOwner, gitRepoName, headBranchRef,
		)
		if err != nil {
			return err
		}
	} else if resp.StatusCode != http.StatusNotFound {
		return err
	}

	base, _, err := client.Repositories.GetBranch(
		context.Background(), gitRepoOwner, gitRepoName, baseBranch, true,
	)
	if err != nil {
		return err
	}

	_, _, err = client.Git.CreateRef(
		context.Background(), gitRepoOwner, gitRepoName, &github.Reference{
			Ref: github.String(headBranchRef),
			Object: &github.GitObject{
				SHA: base.Commit.SHA,
			},
		},
	)

	if err != nil {
		return err
	}

	return nil
}
