// Package content owns VersionedContent/ContentVersion lineage, quota,
// head CAS, bounded history/compare, and payload-backend coordination.
package content

import (
	"archive/zip"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/contentpayload"
	"github.com/tidbcloud/AgentTeam/internal/observability/logging"
	store "github.com/tidbcloud/AgentTeam/internal/store/content"
	"gorm.io/gorm"
)

type Store interface {
	CreateVersionedContent(context.Context, store.VersionedContent) (store.VersionedContent, error)
	GetVersionedContent(context.Context, string, string) (store.VersionedContent, error)
	GetVersionedContentByBinding(context.Context, string, string, string) (store.VersionedContent, error)
	GetContentVersion(context.Context, string, string, string) (store.ContentVersion, error)
	GetVersionOperation(context.Context, string, string, string) (store.VersionOperation, error)
	RegisterStagedPayloadGC(context.Context, store.StagedPayloadGC) error
	UpdateStagedPayloadReference(context.Context, string, string, string, string, string, string) error
	PublishVersion(context.Context, store.PublishVersionParams) (store.PublishVersionResult, error)
	PublishDetachedVersion(context.Context, store.PublishDetachedVersionParams) (store.PublishVersionResult, error)
	ListVersions(context.Context, string, string, int) ([]store.ContentVersion, error)
	LoadVersionPaths(context.Context, string, string, string, int) ([]store.VersionPath, error)
	PathHistory(context.Context, string, string, string, int) ([]store.VersionPath, error)
	CreateVersionPin(context.Context, store.VersionPin) (store.VersionPin, error)
	ListVersionPins(context.Context, string, string, int) ([]store.VersionPin, error)
	DeleteVersionPin(context.Context, string, string, string) error
}

// TreeModel is the only model in the active Content Store contract. The
// persistence model column remains during rolling upgrades, but public/domain
// callers do not select a payload model.
const TreeModel = store.ModelTree

// API-facing list bounds are owned by the service contract. Store methods
// enforce the same ceilings independently as a persistence safety boundary.
const (
	MaxVersionListRows = 500
	MaxPathHistoryRows = 500
	MaxPinListRows     = 256
)

type IDGenerator func(prefix string) (string, error)

type Service struct {
	store    Store
	tree     contentpayload.TreePayloadBackend
	document contentpayload.DocumentPayloadBackend
	treeOnly bool
	newID    IDGenerator
	now      func() time.Time
}

// NewTree constructs the active tree-only Content Store. Document support in
// the inactive foundation is deliberately not reachable from this runtime.
func NewTree(store Store, tree contentpayload.TreePayloadBackend, newID IDGenerator) (*Service, error) {
	if store == nil || tree == nil || newID == nil {
		return nil, fmt.Errorf("tree content store dependencies are required")
	}
	return &Service{store: store, tree: tree, treeOnly: true, newID: newID, now: func() time.Time { return time.Now().UTC() }}, nil
}

func New(store Store, tree contentpayload.TreePayloadBackend, document contentpayload.DocumentPayloadBackend, newID IDGenerator) (*Service, error) {
	if store == nil || tree == nil || document == nil || newID == nil {
		return nil, fmt.Errorf("content store dependencies are required")
	}
	return &Service{store: store, tree: tree, document: document, newID: newID, now: func() time.Time { return time.Now().UTC() }}, nil
}

type Content struct {
	ID                    string
	WorkspaceID           string
	Domain                string
	BindingKey            string
	Model                 string
	HeadVersionID         string
	Revision              uint64
	Generation            uint64
	RetainedVersionCount  uint64
	StoredPayloadBytes    int64
	MaxRetainedVersions   uint64
	MaxStoredPayloadBytes int64
	MaxPins               uint64
	CreatedBy             string
	UpdatedBy             string
	CreatedAt             time.Time
	UpdatedAt             time.Time
}

type Version struct {
	ID              string
	WorkspaceID     string
	ContentID       string
	ParentVersionID string
	Generation      uint64
	Model           string
	PayloadRef      string
	RootDigest      string
	SizeBytes       int64
	FileCount       int
	SummaryBytes    int64
	Checkpoint      bool
	CreatedBy       string
	CreatedAt       time.Time
}

type Pin struct {
	ID          string
	WorkspaceID string
	ContentID   string
	VersionID   string
	Label       string
	CreatedBy   string
	CreatedAt   time.Time
}

type Path struct {
	VersionID string
	Path      string
	Digest    string
	Kind      string
	SizeBytes int64
	CreatedAt time.Time
}

type ArchiveLimits struct {
	MaxFiles      int
	MaxFileBytes  int64
	MaxTotalBytes int64
}

type ArchiveMeta struct {
	FileCount int
	SizeBytes int64
}

// QuotaProfile is the complete persistence quota for one content lineage.
// The zero value selects the model defaults. Internal owners may instead set
// every field explicitly; partial profiles are rejected so creation never
// materializes a mixed default/owner policy.
type QuotaProfile struct {
	MaxRetainedVersions   uint64
	MaxStoredPayloadBytes int64
	MaxPins               uint64
}

type CreateContentRequest struct {
	WorkspaceID string
	Domain      string
	BindingKey  string
	Model       string
	Actor       string
	Quota       QuotaProfile
}

