package gitstore

import (
	"bufio"
	"bytes"
	"context"
	"errors"
	"fmt"
	"io"
	"os/exec"
	"path"
	"strconv"
	"strings"
)

var (
	// ErrRepositorySnapshotEscape is returned when a requested object is not a
	// regular file in the immutable Git tree. Symbolic links are never followed.
	ErrRepositorySnapshotEscape = errors.New("repository snapshot path escape")
	// ErrRepositorySnapshotResourceExhausted reports a host protection limit,
	// not an incomplete successful search.
	ErrRepositorySnapshotResourceExhausted = errors.New("repository snapshot resources exhausted")
)

const (
	repositorySnapshotBinaryProbeBytes = 8 << 10
	repositorySnapshotMaxBlobBytes     = 16 << 20
	repositorySnapshotMaxLineBytes     = 1 << 20
	repositorySnapshotMaxPathBytes     = 64 << 10
	repositorySnapshotMaxErrorBytes    = 64 << 10
)

type RepositorySnapshotGrepRequest struct {
	RepositoryFullName string
	CommitSHA          string
	Pattern            string
	PathGlobs          []string
}

type RepositorySnapshotGrepMatch struct {
	Path    string
	Line    int
	Preview string
}

// RepositorySnapshotListRequest selects one directory at an exact repository
// commit. Path is relative to the repository root and may be empty.
type RepositorySnapshotListRequest struct {
	RepositoryFullName string
	CommitSHA          string
	Path               string
}

// RepositorySnapshotListEntry is one safe immediate file or directory child
// from an immutable repository tree.
type RepositorySnapshotListEntry struct {
	Path string
	Kind string
}

type RepositorySnapshotReadRequest struct {
	RepositoryFullName string
	CommitSHA          string
	Path               string
	LineStart          int
	LineEnd            int
}

type RepositorySnapshotReadChunk struct {
	Content   []byte
	LineStart int
	LineEnd   int
}

// GrepRepositorySnapshot streams git-grep matches from the exact authorized
// commit. Product authorization remains in MeshX; this primitive accepts an
// AGS repository identity, never a caller-controlled filesystem path.
func (s *Store) GrepRepositorySnapshot(ctx context.Context, request RepositorySnapshotGrepRequest, emit func(RepositorySnapshotGrepMatch) error) error {
	repositoryPath, err := s.openRepositorySnapshot(ctx, request.RepositoryFullName)
	if err != nil {
		return err
	}
	return grepRepositorySnapshot(ctx, repositoryPath, request, emit)
}

// ListRepositorySnapshot lists one tree level at the exact authorized commit.
func (s *Store) ListRepositorySnapshot(ctx context.Context, request RepositorySnapshotListRequest, emit func(RepositorySnapshotListEntry) error) error {
	repositoryPath, err := s.openRepositorySnapshot(ctx, request.RepositoryFullName)
	if err != nil {
		return err
	}
	return listRepositorySnapshot(ctx, repositoryPath, request, emit)
}

// ReadRepositorySnapshot streams a line range from a regular blob at the exact
// authorized commit. It never checks out a worktree or follows a symlink.
func (s *Store) ReadRepositorySnapshot(ctx context.Context, request RepositorySnapshotReadRequest, emit func(RepositorySnapshotReadChunk) error) error {
	repositoryPath, err := s.openRepositorySnapshot(ctx, request.RepositoryFullName)
	if err != nil {
		return err
	}
	return readRepositorySnapshot(ctx, repositoryPath, request, emit)
}

func (s *Store) openRepositorySnapshot(ctx context.Context, fullName string) (string, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	if s == nil {
		return "", fmt.Errorf("git store is not initialized")
	}
	repositoryPath, err := s.GetRepoPath(ctx, strings.TrimSpace(fullName))
	if err != nil {
		return "", fmt.Errorf("resolve repository snapshot: %w", err)
	}
	return repositoryPath, nil
}

