package contentpayload

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"sync"
	"time"

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

const (
	// Git does not impose a useful product-level tree depth or entry bound.
	// The entry cap also bounds empty directories that do not contribute to the
	// file count, while allowing the normal tree nodes required by each file.
	maxGitTreeDepth          = 64
	maxGitTreeEntriesPerFile = 2
)

// gitTreeStorage is the internal tree payload engine. It deliberately exposes
// neither repository paths nor mutable Git refs to product callers.
type gitTreeStorage struct {
	root                        string
	rootFile                    *os.File
	mu                          sync.RWMutex
	closed                      bool
	locks                       map[string]*gitLineageLock
	syncPath                    func(string, bool) error
	syncFile                    func(*os.File, string) error
	afterValidateRepositoryPath func()
	afterOpenRepositoryRoot     func()
}

type gitLineageLock struct {
	ready chan struct{}
	users int
}

func NewGitTreeBackend(root string) (TreePayloadBackend, error) {
	root = strings.TrimSpace(root)
	if root == "" {
		return nil, fmt.Errorf("content git tree root is required")
	}
	absolute, err := filepath.Abs(root)
	if err != nil {
		return nil, fmt.Errorf("resolve content git tree root: %w", err)
	}
	if err := os.MkdirAll(absolute, 0o750); err != nil {
		return nil, fmt.Errorf("create content git tree root: %w", err)
	}
	if err := validateContentStoreOwnedDirectory(absolute); err != nil {
		return nil, err
	}
	rootFile, err := openRoot(absolute)
	if err != nil {
		return nil, err
	}
	return &gitTreeStorage{root: absolute, rootFile: rootFile, locks: make(map[string]*gitLineageLock), syncPath: syncGitPath, syncFile: syncGitFile, afterValidateRepositoryPath: func() {}, afterOpenRepositoryRoot: func() {}}, nil
}

// publishImmutableRef writes a loose ref with O_EXCL rather than SetReference.
// The filesystem create is the cross-process create-if-absent boundary for a
// version ID. Maintenance that packs refs is intentionally excluded while
// product writers are running.
func (s *gitTreeStorage) publishImmutableRef(ctx context.Context, opened *openGitRepository, versionID string, commitHash plumbing.Hash) (bool, error) {
	if err := ctx.Err(); err != nil {
		return false, err
	}
	repo := opened.Repository
	name := plumbing.ReferenceName("refs/content-versions/" + versionID)
	if existing, err := repo.Reference(name, true); err == nil {
		if existing.Hash() != commitHash {
			return false, ErrAlreadyExists
		}
		return false, nil
	} else if !errors.Is(err, plumbing.ErrReferenceNotFound) {
		return false, fmt.Errorf("read immutable content version ref: %w", err)
	}
	directory, err := openDirectoryAt(opened.root, []string{"refs", "content-versions"}, false)
	if err != nil {
		return false, fmt.Errorf("open immutable content version ref directory: %w", err)
	}
	defer directory.Close()
	temporaryName, err := randomEntryName(".immutable-ref-")
	if err != nil {
		return false, err
	}
	temporary, err := openRegularAt(directory, temporaryName, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o640)
	if err != nil {
		return false, fmt.Errorf("create temporary immutable content version ref: %w", err)
	}
	defer func() {
		_ = temporary.Close()
		_ = unlinkAt(directory, temporaryName, false)
	}()
	if _, err := io.WriteString(temporary, commitHash.String()+"\n"); err != nil {
		return false, fmt.Errorf("write immutable content version ref: %w", err)
	}
	if err := temporary.Sync(); err != nil {
		return false, fmt.Errorf("sync immutable content version ref: %w", err)
	}
	if err := temporary.Close(); err != nil {
		return false, fmt.Errorf("close immutable content version ref: %w", err)
	}
	if err := linkAt(directory, temporaryName, directory, versionID); errors.Is(err, os.ErrExist) {
		existing, readErr := repo.Reference(name, true)
		if readErr != nil {
			return false, fmt.Errorf("read concurrently published immutable content version ref: %w", readErr)
		}
		if existing.Hash() != commitHash {
			return false, ErrAlreadyExists
		}
		return false, nil
	} else if err != nil {
		return false, fmt.Errorf("publish immutable content version ref: %w", err)
	}
	if err := s.syncFile(directory, "refs/content-versions"); err != nil {
		_ = unlinkAt(directory, versionID, false)
		return false, fmt.Errorf("sync immutable content version ref directory: %w", err)
	}
	return true, nil
}

