package content

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

var (
	ErrHeadConflict       = errors.New("content head conflict")
	ErrGenerationConflict = errors.New("content generation conflict")
	ErrQuotaExceeded      = errors.New("content quota exceeded")
	ErrModelConflict      = errors.New("content model conflict")
	ErrPinConflict        = errors.New("content version pin conflict")
	ErrBindingConflict    = errors.New("content binding conflict")
	ErrOperationConflict  = errors.New("content version operation conflict")
	ErrParentConflict     = errors.New("content version parent conflict")
)

const (
	MaxSummaryBytes       = int64(1 << 20)
	MaxSummaryRows        = 10_000
	MaxHistoryRows        = 500
	MaxHistoryResultBytes = int64(1 << 20)
	MaxCompareEntries     = 2_048
	MaxCompareResultBytes = int64(2 << 20)

	DefaultDocumentVersions = uint64(512)
	DefaultDocumentBytes    = int64(1 << 30)
	DefaultDocumentPins     = uint64(64)
	DefaultTreeVersions     = uint64(2_048)
	DefaultTreeBytes        = int64(4 << 30)
	DefaultTreePins         = uint64(256)
)

type PublishVersionParams struct {
	ContentID             string
	WorkspaceID           string
	ExpectedHeadVersionID string
	ExpectedGeneration    uint64
	Version               ContentVersion
	Paths                 []VersionPath
	Outbox                Outbox
	GC                    StagedPayloadGC
	Actor                 string
	PublishedAt           time.Time
	Operation             *VersionOperation
	AuthorityFence        *LegacyAuthorityFence
}

type PublishVersionResult struct {
	VersionedContent
	Version  ContentVersion
	Replayed bool
}

// PublishDetachedVersionParams is reserved for bounded migration owners. The
// live head is an exact CAS fence, while ParentVersionID independently selects
// the immutable parent of the imported version.
type PublishDetachedVersionParams struct {
	PublishVersionParams
	ParentVersionID string
}

type publicationKind uint8

const (
	publicationAdvanceHead publicationKind = iota
	publicationDetached
)

// RegisterStagedPayloadGC durably records the intended payload before object
// staging. A reconciler may delete it only after retention and after proving no
// content_versions row references the version.
func (s *Store) RegisterStagedPayloadGC(ctx context.Context, row StagedPayloadGC) error {
	if err := s.ensureDB(); err != nil {
		return err
	}
	row.ID = strings.TrimSpace(row.ID)
	row.WorkspaceID = strings.TrimSpace(row.WorkspaceID)
	row.ContentID = strings.TrimSpace(row.ContentID)
	row.VersionID = strings.TrimSpace(row.VersionID)
	row.Model = strings.TrimSpace(row.Model)
	row.PayloadRef = strings.TrimSpace(row.PayloadRef)
	if row.ID == "" || row.WorkspaceID == "" || row.ContentID == "" || row.VersionID == "" || (row.Model != ModelTree && row.Model != ModelDocument) || row.PayloadRef == "" || row.DeleteAfter.IsZero() {
		return fmt.Errorf("content gc identity and retention are required")
	}
	row.Status = GCStatusPending
	if row.CreatedAt.IsZero() {
		row.CreatedAt = time.Now().UTC()
	}
	row.UpdatedAt = row.CreatedAt
	if err := s.db.WithContext(ctx).Create(&row).Error; err != nil {
		return fmt.Errorf("register staged content payload: %w", err)
	}
	return nil
}

