package gitattributes

import (
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/go-git/go-billy/v6"

	"github.com/go-git/go-git/v6/plumbing/format/config"
	gioutil "github.com/go-git/go-git/v6/utils/ioutil"
)

const (
	coreSection       = "core"
	attributesfile    = "attributesfile"
	gitDir            = ".git"
	gitattributesFile = ".gitattributes"
	gitconfigFile     = ".gitconfig"
	systemFile        = "/etc/gitconfig"
)

// ReadAttributesFile reads a gitattributes file from the given filesystem and path.
func ReadAttributesFile(fs billy.Filesystem, path []string, attributesFile string, allowMacro bool) ([]MatchAttribute, error) {
	f, err := fs.Open(fs.Join(append(path, attributesFile)...))
	if os.IsNotExist(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}

	defer gioutil.CheckClose(f, &err)

	return ReadAttributes(f, path, allowMacro)
}

// ReadPatterns reads gitattributes patterns recursively through the directory
// structure. The result is in ascending order of priority (last higher).
//
// The .gitattribute file in the root directory will allow custom macro
// definitions. Custom macro definitions in other directories .gitattributes
// will return an error.
func ReadPatterns(fs billy.Filesystem, path []string) (attributes []MatchAttribute, err error) {
	attributes, err = ReadAttributesFile(fs, path, gitattributesFile, true)
	if err != nil {
		return attributes, err
	}

	attrs, err := walkDirectory(fs, path)
	return append(attributes, attrs...), err
}

func walkDirectory(fs billy.Filesystem, root []string) (attributes []MatchAttribute, err error) {
	fis, err := fs.ReadDir(fs.Join(root...))
	if err != nil {
		return attributes, err
	}

	for _, fi := range fis {
		if !fi.IsDir() || fi.Name() == ".git" {
			continue
		}

		p := fi.Name()

		// Handles the case whereby just the volume name ("C:") is appended,
		// to root. Change it to "C:\", which is better handled by fs.Join().
		if filepath.VolumeName(p) != "" && !strings.HasSuffix(p, string(filepath.Separator)) {
			p += string(filepath.Separator)
		}
		path := slices.Concat(root, []string{p})

		dirAttributes, err := ReadAttributesFile(fs, path, gitattributesFile, false)
		if err != nil {
			return attributes, err
		}

		subAttributes, err := walkDirectory(fs, path)
		if err != nil {
			return attributes, err
		}

		attributes = append(attributes, append(dirAttributes, subAttributes...)...)
	}

	return attributes, err
}

func loadPatterns(fs billy.Filesystem, path string) ([]MatchAttribute, error) {
	f, err := fs.Open(path)
	if os.IsNotExist(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	defer gioutil.CheckClose(f, &err)

	raw := config.New()
	if err = config.NewDecoder(f).Decode(raw); err != nil {
		return nil, nil
	}

	path = raw.Section(coreSection).Options.Get(attributesfile)
	if path == "" {
		return nil, nil
	}

	return ReadAttributesFile(fs, nil, path, true)
}

// LoadGlobalPatterns loads gitattributes patterns and attributes from the
// gitattributes file declared in a user's ~/.gitconfig file.  If the
// ~/.gitconfig file does not exist the function will return nil. If the
// core.attributesFile property is not declared, the function will return nil.
// If the file pointed to by the core.attributesfile property does not exist,
// the function will return nil. The function assumes fs is rooted at the root
// filesystem.
func LoadGlobalPatterns(fs billy.Filesystem) (attributes []MatchAttribute, err error) {
	home, err := os.UserHomeDir()
	if err != nil {
		return attributes, err
	}

	return loadPatterns(fs, fs.Join(home, gitconfigFile))
}

// LoadSystemPatterns loads gitattributes patterns and attributes from the
// gitattributes file declared in a system's /etc/gitconfig file.  If the
// /etc/gitconfig file does not exist the function will return nil. If the
// core.attributesfile property is not declared, the function will return nil.
// If the file pointed to by the core.attributesfile property does not exist,
// the function will return nil. The function assumes fs is rooted at the root
// filesystem.
func LoadSystemPatterns(fs billy.Filesystem) (attributes []MatchAttribute, err error) {
	return loadPatterns(fs, systemFile)
}