func (s *gitTreeStorage) Close() error {
	s.mu.Lock()
	defer s.mu.Unlock()
	if s.closed {
		return nil
	}
	if len(s.locks) != 0 {
		return fmt.Errorf("content git tree backend has in-flight operations")
	}
	if err := s.rootFile.Close(); err != nil {
		return err
	}
	s.rootFile = nil
	s.closed = true
	return nil
}

func (s *gitTreeStorage) lock(ctx context.Context, workspaceID, contentID string) (func(), error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	key := workspaceID + "/" + contentID
	s.mu.Lock()
	if s.closed {
		s.mu.Unlock()
		return nil, errFilesystemClosed
	}
	entry := s.locks[key]
	if entry == nil {
		entry = &gitLineageLock{ready: make(chan struct{}, 1)}
		entry.ready <- struct{}{}
		s.locks[key] = entry
	}
	entry.users++
	s.mu.Unlock()
	select {
	case <-entry.ready:
	case <-ctx.Done():
		s.mu.Lock()
		entry.users--
		if entry.users == 0 {
			delete(s.locks, key)
		}
		s.mu.Unlock()
		return nil, ctx.Err()
	}
	return func() {
		entry.ready <- struct{}{}
		s.mu.Lock()
		entry.users--
		if entry.users == 0 {
			delete(s.locks, key)
		}
		s.mu.Unlock()
	}, nil
}

func (s *gitTreeStorage) StageTree(ctx context.Context, request StageTreeRequest) (StagedTree, error) {
	workspaceID, contentID, versionID, files, limits, err := normalizeStageTreeRequest(ctx, request)
	if err != nil {
		return StagedTree{}, err
	}
	unlock, err := s.lock(ctx, workspaceID, contentID)
	if err != nil {
		return StagedTree{}, err
	}
	defer unlock()
	opened, initialized, err := s.openOrInit(workspaceID, contentID)
	if err != nil {
		return StagedTree{}, err
	}
	defer opened.close()
	repo := opened.Repository
	versionRef := plumbing.ReferenceName("refs/content-versions/" + versionID)
	if existing, err := repo.Reference(versionRef, true); err == nil {
		manifest, manifestErr := s.manifestForCommit(ctx, repo, workspaceID, contentID, versionID, existing.Hash(), limits)
		if manifestErr != nil {
			return StagedTree{}, manifestErr
		}
		requested, requestedErr := requestedManifest(files, limits)
		if requestedErr != nil {
			return StagedTree{}, requestedErr
		}
		if manifest.RootDigest != requested.RootDigest {
			return StagedTree{}, ErrAlreadyExists
		}
		return stagedFromManifest(gitPayloadRef(workspaceID, contentID, versionID, existing.Hash()), manifest, false), nil
	} else if !errors.Is(err, plumbing.ErrReferenceNotFound) {
		return StagedTree{}, fmt.Errorf("read immutable content version ref: %w", err)
	}

	parent, err := s.resolveParent(repo, request.ParentPayloadRef, workspaceID, contentID)
	if err != nil {
		return StagedTree{}, err
	}
	treeHash, manifest, err := writeGitTree(ctx, repo, workspaceID, contentID, versionID, files, limits)
	if err != nil {
		return StagedTree{}, err
	}
	now := time.Now().UTC()
	actor := strings.TrimSpace(request.Actor)
	if actor == "" {
		actor = "content-store"
	}
	commit := &object.Commit{
		Author:    object.Signature{Name: actor, Email: "content-store@agentteam.invalid", When: now},
		Committer: object.Signature{Name: actor, Email: "content-store@agentteam.invalid", When: now},
		Message:   "Content version " + versionID + "\n", TreeHash: treeHash,
	}
	if !parent.IsZero() {
		commit.ParentHashes = []plumbing.Hash{parent}
	}
	encoded := repo.Storer.NewEncodedObject()
	if err := commit.Encode(encoded); err != nil {
		return StagedTree{}, fmt.Errorf("encode content tree commit: %w", err)
	}
	commitHash, err := repo.Storer.SetEncodedObject(encoded)
	if err != nil {
		return StagedTree{}, fmt.Errorf("write content tree commit: %w", err)
	}
	if err := s.syncCommitObjects(ctx, repo, opened.root, commitHash); err != nil {
		return StagedTree{}, err
	}
	if initialized {
		if err := s.syncInitializedRepository(opened.root, workspaceID); err != nil {
			return StagedTree{}, err
		}
	}
	created, err := s.publishImmutableRef(ctx, opened, versionID, commitHash)
	if err != nil {
		return StagedTree{}, err
	}
	if !created {
		existing, readErr := repo.Reference(versionRef, true)
		if readErr != nil {
			return StagedTree{}, fmt.Errorf("read concurrently published immutable content version ref: %w", readErr)
		}
		if existing.Hash() != commitHash {
			return StagedTree{}, ErrAlreadyExists
		}
	}
	return stagedFromManifest(gitPayloadRef(workspaceID, contentID, versionID, commitHash), manifest, created), nil
}