// UpdateStagedPayloadReference replaces the deterministic pre-stage reference
// with the backend's exact immutable reference before publication.
func (s *Store) UpdateStagedPayloadReference(ctx context.Context, id, workspaceID, contentID, versionID, oldRef, exactRef string) error {
	if err := s.ensureDB(); err != nil {
		return err
	}
	result := s.db.WithContext(ctx).Model(&StagedPayloadGC{}).
		Where("id = ? AND workspace_id = ? AND content_id = ? AND version_id = ? AND payload_ref = ? AND status = ?", strings.TrimSpace(id), strings.TrimSpace(workspaceID), strings.TrimSpace(contentID), strings.TrimSpace(versionID), strings.TrimSpace(oldRef), GCStatusPending).
		Update("payload_ref", strings.TrimSpace(exactRef))
	if result.Error != nil {
		return fmt.Errorf("update staged content payload reference: %w", result.Error)
	}
	if result.RowsAffected != 1 {
		return fmt.Errorf("update staged content payload reference: expected one pending row")
	}
	return nil
}

// PublishVersion atomically inserts one immutable version and its bounded path
// summary, advances the head with compare-and-swap, accounts quota, and
// enqueues its event. Work is bounded to one requested content/version.
func (s *Store) PublishVersion(ctx context.Context, params PublishVersionParams) (PublishVersionResult, error) {
	return s.publishVersion(ctx, params, publicationAdvanceHead)
}

// PublishDetachedVersion commits the same immutable publication transaction
// as PublishVersion but leaves the live head unchanged. It is a narrow store
// primitive for the migration service, not a mode on ordinary publication.
func (s *Store) PublishDetachedVersion(ctx context.Context, params PublishDetachedVersionParams) (PublishVersionResult, error) {
	base := params.PublishVersionParams
	base.Version.ParentVersionID = params.ParentVersionID
	return s.publishVersion(ctx, base, publicationDetached)
}

func (s *Store) publishVersion(ctx context.Context, params PublishVersionParams, kind publicationKind) (PublishVersionResult, error) {
	if err := s.ensureDB(); err != nil {
		return PublishVersionResult{}, err
	}
	if err := normalizePublishParams(&params, kind); err != nil {
		return PublishVersionResult{}, err
	}

	var result PublishVersionResult
	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var locked VersionedContent
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("workspace_id = ? AND id = ?", params.WorkspaceID, params.ContentID).
			First(&locked).Error; err != nil {
			return fmt.Errorf("lock versioned content: %w", err)
		}
		if params.AuthorityFence != nil {
			if err := New(tx).RequireLegacyAuthorityFence(ctx, *params.AuthorityFence, params.PublishedAt); err != nil {
				return err
			}
		}
		if kind == publicationDetached {
			if err := validateLockedPublication(locked, params); err != nil {
				return err
			}
			if err := validateDetachedParent(tx, params); err != nil {
				return err
			}
		}
		if params.Operation != nil {
			replayed, found, err := replayVersionOperation(tx, params, kind, locked)
			if err != nil {
				return err
			}
			if found {
				result = replayed
				return nil
			}
		}
		if err := validateLockedPublication(locked, params); err != nil {
			return err
		}
		if locked.RetainedVersionCount >= locked.MaxRetainedVersions || params.Version.SizeBytes > locked.MaxStoredPayloadBytes-locked.StoredPayloadBytes {
			return ErrQuotaExceeded
		}
		if err := tx.Create(&params.Version).Error; err != nil {
			return fmt.Errorf("create content version: %w", err)
		}
		if len(params.Paths) > 0 {
			if err := tx.CreateInBatches(&params.Paths, 256).Error; err != nil {
				return fmt.Errorf("create content version path summary: %w", err)
			}
		}
		updates := map[string]any{
			"revision":               gorm.Expr("revision + 1"),
			"retained_version_count": gorm.Expr("retained_version_count + 1"),
			"stored_payload_bytes":   gorm.Expr("stored_payload_bytes + ?", params.Version.SizeBytes),
			"updated_by":             params.Actor,
			"updated_at":             params.PublishedAt,
		}
		if kind == publicationAdvanceHead {
			updates["head_version_id"] = params.Version.ID
		}
		update := tx.Model(&VersionedContent{}).
			Where("workspace_id = ? AND id = ? AND head_version_id = ? AND generation = ?", params.WorkspaceID, params.ContentID, params.ExpectedHeadVersionID, params.ExpectedGeneration).
			Updates(updates)
		if update.Error != nil {
			return fmt.Errorf("publish content version: %w", update.Error)
		}
		if update.RowsAffected != 1 {
			return ErrHeadConflict
		}
		if err := tx.Create(&params.Outbox).Error; err != nil {
			return fmt.Errorf("create content outbox event: %w", err)
		}
		gcUpdate := tx.Model(&StagedPayloadGC{}).
			Where("id = ? AND workspace_id = ? AND content_id = ? AND version_id = ? AND model = ? AND payload_ref = ? AND status = ?", params.GC.ID, params.WorkspaceID, params.ContentID, params.Version.ID, params.Version.Model, params.Version.PayloadRef, GCStatusPending).
			Updates(map[string]any{"status": GCStatusRetained, "lease_owner": "", "lease_until": nil, "last_error": "", "completed_at": params.PublishedAt, "updated_at": params.PublishedAt})
		if gcUpdate.Error != nil {
			return fmt.Errorf("retain content gc record: %w", gcUpdate.Error)
		}
		if gcUpdate.RowsAffected != 1 {
			return fmt.Errorf("retain content gc record: expected one pending row")
		}
		if err := tx.Where("workspace_id = ? AND id = ?", params.WorkspaceID, params.ContentID).First(&result.VersionedContent).Error; err != nil {
			return fmt.Errorf("load published versioned content: %w", err)
		}
		if params.Operation != nil {
			params.Operation.ContentRevision = result.VersionedContent.Revision
			params.Operation.RetainedVersionCount = result.VersionedContent.RetainedVersionCount
			params.Operation.StoredPayloadBytes = result.VersionedContent.StoredPayloadBytes
			if err := tx.Create(params.Operation).Error; err != nil {
				return fmt.Errorf("create content version operation: %w", err)
			}
		}
		result.Version = params.Version
		return nil
	})
	return result, err
}