func grepRepositorySnapshot(ctx context.Context, repositoryPath string, request RepositorySnapshotGrepRequest, emit func(RepositorySnapshotGrepMatch) error) error {
	if emit == nil {
		return fmt.Errorf("repository snapshot grep consumer is required")
	}
	sha, err := validateRepositorySnapshot(ctx, repositoryPath, request.CommitSHA)
	if err != nil {
		return err
	}
	pattern := strings.TrimSpace(request.Pattern)
	if pattern == "" {
		return fmt.Errorf("repository snapshot grep pattern is required")
	}
	globs, err := normalizeSnapshotGlobs(request.PathGlobs)
	if err != nil {
		return err
	}
	args := []string{"grep", "--no-ext-grep", "--full-name", "-n", "-I", "-z", "-E", "-e", pattern, sha, "--"}
	for _, glob := range globs {
		args = append(args, ":(glob)"+glob)
	}
	command := snapshotGitCommand(ctx, repositoryPath, args...)
	stdout, err := command.StdoutPipe()
	if err != nil {
		return fmt.Errorf("open git grep output: %w", err)
	}
	stderr := &boundedBuffer{limit: repositorySnapshotMaxErrorBytes}
	command.Stderr = stderr
	if err := command.Start(); err != nil {
		return fmt.Errorf("start git grep: %w", err)
	}
	reader := bufio.NewReaderSize(stdout, 64<<10)
	for {
		objectPath, readErr := readSnapshotField(reader, 0, repositorySnapshotMaxPathBytes)
		if errors.Is(readErr, io.EOF) && len(objectPath) == 0 {
			break
		}
		if readErr != nil {
			stopSnapshotCommand(command)
			return readErr
		}
		lineRaw, readErr := readSnapshotField(reader, 0, 32)
		if readErr != nil {
			stopSnapshotCommand(command)
			return fmt.Errorf("parse git grep line: %w", readErr)
		}
		preview, readErr := readSnapshotField(reader, '\n', repositorySnapshotMaxLineBytes)
		if readErr != nil && !errors.Is(readErr, io.EOF) {
			stopSnapshotCommand(command)
			return readErr
		}
		filePath := strings.TrimPrefix(string(objectPath), sha+":")
		filePath, err = normalizeSnapshotPath(filePath, false)
		line, lineErr := strconv.Atoi(string(lineRaw))
		if err != nil || lineErr != nil || line < 1 {
			stopSnapshotCommand(command)
			return ErrRepositorySnapshotEscape
		}
		if err := emit(RepositorySnapshotGrepMatch{
			Path: filePath, Line: line, Preview: strings.TrimSuffix(string(preview), "\r"),
		}); err != nil {
			stopSnapshotCommand(command)
			return err
		}
		if errors.Is(readErr, io.EOF) {
			break
		}
	}
	if err := command.Wait(); err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
			return nil
		}
		if ctx != nil && ctx.Err() != nil {
			return ctx.Err()
		}
		return fmt.Errorf("git grep snapshot: %w: %s", err, strings.TrimSpace(stderr.String()))
	}
	return nil
}

