package release

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"time"

	"github.com/caarlos0/log"
	"github.com/goreleaser/goreleaser/internal/artifact"
	"github.com/goreleaser/goreleaser/internal/client"
	"github.com/goreleaser/goreleaser/internal/extrafiles"
	"github.com/goreleaser/goreleaser/internal/git"
	"github.com/goreleaser/goreleaser/internal/pipe"
	"github.com/goreleaser/goreleaser/internal/semerrgroup"
	"github.com/goreleaser/goreleaser/internal/tmpl"
	"github.com/goreleaser/goreleaser/pkg/config"
	"github.com/goreleaser/goreleaser/pkg/context"
)

// ErrMultipleReleases indicates that multiple releases are defined. ATM only one of them is allowed.
// See https://github.com/goreleaser/goreleaser/pull/809
var ErrMultipleReleases = errors.New("multiple releases are defined. Only one is allowed")

// Pipe for github release.
type Pipe struct{}

func (Pipe) String() string { return "scm releases" }

func (Pipe) Skip(ctx *context.Context) (bool, error) {
	return tmpl.New(ctx).Bool(ctx.Config.Release.Disable)
}

// Default sets the pipe defaults.
func (Pipe) Default(ctx *context.Context) error {
	numOfReleases := 0
	if ctx.Config.Release.GitHub.String() != "" {
		numOfReleases++
	}
	if ctx.Config.Release.GitLab.String() != "" {
		numOfReleases++
	}
	if ctx.Config.Release.Gitea.String() != "" {
		numOfReleases++
	}
	if numOfReleases > 1 {
		return ErrMultipleReleases
	}

	if ctx.Config.Release.NameTemplate == "" {
		ctx.Config.Release.NameTemplate = "{{.Tag}}"
	}

	switch ctx.TokenType {
	case context.TokenTypeGitLab:
		if err := setupGitLab(ctx); err != nil {
			return err
		}
	case context.TokenTypeGitea:
		if err := setupGitea(ctx); err != nil {
			return err
		}
	default:
		// We keep github as default for now
		if err := setupGitHub(ctx); err != nil {
			return err
		}
	}

	// Check if we have to check the git tag for an indicator to mark as pre release
	switch ctx.Config.Release.Prerelease {
	case "auto":
		if ctx.Semver.Prerelease != "" {
			ctx.PreRelease = true
		}
		log.Debugf("pre-release was detected for tag %s: %v", ctx.Git.CurrentTag, ctx.PreRelease)
	case "true":
		ctx.PreRelease = true
	}
	log.Debugf("pre-release for tag %s set to %v", ctx.Git.CurrentTag, ctx.PreRelease)

	return nil
}

func getRepository(ctx *context.Context) (config.Repo, error) {
	repo, err := git.ExtractRepoFromConfig(ctx)
	if err != nil {
		return config.Repo{}, err
	}
	if err := repo.CheckSCM(); err != nil {
		return config.Repo{}, err
	}
	return repo, nil
}

// Publish the release.
func (Pipe) Publish(ctx *context.Context) error {
	c, err := client.New(ctx)
	if err != nil {
		return err
	}
	if err := doPublish(ctx, c); err != nil {
		return err
	}
	log.WithField("url", ctx.ReleaseURL).Info("published")
	return nil
}

func doPublish(ctx *context.Context, client client.Client) error {
	log.WithField("tag", ctx.Git.CurrentTag).
		WithField("repo", ctx.Config.Release.GitHub.String()).
		Info("creating or updating release")
	body, err := describeBody(ctx)
	if err != nil {
		return err
	}
	releaseID, err := client.CreateRelease(ctx, body.String())
	if err != nil {
		return err
	}

	skipUpload, err := tmpl.New(ctx).Bool(ctx.Config.Release.SkipUpload)
	if err != nil {
		return err
	}
	if skipUpload {
		return pipe.Skip("release.skip_upload is set")
	}

	extraFiles, err := extrafiles.Find(ctx, ctx.Config.Release.ExtraFiles)
	if err != nil {
		return err
	}

	for name, path := range extraFiles {
		if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
			return fmt.Errorf("failed to upload %s: %w", name, err)
		}
		ctx.Artifacts.Add(&artifact.Artifact{
			Name: name,
			Path: path,
			Type: artifact.UploadableFile,
		})
	}

	filters := artifact.Or(
		artifact.ByType(artifact.UploadableArchive),
		artifact.ByType(artifact.UploadableBinary),
		artifact.ByType(artifact.UploadableSourceArchive),
		artifact.ByType(artifact.Checksum),
		artifact.ByType(artifact.Signature),
		artifact.ByType(artifact.Certificate),
		artifact.ByType(artifact.LinuxPackage),
		artifact.ByType(artifact.SBOM),
	)

	if len(ctx.Config.Release.IDs) > 0 {
		filters = artifact.And(filters, artifact.ByIDs(ctx.Config.Release.IDs...))
	}

	filters = artifact.Or(filters, artifact.ByType(artifact.UploadableFile))

	g := semerrgroup.New(ctx.Parallelism)
	for _, artifact := range ctx.Artifacts.Filter(filters).List() {
		artifact := artifact
		g.Go(func() error {
			return upload(ctx, client, releaseID, artifact)
		})
	}
	return g.Wait()
}

func upload(ctx *context.Context, cli client.Client, releaseID string, artifact *artifact.Artifact) error {
	var try int
	tryUpload := func() error {
		try++
		file, err := os.Open(artifact.Path)
		if err != nil {
			return err
		}
		defer file.Close()
		log.WithField("file", file.Name()).WithField("name", artifact.Name).Info("uploading to release")
		if err := cli.Upload(ctx, releaseID, artifact, file); err != nil {
			log.WithField("try", try).
				WithField("artifact", artifact.Name).
				WithError(err).
				Warnf("failed to upload artifact, will retry")
			return err
		}
		return nil
	}

	var err error
	for try < 10 {
		err = tryUpload()
		if err == nil {
			return nil
		}
		if errors.As(err, &client.RetriableError{}) {
			time.Sleep(time.Duration(try*50) * time.Millisecond)
			continue
		}
		break
	}

	return fmt.Errorf("failed to upload %s after %d tries: %w", artifact.Name, try, err)
}
