// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
//
// SPDX-License-Identifier: EUPL-1.2

package repository

import (
	"errors"
	"fmt"
	"io"

	git "github.com/go-git/go-git/v6"
	"github.com/go-git/go-git/v6/plumbing"
	"github.com/go-git/go-git/v6/plumbing/object"
	"github.com/go-git/go-git/v6/plumbing/storer"
	"github.com/samber/oops"
	"github.com/sergi/go-diff/diffmatchpatch"
	pluginLib "gitroot.dev/libs/golang/plugin/model"
)

type LastCommit struct {
	Commit   *object.Commit
	Filepath []pluginLib.File
}

func commitChange(com *object.Commit, useToFound bool, toFound map[string]bool) (LastCommit, error) {
	currentRes := LastCommit{Commit: com, Filepath: []pluginLib.File{}}
	tree, err := com.Tree()
	if err != nil {
		return currentRes, oops.Wrapf(err, "can't tree commit %s", com.Hash.String())
	}
	p, err := com.Parents().Next()
	if err != nil && !errors.Is(err, io.EOF) {
		return currentRes, oops.Wrapf(err, "can't parent %s", com.Hash.String())
	}
	var pTree *object.Tree
	if p != nil {
		pTree, err = p.Tree()
		if err != nil {
			return currentRes, oops.Wrapf(err, "can't pTree commit %s", p.Hash.String())
		}
	}
	changes, err := object.DiffTree(pTree, tree)
	if err != nil {
		return currentRes, oops.Wrapf(err, "can't DiffTree commit %s and %s", com.Hash.String(), p.Hash.String())
	}
	for _, file := range changes {
		var f pluginLib.File
		if file.From.Name == "" && file.To.Name != "" {
			f = pluginLib.File{
				Path:     file.To.Name,
				FileHash: file.To.TreeEntry.Hash.String(),
				Action:   pluginLib.FileActionTypeAdd,
			}
		} else if file.From.Name != "" && file.To.Name == "" {
			f = pluginLib.File{
				Path:     file.From.Name,
				FileHash: file.From.TreeEntry.Hash.String(),
				Action:   pluginLib.FileActionTypeDel,
			}
		} else {
			f = pluginLib.File{
				Path:        file.To.Name,
				FileHash:    file.To.TreeEntry.Hash.String(),
				OldPath:     file.From.Name,
				OldFileHash: file.From.TreeEntry.Hash.String(),
				Action:      pluginLib.FileActionTypeMod,
			}
		}

		if useToFound {
			if val, ok := toFound[f.Path]; ok && !val {
				currentRes.Filepath = append(currentRes.Filepath, f)
				toFound[f.Path] = true
			}
		} else {
			currentRes.Filepath = append(currentRes.Filepath, f)
		}
	}
	return currentRes, nil
}

func (repo *GitRootRepository) GetLastCommits(filepath []string) ([]LastCommit, error) {
	comIter, err := repo.repo.Log(&git.LogOptions{Order: git.LogOrderCommitterTime})
	if err != nil {
		return nil, repo.errorHandler.Wrapf(err, "can't Log")
	}
	defer comIter.Close()
	nbToFound := len(filepath)
	found := make(map[string]bool, len(filepath))
	for _, path := range filepath {
		found[path] = false
	}
	res := []LastCommit{}
	err = comIter.ForEach(func(com *object.Commit) error {
		currentRes, err := commitChange(com, true, found)
		if err != nil {
			return oops.Wrapf(err, "commitChange")
		}

		if len(currentRes.Filepath) > 0 {
			res = append(res, currentRes)
			for _, path := range currentRes.Filepath {
				found[path.Path] = true
				nbToFound = nbToFound - 1
			}
		}

		if nbToFound == 0 {
			return storer.ErrStop
		}
		return nil
	})
	return res, oops.Wrapf(err, "err in log fro GetLastCommit")
}

func (repo *GitRootRepositoryWrite) GetDiff(hash plumbing.Hash, oldFilepath string, newFilepath string) (string, error) {
	com, err := repo.repoWrite.CommitObject(hash)
	if err != nil {
		return "", repo.errorHandler.Wrapf(err, "can't find CommitObject")
	}
	parent, _ := com.Parent(0)
	src, err := getFileContent(parent, oldFilepath)
	if err != nil {
		return "", repo.errorHandler.With("hash", parent.Hash.String(), "filepath", oldFilepath).Wrapf(err, "can't getFileContent parent")
	}
	dst, err := getFileContent(com, newFilepath)
	if err != nil {
		return "", repo.errorHandler.With("hash", hash.String(), "filepath", newFilepath).Wrapf(err, "can't getFileContent")
	}
	return "```diff\n" + diffmatchpatch.New().Unified(
		src,
		dst,
		diffmatchpatch.UnifiedContextLines(3),
		diffmatchpatch.UnifiedLabels(fmt.Sprintf("a/%s", oldFilepath), fmt.Sprintf("b/%s", newFilepath)),
	) + "```\n", nil
}

func getFileContent(com *object.Commit, filepath string) (string, error) {
	fromTree, err := com.Tree()
	if err != nil {
		return "", err
	}
	fromFile, err := fromTree.File(filepath)
	if err != nil {
		return "", err
	}
	return fromFile.Contents()
}