func (service *Service) CreateContent(ctx context.Context, request CreateContentRequest) (Content, error) {
	if service.treeOnly && request.Model != store.ModelTree {
		return Content{}, fmt.Errorf("active content store supports tree content only")
	}
	if err := validateQuotaProfile(request.Quota); err != nil {
		return Content{}, err
	}
	id, err := service.newID("ctn")
	if err != nil {
		return Content{}, fmt.Errorf("generate versioned content id: %w", err)
	}
	created, err := service.store.CreateVersionedContent(ctx, store.VersionedContent{
		ID: id, WorkspaceID: request.WorkspaceID, Domain: request.Domain,
		BindingKey: request.BindingKey, Model: request.Model, CreatedBy: request.Actor,
		MaxRetainedVersions:   request.Quota.MaxRetainedVersions,
		MaxStoredPayloadBytes: request.Quota.MaxStoredPayloadBytes,
		MaxPins:               request.Quota.MaxPins,
	})
	if err != nil {
		return Content{}, err
	}
	return contentFromStore(created), nil
}

func validateQuotaProfile(profile QuotaProfile) error {
	allDefault := profile == (QuotaProfile{})
	allExplicit := profile.MaxRetainedVersions > 0 && profile.MaxStoredPayloadBytes > 0 && profile.MaxPins > 0
	if !allDefault && !allExplicit {
		return fmt.Errorf("content quota profile must be fully defaulted or fully explicit")
	}
	return nil
}

type CreatePinRequest struct {
	WorkspaceID string
	ContentID   string
	VersionID   string
	Label       string
	Actor       string
}

func (service *Service) CreatePin(ctx context.Context, request CreatePinRequest) (Pin, error) {
	id, err := service.newID("ctp")
	if err != nil {
		return Pin{}, fmt.Errorf("generate content version pin id: %w", err)
	}
	created, err := service.store.CreateVersionPin(ctx, store.VersionPin{
		ID: id, WorkspaceID: request.WorkspaceID, ContentID: request.ContentID,
		VersionID: request.VersionID, Label: request.Label, CreatedBy: request.Actor,
		CreatedAt: service.now(),
	})
	if err != nil {
		return Pin{}, err
	}
	return pinFromStore(created), nil
}

func (service *Service) ListPins(ctx context.Context, workspaceID, contentID string, limit int) ([]Pin, error) {
	if limit <= 0 || limit > MaxPinListRows {
		return nil, fmt.Errorf("content pin list limit must be between 1 and %d", MaxPinListRows)
	}
	rows, err := service.store.ListVersionPins(ctx, workspaceID, contentID, limit)
	if err != nil {
		return nil, err
	}
	result := make([]Pin, len(rows))
	for index := range rows {
		result[index] = pinFromStore(rows[index])
	}
	return result, nil
}

func (service *Service) DeletePin(ctx context.Context, workspaceID, contentID, pinID string) error {
	if strings.TrimSpace(workspaceID) == "" || strings.TrimSpace(contentID) == "" || strings.TrimSpace(pinID) == "" {
		return fmt.Errorf("content version pin identity is invalid")
	}
	return service.store.DeleteVersionPin(ctx, workspaceID, contentID, pinID)
}

type CommitVersionRequest struct {
	WorkspaceID           string
	ContentID             string
	ExpectedHeadVersionID string
	ExpectedGeneration    uint64
	Actor                 string
	Model                 string
	Checkpoint            bool
	Files                 []contentpayload.File
	TreeLimits            contentpayload.Limits
	// OperationKey is a server-owned, namespaced idempotency key. It is not
	// accepted by public Content Store routes and never grants authorization.
	OperationKey          string
	OperationNamespace    string
	OperationVersion      uint64
	ExpectedRequestDigest string
	// AuthorityFence is reserved for migration owners that must prove the
	// exact legacy binding lease in the same transaction that advances head.
	AuthorityFence *store.LegacyAuthorityFence
}

type CommitVersionResult struct {
	Content       Content
	Version       Version
	RequestDigest string
}

// PublishDetachedMigrationVersionRequest is the internal migration-owner
// publication contract. The live head is only a CAS fence; ParentVersionID is
// the independent immutable parent of the imported version.
type PublishDetachedMigrationVersionRequest struct {
	WorkspaceID           string
	ContentID             string
	ExpectedHeadVersionID string
	ParentVersionID       string
	ExpectedGeneration    uint64
	Actor                 string
	Model                 string
	Checkpoint            bool
	Files                 []contentpayload.File
	TreeLimits            contentpayload.Limits
	OperationKey          string
	ExpectedRequestDigest string
}

const (
	detachedMigrationOperationNamespace = "content.migration"
	detachedMigrationOperationVersion   = uint64(1)
)

type servicePublicationKind uint8

const (
	servicePublicationAdvanceHead servicePublicationKind = iota
	servicePublicationDetachedMigration
)

// VersionOperationResult is the immutable result of one exact server-owned
// publication key. Product domains use it to resume their own projection
// transaction after Content Store committed but the caller lost or failed the
// subsequent product commit.
type VersionOperationResult struct {
	OperationKey          string
	OperationNamespace    string
	OperationVersion      uint64
	ExpectedHeadVersionID string
	ExpectedGeneration    uint64
	VersionID             string
	RootDigest            string
	SizeBytes             int64
	FileCount             int
}

