package commands

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	securejoin "github.com/cyphar/filepath-securejoin"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli/v2"

	"gitlab.com/gitlab-org/release-cli/internal/flags"
	"gitlab.com/gitlab-org/release-cli/internal/gitlab"
)

type httpClientFn func(ctx *cli.Context, log logrus.FieldLogger) (gitlab.HTTPClient, error)

// Create defines the create command to be used by the CLI
func Create(log logrus.FieldLogger, httpClientFn httpClientFn) *cli.Command {
	return createReleaseWithFlags(log, httpClientFn, flags.ParameterFlag{}, "create")
}

// CreateFromFile creates a release when passing in a file with data
func CreateFromFile(log logrus.FieldLogger, httpClientFn httpClientFn) *cli.Command {
	return createReleaseWithFlags(log, httpClientFn, flags.FileFlag{}, "create-from-file")
}

func createReleaseWithFlags(log logrus.FieldLogger, httpClientFn httpClientFn, flags flags.PassedInFlags, name string) *cli.Command {
	flagList := *flags.ListFlags()

	return &cli.Command{
		Name:  name,
		Usage: "Create a Release using GitLab's Releases API https://docs.gitlab.com/ee/api/releases/#create-a-release",
		Action: func(ctx *cli.Context) error {
			client, err := httpClientFn(ctx, log)
			if err != nil {
				return err
			}

			return createRelease(ctx, log, client)
		},
		Before:      flags.BeforeHook(flagList),
		Subcommands: nil,
		Flags:       flagList,
	}
}

func createRelease(ctx *cli.Context, log logrus.FieldLogger, httpClient gitlab.HTTPClient) error {
	projectID := ctx.String(flags.ProjectID)
	serverURL := ctx.String(flags.ServerURL)
	jobToken := ctx.String(flags.JobToken)
	privateToken := ctx.String(flags.PrivateToken)

	l := log.WithFields(logrus.Fields{
		"command":            ctx.Command.Name,
		flags.ServerURL:      serverURL,
		flags.ProjectID:      projectID,
		flags.Name:           ctx.String(flags.Name),
		flags.TagName:        ctx.String(flags.TagName),
		flags.TagMessage:     ctx.String(flags.TagMessage),
		flags.Ref:            ctx.String(flags.Ref),
		flags.CatalogPublish: ctx.Bool(flags.CatalogPublish),
	})

	l.Info("Creating Release...")

	gitlabClient, err := gitlab.New(serverURL, jobToken, privateToken, projectID, httpClient, log)
	if err != nil {
		return fmt.Errorf("failed to create GitLab client: %w", err)
	}

	crr, err := newCreateReleaseReq(ctx, log)
	if err != nil {
		return fmt.Errorf("new CreateReleaseRequest: %w", err)
	}

	release, err := gitlabClient.CreateRelease(ctx.Context, crr)
	if err != nil {
		return fmt.Errorf("failed to create release: %w", err)
	}

	printReleaseOutput(ctx.App.Writer, release, log)

	l.Info("release created successfully!")

	return nil
}

func getDescription(description string, log logrus.FieldLogger) (string, error) {
	l := log.WithFields(logrus.Fields{
		"description": description,
	})

	if description == "" || strings.Contains(strings.TrimSpace(description), " ") {
		return description, nil
	}

	baseDir, err := os.Getwd()
	if err != nil {
		l.WithError(err).Warn("failed to get working directory, using string value for --description")
		return description, nil
	}

	filePath, err := securejoin.SecureJoin(baseDir, description)
	if err != nil {
		l.WithError(err).Warn("failed to resolve filepath, using string value for --description")
		return description, nil
	}

	content, err := os.ReadFile(filepath.Clean(filePath))
	if errors.Is(err, os.ErrNotExist) {
		l.WithError(err).Warn("file does not exist, using string value for --description")
		return description, nil
	}

	return string(content), err
}

func newCreateReleaseReq(ctx *cli.Context, log logrus.FieldLogger) (*gitlab.CreateReleaseRequest, error) {
	assetsLink := ctx.StringSlice(flags.AssetsLink)
	description := ctx.String(flags.Description)
	releasedAt := ctx.String(flags.ReleasedAt)
	legacyCatalogPublish := new(bool)

	assets, err := gitlab.ParseAssets(assetsLink)
	if err != nil {
		return nil, fmt.Errorf("failed to parse assets: %w", err)
	}

	descriptionString, err := getDescription(description, log)
	if err != nil {
		return nil, err
	}

	// We define `legacyCatalogPublish` as a pointer to assign `nil` to it along with boolean values.
	// That's because we don't want to send this parameter via API if it's not `true`.
	*legacyCatalogPublish = ctx.Bool(flags.CatalogPublish)
	if !*legacyCatalogPublish {
		legacyCatalogPublish = nil
	}

	crr := &gitlab.CreateReleaseRequest{
		ID:                   ctx.String(flags.ProjectID),
		Name:                 ctx.String(flags.Name),
		Description:          descriptionString,
		TagName:              ctx.String(flags.TagName),
		TagMessage:           ctx.String(flags.TagMessage),
		Ref:                  ctx.String(flags.Ref),
		Assets:               assets,
		Milestones:           ctx.StringSlice(flags.Milestone),
		LegacyCatalogPublish: legacyCatalogPublish,
	}

	if releasedAt != "" {
		timeReleasedAt, err := gitlab.ParseDateTime(releasedAt)
		if err != nil {
			return nil, fmt.Errorf("failed to parse released-at: %w", err)
		}

		crr.ReleasedAt = &timeReleasedAt
	}

	return crr, nil
}

func printReleaseOutput(w io.Writer, release *gitlab.ReleaseResponse, logger logrus.FieldLogger) {
	printReleaseDetails(w, release)

	if release.Assets != nil {
		printAssetNamesAndUrls(w, release)

		if logger.(*logrus.Entry).Logger.Level == logrus.DebugLevel {
			printAllAssetsAsJSON(w, release, logger)
		}
	}

	printMilestone(w, release)

	fmt.Fprintf(w, "See all available releases here: %s/-/releases\n", os.Getenv("CI_PROJECT_URL"))
}

func printReleaseDetails(w io.Writer, release *gitlab.ReleaseResponse) {
	fmt.Fprintf(w, `

Tag: %s
Name: %s
Description: %s
Created At: %s
Released At: %s

`,
		release.TagName,
		release.Name,
		release.Description,
		release.CreatedAt,
		release.ReleasedAt,
	)
}

func printAssetNamesAndUrls(w io.Writer, release *gitlab.ReleaseResponse) {
	for _, link := range release.Assets.Links {
		fmt.Fprintf(w, `
Asset::Link::Name: %s
Asset::Link::URL: %s

`,
			link.Name, link.URL)
	}
}

func printMilestone(w io.Writer, release *gitlab.ReleaseResponse) {
	for _, m := range release.Milestones {
		fmt.Fprintf(w, `
Milestone: %s - %s

`, m.Title, m.Description)
	}
}

func printAllAssetsAsJSON(w io.Writer, release *gitlab.ReleaseResponse, logger logrus.FieldLogger) {
	o, err := json.MarshalIndent(release.Assets.Links, "", "    ")
	if err != nil {
		logger.WithError(err).Error("parse asset JSON object for debugging")

		return
	}

	logger.Debug("JSON object for generated assets in received response:")

	fmt.Fprintln(w, string(o))
}