func validateLockedPublication(locked VersionedContent, params PublishVersionParams) error {
	if locked.HeadVersionID != params.ExpectedHeadVersionID {
		return ErrHeadConflict
	}
	if locked.Generation != params.ExpectedGeneration {
		return ErrGenerationConflict
	}
	if locked.Model != params.Version.Model {
		return ErrModelConflict
	}
	return nil
}

func validateDetachedParent(tx *gorm.DB, params PublishVersionParams) error {
	if params.Version.ParentVersionID == "" {
		return nil
	}
	var parent ContentVersion
	err := tx.Where("id = ?", params.Version.ParentVersionID).First(&parent).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return ErrParentConflict
	}
	if err != nil {
		return fmt.Errorf("load detached content version parent: %w", err)
	}
	if parent.WorkspaceID != params.WorkspaceID || parent.ContentID != params.ContentID ||
		parent.Generation != params.ExpectedGeneration || parent.Model != params.Version.Model {
		return ErrParentConflict
	}
	return nil
}

func replayVersionOperation(tx *gorm.DB, params PublishVersionParams, kind publicationKind, locked VersionedContent) (PublishVersionResult, bool, error) {
	var existing VersionOperation
	err := tx.Where("workspace_id = ? AND content_id = ? AND operation_key = ?", params.WorkspaceID, params.ContentID, params.Operation.OperationKey).First(&existing).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return PublishVersionResult{}, false, nil
	}
	if err != nil {
		return PublishVersionResult{}, false, fmt.Errorf("load content version operation: %w", err)
	}
	if existing.RequestDigest != params.Operation.RequestDigest ||
		existing.OperationNamespace != params.Operation.OperationNamespace ||
		existing.OperationVersion != params.Operation.OperationVersion ||
		existing.BoundsPolicyNamespace != params.Operation.BoundsPolicyNamespace ||
		existing.BoundsPolicyVersion != params.Operation.BoundsPolicyVersion ||
		existing.MaxFiles != params.Operation.MaxFiles ||
		existing.MaxFileBytes != params.Operation.MaxFileBytes ||
		existing.MaxTotalBytes != params.Operation.MaxTotalBytes ||
		existing.MaxManifestBytes != params.Operation.MaxManifestBytes ||
		existing.ExpectedHeadVersionID != params.ExpectedHeadVersionID ||
		existing.ExpectedGeneration != params.ExpectedGeneration ||
		existing.Actor != params.Actor || existing.Model != params.Version.Model || existing.Checkpoint != params.Version.Checkpoint {
		return PublishVersionResult{}, false, ErrOperationConflict
	}
	var version ContentVersion
	if err := tx.Where("workspace_id = ? AND content_id = ? AND id = ?", params.WorkspaceID, params.ContentID, existing.VersionID).First(&version).Error; err != nil {
		return PublishVersionResult{}, false, fmt.Errorf("load content version operation result: %w", err)
	}
	if version.RootDigest != existing.RootDigest || version.SizeBytes != existing.SizeBytes || version.FileCount != existing.FileCount || version.SummaryBytes != existing.SummaryBytes ||
		version.ParentVersionID != params.Version.ParentVersionID || version.Generation != params.ExpectedGeneration || version.Model != params.Version.Model {
		return PublishVersionResult{}, false, ErrOperationConflict
	}
	content := locked
	if kind == publicationDetached {
		content.HeadVersionID = params.ExpectedHeadVersionID
	} else {
		content.HeadVersionID = existing.VersionID
	}
	content.Revision = existing.ContentRevision
	content.Generation = existing.ExpectedGeneration
	content.RetainedVersionCount = existing.RetainedVersionCount
	content.StoredPayloadBytes = existing.StoredPayloadBytes
	content.UpdatedBy = existing.Actor
	content.UpdatedAt = existing.CreatedAt
	return PublishVersionResult{VersionedContent: content, Version: version, Replayed: true}, true, nil
}