func listRepositorySnapshot(ctx context.Context, repositoryPath string, request RepositorySnapshotListRequest, emit func(RepositorySnapshotListEntry) error) error {
	if emit == nil {
		return fmt.Errorf("repository snapshot list consumer is required")
	}
	sha, err := validateRepositorySnapshot(ctx, repositoryPath, request.CommitSHA)
	if err != nil {
		return err
	}
	directory, err := normalizeSnapshotPath(request.Path, true)
	if err != nil {
		return err
	}
	treeish := sha + "^{tree}"
	if directory != "" {
		treeish = sha + ":" + directory
		kind, kindErr := snapshotGitOutput(ctx, repositoryPath, "cat-file", "-t", treeish)
		if kindErr != nil || strings.TrimSpace(string(kind)) != "tree" {
			return ErrRepositorySnapshotEscape
		}
	}
	command := snapshotGitCommand(ctx, repositoryPath, "ls-tree", "-z", treeish)
	stdout, err := command.StdoutPipe()
	if err != nil {
		return fmt.Errorf("open git ls-tree output: %w", err)
	}
	stderr := &boundedBuffer{limit: repositorySnapshotMaxErrorBytes}
	command.Stderr = stderr
	if err := command.Start(); err != nil {
		return fmt.Errorf("start git ls-tree: %w", err)
	}
	reader := bufio.NewReaderSize(stdout, 64<<10)
	for {
		record, readErr := readSnapshotField(reader, 0, repositorySnapshotMaxPathBytes+128)
		if errors.Is(readErr, io.EOF) && len(record) == 0 {
			break
		}
		if readErr != nil {
			stopSnapshotCommand(command)
			return readErr
		}
		header, name, ok := bytes.Cut(record, []byte{'\t'})
		fields := bytes.Fields(header)
		if !ok || len(fields) != 3 {
			stopSnapshotCommand(command)
			return fmt.Errorf("git ls-tree returned invalid entry")
		}
		entryKind := ""
		switch {
		case string(fields[1]) == "tree":
			entryKind = "directory"
		case string(fields[1]) == "blob" && (string(fields[0]) == "100644" || string(fields[0]) == "100755"):
			entryKind = "file"
		default:
			continue
		}
		entryPath := string(name)
		if directory != "" {
			entryPath = directory + "/" + entryPath
		}
		entryPath, err = normalizeSnapshotPath(entryPath, false)
		if err != nil {
			stopSnapshotCommand(command)
			return ErrRepositorySnapshotEscape
		}
		if err := emit(RepositorySnapshotListEntry{Path: entryPath, Kind: entryKind}); err != nil {
			stopSnapshotCommand(command)
			return err
		}
	}
	if err := command.Wait(); err != nil {
		if ctx != nil && ctx.Err() != nil {
			return ctx.Err()
		}
		return fmt.Errorf("git ls-tree snapshot: %w: %s", err, strings.TrimSpace(stderr.String()))
	}
	return nil
}

func readRepositorySnapshot(ctx context.Context, repositoryPath string, request RepositorySnapshotReadRequest, emit func(RepositorySnapshotReadChunk) error) error {
	if emit == nil {
		return fmt.Errorf("repository snapshot read consumer is required")
	}
	sha, err := validateRepositorySnapshot(ctx, repositoryPath, request.CommitSHA)
	if err != nil {
		return err
	}
	filePath, err := normalizeSnapshotPath(request.Path, false)
	if err != nil {
		return err
	}
	if request.LineStart < 1 || request.LineEnd < request.LineStart {
		return fmt.Errorf("repository snapshot line range is invalid")
	}
	mode, err := snapshotTreeEntryMode(ctx, repositoryPath, sha, filePath)
	if err != nil || (mode != "100644" && mode != "100755") {
		return ErrRepositorySnapshotEscape
	}
	size, err := snapshotGitOutput(ctx, repositoryPath, "cat-file", "-s", sha+":"+filePath)
	if err != nil {
		return err
	}
	if err := validateSnapshotBlobSize(size); err != nil {
		return err
	}
	command := snapshotGitCommand(ctx, repositoryPath, "show", sha+":"+filePath)
	stdout, err := command.StdoutPipe()
	if err != nil {
		return fmt.Errorf("open git show output: %w", err)
	}
	stderr := &boundedBuffer{limit: repositorySnapshotMaxErrorBytes}
	command.Stderr = stderr
	if err := command.Start(); err != nil {
		return fmt.Errorf("start git show: %w", err)
	}
	reader := bufio.NewReaderSize(stdout, 64<<10)
	probe, probeErr := reader.Peek(repositorySnapshotBinaryProbeBytes)
	if probeErr != nil && !errors.Is(probeErr, io.EOF) && !errors.Is(probeErr, bufio.ErrBufferFull) {
		stopSnapshotCommand(command)
		return probeErr
	}
	if bytes.IndexByte(probe, 0) >= 0 {
		stopSnapshotCommand(command)
		return ErrRepositorySnapshotEscape
	}
	var chunks []RepositorySnapshotReadChunk
	reachedEOF := false
	for line := 1; line <= request.LineEnd; line++ {
		content, readErr := readSnapshotField(reader, '\n', repositorySnapshotMaxLineBytes)
		if len(content) > 0 && bytes.IndexByte(content, 0) >= 0 {
			stopSnapshotCommand(command)
			return ErrRepositorySnapshotEscape
		}
		if line >= request.LineStart && (readErr == nil || len(content) > 0) {
			if readErr == nil {
				content = append(content, '\n')
			}
			chunks = append(chunks, RepositorySnapshotReadChunk{
				Content: append([]byte(nil), content...), LineStart: line, LineEnd: line,
			})
		}
		if errors.Is(readErr, io.EOF) {
			reachedEOF = true
			break
		}
		if readErr != nil {
			stopSnapshotCommand(command)
			return readErr
		}
	}
	if !reachedEOF {
		// Binary data after the requested range must still fail the whole read
		// closed. The blob-size check bounds this validation to 16 MiB.
		remainder := make([]byte, 64<<10)
		for {
			count, readErr := reader.Read(remainder)
			if bytes.IndexByte(remainder[:count], 0) >= 0 {
				stopSnapshotCommand(command)
				return ErrRepositorySnapshotEscape
			}
			if errors.Is(readErr, io.EOF) {
				break
			}
			if readErr != nil {
				stopSnapshotCommand(command)
				return readErr
			}
		}
	}
	if err := command.Wait(); err != nil {
		if ctx != nil && ctx.Err() != nil {
			return ctx.Err()
		}
		return fmt.Errorf("git show snapshot: %w: %s", err, strings.TrimSpace(stderr.String()))
	}
	for _, chunk := range chunks {
		if err := emit(chunk); err != nil {
			return err
		}
	}
	return nil
}

