package contentpayload

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-git/go-billy/v5/osfs"
	git "github.com/go-git/go-git/v5"
	gitconfig "github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/cache"
	"github.com/go-git/go-git/v5/storage/filesystem"
)

func validateRepositoryConfig(repoPath string) error {
	payload, err := os.ReadFile(filepath.Join(repoPath, "config"))
	if err != nil {
		return fmt.Errorf("read content git repository config: %w", err)
	}
	return validateRepositoryConfigPayload(payload)
}

func validateRepositoryConfigPayload(payload []byte) error {
	config, err := gitconfig.ReadConfig(bytes.NewReader(payload))
	if err != nil {
		return fmt.Errorf("parse content git repository config: %w", err)
	}
	if !config.Core.IsBare || strings.TrimSpace(config.Core.Worktree) != "" {
		return fmt.Errorf("content git repository must be bare and self-contained")
	}
	// Runtime uses go-git plumbing only, but reject redirects so maintenance
	// commands and future readers cannot consult paths outside this repository.
	for _, section := range config.Raw.Sections {
		name := strings.ToLower(section.Name)
		switch name {
		case "include", "includeif", "url", "remote", "filter", "difftool", "mergetool", "credential", "http":
			if len(section.Options) != 0 || len(section.Subsections) != 0 {
				return fmt.Errorf("content git repository config uses forbidden section: %s", section.Name)
			}
		case "core":
			for _, key := range []string{"worktree", "hookspath", "sshcommand", "gitproxy", "alternateobjectdirectories"} {
				if section.HasOption(key) {
					return fmt.Errorf("content git repository config uses forbidden core.%s", key)
				}
			}
		case "gpg":
			if section.HasOption("program") {
				return fmt.Errorf("content git repository config uses forbidden gpg.program")
			}
		case "diff":
			if section.HasOption("external") {
				return fmt.Errorf("content git repository config uses forbidden diff.external")
			}
		}
	}
	return nil
}

func validatePinnedRepository(repoRoot *os.File, repo *git.Repository) error {
	for _, parts := range [][]string{nil, {"objects"}, {"refs"}, {"refs", "content-versions"}} {
		directory, err := openDirectoryAt(repoRoot, parts, false)
		if err != nil {
			return fmt.Errorf("open pinned content git owned directory %q: %w", filepath.Join(parts...), err)
		}
		info, err := directory.Stat()
		directory.Close()
		if err != nil || info.Mode().Perm()&0o022 != 0 || !directoryOwnedByProcess(info) {
			return fmt.Errorf("pinned content git directory must be owned by the service account and not group/world writable: %s", filepath.Join(parts...))
		}
	}
	for _, forbidden := range [][]string{{"shallow"}, {"commondir"}, {"config.worktree"}, {"objects", "info", "alternates"}, {"objects", "info", "http-alternates"}, {"info", "grafts"}, {"refs", "replace"}} {
		file, err := openRegularAtPath(repoRoot, forbidden)
		if err == nil {
			file.Close()
			return fmt.Errorf("content git repository uses forbidden external state: %s", filepath.Join(forbidden...))
		}
		if !errors.Is(err, os.ErrNotExist) {
			return fmt.Errorf("inspect forbidden pinned content git entry %s: %w", filepath.Join(forbidden...), err)
		}
		if directory, dirErr := openDirectoryAt(repoRoot, forbidden, false); dirErr == nil {
			directory.Close()
			return fmt.Errorf("content git repository uses forbidden external state: %s", filepath.Join(forbidden...))
		} else if !errors.Is(dirErr, os.ErrNotExist) {
			return fmt.Errorf("inspect forbidden pinned content git directory %s: %w", filepath.Join(forbidden...), dirErr)
		}
	}
	configFile, err := openRegularAt(repoRoot, "config", os.O_RDONLY, 0)
	if err != nil {
		return fmt.Errorf("open pinned content git config: %w", err)
	}
	payload, err := io.ReadAll(io.LimitReader(configFile, DefaultLimits.MaxManifestBytes+1))
	configFile.Close()
	if err != nil {
		return err
	}
	if err := validateRepositoryConfigPayload(payload); err != nil {
		return err
	}
	hooks, err := openDirectoryAt(repoRoot, []string{"hooks"}, false)
	if err == nil {
		entries, readErr := hooks.ReadDir(1)
		hooks.Close()
		if readErr != nil && !errors.Is(readErr, io.EOF) {
			return readErr
		}
		if len(entries) != 0 {
			return fmt.Errorf("content git repository hooks are forbidden")
		}
	} else if !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("inspect pinned content git hooks: %w", err)
	}
	if err := rejectPackedContentVersionRefs(repoRoot); err != nil {
		return err
	}
	return validateRepositoryRefsObject(repo, false)
}