func normalizeStageTreeRequest(ctx context.Context, request StageTreeRequest) (string, string, string, []File, Limits, error) {
	if err := ctx.Err(); err != nil {
		return "", "", "", nil, Limits{}, err
	}
	workspaceID, err := safeID("workspace", request.WorkspaceID)
	if err != nil {
		return "", "", "", nil, Limits{}, err
	}
	contentID, err := safeID("content", request.ContentID)
	if err != nil {
		return "", "", "", nil, Limits{}, err
	}
	versionID, err := safeID("version", request.VersionID)
	if err != nil {
		return "", "", "", nil, Limits{}, err
	}
	limits, err := NormalizeLimits(request.Limits)
	if err != nil {
		return "", "", "", nil, Limits{}, err
	}
	if len(request.Files) > limits.MaxFiles {
		return "", "", "", nil, Limits{}, fmt.Errorf("content version has %d files, limit is %d", len(request.Files), limits.MaxFiles)
	}
	files := append([]File(nil), request.Files...)
	for index := range files {
		files[index].Path, err = cleanFilePath(files[index].Path)
		if err != nil {
			return "", "", "", nil, Limits{}, err
		}
	}
	sort.Slice(files, func(i, j int) bool { return files[i].Path < files[j].Path })
	for index := 1; index < len(files); index++ {
		if files[index-1].Path == files[index].Path {
			return "", "", "", nil, Limits{}, fmt.Errorf("content version contains duplicate path %q", files[index].Path)
		}
	}
	if err := validateGitTreeShape(files, limits); err != nil {
		return "", "", "", nil, Limits{}, err
	}
	return workspaceID, contentID, versionID, files, limits, nil
}

func validateGitTreeShape(files []File, limits Limits) error {
	directories := make(map[string]struct{})
	for _, file := range files {
		parts := strings.Split(file.Path, "/")
		if len(parts)-1 > maxGitTreeDepth {
			return fmt.Errorf("content tree exceeds depth limit")
		}
		for index := 1; index < len(parts); index++ {
			directories[strings.Join(parts[:index], "/")] = struct{}{}
		}
	}
	if len(files)+len(directories) > limits.MaxFiles*maxGitTreeEntriesPerFile+maxGitTreeDepth {
		return fmt.Errorf("content tree exceeds entry limit")
	}
	return nil
}