func validateRepositorySnapshot(ctx context.Context, repositoryPath, rawSHA string) (string, error) {
	sha := strings.TrimSpace(rawSHA)
	if !validSnapshotCommitSHA(sha) {
		return "", fmt.Errorf("repository snapshot commit SHA is invalid")
	}
	kind, err := snapshotGitOutput(ctx, repositoryPath, "cat-file", "-t", sha)
	if err != nil {
		return "", fmt.Errorf("resolve repository snapshot commit: %w", err)
	}
	if strings.TrimSpace(string(kind)) != "commit" {
		return "", fmt.Errorf("resolve repository snapshot commit: object is not a commit")
	}
	return sha, nil
}

func snapshotTreeEntryMode(ctx context.Context, repositoryPath, sha, filePath string) (string, error) {
	output, err := snapshotGitOutput(ctx, repositoryPath, "ls-tree", "-z", sha, "--", ":(literal)"+filePath)
	if err != nil {
		return "", err
	}
	output = bytes.TrimSuffix(output, []byte{0})
	if bytes.IndexByte(output, 0) >= 0 {
		return "", ErrRepositorySnapshotEscape
	}
	header, foundPath, ok := bytes.Cut(output, []byte{'\t'})
	fields := bytes.Fields(header)
	if !ok || string(foundPath) != filePath || len(fields) != 3 || string(fields[1]) != "blob" {
		return "", ErrRepositorySnapshotEscape
	}
	return string(fields[0]), nil
}

func snapshotGitOutput(ctx context.Context, repositoryPath string, args ...string) ([]byte, error) {
	command := snapshotGitCommand(ctx, repositoryPath, args...)
	stdout, err := command.StdoutPipe()
	if err != nil {
		return nil, err
	}
	stderr := &boundedBuffer{limit: repositorySnapshotMaxErrorBytes}
	command.Stderr = stderr
	if err := command.Start(); err != nil {
		return nil, err
	}
	output, readErr := io.ReadAll(io.LimitReader(stdout, repositorySnapshotMaxErrorBytes+1))
	if readErr != nil {
		stopSnapshotCommand(command)
		return nil, readErr
	}
	if len(output) > repositorySnapshotMaxErrorBytes {
		stopSnapshotCommand(command)
		return nil, ErrRepositorySnapshotResourceExhausted
	}
	if err := command.Wait(); err != nil {
		if ctx != nil && ctx.Err() != nil {
			return nil, ctx.Err()
		}
		return nil, fmt.Errorf("git %s: %w: %s", args[0], err, strings.TrimSpace(stderr.String()))
	}
	return output, nil
}

func snapshotGitCommand(ctx context.Context, repositoryPath string, args ...string) *exec.Cmd {
	if ctx == nil {
		ctx = context.Background()
	}
	commandArgs := []string{"-c", "core.pager=cat", "-c", "color.ui=false", "-C", repositoryPath}
	commandArgs = append(commandArgs, args...)
	command := exec.CommandContext(ctx, "git", commandArgs...)
	command.Env = snapshotGitEnvironment()
	return command
}

