package types

import (
	"bytes"
	"encoding/json"
	"fmt"
	"maps"
	"regexp"
	"strings"

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

type Commit struct {
	// hash of the commit object.
	Hash plumbing.Hash `json:"hash,omitempty"`

	// author is the original author of the commit.
	Author object.Signature `json:"author"`

	// committer is the one performing the commit, might be different from author.
	Committer object.Signature `json:"committer"`

	// message is the commit message, contains arbitrary text.
	Message string `json:"message"`

	// treehash is the hash of the root tree of the commit.
	Tree string `json:"tree"`

	// parents are the hashes of the parent commits of the commit.
	ParentHashes []plumbing.Hash `json:"parent_hashes,omitempty"`

	// pgpsignature is the pgp signature of the commit.
	PGPSignature string `json:"pgp_signature,omitempty"`

	// mergetag is the embedded tag object when a merge commit is created by
	// merging a signed tag.
	MergeTag string `json:"merge_tag,omitempty"`

	// changeid is a unique identifier for the change (e.g., gerrit change-id).
	ChangeId string `json:"change_id,omitempty"`

	// extraheaders contains additional headers not captured by other fields.
	ExtraHeaders map[string][]byte `json:"extra_headers,omitempty"`

	// deprecated: kept for backwards compatibility with old json format.
	This string `json:"this,omitempty"`

	// deprecated: kept for backwards compatibility with old json format.
	Parent string `json:"parent,omitempty"`
}

// types.Commit is an unify two commit structs:
//   - git.object.Commit from
//   - types.NiceDiff.commit
//
// to do this in backwards compatible fashion, we define the base struct
// to use the same fields as NiceDiff.Commit, and then we also unmarshal
// the struct fields from go-git structs, this custom unmarshal makes sense
// of both representations and unifies them to have maximal data in either
// form.
func (c *Commit) UnmarshalJSON(data []byte) error {
	type Alias Commit

	aux := &struct {
		*object.Commit
		*Alias
	}{
		Alias: (*Alias)(c),
	}

	if err := json.Unmarshal(data, aux); err != nil {
		return err
	}

	c.FromGoGitCommit(aux.Commit)

	return nil
}

// fill in as much of Commit as possible from the given go-git commit
func (c *Commit) FromGoGitCommit(gc *object.Commit) {
	if gc == nil {
		return
	}

	if c.Hash.IsZero() {
		c.Hash = gc.Hash
	}
	if c.This == "" {
		c.This = gc.Hash.String()
	}
	if isEmptySignature(c.Author) {
		c.Author = gc.Author
	}
	if isEmptySignature(c.Committer) {
		c.Committer = gc.Committer
	}
	if c.Message == "" {
		c.Message = gc.Message
	}
	if c.Tree == "" {
		c.Tree = gc.TreeHash.String()
	}
	if c.PGPSignature == "" {
		c.PGPSignature = gc.PGPSignature
	}
	if c.MergeTag == "" {
		c.MergeTag = gc.MergeTag
	}

	if len(c.ParentHashes) == 0 {
		c.ParentHashes = gc.ParentHashes
	}
	if c.Parent == "" && len(gc.ParentHashes) > 0 {
		c.Parent = gc.ParentHashes[0].String()
	}

	if len(c.ExtraHeaders) == 0 {
		c.ExtraHeaders = make(map[string][]byte)
		maps.Copy(c.ExtraHeaders, gc.ExtraHeaders)
	}

	if c.ChangeId == "" {
		if v, ok := gc.ExtraHeaders["change-id"]; ok {
			c.ChangeId = string(v)
		}
	}
}

func isEmptySignature(s object.Signature) bool {
	return s.Email == "" && s.Name == "" && s.When.IsZero()
}

// produce a verifiable payload from this commit's metadata
func (c *Commit) Payload() string {
	author := bytes.NewBuffer([]byte{})
	c.Author.Encode(author)

	committer := bytes.NewBuffer([]byte{})
	c.Committer.Encode(committer)

	payload := strings.Builder{}

	fmt.Fprintf(&payload, "tree %s\n", c.Tree)

	if len(c.ParentHashes) > 0 {
		for _, p := range c.ParentHashes {
			fmt.Fprintf(&payload, "parent %s\n", p.String())
		}
	} else {
		// present for backwards compatibility
		fmt.Fprintf(&payload, "parent %s\n", c.Parent)
	}

	fmt.Fprintf(&payload, "author %s\n", author.String())
	fmt.Fprintf(&payload, "committer %s\n", committer.String())

	if c.ChangeId != "" {
		fmt.Fprintf(&payload, "change-id %s\n", c.ChangeId)
	} else if v, ok := c.ExtraHeaders["change-id"]; ok {
		fmt.Fprintf(&payload, "change-id %s\n", string(v))
	}

	fmt.Fprintf(&payload, "\n%s", c.Message)

	return payload.String()
}

var (
	coAuthorRegex = regexp.MustCompile(`(?im)^Co-authored-by:\s*(.+?)\s*<([^>]+)>`)
)

func (commit Commit) CoAuthors() []object.Signature {
	var coAuthors []object.Signature
	seen := make(map[string]bool)
	matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)

	for _, match := range matches {
		if len(match) >= 3 {
			name := strings.TrimSpace(match[1])
			email := strings.TrimSpace(match[2])

			if seen[email] {
				continue
			}
			seen[email] = true

			coAuthors = append(coAuthors, object.Signature{
				Name:  name,
				Email: email,
				When:  commit.Committer.When,
			})
		}
	}

	return coAuthors
}