func validateRepositoryPath(root, workspaceID, repoPath string) error {
	rootInfo, err := os.Lstat(root)
	if err != nil || !rootInfo.IsDir() || rootInfo.Mode()&os.ModeSymlink != 0 {
		return fmt.Errorf("content git tree root is unsafe")
	}
	workspacePath := filepath.Join(root, workspaceID)
	workspaceInfo, err := os.Lstat(workspacePath)
	if err != nil || !workspaceInfo.IsDir() || workspaceInfo.Mode()&os.ModeSymlink != 0 {
		return fmt.Errorf("content git workspace directory is unsafe")
	}
	if err := validateContentStoreOwnedDirectory(workspacePath); err != nil {
		return err
	}
	repoInfo, err := os.Lstat(repoPath)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil || !repoInfo.IsDir() || repoInfo.Mode()&os.ModeSymlink != 0 {
		return fmt.Errorf("content git repository directory is unsafe")
	}
	if err := validateContentStoreOwnedDirectory(repoPath); err != nil {
		return err
	}
	for _, forbidden := range []string{"shallow", "commondir", "config.worktree", filepath.Join("objects", "info", "alternates"), filepath.Join("objects", "info", "http-alternates"), filepath.Join("info", "grafts"), filepath.Join("refs", "replace")} {
		if _, err := os.Lstat(filepath.Join(repoPath, forbidden)); err == nil {
			return fmt.Errorf("content git repository uses forbidden external state: %s", forbidden)
		} else if !errors.Is(err, os.ErrNotExist) {
			return err
		}
	}
	for _, directory := range []string{"objects", "refs", filepath.Join("refs", "content-versions")} {
		info, err := os.Lstat(filepath.Join(repoPath, directory))
		if errors.Is(err, os.ErrNotExist) && directory == filepath.Join("refs", "content-versions") {
			continue
		}
		if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
			return fmt.Errorf("content git repository directory is unsafe: %s", directory)
		}
		if err := validateContentStoreOwnedDirectory(filepath.Join(repoPath, directory)); err != nil {
			return err
		}
	}
	if err := validateRepositoryConfig(repoPath); err != nil {
		return err
	}
	if err := validateRepositoryRefs(repoPath, false); err != nil {
		return err
	}
	if hooks, err := os.ReadDir(filepath.Join(repoPath, "hooks")); err == nil && len(hooks) != 0 {
		return fmt.Errorf("content git repository hooks are forbidden")
	} else if err != nil && !errors.Is(err, os.ErrNotExist) {
		return err
	}
	return nil
}