func normalizePublishParams(params *PublishVersionParams, kind publicationKind) error {
	params.ContentID = strings.TrimSpace(params.ContentID)
	params.WorkspaceID = strings.TrimSpace(params.WorkspaceID)
	params.ExpectedHeadVersionID = strings.TrimSpace(params.ExpectedHeadVersionID)
	params.Actor = strings.TrimSpace(params.Actor)
	params.Version.ID = strings.TrimSpace(params.Version.ID)
	params.Version.PayloadRef = strings.TrimSpace(params.Version.PayloadRef)
	params.Version.RootDigest = strings.TrimSpace(params.Version.RootDigest)
	params.Version.Model = strings.TrimSpace(params.Version.Model)
	if params.ContentID == "" || params.WorkspaceID == "" || params.Actor == "" || len(params.Actor) > 64 || params.Version.ID == "" || params.Version.PayloadRef == "" || params.Version.RootDigest == "" {
		return fmt.Errorf("content publish identity and payload are required")
	}
	if params.ExpectedGeneration == 0 || params.Version.SizeBytes < 0 || params.Version.FileCount < 0 || params.Version.SummaryBytes < 0 || params.Version.SummaryBytes > MaxSummaryBytes || len(params.Paths) > MaxSummaryRows {
		return fmt.Errorf("content version bounds are invalid")
	}
	if params.Version.ContentID != "" && strings.TrimSpace(params.Version.ContentID) != params.ContentID {
		return fmt.Errorf("content version belongs to another content")
	}
	if params.Version.WorkspaceID != "" && strings.TrimSpace(params.Version.WorkspaceID) != params.WorkspaceID {
		return fmt.Errorf("content version belongs to another workspace")
	}
	now := params.PublishedAt.UTC()
	if now.IsZero() {
		now = time.Now().UTC()
	}
	params.PublishedAt = now
	params.Version.ContentID = params.ContentID
	params.Version.WorkspaceID = params.WorkspaceID
	if kind == publicationAdvanceHead {
		params.Version.ParentVersionID = params.ExpectedHeadVersionID
	} else {
		params.Version.ParentVersionID = strings.TrimSpace(params.Version.ParentVersionID)
		if params.Version.ParentVersionID == params.Version.ID {
			return ErrParentConflict
		}
	}
	params.Version.Generation = params.ExpectedGeneration
	params.Version.CreatedBy = params.Actor
	params.Version.CreatedAt = now
	if err := normalizePaths(params); err != nil {
		return err
	}
	params.Outbox.ID = strings.TrimSpace(params.Outbox.ID)
	if params.Outbox.ID == "" {
		return fmt.Errorf("content outbox id is required")
	}
	if params.Outbox.WorkspaceID != "" && strings.TrimSpace(params.Outbox.WorkspaceID) != params.WorkspaceID {
		return fmt.Errorf("content outbox belongs to another workspace")
	}
	params.Outbox.WorkspaceID = params.WorkspaceID
	params.Outbox.ContentID = params.ContentID
	params.Outbox.VersionID = params.Version.ID
	params.Outbox.EventType = OutboxVersionCommitted
	params.Outbox.Status = OutboxStatusPending
	params.Outbox.NextAttemptAt = now
	params.Outbox.CreatedAt = now
	params.Outbox.UpdatedAt = now
	params.GC.ID = strings.TrimSpace(params.GC.ID)
	if params.GC.ID == "" {
		return fmt.Errorf("content gc id is required")
	}
	params.GC.WorkspaceID = params.WorkspaceID
	params.GC.ContentID = params.ContentID
	params.GC.VersionID = params.Version.ID
	params.GC.Model = params.Version.Model
	params.GC.PayloadRef = params.Version.PayloadRef
	params.GC.Status = GCStatusRetained
	params.GC.DeleteAfter = now
	params.GC.CreatedAt = now
	params.GC.UpdatedAt = now
	params.GC.CompletedAt = &now
	if params.Operation != nil {
		op := params.Operation
		op.ID = strings.TrimSpace(op.ID)
		op.WorkspaceID = params.WorkspaceID
		op.ContentID = params.ContentID
		op.OperationKey = strings.TrimSpace(op.OperationKey)
		op.RequestDigest = strings.TrimSpace(op.RequestDigest)
		op.OperationNamespace = strings.TrimSpace(op.OperationNamespace)
		op.BoundsPolicyNamespace = strings.TrimSpace(op.BoundsPolicyNamespace)
		if op.ID == "" || op.OperationKey == "" || len(op.OperationKey) > 255 || op.RequestDigest == "" || len(op.RequestDigest) != 64 || op.OperationNamespace == "" || len(op.OperationNamespace) > 64 || op.OperationVersion == 0 || op.BoundsPolicyNamespace == "" || len(op.BoundsPolicyNamespace) > 64 || op.BoundsPolicyVersion == 0 || op.MaxFiles < 0 || op.MaxFileBytes < 0 || op.MaxTotalBytes < 0 || op.MaxManifestBytes < 0 {
			return fmt.Errorf("content version operation identity is invalid")
		}
		op.ExpectedHeadVersionID = params.ExpectedHeadVersionID
		op.ExpectedGeneration = params.ExpectedGeneration
		op.Actor = params.Actor
		op.Model = params.Version.Model
		op.Checkpoint = params.Version.Checkpoint
		op.VersionID = params.Version.ID
		op.RootDigest = params.Version.RootDigest
		op.SizeBytes = params.Version.SizeBytes
		op.FileCount = params.Version.FileCount
		op.SummaryBytes = params.Version.SummaryBytes
		op.CreatedAt = now
	}
	return nil
}

