package git

import (
	"context"
	"errors"
	"io"
	"path/filepath"
	"strings"

	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/memfs"
	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/transport"
	"github.com/go-git/go-git/v5/plumbing/transport/http"
	"github.com/go-git/go-git/v5/storage/memory"
)

type ProjectOption func(*Project)

func WithBasicAuth(user, pass string) ProjectOption {
	return func(p *Project) {
		p.auth = &http.BasicAuth{
			Username: user,
			Password: pass,
		}
	}
}

func WithBranch(branch string) ProjectOption {
	return func(p *Project) {
		p.Branch = branch
	}
}

func WithOutputWriter(o io.Writer) ProjectOption {
	return func(p *Project) {
		p.writer = o
	}
}

type Project struct {
	Branch     string
	Force      bool
	RepoURL    string
	CommitLogs map[string]*object.Commit

	auth       transport.AuthMethod
	fs         billy.Filesystem
	storer     *memory.Storage
	repository *git.Repository
	worktree   *git.Worktree
	writer     io.Writer
}

func NewProject(url string, options ...ProjectOption) *Project {
	project := &Project{
		RepoURL:    url,
		CommitLogs: make(map[string]*object.Commit),
		fs:         memfs.New(),
		storer:     memory.NewStorage(),
		repository: nil,
	}

	for _, option := range options {
		option(project)
	}

	return project
}

func (p *Project) Checkout() error {
	branchRef := plumbing.NewBranchReferenceName(p.Branch)

	_, err := p.repository.Reference(branchRef, true)
	if errors.Is(err, plumbing.ErrReferenceNotFound) {
		var headRef *plumbing.Reference
		headRef, err = p.repository.Head()
		if err != nil {
			return err
		}

		ref := plumbing.NewHashReference(branchRef, headRef.Hash())
		if err = p.repository.Storer.SetReference(ref); err != nil {
			return err
		}
	} else if err != nil {
		return err
	}

	p.worktree, err = p.repository.Worktree()
	if err != nil {
		return err
	}

	checkoutOpts := git.CheckoutOptions{
		Branch: branchRef,
		Create: false,
	}

	if err = checkoutOpts.Validate(); err != nil {
		return err
	}

	return p.worktree.Checkout(&checkoutOpts)
}

func (p *Project) Clone(ctx context.Context) error {
	cloneOpts := git.CloneOptions{
		URL:               p.RepoURL,
		RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
	}

	if p.auth != nil {
		cloneOpts.Auth = p.auth
	}

	if p.writer != nil {
		cloneOpts.Progress = p.writer
	}

	if err := cloneOpts.Validate(); err != nil {
		return err
	}

	var err error
	p.repository, err = git.CloneContext(
		ctx,
		p.storer,
		p.fs,
		&cloneOpts,
	)
	if err != nil {
		return err
	}

	return nil
}

func (p *Project) LoadLogs() error {
	commitIter, err := p.repository.Log(&git.LogOptions{})
	if err != nil {
		return err
	}

	return commitIter.ForEach(func(c *object.Commit) error {
		p.CommitLogs[c.Message] = c
		return nil
	})
}

func (p *Project) CommitExists(commitmsg string) bool {
	if _, ok := p.CommitLogs[commitmsg]; ok {
		return true
	}

	return false
}

func (p *Project) HasChanges() bool {
	localBranchRef, err := p.repository.Head()
	if err != nil {
		return false
	}

	remoteBranchRef, err := p.repository.Reference(plumbing.NewRemoteReferenceName("origin", p.Branch), true)
	if errors.Is(err, plumbing.ErrReferenceNotFound) {
		return true
	} else if err != nil {
		return false
	}

	return localBranchRef.Hash() != remoteBranchRef.Hash()
}

func (p *Project) Pull(ctx context.Context) error {
	pullOpts := git.PullOptions{
		ReferenceName: plumbing.NewBranchReferenceName(p.Branch),
	}

	if p.writer != nil {
		pullOpts.Progress = p.writer
	}

	if err := pullOpts.Validate(); err != nil {
		return err
	}

	err := p.worktree.PullContext(ctx, &pullOpts)
	if !errors.Is(err, plumbing.ErrReferenceNotFound) &&
		!errors.Is(err, git.NoErrAlreadyUpToDate) &&
		err != nil {
		return err
	}

	return nil
}

func (p *Project) Push(ctx context.Context) error {
	pushOpts := git.PushOptions{
		RemoteName: "origin",
	}

	if p.auth != nil {
		pushOpts.Auth = p.auth
	}

	if p.writer != nil {
		pushOpts.Progress = p.writer
	}

	if err := pushOpts.Validate(); err != nil {
		return err
	}

	return p.repository.PushContext(ctx, &pushOpts)
}

func (p *Project) ListJSONFiles(directory string) ([]string, error) {
	files, err := p.fs.ReadDir(directory)
	if err != nil {
		return nil, err
	}

	var allFiles []string
	for _, file := range files {
		if file.IsDir() {
			var childFiles []string
			childFiles, err = p.ListJSONFiles(filepath.Join(directory, file.Name()))
			if err != nil {
				return nil, err
			}
			allFiles = append(allFiles, childFiles...)
		} else if strings.HasSuffix(file.Name(), ".json") {
			allFiles = append(allFiles, filepath.Join(directory, file.Name()))
		}
	}

	return allFiles, nil
}

func (p *Project) ReadFile(filepath string) ([]byte, error) {
	file, err := p.fs.Open(filepath)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	return io.ReadAll(file)
}