func writeGitTree(ctx context.Context, repo *git.Repository, workspaceID, contentID, versionID string, files []File, limits Limits) (plumbing.Hash, Manifest, error) {
	root := &gitTreeNode{children: make(map[string]*gitTreeNode)}
	manifest := Manifest{WorkspaceID: workspaceID, ContentID: contentID, VersionID: versionID, Files: make([]ManifestEntry, 0, len(files))}
	for _, file := range files {
		if err := ctx.Err(); err != nil {
			return plumbing.ZeroHash, Manifest{}, err
		}
		if file.Content == nil || file.Size < 0 || file.Size > limits.MaxFileBytes {
			return plumbing.ZeroHash, Manifest{}, fmt.Errorf("content file %q has invalid size %d", file.Path, file.Size)
		}
		if manifest.SizeBytes > limits.MaxTotalBytes-file.Size {
			return plumbing.ZeroHash, Manifest{}, fmt.Errorf("content version exceeds %d byte limit", limits.MaxTotalBytes)
		}
		encoded := repo.Storer.NewEncodedObject()
		encoded.SetType(plumbing.BlobObject)
		writer, err := encoded.Writer()
		if err != nil {
			return plumbing.ZeroHash, Manifest{}, err
		}
		hash := sha256.New()
		written, copyErr := io.Copy(io.MultiWriter(writer, hash), &contextLimitedReader{ctx: ctx, reader: file.Content, remaining: file.Size + 1})
		closeErr := writer.Close()
		if copyErr != nil {
			return plumbing.ZeroHash, Manifest{}, copyErr
		}
		if closeErr != nil {
			return plumbing.ZeroHash, Manifest{}, closeErr
		}
		if written != file.Size {
			return plumbing.ZeroHash, Manifest{}, fmt.Errorf("content blob size mismatch: got %d, want %d", written, file.Size)
		}
		blobHash, err := repo.Storer.SetEncodedObject(encoded)
		if err != nil {
			return plumbing.ZeroHash, Manifest{}, err
		}
		mode := filemode.Regular
		if file.Executable {
			mode = filemode.Executable
		}
		if err := root.add(file.Path, mode, blobHash); err != nil {
			return plumbing.ZeroHash, Manifest{}, err
		}
		manifest.Files = append(manifest.Files, ManifestEntry{Path: file.Path, Digest: Digest(hex.EncodeToString(hash.Sum(nil))), SizeBytes: file.Size, Executable: file.Executable})
		manifest.SizeBytes += file.Size
	}
	manifest.RootDigest, _ = manifestDigest(manifest.Files)
	if err := validateGitManifestSize(manifest, limits); err != nil {
		return plumbing.ZeroHash, Manifest{}, err
	}
	treeHash, err := root.write(repo)
	if err != nil {
		return plumbing.ZeroHash, Manifest{}, err
	}
	return treeHash, manifest, nil
}

type gitTreeNode struct {
	children map[string]*gitTreeNode
	files    map[string]object.TreeEntry
}

func (node *gitTreeNode) add(filePath string, mode filemode.FileMode, hash plumbing.Hash) error {
	parts := strings.Split(filePath, "/")
	current := node
	for _, part := range parts[:len(parts)-1] {
		if current.files != nil {
			if _, exists := current.files[part]; exists {
				return fmt.Errorf("content tree path conflicts with file %q", filePath)
			}
		}
		next := current.children[part]
		if next == nil {
			next = &gitTreeNode{children: make(map[string]*gitTreeNode)}
			current.children[part] = next
		}
		current = next
	}
	name := parts[len(parts)-1]
	if current.children[name] != nil {
		return fmt.Errorf("content tree file conflicts with directory %q", filePath)
	}
	if current.files == nil {
		current.files = make(map[string]object.TreeEntry)
	}
	current.files[name] = object.TreeEntry{Name: name, Mode: mode, Hash: hash}
	return nil
}

func (node *gitTreeNode) write(repo *git.Repository) (plumbing.Hash, error) {
	entries := make([]object.TreeEntry, 0, len(node.children)+len(node.files))
	for name, child := range node.children {
		hash, err := child.write(repo)
		if err != nil {
			return plumbing.ZeroHash, err
		}
		entries = append(entries, object.TreeEntry{Name: name, Mode: filemode.Dir, Hash: hash})
	}
	for _, entry := range node.files {
		entries = append(entries, entry)
	}
	sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
	tree := object.Tree{Entries: entries}
	encoded := repo.Storer.NewEncodedObject()
	if err := tree.Encode(encoded); err != nil {
		return plumbing.ZeroHash, fmt.Errorf("encode content tree: %w", err)
	}
	hash, err := repo.Storer.SetEncodedObject(encoded)
	if err != nil {
		return plumbing.ZeroHash, fmt.Errorf("write content tree: %w", err)
	}
	return hash, nil
}

