package gitstore

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strconv"
	"strings"
)

// GitBlobObject is the low-level git database blob representation returned
// by CreateBlobObject and GetGitBlob. It mirrors GitTreeObject / GitCommitObject.
// CreateBlobObject leaves Content empty; GetGitBlob fills it with the raw bytes.
type GitBlobObject struct {
	SHA     string
	Size    int64
	Content []byte
}

// ErrNotBlob is returned by GetGitBlob when the requested SHA exists but
// is not a blob object (e.g. it is a tree, commit, or annotated tag).
var ErrNotBlob = errors.New("object is not a blob")

// ErrBlobTooLarge is returned by GetGitBlob when the blob exceeds
// gitBlobMaxBytes. Mirrors GitHub's 100 MiB cap; protects the server from
// per-request 1.33x memory expansion (raw -> base64 -> JSON) on any blob
// that landed via push or libgit2 outside this REST surface's control.
var ErrBlobTooLarge = errors.New("blob is too large to retrieve via this API")

// gitBlobMaxBytes caps the response size for GET /git/blobs/{sha}.
// Matches GitHub's documented 100 MiB cap.
const gitBlobMaxBytes int64 = 100 * 1024 * 1024

// CreateTreeEntryInput is one entry passed to the private tree-object owner.
//
// Exactly one of SHA, Content, or DeleteSHA must be set:
//   - SHA references an existing object (blob/tree/commit).
//   - Content is only valid when Type == "blob"; the bytes are written as a
//     new blob and the resulting sha is used in the tree.
//   - DeleteSHA removes the path from BaseTree.
type CreateTreeEntryInput struct {
	Path      string
	Mode      string
	Type      string
	SHA       string
	Content   *string
	DeleteSHA bool
}

// CreateTreeOptions configures one immutable tree-object write.
type CreateTreeOptions struct {
	BaseTree string
	Entries  []CreateTreeEntryInput
}

// ErrInvalidTreeEntry is returned for an invalid path / mode / type / sha /
// content combination.
var ErrInvalidTreeEntry = errors.New("invalid tree entry")

// ErrBaseTreeNotFound is returned when BaseTree does not resolve to an existing
// tree object in the repository.
var ErrBaseTreeNotFound = errors.New("base_tree not found")

// GetGitBlob reads a blob object by SHA and returns its raw bytes.
// Returns ErrNotBlob if the SHA exists but identifies a non-blob object.
func (s *Store) GetGitBlob(ctx context.Context, fullName, sha string) (GitBlobObject, error) {
	dir, err := s.repoPath(ctx, fullName)
	if err != nil {
		return GitBlobObject{}, err
	}
	if _, err := os.Stat(dir); err != nil {
		return GitBlobObject{}, err
	}

	objSHA, err := s.resolveObjectSHA(ctx, dir, sha)
	if err != nil {
		return GitBlobObject{}, err
	}

	typeOut, err := exec.CommandContext(ctx, "git", "-C", dir, "cat-file", "-t", objSHA).Output()
	if err != nil {
		return GitBlobObject{}, fmt.Errorf("cat-file -t failed: %w", err)
	}
	if t := strings.TrimSpace(string(typeOut)); t != "blob" {
		return GitBlobObject{}, fmt.Errorf("%w: %s", ErrNotBlob, t)
	}

	// Check size before reading content to avoid OOM on large blobs.
	sizeOut, err := exec.CommandContext(ctx, "git", "-C", dir, "cat-file", "-s", objSHA).Output()
	if err != nil {
		return GitBlobObject{}, fmt.Errorf("cat-file -s failed: %w", err)
	}
	size, err := strconv.ParseInt(strings.TrimSpace(string(sizeOut)), 10, 64)
	if err != nil {
		return GitBlobObject{}, fmt.Errorf("parse blob size %q: %w", sizeOut, err)
	}
	if size > gitBlobMaxBytes {
		return GitBlobObject{}, fmt.Errorf("%w: %d bytes exceeds %d", ErrBlobTooLarge, size, gitBlobMaxBytes)
	}

	content, err := exec.CommandContext(ctx, "git", "-C", dir, "cat-file", "blob", objSHA).Output()
	if err != nil {
		return GitBlobObject{}, fmt.Errorf("cat-file blob failed: %w", err)
	}
	return GitBlobObject{SHA: objSHA, Size: int64(len(content)), Content: content}, nil
}