func normalizePaths(params *PublishVersionParams) error {
	var summaryBytes int64
	seen := make(map[string]struct{}, len(params.Paths))
	for index := range params.Paths {
		row := &params.Paths[index]
		row.Path = strings.TrimSpace(row.Path)
		row.Digest = strings.TrimSpace(row.Digest)
		row.Kind = strings.TrimSpace(row.Kind)
		if row.Path == "" || row.Digest == "" || row.Kind == "" || row.SizeBytes < 0 {
			return fmt.Errorf("content version path summary is invalid")
		}
		row.PathKey = PathKey(row.Path)
		if _, ok := seen[row.PathKey]; ok {
			return fmt.Errorf("content version path summary contains duplicate path %q", row.Path)
		}
		seen[row.PathKey] = struct{}{}
		row.ID = VersionPathID(params.Version.ID, row.PathKey)
		row.WorkspaceID = params.WorkspaceID
		row.ContentID = params.ContentID
		row.VersionID = params.Version.ID
		row.CreatedAt = params.PublishedAt
		summaryBytes += int64(len(row.Path) + len(row.Digest) + len(row.Kind) + 32)
		if summaryBytes > MaxSummaryBytes {
			return fmt.Errorf("content version path summary exceeds byte limit")
		}
	}
	if params.Version.SummaryBytes != 0 && params.Version.SummaryBytes != summaryBytes {
		return fmt.Errorf("content version summary byte count mismatch")
	}
	params.Version.SummaryBytes = summaryBytes
	return nil
}