func rejectPackedContentVersionRefs(repoRoot *os.File) error {
	file, err := openRegularAt(repoRoot, "packed-refs", os.O_RDONLY, 0)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("inspect pinned content git packed refs: %w", err)
	}
	defer file.Close()
	payload, err := io.ReadAll(io.LimitReader(file, DefaultLimits.MaxManifestBytes+1))
	if err != nil {
		return fmt.Errorf("read pinned content git packed refs: %w", err)
	}
	if int64(len(payload)) > DefaultLimits.MaxManifestBytes {
		return fmt.Errorf("content git packed refs exceed limit")
	}
	for _, line := range strings.Split(string(payload), "\n") {
		fields := strings.Fields(line)
		if len(fields) >= 2 && strings.HasPrefix(fields[1], "refs/content-versions/") {
			return fmt.Errorf("content git immutable refs must remain loose")
		}
	}
	return nil
}

func openRegularAtPath(root *os.File, parts []string) (*os.File, error) {
	parent, err := openDirectoryAt(root, parts[:len(parts)-1], false)
	if err != nil {
		return nil, err
	}
	defer parent.Close()
	return openRegularAt(parent, parts[len(parts)-1], os.O_RDONLY, 0)
}

func validateRepositoryRefs(repoPath string, rejectContentVersions bool) error {
	repo, err := git.Open(filesystem.NewStorage(osfs.New(repoPath), cache.NewObjectLRUDefault()), nil)
	if err != nil {
		return fmt.Errorf("open content git repository refs: %w", err)
	}
	return validateRepositoryRefsObject(repo, rejectContentVersions)
}

func validateRepositoryRefsObject(repo *git.Repository, rejectContentVersions bool) error {
	iterator, err := repo.References()
	if err != nil {
		return fmt.Errorf("enumerate content git repository refs: %w", err)
	}
	defer iterator.Close()
	return iterator.ForEach(func(ref *plumbing.Reference) error {
		name := ref.Name().String()
		if strings.HasPrefix(name, "refs/replace/") {
			return fmt.Errorf("content git repository uses forbidden replace ref")
		}
		if rejectContentVersions && strings.HasPrefix(name, "refs/content-versions/") {
			return fmt.Errorf("content git migration source uses reserved content version ref")
		}
		if !rejectContentVersions && strings.HasPrefix(name, "refs/") && !strings.HasPrefix(name, "refs/content-versions/") {
			return fmt.Errorf("content git repository uses unsupported mutable ref: %s", name)
		}
		return nil
	})
}

func removeImportedMutableRefs(repoPath string) error {
	repo, err := git.Open(filesystem.NewStorage(osfs.New(repoPath), cache.NewObjectLRUDefault()), nil)
	if err != nil {
		return fmt.Errorf("open imported content repository refs: %w", err)
	}
	iterator, err := repo.References()
	if err != nil {
		return err
	}
	defer iterator.Close()
	var names []plumbing.ReferenceName
	err = iterator.ForEach(func(ref *plumbing.Reference) error {
		name := ref.Name().String()
		if strings.HasPrefix(name, "refs/replace/") || strings.HasPrefix(name, "refs/content-versions/") {
			return fmt.Errorf("imported content repository contains forbidden ref: %s", name)
		}
		if strings.HasPrefix(name, "refs/") {
			names = append(names, ref.Name())
		}
		return nil
	})
	if err != nil {
		return err
	}
	for _, name := range names {
		if err := repo.Storer.RemoveReference(name); err != nil {
			return fmt.Errorf("remove imported mutable ref %s: %w", name, err)
		}
	}
	return nil
}