// FindVersionOperation performs one exact unique-key lookup. It never lists
// receipts and a missing receipt is not an error.
func (service *Service) FindVersionOperation(ctx context.Context, workspaceID, contentID, operationKey string) (VersionOperationResult, bool, error) {
	workspaceID = strings.TrimSpace(workspaceID)
	contentID = strings.TrimSpace(contentID)
	operationKey = strings.TrimSpace(operationKey)
	if workspaceID == "" || contentID == "" || operationKey == "" {
		return VersionOperationResult{}, false, fmt.Errorf("content version operation identity is invalid")
	}
	receipt, err := service.store.GetVersionOperation(ctx, workspaceID, contentID, operationKey)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return VersionOperationResult{}, false, nil
	}
	if err != nil {
		return VersionOperationResult{}, false, err
	}
	return VersionOperationResult{
		OperationKey: receipt.OperationKey, OperationNamespace: receipt.OperationNamespace,
		OperationVersion: receipt.OperationVersion, ExpectedHeadVersionID: receipt.ExpectedHeadVersionID,
		ExpectedGeneration: receipt.ExpectedGeneration, VersionID: receipt.VersionID,
		RootDigest: receipt.RootDigest, SizeBytes: receipt.SizeBytes, FileCount: receipt.FileCount,
	}, true, nil
}

const (
	treeBoundsPolicyNamespace     = "content.tree"
	documentBoundsPolicyNamespace = "content.document"
	boundsPolicyVersion           = uint64(1)
)

func (service *Service) CommitVersion(ctx context.Context, request CommitVersionRequest) (CommitVersionResult, error) {
	return service.publishVersion(ctx, request, servicePublicationAdvanceHead, "")
}

// publishDetachedMigrationVersion stages and atomically publishes one bounded
// migration version without moving the live head. The distinct method keeps
// detached semantics out of ordinary CommitVersion callers.
func (service *Service) publishDetachedMigrationVersion(ctx context.Context, request PublishDetachedMigrationVersionRequest) (CommitVersionResult, error) {
	if strings.TrimSpace(request.OperationKey) == "" {
		return CommitVersionResult{}, fmt.Errorf("detached migration operation key is required")
	}
	commit := CommitVersionRequest{
		WorkspaceID: request.WorkspaceID, ContentID: request.ContentID,
		ExpectedHeadVersionID: request.ExpectedHeadVersionID, ExpectedGeneration: request.ExpectedGeneration,
		Actor: request.Actor, Model: request.Model, Checkpoint: request.Checkpoint,
		Files: request.Files, TreeLimits: request.TreeLimits, OperationKey: request.OperationKey,
		OperationNamespace: detachedMigrationOperationNamespace, OperationVersion: detachedMigrationOperationVersion,
		ExpectedRequestDigest: request.ExpectedRequestDigest,
	}
	return service.publishVersion(ctx, commit, servicePublicationDetachedMigration, request.ParentVersionID)
}

func (service *Service) publishVersion(ctx context.Context, request CommitVersionRequest, kind servicePublicationKind, parentVersionID string) (CommitVersionResult, error) {
	if service.treeOnly && request.Model != store.ModelTree {
		return CommitVersionResult{}, fmt.Errorf("active content store supports tree content only")
	}
	current, err := service.store.GetVersionedContent(ctx, request.WorkspaceID, request.ContentID)
	if err != nil {
		return CommitVersionResult{}, err
	}
	if current.Model != request.Model {
		return CommitVersionResult{}, store.ErrModelConflict
	}
	if err := validateCommitRequestSemantics(request); err != nil {
		return CommitVersionResult{}, err
	}
	hardLimits, err := payloadHardLimits(request)
	if err != nil {
		return CommitVersionResult{}, err
	}
	if len(request.Files) > hardLimits.MaxFiles {
		return CommitVersionResult{}, store.ErrQuotaExceeded
	}
	var requestedBytes int64
	for _, file := range request.Files {
		if file.Size < 0 || file.Size > hardLimits.MaxFileBytes || requestedBytes > hardLimits.MaxTotalBytes-file.Size {
			return CommitVersionResult{}, store.ErrQuotaExceeded
		}
		requestedBytes += file.Size
	}
	versionID, err := service.newID("ctv")
	if err != nil {
		return CommitVersionResult{}, fmt.Errorf("generate content version id: %w", err)
	}
	outboxID, err := service.newID("cto")
	if err != nil {
		return CommitVersionResult{}, fmt.Errorf("generate content outbox id: %w", err)
	}
	gcID, err := service.newID("ctg")
	if err != nil {
		return CommitVersionResult{}, fmt.Errorf("generate content gc id: %w", err)
	}
	payloadRef, err := contentpayload.PayloadReference(request.WorkspaceID, request.ContentID, versionID)
	if err != nil {
		return CommitVersionResult{}, err
	}
	now := service.now()
	gc := store.StagedPayloadGC{
		ID: gcID, WorkspaceID: request.WorkspaceID, ContentID: request.ContentID,
		VersionID: versionID, Model: request.Model, PayloadRef: string(payloadRef), DeleteAfter: now.Add(24 * time.Hour),
	}
	if err := service.store.RegisterStagedPayloadGC(ctx, gc); err != nil {
		return CommitVersionResult{}, err
	}
	stageParentVersionID := request.ExpectedHeadVersionID
	if kind == servicePublicationDetachedMigration {
		stageParentVersionID = strings.TrimSpace(parentVersionID)
	}
	staged, manifest, err := service.stage(ctx, request, versionID, stageParentVersionID)
	if err != nil {
		return CommitVersionResult{}, err
	}
	gc.PayloadRef = staged.payloadRef
	if gc.PayloadRef != string(payloadRef) {
		if err := service.store.UpdateStagedPayloadReference(ctx, gc.ID, request.WorkspaceID, request.ContentID, versionID, string(payloadRef), gc.PayloadRef); err != nil {
			return CommitVersionResult{}, err
		}
	}
	paths := make([]store.VersionPath, 0, len(manifest.Files))
	for _, entry := range manifest.Files {
		kind := "file"
		if entry.Executable {
			kind = "executable"
		}
		paths = append(paths, store.VersionPath{Path: entry.Path, Digest: string(entry.Digest), Kind: kind, SizeBytes: entry.SizeBytes})
	}
	version := store.ContentVersion{
		ID: versionID, Model: request.Model, PayloadRef: staged.payloadRef,
		RootDigest: staged.rootDigest, SizeBytes: staged.sizeBytes,
		FileCount: staged.fileCount, Checkpoint: request.Checkpoint,
	}
	operation, err := service.versionOperation(request, manifest, version, paths, now, kind, stageParentVersionID)
	if err != nil {
		return CommitVersionResult{}, err
	}
	params := store.PublishVersionParams{
		WorkspaceID: request.WorkspaceID, ContentID: request.ContentID,
		ExpectedHeadVersionID: request.ExpectedHeadVersionID, ExpectedGeneration: request.ExpectedGeneration,
		Actor: request.Actor, PublishedAt: now, Version: version, Paths: paths,
		Outbox: store.Outbox{ID: outboxID}, GC: gc, Operation: operation, AuthorityFence: request.AuthorityFence,
	}
	var published store.PublishVersionResult
	if kind == servicePublicationDetachedMigration {
		published, err = service.store.PublishDetachedVersion(ctx, store.PublishDetachedVersionParams{
			PublishVersionParams: params, ParentVersionID: stageParentVersionID,
		})
	} else {
		published, err = service.store.PublishVersion(ctx, params)
	}
	if err != nil {
		// A lost commit acknowledgement is ambiguous. The durable GC ledger must
		// reconcile reachability before staged payload deletion.
		return CommitVersionResult{}, err
	}
	requestDigest := ""
	if operation != nil {
		requestDigest = operation.RequestDigest
	}
	return CommitVersionResult{Content: contentFromStore(published.VersionedContent), Version: versionFromStore(published.Version), RequestDigest: requestDigest}, nil
}

