package git

import (
	"errors"
	"fmt"
	"io"

	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
)

var ()

var (
	MissingRefErr  = errors.New("Reference not found")
	TreeForFileErr = errors.New("Trying to get tree of a file")
)

type (
	GitRepository struct {
		path       string
		repository *git.Repository

		ref plumbing.Hash
		// this is setRef when ref is setRef
		setRef bool
	}
)

func OpenRepository(dir string) (*GitRepository, error) {
	g := &GitRepository{
		path: dir,
	}

	repo, err := git.PlainOpen(dir)
	if err != nil {
		return nil, err
	}
	g.repository = repo

	return g, nil
}

func (g *GitRepository) SetRef(ref string) error {
	if ref == "" {
		head, err := g.repository.Head()
		if err != nil {
			return errors.Join(MissingRefErr, err)
		}
		g.ref = head.Hash()
	} else {
		hash, err := g.repository.ResolveRevision(plumbing.Revision(ref))
		if err != nil {
			return errors.Join(MissingRefErr, err)
		}
		g.ref = *hash
	}
	g.setRef = true
	return nil
}

func (g *GitRepository) Path() string {
	return g.path
}

func (g *GitRepository) LastCommit() (*object.Commit, error) {
	err := g.validateRef()
	if err != nil {
		return nil, err
	}

	c, err := g.repository.CommitObject(g.ref)
	if err != nil {
		return nil, err
	}
	return c, nil
}

func (g *GitRepository) Commits() ([]*object.Commit, error) {
	err := g.validateRef()
	if err != nil {
		return nil, err
	}

	ci, err := g.repository.Log(&git.LogOptions{From: g.ref})
	if err != nil {
		return nil, fmt.Errorf("commits from ref: %w", err)
	}

	commits := []*object.Commit{}
	// TODO: for now only load first 1000
	for x := 0; x < 1000; x++ {
		c, err := ci.Next()
		if err != nil && errors.Is(err, io.EOF) {
			break
		} else if err != nil {
			return nil, err
		}
		commits = append(commits, c)
	}

	return commits, nil
}

func (g *GitRepository) Head() (*plumbing.Reference, error) {
	return g.repository.Head()
}

func (g *GitRepository) Tags() ([]*object.Tag, error) {
	ti, err := g.repository.TagObjects()
	if err != nil {
		return nil, err
	}

	tags := []*object.Tag{}
	err = ti.ForEach(func(t *object.Tag) error {
		tags = append(tags, t)
		return nil
	})
	if err != nil {
		return nil, err
	}

	return tags, nil
}

func (g *GitRepository) Branches() ([]*plumbing.Reference, error) {
	bs, err := g.repository.Branches()
	if err != nil {
		return nil, err
	}

	branches := []*plumbing.Reference{}
	err = bs.ForEach(func(ref *plumbing.Reference) error {
		branches = append(branches, ref)
		return nil
	})
	if err != nil {
		return nil, err
	}

	return branches, nil
}

func (g *GitRepository) Tree(path string) (*object.Tree, error) {
	err := g.validateRef()
	if err != nil {
		return nil, err
	}

	c, err := g.repository.CommitObject(g.ref)
	if err != nil {
		return nil, err
	}

	tree, err := c.Tree()
	if err != nil {
		return nil, err
	}

	if path == "" {
		return tree, nil
	} else {
		o, err := tree.FindEntry(path)
		if err != nil {
			return nil, err
		}

		if !o.Mode.IsFile() {
			subtree, err := tree.Tree(path)
			if err != nil {
				return nil, err
			}
			return subtree, nil
		} else {
			return nil, TreeForFileErr
		}
	}
}

func (g *GitRepository) validateRef() error {
	if !g.setRef {
		return g.SetRef("")
	}
	return nil
}

func (g *GitRepository) FileContent(path string) (string, error) {
	c, err := g.repository.CommitObject(g.ref)
	if err != nil {
		return "", err
	}

	tree, err := c.Tree()
	if err != nil {
		return "", err
	}

	file, err := tree.File(path)
	if err != nil {
		return "", err
	}

	isbin, err := file.IsBinary()
	if err != nil {
		return "", err
	}

	if !isbin {
		return file.Contents()
	} else {
		return "Binary file", nil
	}
}