// Package contentpayload owns immutable content version payload storage.
package contentpayload

import (
	"context"
	"errors"
	"io"
)

var (
	ErrNotFound      = errors.New("content object not found")
	ErrAlreadyExists = errors.New("content version already exists with different content")
)

// Digest is a lowercase SHA-256 content digest.
type Digest string

// ManifestRef is an opaque reference returned by TreePayloadBackend. Callers must not
// construct filesystem or object-store paths from it.
type ManifestRef string

// StagedPayloadReference returns the deterministic pre-publication reference
// used by the GC reconciliation ledger. A backend may return a richer exact
// payload reference after staging (for example an immutable Git commit).
func StagedPayloadReference(workspaceID, contentID, versionID string) (ManifestRef, error) {
	ref, err := PayloadReference(workspaceID, contentID, versionID)
	return ref, err
}

// Limits bounds work to the content version currently being processed.
type Limits struct {
	MaxFiles         int
	MaxFileBytes     int64
	MaxTotalBytes    int64
	MaxManifestBytes int64
}

// DefaultLimits are deliberately conservative until product-specific limits
// are passed by the owning Content Store.
var DefaultLimits = Limits{
	MaxFiles:         10_000,
	MaxFileBytes:     128 << 20,
	MaxTotalBytes:    512 << 20,
	MaxManifestBytes: 8 << 20,
}

// File supplies one regular file for a new immutable version. Symlinks and
// other special files are intentionally not representable.
type File struct {
	Path       string
	Size       int64
	Executable bool
	Content    io.Reader
}

type StageTreeRequest struct {
	WorkspaceID string
	ContentID   string
	VersionID   string
	// ParentPayloadRef names the exact preceding tree commit when one exists.
	// It is backend-opaque and never selects the product head.
	ParentPayloadRef ManifestRef
	Actor            string
	Files            []File
	Limits           Limits
}

// PayloadReference returns the opaque manifest reference for a validated
// scope. It centralizes validation so services do not concatenate storage keys.
func PayloadReference(workspaceID, contentID, versionID string) (ManifestRef, error) {
	workspaceID, err := safeID("workspace", workspaceID)
	if err != nil {
		return "", err
	}
	contentID, err = safeID("content", contentID)
	if err != nil {
		return "", err
	}
	versionID, err = safeID("version", versionID)
	if err != nil {
		return "", err
	}
	return ManifestRef(workspaceID + "/" + contentID + "/" + versionID), nil
}

type StagedTree struct {
	ManifestRef ManifestRef
	RootDigest  Digest
	SizeBytes   int64
	FileCount   int
	// Created is true only when this call published the manifest. It lets the
	// service compensate its own newly staged version without deleting a
	// manifest created by an earlier idempotent attempt.
	Created bool
}

type Manifest struct {
	WorkspaceID string          `json:"workspaceId"`
	ContentID   string          `json:"contentId"`
	VersionID   string          `json:"versionId"`
	RootDigest  Digest          `json:"rootDigest"`
	SizeBytes   int64           `json:"sizeBytes"`
	Files       []ManifestEntry `json:"files"`
}

type ManifestEntry struct {
	Path       string `json:"path"`
	Digest     Digest `json:"digest"`
	SizeBytes  int64  `json:"sizeBytes"`
	Executable bool   `json:"executable,omitempty"`
}

type ByteRange struct {
	Offset int64
	Length int64
}

type BlobMeta struct {
	Digest    Digest
	SizeBytes int64
}

type MaterializeLimits struct {
	MaxFiles      int
	MaxFileBytes  int64
	MaxTotalBytes int64
}

// TreePayloadBackend manages immutable manifests and content-addressed blobs. Content
// head publication and compare-and-swap belong to the transactional store,
// not this interface.
type PayloadBackend interface {
	OpenManifest(context.Context, ManifestRef) (Manifest, error)
	OpenFile(context.Context, ManifestRef, string, *ByteRange) (io.ReadCloser, BlobMeta, error)
	OpenBlob(context.Context, Digest, *ByteRange) (io.ReadCloser, BlobMeta, error)
	// DeletePayload is idempotent and is only for compensation or asynchronous
	// GC. Shared content-addressed blobs are retained for a separate GC pass.
	DeletePayload(context.Context, ManifestRef) error
}

type TreePayloadBackend interface {
	PayloadBackend
	// Close releases backend-owned resources. Callers close TreePayloadBackend only after
	// all in-flight operations have drained.
	Close() error
	StageTree(context.Context, StageTreeRequest) (StagedTree, error)
	Materialize(context.Context, ManifestRef, string, MaterializeLimits) error
}