type versionOperationEnvelope struct {
	Namespace             string                         `json:"namespace"`
	Version               uint64                         `json:"version"`
	WorkspaceID           string                         `json:"workspaceId"`
	ContentID             string                         `json:"contentId"`
	Model                 string                         `json:"model"`
	Checkpoint            bool                           `json:"checkpoint"`
	ExpectedHeadVersionID string                         `json:"expectedHeadVersionId"`
	ExpectedGeneration    uint64                         `json:"expectedGeneration"`
	Actor                 string                         `json:"actor"`
	BoundsPolicyNamespace string                         `json:"boundsPolicyNamespace"`
	BoundsPolicyVersion   uint64                         `json:"boundsPolicyVersion"`
	TreeLimits            contentpayload.Limits          `json:"treeLimits"`
	RootDigest            contentpayload.Digest          `json:"rootDigest"`
	SizeBytes             int64                          `json:"sizeBytes"`
	Files                 []contentpayload.ManifestEntry `json:"files"`
	PublicationKind       string                         `json:"publicationKind,omitempty"`
	ParentVersionID       string                         `json:"parentVersionId,omitempty"`
}

func (service *Service) versionOperation(request CommitVersionRequest, manifest contentpayload.Manifest, version store.ContentVersion, paths []store.VersionPath, now time.Time, kind servicePublicationKind, parentVersionID string) (*store.VersionOperation, error) {
	key := strings.TrimSpace(request.OperationKey)
	if key == "" {
		if strings.TrimSpace(request.ExpectedRequestDigest) != "" {
			return nil, fmt.Errorf("content version expected digest requires an operation key")
		}
		return nil, nil
	}
	boundsNamespace, limits, err := canonicalBoundsPolicy(request)
	if err != nil {
		return nil, err
	}
	envelope := versionOperationEnvelope{
		Namespace: strings.TrimSpace(request.OperationNamespace), Version: request.OperationVersion,
		WorkspaceID: strings.TrimSpace(request.WorkspaceID), ContentID: strings.TrimSpace(request.ContentID),
		Model: request.Model, Checkpoint: request.Checkpoint,
		ExpectedHeadVersionID: strings.TrimSpace(request.ExpectedHeadVersionID), ExpectedGeneration: request.ExpectedGeneration,
		Actor: strings.TrimSpace(request.Actor), BoundsPolicyNamespace: boundsNamespace,
		BoundsPolicyVersion: boundsPolicyVersion, TreeLimits: limits,
		RootDigest: manifest.RootDigest, SizeBytes: manifest.SizeBytes,
		Files: append([]contentpayload.ManifestEntry(nil), manifest.Files...),
	}
	if kind == servicePublicationDetachedMigration {
		envelope.PublicationKind = "detached_migration"
		envelope.ParentVersionID = strings.TrimSpace(parentVersionID)
	}
	encoded, err := json.Marshal(envelope)
	if err != nil {
		return nil, fmt.Errorf("encode content version operation envelope: %w", err)
	}
	sum := sha256.Sum256(encoded)
	digest := hex.EncodeToString(sum[:])
	if expected := strings.TrimSpace(request.ExpectedRequestDigest); expected != "" && expected != digest {
		return nil, store.ErrOperationConflict
	}
	id, err := service.newID("ctop")
	if err != nil {
		return nil, fmt.Errorf("generate content version operation id: %w", err)
	}
	var summaryBytes int64
	for _, path := range paths {
		summaryBytes += int64(len(path.Path) + len(path.Digest) + len(path.Kind) + 32)
	}
	version.SummaryBytes = summaryBytes
	return &store.VersionOperation{ID: id, OperationKey: key, RequestDigest: digest,
		OperationNamespace: envelope.Namespace, OperationVersion: envelope.Version,
		BoundsPolicyNamespace: envelope.BoundsPolicyNamespace, BoundsPolicyVersion: envelope.BoundsPolicyVersion,
		MaxFiles: limits.MaxFiles, MaxFileBytes: limits.MaxFileBytes,
		MaxTotalBytes: limits.MaxTotalBytes, MaxManifestBytes: limits.MaxManifestBytes,
		VersionID: version.ID, RootDigest: version.RootDigest, SizeBytes: version.SizeBytes,
		FileCount: version.FileCount, SummaryBytes: version.SummaryBytes, CreatedAt: now}, nil
}