func snapshotGitEnvironment() []string {
	return []string{
		"PATH=/usr/bin:/bin",
		"HOME=/nonexistent",
		"XDG_CONFIG_HOME=/nonexistent",
		"GIT_CONFIG=/dev/null",
		"GIT_CONFIG_GLOBAL=/dev/null",
		"GIT_CONFIG_SYSTEM=/dev/null",
		"GIT_CONFIG_NOSYSTEM=1",
		"GIT_ATTR_NOSYSTEM=1",
		"GIT_NO_LAZY_FETCH=1",
		"GIT_NO_REPLACE_OBJECTS=1",
		"GIT_OPTIONAL_LOCKS=0",
		"GIT_TERMINAL_PROMPT=0",
		"LC_ALL=C",
	}
}

func validateSnapshotBlobSize(raw []byte) error {
	size, err := strconv.ParseInt(strings.TrimSpace(string(raw)), 10, 64)
	if err != nil || size < 0 {
		return fmt.Errorf("git cat-file returned an invalid blob size")
	}
	if size > repositorySnapshotMaxBlobBytes {
		return ErrRepositorySnapshotResourceExhausted
	}
	return nil
}

func stopSnapshotCommand(command *exec.Cmd) {
	if command != nil && command.Process != nil {
		_ = command.Process.Kill()
		_ = command.Wait()
	}
}

func readSnapshotField(reader *bufio.Reader, delimiter byte, limit int) ([]byte, error) {
	var result []byte
	for {
		part, err := reader.ReadSlice(delimiter)
		if len(result)+len(part) > limit {
			return nil, ErrRepositorySnapshotResourceExhausted
		}
		result = append(result, part...)
		if err == nil {
			return bytes.TrimSuffix(result, []byte{delimiter}), nil
		}
		if errors.Is(err, bufio.ErrBufferFull) {
			continue
		}
		return result, err
	}
}

func validSnapshotCommitSHA(value string) bool {
	if len(value) != 40 {
		return false
	}
	for _, character := range value {
		if (character < '0' || character > '9') && (character < 'a' || character > 'f') && (character < 'A' || character > 'F') {
			return false
		}
	}
	return true
}

func normalizeSnapshotGlobs(input []string) ([]string, error) {
	seen := make(map[string]struct{}, len(input))
	globs := make([]string, 0, len(input))
	for _, raw := range input {
		glob, err := normalizeSnapshotPath(raw, false)
		if err != nil {
			return nil, fmt.Errorf("repository snapshot path glob is invalid")
		}
		if _, err := path.Match(glob, "placeholder"); err != nil {
			return nil, fmt.Errorf("repository snapshot path glob is invalid")
		}
		if _, ok := seen[glob]; ok {
			continue
		}
		seen[glob] = struct{}{}
		globs = append(globs, glob)
	}
	return globs, nil
}

func normalizeSnapshotPath(raw string, allowEmpty bool) (string, error) {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		if allowEmpty {
			return "", nil
		}
		return "", fmt.Errorf("repository snapshot path is required")
	}
	if strings.HasPrefix(raw, "/") || strings.Contains(raw, `\`) || strings.ContainsRune(raw, '\x00') {
		return "", fmt.Errorf("repository snapshot path must be relative")
	}
	for _, segment := range strings.Split(raw, "/") {
		if segment == "" || segment == "." || segment == ".." {
			return "", fmt.Errorf("repository snapshot path must stay inside the repository")
		}
	}
	cleaned := path.Clean(raw)
	if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, "../") {
		return "", fmt.Errorf("repository snapshot path must stay inside the repository")
	}
	return cleaned, nil
}

type boundedBuffer struct {
	buffer bytes.Buffer
	limit  int
}

func (buffer *boundedBuffer) Write(value []byte) (int, error) {
	accepted := len(value)
	if remaining := buffer.limit - buffer.buffer.Len(); remaining > 0 {
		_, _ = buffer.buffer.Write(value[:min(len(value), remaining)])
	}
	return accepted, nil
}

func (buffer *boundedBuffer) String() string { return buffer.buffer.String() }
