package gitserver

import (
	"context"
	"os"
	"path/filepath"
	"time"

	"github.com/sorintlab/errors"

	"agola.io/agola/internal/util"
)

func (s *Gitserver) repoCleanerLoop(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			s.log.Info().Msg("repoCleaner exiting")

			return
		case <-time.After(s.c.RepositoryCleanupInterval):
			if err := s.scanRepos(ctx); err != nil {
				s.log.Err(err).Msg("scanRepos error")
			}
		}
	}
}

func (s *Gitserver) scanRepos(ctx context.Context) error {
	s.log.Info().Msg("repoCleaner scanRepos start")

	usersDir, err := os.ReadDir(s.c.DataDir)
	if err != nil {
		return errors.WithStack(err)
	}

	for _, u := range usersDir {
		if !u.IsDir() {
			continue
		}

		reposDir, _ := os.ReadDir(filepath.Join(s.c.DataDir, u.Name()))
		for _, r := range reposDir {
			if !r.IsDir() {
				continue
			}

			if err := s.scanRepo(ctx, filepath.Join(s.c.DataDir, u.Name(), r.Name())); err != nil {
				s.log.Err(err).Msg("scanRepo error")
			}
		}
	}

	s.log.Info().Msg("repoCleaner scanRepos end")

	return nil
}

func (s *Gitserver) scanRepo(ctx context.Context, repoDir string) error {
	git := &util.Git{GitDir: repoDir}

	branches, _ := s.getBranches(git, ctx)
	for _, b := range branches {
		committerTime, err := s.getLastCommiterTime(ctx, git, "refs/heads/"+b)
		if err != nil {
			return errors.Wrapf(err, "return failed to get last commit time")
		}

		if time.Since(committerTime) >= s.c.RepositoryRefsExpireInterval {
			if err := s.deleteBranch(ctx, git, b); err != nil {
				return errors.Wrapf(err, "failed to delete git branch")
			}
		}
	}

	tags, _ := s.getTags(git, ctx)
	for _, tag := range tags {
		committerTime, err := s.getLastCommiterTime(ctx, git, "refs/tags/"+tag)
		if err != nil {
			return errors.Wrapf(err, "failed to get last commit time")
		}

		if time.Since(committerTime) >= s.c.RepositoryRefsExpireInterval {
			if err := s.deleteTag(ctx, git, tag); err != nil {
				return errors.Wrapf(err, "failed to delete git tag")
			}
		}
	}

	if _, err := git.Output(ctx, nil, "prune"); err != nil {
		return errors.Wrapf(err, "git prune failed")
	}

	b, err := s.getBranches(git, ctx)
	if err != nil {
		return errors.Wrapf(err, "failed to get git branches")
	}

	t, err := s.getTags(git, ctx)
	if err != nil {
		return errors.Wrapf(err, "failed to get git tags")
	}

	if len(b) == 0 && len(t) == 0 {
		s.log.Info().Msgf("deleting repo: %q", repoDir)
		if err := s.deleteRepo(ctx, repoDir); err != nil {
			return errors.Wrapf(err, "failed to delete repository")
		}
	}

	return nil
}

func (s *Gitserver) getBranches(git *util.Git, ctx context.Context) ([]string, error) {
	branches, err := git.OutputLines(ctx, nil, "for-each-ref", "--format=%(refname:short)", "refs/heads/")
	if err != nil {
		return nil, errors.WithStack(err)
	}

	return branches, nil
}

func (s *Gitserver) getTags(git *util.Git, ctx context.Context) ([]string, error) {
	tags, err := git.OutputLines(ctx, nil, "for-each-ref", "--format=%(refname:short)", "refs/tags/")
	if err != nil {
		return nil, errors.WithStack(err)
	}

	return tags, nil
}

func (s *Gitserver) getLastCommiterTime(ctx context.Context, git *util.Git, ref string) (time.Time, error) {
	output, err := git.OutputLines(ctx, nil, "log", "-1", "--format=%cI", ref)
	if err != nil {
		return time.Time{}, errors.WithStack(err)
	}

	if len(output) != 1 {
		return time.Time{}, errors.New("git log error: must return one line")
	}

	committerTime, err := time.Parse(time.RFC3339, output[0])
	if err != nil {
		return time.Time{}, errors.WithStack(err)
	}

	return committerTime, nil
}

func (s *Gitserver) deleteBranch(ctx context.Context, git *util.Git, branch string) error {
	_, err := git.Output(ctx, nil, "branch", "-D", branch)
	return errors.WithStack(err)
}

func (s *Gitserver) deleteTag(ctx context.Context, git *util.Git, tag string) error {
	_, err := git.Output(ctx, nil, "tag", "-d", tag)
	return errors.WithStack(err)
}

func (s *Gitserver) deleteRepo(ctx context.Context, repoDir string) error {
	return errors.WithStack(os.RemoveAll(repoDir))
}