func validateCommitRequestSemantics(request CommitVersionRequest) error {
	actor := strings.TrimSpace(request.Actor)
	if actor == "" || len(actor) > 64 {
		return fmt.Errorf("content version actor is invalid")
	}
	key := strings.TrimSpace(request.OperationKey)
	namespace := strings.TrimSpace(request.OperationNamespace)
	if (key == "") != (namespace == "") {
		return fmt.Errorf("content version operation identity is incomplete")
	}
	if key != "" {
		if request.OperationVersion == 0 {
			return fmt.Errorf("content version operation version is required")
		}
		if len(key) > 255 || len(namespace) > 64 || !validOperationNamespace(namespace) || !strings.HasPrefix(key, namespace+":") {
			return fmt.Errorf("content version operation namespace is invalid")
		}
	}
	if request.Model == store.ModelDocument && request.TreeLimits != (contentpayload.Limits{}) {
		return fmt.Errorf("document content does not accept tree limits")
	}
	return nil
}

func canonicalBoundsPolicy(request CommitVersionRequest) (string, contentpayload.Limits, error) {
	if request.Model == store.ModelTree {
		limits, err := contentpayload.NormalizeLimits(request.TreeLimits)
		return treeBoundsPolicyNamespace, limits, err
	}
	return documentBoundsPolicyNamespace, contentpayload.Limits{}, nil
}

// payloadHardLimits are the model owner's immutable staging bounds. They are
// deliberately separate from the receipt representation: document receipts
// persist fixed zero tree limits because callers cannot select document
// bounds, while the document backend still enforces its own non-zero limits.
func payloadHardLimits(request CommitVersionRequest) (contentpayload.Limits, error) {
	if request.Model == store.ModelTree {
		return contentpayload.NormalizeLimits(request.TreeLimits)
	}
	return contentpayload.Limits{
		MaxFiles:         contentpayload.DefaultDocumentMaxFiles,
		MaxFileBytes:     contentpayload.DefaultDocumentMaxFileBytes,
		MaxTotalBytes:    contentpayload.DefaultDocumentMaxTotalBytes,
		MaxManifestBytes: contentpayload.DefaultDocumentMaxIndexBytes,
	}, nil
}

func validOperationNamespace(value string) bool {
	if value == "" {
		return false
	}
	for _, char := range value {
		if (char < 'a' || char > 'z') && (char < '0' || char > '9') && char != '_' && char != '-' && char != '.' {
			return false
		}
	}
	return true
}

type stagedPayload struct {
	payloadRef string
	rootDigest string
	sizeBytes  int64
	fileCount  int
}

func (service *Service) stage(ctx context.Context, request CommitVersionRequest, versionID, parentVersionID string) (stagedPayload, contentpayload.Manifest, error) {
	switch request.Model {
	case store.ModelTree:
		var parentPayloadRef contentpayload.ManifestRef
		if strings.TrimSpace(parentVersionID) != "" {
			parent, err := service.store.GetContentVersion(ctx, request.WorkspaceID, request.ContentID, parentVersionID)
			if err != nil {
				if !errors.Is(err, gorm.ErrRecordNotFound) {
					return stagedPayload{}, contentpayload.Manifest{}, err
				}
			} else {
				parentPayloadRef = contentpayload.ManifestRef(parent.PayloadRef)
			}
		}
		staged, err := service.tree.StageTree(ctx, contentpayload.StageTreeRequest{
			WorkspaceID: request.WorkspaceID, ContentID: request.ContentID,
			VersionID: versionID, Files: request.Files, Limits: request.TreeLimits,
			ParentPayloadRef: parentPayloadRef, Actor: request.Actor,
		})
		if err != nil {
			return stagedPayload{}, contentpayload.Manifest{}, err
		}
		manifest, err := service.tree.OpenManifest(ctx, staged.ManifestRef)
		return stagedPayload{payloadRef: string(staged.ManifestRef), rootDigest: string(staged.RootDigest), sizeBytes: staged.SizeBytes, fileCount: staged.FileCount}, manifest, err
	case store.ModelDocument:
		staged, err := service.document.StageDocument(ctx, contentpayload.StageDocumentRequest{
			WorkspaceID: request.WorkspaceID, ContentID: request.ContentID,
			VersionID: versionID, Files: request.Files,
		})
		if err != nil {
			return stagedPayload{}, contentpayload.Manifest{}, err
		}
		manifest, err := service.document.OpenManifest(ctx, staged.PayloadRef)
		return stagedPayload{payloadRef: string(staged.PayloadRef), rootDigest: string(staged.RootDigest), sizeBytes: staged.SizeBytes, fileCount: staged.FileCount}, manifest, err
	default:
		return stagedPayload{}, contentpayload.Manifest{}, fmt.Errorf("unsupported content model %q", request.Model)
	}
}