func requestedManifest(files []File, limits Limits) (Manifest, error) {
	manifest := Manifest{Files: make([]ManifestEntry, 0, len(files))}
	for _, file := range files {
		hash := sha256.New()
		written, err := io.Copy(hash, file.Content)
		if err != nil {
			return Manifest{}, err
		}
		if written != file.Size {
			return Manifest{}, fmt.Errorf("content blob size mismatch: got %d, want %d", written, file.Size)
		}
		manifest.Files = append(manifest.Files, ManifestEntry{Path: file.Path, Digest: Digest(hex.EncodeToString(hash.Sum(nil))), SizeBytes: file.Size, Executable: file.Executable})
		manifest.SizeBytes += file.Size
	}
	manifest.RootDigest, _ = manifestDigest(manifest.Files)
	if err := validateGitManifestSize(manifest, limits); err != nil {
		return Manifest{}, err
	}
	return manifest, nil
}

func stagedFromManifest(ref ManifestRef, manifest Manifest, created bool) StagedTree {
	return StagedTree{ManifestRef: ref, RootDigest: manifest.RootDigest, SizeBytes: manifest.SizeBytes, FileCount: len(manifest.Files), Created: created}
}

func (s *gitTreeStorage) manifestForCommit(ctx context.Context, repo *git.Repository, workspaceID, contentID, versionID string, hash plumbing.Hash, limits Limits) (Manifest, error) {
	commit, err := repo.CommitObject(hash)
	if err != nil {
		return Manifest{}, fmt.Errorf("open content commit: %w", err)
	}
	tree, err := commit.Tree()
	if err != nil {
		return Manifest{}, fmt.Errorf("open content tree: %w", err)
	}
	manifest := Manifest{WorkspaceID: workspaceID, ContentID: contentID, VersionID: versionID}
	err = walkValidatedGitTree(ctx, repo, tree, limits, func(file *object.File) error {
		reader, err := file.Reader()
		if err != nil {
			return err
		}
		hash := sha256.New()
		_, copyErr := io.Copy(hash, &contextLimitedReader{ctx: ctx, reader: reader, remaining: file.Size + 1})
		closeErr := reader.Close()
		if copyErr != nil {
			return copyErr
		}
		if closeErr != nil {
			return closeErr
		}
		manifest.Files = append(manifest.Files, ManifestEntry{Path: file.Name, Digest: Digest(hex.EncodeToString(hash.Sum(nil))), SizeBytes: file.Size, Executable: file.Mode == filemode.Executable})
		manifest.SizeBytes += file.Size
		return nil
	})
	if err != nil {
		return Manifest{}, err
	}
	sort.Slice(manifest.Files, func(i, j int) bool { return manifest.Files[i].Path < manifest.Files[j].Path })
	manifest.RootDigest, err = manifestDigest(manifest.Files)
	if err != nil {
		return Manifest{}, err
	}
	if err := validateGitManifestSize(manifest, limits); err != nil {
		return Manifest{}, err
	}
	return manifest, nil
}

func walkValidatedGitTree(ctx context.Context, _ *git.Repository, tree *object.Tree, limits Limits, visit func(*object.File) error) error {
	var count int
	var entries int
	var total int64
	seen := make(map[string]struct{})
	maxEntries := limits.MaxFiles*maxGitTreeEntriesPerFile + maxGitTreeDepth
	var walk func(*object.Tree, string, int) error
	walk = func(current *object.Tree, prefix string, depth int) error {
		if err := ctx.Err(); err != nil {
			return err
		}
		if depth > maxGitTreeDepth {
			return fmt.Errorf("content tree exceeds depth limit")
		}
		for index := range current.Entries {
			entries++
			if entries > maxEntries {
				return fmt.Errorf("content tree exceeds entry limit")
			}
			entry := &current.Entries[index]
			entryPath := entry.Name
			if prefix != "" {
				entryPath = prefix + "/" + entry.Name
			}
			cleaned, err := cleanFilePath(entryPath)
			if err != nil || cleaned != entryPath {
				return fmt.Errorf("content tree contains invalid path")
			}
			switch entry.Mode {
			case filemode.Dir:
				child, err := current.Tree(entry.Name)
				if err != nil {
					return err
				}
				if err := walk(child, entryPath, depth+1); err != nil {
					return err
				}
			case filemode.Regular, filemode.Executable:
				if _, duplicate := seen[entryPath]; duplicate {
					return fmt.Errorf("content tree contains duplicate path")
				}
				seen[entryPath] = struct{}{}
				file, err := current.TreeEntryFile(entry)
				if err != nil {
					return err
				}
				file.Name = entryPath
				count++
				if count > limits.MaxFiles || file.Size < 0 || file.Size > limits.MaxFileBytes || total > limits.MaxTotalBytes-file.Size {
					return fmt.Errorf("content tree exceeds limits")
				}
				total += file.Size
				if err := visit(file); err != nil {
					return err
				}
			default:
				return fmt.Errorf("content tree contains unsupported entry")
			}
		}
		return nil
	}
	return walk(tree, "", 0)
}