func PathKey(path string) string {
	sum := sha256.Sum256([]byte(path))
	return hex.EncodeToString(sum[:])
}

func VersionPathID(versionID, pathKey string) string {
	sum := sha256.Sum256([]byte(strings.TrimSpace(versionID) + "\x00" + strings.TrimSpace(pathKey)))
	return hex.EncodeToString(sum[:])
}

func (s *Store) CreateVersionedContent(ctx context.Context, content VersionedContent) (VersionedContent, error) {
	if err := s.ensureDB(); err != nil {
		return VersionedContent{}, err
	}
	content.ID = strings.TrimSpace(content.ID)
	content.WorkspaceID = strings.TrimSpace(content.WorkspaceID)
	content.Domain = strings.TrimSpace(content.Domain)
	content.BindingKey = strings.TrimSpace(content.BindingKey)
	content.Model = strings.TrimSpace(content.Model)
	content.CreatedBy = strings.TrimSpace(content.CreatedBy)
	if content.ID == "" || content.WorkspaceID == "" || content.Domain == "" || content.BindingKey == "" || content.CreatedBy == "" || (content.Model != ModelTree && content.Model != ModelDocument) {
		return VersionedContent{}, fmt.Errorf("versioned content identity is required")
	}
	if content.Generation == 0 {
		content.Generation = 1
	}
	applyDefaultQuota(&content)
	if content.UpdatedBy == "" {
		content.UpdatedBy = content.CreatedBy
	}
	if content.CreatedAt.IsZero() {
		content.CreatedAt = time.Now().UTC()
	}
	if content.UpdatedAt.IsZero() {
		content.UpdatedAt = content.CreatedAt
	}
	created := s.db.WithContext(ctx).Clauses(clause.OnConflict{
		Columns:   []clause.Column{{Name: "workspace_id"}, {Name: "domain"}, {Name: "binding_key"}},
		DoNothing: true,
	}).Create(&content)
	if created.Error != nil {
		return VersionedContent{}, fmt.Errorf("create versioned content: %w", created.Error)
	}
	if created.RowsAffected != 1 {
		return VersionedContent{}, ErrBindingConflict
	}
	return content, nil
}

func applyDefaultQuota(content *VersionedContent) {
	if content.Model == ModelDocument {
		if content.MaxRetainedVersions == 0 {
			content.MaxRetainedVersions = DefaultDocumentVersions
		}
		if content.MaxStoredPayloadBytes == 0 {
			content.MaxStoredPayloadBytes = DefaultDocumentBytes
		}
		if content.MaxPins == 0 {
			content.MaxPins = DefaultDocumentPins
		}
		return
	}
	if content.MaxRetainedVersions == 0 {
		content.MaxRetainedVersions = DefaultTreeVersions
	}
	if content.MaxStoredPayloadBytes == 0 {
		content.MaxStoredPayloadBytes = DefaultTreeBytes
	}
	if content.MaxPins == 0 {
		content.MaxPins = DefaultTreePins
	}
}