func (service *Service) GetContent(ctx context.Context, workspaceID, contentID string) (Content, error) {
	content, err := service.store.GetVersionedContent(ctx, workspaceID, contentID)
	if err == nil {
		err = service.ensureActiveTreeModel(content.Model)
	}
	return contentFromStore(content), err
}

// FindContentByBinding resolves one exact workspace/domain binding without
// enumerating other content in the workspace.
func (service *Service) FindContentByBinding(ctx context.Context, workspaceID, domain, bindingKey string) (Content, bool, error) {
	content, err := service.store.GetVersionedContentByBinding(ctx, workspaceID, domain, bindingKey)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return Content{}, false, nil
	}
	if err != nil {
		return Content{}, false, err
	}
	if err := service.ensureActiveTreeModel(content.Model); err != nil {
		return Content{}, false, err
	}
	return contentFromStore(content), true, nil
}

func (service *Service) ensureActiveTreeModel(model string) error {
	if service.treeOnly && model != store.ModelTree {
		return store.ErrModelConflict
	}
	return nil
}

// IsBindingConflict reports a concurrent or duplicate binding creation.
func IsBindingConflict(err error) bool { return errors.Is(err, store.ErrBindingConflict) }

// IsNotFound keeps persistence implementation errors behind the service
// boundary while preserving errors.Is behavior for existing service callers.
func IsNotFound(err error) bool { return errors.Is(err, gorm.ErrRecordNotFound) }

// IsConflict classifies optimistic concurrency, model, and pin conflicts for
// transports without exposing store-domain sentinels.
func IsConflict(err error) bool {
	return errors.Is(err, store.ErrHeadConflict) ||
		errors.Is(err, store.ErrGenerationConflict) ||
		errors.Is(err, store.ErrModelConflict) ||
		errors.Is(err, store.ErrParentConflict) ||
		errors.Is(err, store.ErrPinConflict)
}

// IsQuotaExceeded classifies bounded admission failures for transports.
func IsQuotaExceeded(err error) bool { return errors.Is(err, store.ErrQuotaExceeded) }

func (service *Service) GetVersion(ctx context.Context, workspaceID, contentID, versionID string) (Version, error) {
	version, err := service.store.GetContentVersion(ctx, workspaceID, contentID, versionID)
	if err == nil {
		err = service.ensureActiveTreeModel(version.Model)
	}
	return versionFromStore(version), err
}

// ListVersionPaths returns the bounded path projection committed with one
// exact immutable version. Consumers use this instead of walking payload
// storage or enumerating unrelated versions.
func (service *Service) ListVersionPaths(ctx context.Context, workspaceID, contentID, versionID string, limit int) ([]Path, error) {
	rows, err := service.store.LoadVersionPaths(ctx, workspaceID, contentID, versionID, limit)
	if err != nil {
		return nil, err
	}
	return pathsFromStore(rows), nil
}

func (service *Service) ListVersions(ctx context.Context, workspaceID, contentID string, limit int) ([]Version, error) {
	if limit <= 0 || limit > MaxVersionListRows {
		return nil, fmt.Errorf("content version list limit must be between 1 and %d", MaxVersionListRows)
	}
	versions, err := service.store.ListVersions(ctx, workspaceID, contentID, limit)
	if err != nil {
		return nil, err
	}
	result := make([]Version, len(versions))
	for index := range versions {
		if err := service.ensureActiveTreeModel(versions[index].Model); err != nil {
			return nil, err
		}
		result[index] = versionFromStore(versions[index])
	}
	return result, nil
}

func (service *Service) PathHistory(ctx context.Context, workspaceID, contentID, path string, limit int) ([]Path, error) {
	if limit <= 0 || limit > MaxPathHistoryRows {
		return nil, fmt.Errorf("content path history limit must be between 1 and %d", MaxPathHistoryRows)
	}
	rows, err := service.store.PathHistory(ctx, workspaceID, contentID, path, limit)
	if err != nil {
		return nil, err
	}
	return pathsFromStore(rows), nil
}

type CompareEntry struct {
	Path         string
	Change       string
	BeforeDigest string
	AfterDigest  string
	BeforeBytes  int64
	AfterBytes   int64
}