func validateGitManifestSize(manifest Manifest, limits Limits) error {
	payload, err := json.Marshal(manifest)
	if err != nil {
		return fmt.Errorf("encode content manifest: %w", err)
	}
	if int64(len(payload)) > limits.MaxManifestBytes {
		return fmt.Errorf("content manifest exceeds %d byte limit", limits.MaxManifestBytes)
	}
	return nil
}

func (s *gitTreeStorage) OpenManifest(ctx context.Context, ref ManifestRef) (Manifest, error) {
	repo, hash, parts, err := s.openRef(ref)
	if err != nil {
		return Manifest{}, err
	}
	defer repo.close()
	return s.manifestForCommit(ctx, repo.Repository, parts[0], parts[1], parts[2], hash, DefaultLimits)
}

// OpenBlob is intentionally unsupported for Git tree payloads because a Git
// object ID is repository-scoped. Tree reads use the exact version ref.
func (s *gitTreeStorage) OpenBlob(context.Context, Digest, *ByteRange) (io.ReadCloser, BlobMeta, error) {
	return nil, BlobMeta{}, fmt.Errorf("content git tree blob requires an exact version reference")
}

func (s *gitTreeStorage) OpenFile(ctx context.Context, ref ManifestRef, filePath string, byteRange *ByteRange) (io.ReadCloser, BlobMeta, error) {
	filePath, err := cleanFilePath(filePath)
	if err != nil {
		return nil, BlobMeta{}, err
	}
	repo, hash, _, err := s.openRef(ref)
	if err != nil {
		return nil, BlobMeta{}, err
	}
	defer repo.close()
	commit, err := repo.CommitObject(hash)
	if err != nil {
		return nil, BlobMeta{}, err
	}
	file, err := commit.File(filePath)
	if errors.Is(err, object.ErrFileNotFound) {
		return nil, BlobMeta{}, ErrNotFound
	}
	if err != nil {
		return nil, BlobMeta{}, err
	}
	if file.Mode != filemode.Regular && file.Mode != filemode.Executable {
		return nil, BlobMeta{}, fmt.Errorf("content tree contains unsupported entry")
	}
	if _, err := cleanFilePath(file.Name); err != nil {
		return nil, BlobMeta{}, fmt.Errorf("content tree contains invalid path")
	}
	if file.Size > DefaultLimits.MaxFileBytes {
		return nil, BlobMeta{}, fmt.Errorf("content blob exceeds read limit")
	}
	reader, err := file.Reader()
	if err != nil {
		return nil, BlobMeta{}, err
	}
	digest := sha256.New()
	if _, err := io.Copy(digest, reader); err != nil {
		_ = reader.Close()
		return nil, BlobMeta{}, err
	}
	if err := reader.Close(); err != nil {
		return nil, BlobMeta{}, err
	}
	reader, err = file.Reader()
	if err != nil {
		return nil, BlobMeta{}, err
	}
	meta := BlobMeta{Digest: Digest(hex.EncodeToString(digest.Sum(nil))), SizeBytes: file.Size}
	if byteRange == nil {
		return reader, meta, nil
	}
	if byteRange.Offset < 0 || byteRange.Length < 0 || byteRange.Offset > file.Size || byteRange.Length > file.Size-byteRange.Offset {
		_ = reader.Close()
		return nil, BlobMeta{}, fmt.Errorf("content blob range is invalid")
	}
	if _, err := io.CopyN(io.Discard, reader, byteRange.Offset); err != nil {
		_ = reader.Close()
		return nil, BlobMeta{}, err
	}
	return struct {
		io.Reader
		io.Closer
	}{Reader: io.LimitReader(reader, byteRange.Length), Closer: reader}, meta, nil
}