func (s *Store) GetVersionedContent(ctx context.Context, workspaceID, contentID string) (VersionedContent, error) {
	if err := s.ensureDB(); err != nil {
		return VersionedContent{}, err
	}
	var content VersionedContent
	err := s.db.WithContext(ctx).Where("workspace_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(contentID)).First(&content).Error
	return content, err
}

// GetVersionedContentByBinding performs one exact unique-key lookup. Its cost
// is independent of other content in the workspace.
func (s *Store) GetVersionedContentByBinding(ctx context.Context, workspaceID, domain, bindingKey string) (VersionedContent, error) {
	if err := s.ensureDB(); err != nil {
		return VersionedContent{}, err
	}
	var content VersionedContent
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND domain = ? AND binding_key = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(domain), strings.TrimSpace(bindingKey)).
		First(&content).Error
	return content, err
}

func (s *Store) GetContentVersion(ctx context.Context, workspaceID, contentID, versionID string) (ContentVersion, error) {
	if err := s.ensureDB(); err != nil {
		return ContentVersion{}, err
	}
	var version ContentVersion
	err := s.db.WithContext(ctx).Where("workspace_id = ? AND content_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(contentID), strings.TrimSpace(versionID)).First(&version).Error
	return version, err
}

// GetVersionOperation performs one exact unique-key lookup. It never searches
// across content or workspace scopes.
func (s *Store) GetVersionOperation(ctx context.Context, workspaceID, contentID, operationKey string) (VersionOperation, error) {
	if err := s.ensureDB(); err != nil {
		return VersionOperation{}, err
	}
	var operation VersionOperation
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND content_id = ? AND operation_key = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(contentID), strings.TrimSpace(operationKey)).
		First(&operation).Error
	return operation, err
}

func (s *Store) ListVersions(ctx context.Context, workspaceID, contentID string, limit int) ([]ContentVersion, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	if limit <= 0 || limit > MaxHistoryRows {
		return nil, fmt.Errorf("content version history limit must be between 1 and %d", MaxHistoryRows)
	}
	var versions []ContentVersion
	err := s.db.WithContext(ctx).Where("workspace_id = ? AND content_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(contentID)).Order("created_at DESC, id DESC").Limit(limit).Find(&versions).Error
	return versions, err
}

func (s *Store) LoadVersionPaths(ctx context.Context, workspaceID, contentID, versionID string, limit int) ([]VersionPath, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	if limit <= 0 || limit > MaxCompareEntries {
		return nil, fmt.Errorf("content version path limit must be between 1 and %d", MaxCompareEntries)
	}
	var paths []VersionPath
	err := s.db.WithContext(ctx).Where("workspace_id = ? AND content_id = ? AND version_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(contentID), strings.TrimSpace(versionID)).Order("path_key, id").Limit(limit + 1).Find(&paths).Error
	if err == nil && len(paths) > limit {
		return nil, fmt.Errorf("content version path result exceeds entry limit")
	}
	return paths, err
}

func (s *Store) PathHistory(ctx context.Context, workspaceID, contentID, path string, limit int) ([]VersionPath, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	path = strings.TrimSpace(path)
	if path == "" || limit <= 0 || limit > MaxHistoryRows {
		return nil, fmt.Errorf("content path history request is invalid")
	}
	var rows []VersionPath
	err := s.db.WithContext(ctx).Where("workspace_id = ? AND content_id = ? AND path_key = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(contentID), PathKey(path)).Order("created_at DESC, version_id DESC").Limit(limit).Find(&rows).Error
	if err != nil {
		return nil, err
	}
	var resultBytes int64
	for _, row := range rows {
		if row.Path != path {
			return nil, fmt.Errorf("content path digest collision")
		}
		resultBytes += int64(len(row.Path) + len(row.Digest) + len(row.Kind) + 64)
		if resultBytes > MaxHistoryResultBytes {
			return nil, fmt.Errorf("content path history exceeds byte limit")
		}
	}
	return rows, nil
}
