package mod

import (
	"context"
	"fmt"
	"os"
	"sort"
	"strings"

	"github.com/go-git/go-git/v5/plumbing/transport/ssh"
	"github.com/rs/zerolog/log"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/hashicorp/go-version"
)

type repo struct {
	contents *git.Repository
	require  *Require
}

func clone(ctx context.Context, require *Require, dir string, privateKeyFile, privateKeyPassword string) (*repo, error) {
	if err := os.RemoveAll(dir); err != nil {
		return nil, fmt.Errorf("error cleaning up tmp directory")
	}
	if err := os.MkdirAll(dir, 0755); err != nil {
		return nil, fmt.Errorf("error creating tmp dir for cloning package")
	}

	o := git.CloneOptions{
		URL: fmt.Sprintf("https://%s", require.cloneRepo),
	}

	if privateKeyFile != "" {
		publicKeys, err := ssh.NewPublicKeysFromFile("git", privateKeyFile, privateKeyPassword)
		if err != nil {
			return nil, err
		}

		o.Auth = publicKeys
		o.URL = fmt.Sprintf("git@%s", strings.Replace(require.cloneRepo, "/", ":", 1))
	}

	r, err := git.PlainClone(dir, false, &o)
	if err != nil {
		return nil, err
	}

	rr := &repo{
		contents: r,
		require:  require,
	}

	if require.version == "" {
		latestTag, err := rr.latestTag(ctx, require.versionConstraint)
		if err != nil {
			return nil, err
		}

		require.version = latestTag
	}

	if err := rr.checkout(ctx, require.version); err != nil {
		return nil, err
	}

	return rr, nil
}

func (r *repo) checkout(ctx context.Context, version string) error {
	lg := log.Ctx(ctx)

	h, err := r.contents.ResolveRevision(plumbing.Revision(version))
	if err != nil {
		return err
	}

	lg.Debug().Str("repository", r.require.repo).Str("version", version).Str("commit", h.String()).Msg("checkout repo")

	w, err := r.contents.Worktree()
	if err != nil {
		return err
	}

	err = w.Checkout(&git.CheckoutOptions{
		Hash: *h,
	})
	if err != nil {
		return err
	}

	return nil
}

func (r *repo) listTagVersions(ctx context.Context, versionConstraint string) ([]string, error) {
	lg := log.Ctx(ctx).With().
		Str("repository", r.require.repo).
		Str("versionConstraint", versionConstraint).
		Logger()

	if versionConstraint == "" {
		versionConstraint = ">= 0"
	}

	constraint, err := version.NewConstraint(versionConstraint)
	if err != nil {
		return nil, err
	}

	iter, err := r.contents.Tags()
	if err != nil {
		return nil, err
	}

	var tags []string
	err = iter.ForEach(func(ref *plumbing.Reference) error {
		tagV := ref.Name().Short()

		if !strings.HasPrefix(tagV, "v") {
			lg.Debug().Str("tag", tagV).Msg("tag version ignored, wrong format")
			return nil
		}

		v, err := version.NewVersion(tagV)
		if err != nil {
			lg.Debug().Str("tag", tagV).Err(err).Msg("tag version ignored, parsing error")
			return nil
		}

		if constraint.Check(v) {
			// Add tag if it matches the version constraint
			tags = append(tags, ref.Name().Short())
			lg.Debug().Str("tag", tagV).Msg("version added")
		} else {
			lg.Debug().Str("tag", tagV).Msg("tag version ignored, does not satisfy constraint")
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return tags, nil
}

func (r *repo) latestTag(ctx context.Context, versionConstraint string) (string, error) {
	versionsRaw, err := r.listTagVersions(ctx, versionConstraint)
	if err != nil {
		return "", err
	}

	versions := make([]*version.Version, len(versionsRaw))
	for i, raw := range versionsRaw {
		v, _ := version.NewVersion(raw)
		versions[i] = v
	}

	if len(versions) == 0 {
		return "", fmt.Errorf("repo doesn't have any tags matching the required version")
	}

	sort.Sort(sort.Reverse(version.Collection(versions)))
	version := versions[0].Original()

	return version, nil
}