func (s *gitTreeStorage) Materialize(ctx context.Context, ref ManifestRef, destination string, requested MaterializeLimits) error {
	repo, hash, _, err := s.openRef(ref)
	if err != nil {
		return err
	}
	defer repo.close()
	commit, err := repo.CommitObject(hash)
	if err != nil {
		return err
	}
	tree, err := commit.Tree()
	if err != nil {
		return err
	}
	limits := normalizedMaterializeLimits(requested)
	if strings.TrimSpace(destination) == "" {
		return fmt.Errorf("content materialization destination is required")
	}
	destination, err = filepath.Abs(destination)
	if err != nil {
		return err
	}
	if _, err := os.Lstat(destination); !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("content materialization destination already exists")
	}
	temporary, err := os.MkdirTemp(filepath.Dir(destination), ".content-tree-")
	if err != nil {
		return err
	}
	published := false
	defer func() {
		if !published {
			_ = os.RemoveAll(temporary)
		}
	}()
	err = walkValidatedGitTree(ctx, repo.Repository, tree, Limits{MaxFiles: limits.MaxFiles, MaxFileBytes: limits.MaxFileBytes, MaxTotalBytes: limits.MaxTotalBytes, MaxManifestBytes: DefaultLimits.MaxManifestBytes}, func(file *object.File) error {
		path := filepath.Join(temporary, filepath.FromSlash(file.Name))
		if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
			return err
		}
		reader, err := file.Reader()
		if err != nil {
			return err
		}
		mode := os.FileMode(0o640)
		if file.Mode == filemode.Executable {
			mode = 0o750
		}
		output, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
		if err != nil {
			_ = reader.Close()
			return err
		}
		written, copyErr := io.Copy(output, &contextLimitedReader{ctx: ctx, reader: reader, remaining: file.Size + 1})
		closeOutErr := output.Close()
		closeReadErr := reader.Close()
		if copyErr != nil {
			return copyErr
		}
		if closeOutErr != nil {
			return closeOutErr
		}
		if closeReadErr != nil {
			return closeReadErr
		}
		if written != file.Size {
			return fmt.Errorf("content blob size mismatch")
		}
		return nil
	})
	if err != nil {
		return err
	}
	if err := os.Rename(temporary, destination); err != nil {
		return err
	}
	published = true
	return nil
}

func (s *gitTreeStorage) DeletePayload(ctx context.Context, ref ManifestRef) error {
	parts, _, err := gitPayloadRefParts(ref)
	if err != nil {
		return err
	}
	unlock, err := s.lock(ctx, parts[0], parts[1])
	if err != nil {
		return err
	}
	defer unlock()
	repo, err := s.openExisting(parts[0], parts[1])
	if errors.Is(err, ErrNotFound) {
		return nil
	}
	if err != nil {
		return err
	}
	defer repo.close()
	directory, err := openDirectoryAt(repo.root, []string{"refs", "content-versions"}, false)
	if err != nil {
		return err
	}
	defer directory.Close()
	err = unlinkAt(directory, parts[2], false)
	if err != nil && !errors.Is(err, os.ErrNotExist) {
		return fmt.Errorf("remove immutable content version ref: %w", err)
	}
	if err := s.syncFile(directory, "delete-ref-directory"); err != nil {
		return fmt.Errorf("sync deleted content version ref directory: %w", err)
	}
	return nil
}