func (service *Service) Compare(ctx context.Context, workspaceID, contentID, beforeVersionID, afterVersionID string) ([]CompareEntry, error) {
	before, err := service.store.LoadVersionPaths(ctx, workspaceID, contentID, beforeVersionID, store.MaxCompareEntries)
	if err != nil {
		return nil, err
	}
	after, err := service.store.LoadVersionPaths(ctx, workspaceID, contentID, afterVersionID, store.MaxCompareEntries)
	if err != nil {
		return nil, err
	}
	beforeByPath := make(map[string]store.VersionPath, len(before))
	afterByPath := make(map[string]store.VersionPath, len(after))
	keys := make(map[string]struct{}, len(before)+len(after))
	for _, row := range before {
		beforeByPath[row.Path] = row
		keys[row.Path] = struct{}{}
	}
	for _, row := range after {
		afterByPath[row.Path] = row
		keys[row.Path] = struct{}{}
	}
	paths := make([]string, 0, len(keys))
	for path := range keys {
		paths = append(paths, path)
	}
	sort.Strings(paths)
	result := make([]CompareEntry, 0, len(paths))
	var resultBytes int64
	for _, path := range paths {
		old, hadOld := beforeByPath[path]
		current, hasCurrent := afterByPath[path]
		if hadOld && hasCurrent && old.Digest == current.Digest && old.Kind == current.Kind {
			continue
		}
		entry := CompareEntry{Path: path, BeforeDigest: old.Digest, AfterDigest: current.Digest, BeforeBytes: old.SizeBytes, AfterBytes: current.SizeBytes}
		switch {
		case !hadOld:
			entry.Change = "added"
		case !hasCurrent:
			entry.Change = "deleted"
		default:
			entry.Change = "modified"
		}
		resultBytes += int64(len(entry.Path) + len(entry.BeforeDigest) + len(entry.AfterDigest) + len(entry.Change) + 32)
		if len(result) >= store.MaxCompareEntries || resultBytes > store.MaxCompareResultBytes {
			return nil, fmt.Errorf("content compare exceeds result bounds")
		}
		result = append(result, entry)
	}
	return result, nil
}

func (service *Service) OpenFile(ctx context.Context, workspaceID, contentID, versionID, path string, byteRange *contentpayload.ByteRange) (io.ReadCloser, contentpayload.BlobMeta, error) {
	version, err := service.store.GetContentVersion(ctx, workspaceID, contentID, versionID)
	if err != nil {
		return nil, contentpayload.BlobMeta{}, err
	}
	if err := service.ensureActiveTreeModel(version.Model); err != nil {
		return nil, contentpayload.BlobMeta{}, err
	}
	ref := contentpayload.ManifestRef(version.PayloadRef)
	if version.Model == store.ModelDocument {
		return service.document.OpenFile(ctx, ref, path, byteRange)
	}
	return service.tree.OpenFile(ctx, ref, path, byteRange)
}

// MaterializeTree restores one exact immutable tree version into an empty
// caller-owned directory. The explicit limits keep consumer work tied to the
// requested version rather than to unrelated retained history.
func (service *Service) MaterializeTree(ctx context.Context, workspaceID, contentID, versionID, target string, limits contentpayload.MaterializeLimits) error {
	if limits.MaxFiles <= 0 || limits.MaxFiles > contentpayload.DefaultLimits.MaxFiles ||
		limits.MaxFileBytes <= 0 || limits.MaxFileBytes > contentpayload.DefaultLimits.MaxFileBytes ||
		limits.MaxTotalBytes <= 0 || limits.MaxTotalBytes > contentpayload.DefaultLimits.MaxTotalBytes {
		return fmt.Errorf("content materialize limits are invalid")
	}
	version, err := service.store.GetContentVersion(ctx, workspaceID, contentID, versionID)
	if err != nil {
		return err
	}
	if version.Model != store.ModelTree {
		return store.ErrModelConflict
	}
	return service.tree.Materialize(ctx, contentpayload.ManifestRef(version.PayloadRef), target, limits)
}

// OpenTreeArchive streams one exact immutable tree version as a bounded ZIP.
// The manifest is loaded and validated before the pipe is returned, so scope,
// model, and limit failures occur before an HTTP response begins.
func (service *Service) OpenTreeArchive(ctx context.Context, workspaceID, contentID, versionID string, limits ArchiveLimits) (io.ReadCloser, ArchiveMeta, error) {
	if limits.MaxFiles <= 0 || limits.MaxFiles > contentpayload.DefaultLimits.MaxFiles || limits.MaxFileBytes <= 0 || limits.MaxFileBytes > contentpayload.DefaultLimits.MaxFileBytes || limits.MaxTotalBytes <= 0 || limits.MaxTotalBytes > contentpayload.DefaultLimits.MaxTotalBytes {
		return nil, ArchiveMeta{}, fmt.Errorf("content archive limits are invalid")
	}
	version, err := service.store.GetContentVersion(ctx, workspaceID, contentID, versionID)
	if err != nil {
		return nil, ArchiveMeta{}, err
	}
	if version.Model != store.ModelTree {
		return nil, ArchiveMeta{}, store.ErrModelConflict
	}
	manifest, err := service.tree.OpenManifest(ctx, contentpayload.ManifestRef(version.PayloadRef))
	if err != nil {
		return nil, ArchiveMeta{}, err
	}
	if manifest.WorkspaceID != version.WorkspaceID || manifest.ContentID != version.ContentID || manifest.VersionID != version.ID || string(manifest.RootDigest) != version.RootDigest || manifest.SizeBytes != version.SizeBytes || len(manifest.Files) != version.FileCount {
		return nil, ArchiveMeta{}, fmt.Errorf("content archive manifest does not match immutable version")
	}
	if len(manifest.Files) > limits.MaxFiles || manifest.SizeBytes > limits.MaxTotalBytes {
		return nil, ArchiveMeta{}, fmt.Errorf("content archive exceeds request bounds")
	}
	for _, entry := range manifest.Files {
		if entry.SizeBytes < 0 || entry.SizeBytes > limits.MaxFileBytes {
			return nil, ArchiveMeta{}, fmt.Errorf("content archive file exceeds request bounds")
		}
	}
	reader, writer := io.Pipe()
	logging.Go(slog.Default(), func() {
		writer.CloseWithError(service.writeTreeArchive(ctx, writer, contentpayload.ManifestRef(version.PayloadRef), manifest, version.CreatedAt))
	})
	return reader, ArchiveMeta{FileCount: len(manifest.Files), SizeBytes: manifest.SizeBytes}, nil
}

func (service *Service) writeTreeArchive(ctx context.Context, output io.Writer, exactRef contentpayload.ManifestRef, manifest contentpayload.Manifest, modifiedAt time.Time) error {
	archive := zip.NewWriter(output)
	for _, entry := range manifest.Files {
		if err := ctx.Err(); err != nil {
			_ = archive.Close()
			return err
		}
		header := &zip.FileHeader{Name: entry.Path, Method: zip.Store}
		header.SetModTime(modifiedAt)
		if entry.Executable {
			header.SetMode(0o755)
		} else {
			header.SetMode(0o644)
		}
		target, err := archive.CreateHeader(header)
		if err != nil {
			_ = archive.Close()
			return fmt.Errorf("create content archive entry %q: %w", entry.Path, err)
		}
		body, meta, err := service.tree.OpenFile(ctx, exactRef, entry.Path, nil)
		if err != nil {
			_ = archive.Close()
			return fmt.Errorf("open content archive entry %q: %w", entry.Path, err)
		}
		if meta.Digest != entry.Digest || meta.SizeBytes != entry.SizeBytes {
			_ = body.Close()
			_ = archive.Close()
			return fmt.Errorf("content archive entry %q metadata mismatch", entry.Path)
		}
		written, copyErr := io.Copy(target, archiveContextReader{ctx: ctx, reader: body})
		closeErr := body.Close()
		if copyErr != nil {
			_ = archive.Close()
			return fmt.Errorf("copy content archive entry %q: %w", entry.Path, copyErr)
		}
		if closeErr != nil {
			_ = archive.Close()
			return fmt.Errorf("close content archive entry %q: %w", entry.Path, closeErr)
		}
		if written != entry.SizeBytes {
			_ = archive.Close()
			return fmt.Errorf("content archive entry %q size mismatch", entry.Path)
		}
	}
	if err := archive.Close(); err != nil {
		return fmt.Errorf("close content archive: %w", err)
	}
	return nil
}

type archiveContextReader struct {
	ctx    context.Context
	reader io.Reader
}

func (reader archiveContextReader) Read(buffer []byte) (int, error) {
	if err := reader.ctx.Err(); err != nil {
		return 0, err
	}
	return reader.reader.Read(buffer)
}

func contentFromStore(content store.VersionedContent) Content {
	return Content{ID: content.ID, WorkspaceID: content.WorkspaceID, Domain: content.Domain, BindingKey: content.BindingKey, Model: content.Model, HeadVersionID: content.HeadVersionID, Revision: content.Revision, Generation: content.Generation, RetainedVersionCount: content.RetainedVersionCount, StoredPayloadBytes: content.StoredPayloadBytes, MaxRetainedVersions: content.MaxRetainedVersions, MaxStoredPayloadBytes: content.MaxStoredPayloadBytes, MaxPins: content.MaxPins, CreatedBy: content.CreatedBy, UpdatedBy: content.UpdatedBy, CreatedAt: content.CreatedAt, UpdatedAt: content.UpdatedAt}
}

func versionFromStore(version store.ContentVersion) Version {
	return Version{ID: version.ID, WorkspaceID: version.WorkspaceID, ContentID: version.ContentID, ParentVersionID: version.ParentVersionID, Generation: version.Generation, Model: version.Model, PayloadRef: version.PayloadRef, RootDigest: version.RootDigest, SizeBytes: version.SizeBytes, FileCount: version.FileCount, SummaryBytes: version.SummaryBytes, Checkpoint: version.Checkpoint, CreatedBy: version.CreatedBy, CreatedAt: version.CreatedAt}
}

func pinFromStore(pin store.VersionPin) Pin {
	return Pin{ID: pin.ID, WorkspaceID: pin.WorkspaceID, ContentID: pin.ContentID, VersionID: pin.VersionID, Label: pin.Label, CreatedBy: pin.CreatedBy, CreatedAt: pin.CreatedAt}
}

func pathsFromStore(rows []store.VersionPath) []Path {
	result := make([]Path, len(rows))
	for index, row := range rows {
		result[index] = Path{VersionID: row.VersionID, Path: row.Path, Digest: row.Digest, Kind: row.Kind, SizeBytes: row.SizeBytes, CreatedAt: row.CreatedAt}
	}
	return result
}

func ParseModel(value string) (string, error) {
	value = strings.TrimSpace(value)
	if value != store.ModelTree && value != store.ModelDocument {
		return "", fmt.Errorf("unsupported content model %q", value)
	}
	return value, nil
}
