import { sql, type SQLWrapper } from 'drizzle-orm'
import type { TransactionIdentity } from '@platform-modules/db'
import { resolveFieldDefinitionVersion } from './definitions.js'
import {
  assertActiveFieldsTransaction,
  assertDefinitionMigration,
  assertFieldsAuthorized,
  assertReceiptReplay,
  authorizedFieldsContext,
  fieldsRequestHash,
  readFieldsReceipt,
  verifyFieldsBackup,
  writeFieldsReceipt,
  FieldsLifecycleError,
  type FieldsDestructiveDependencies,
} from './definition-lifecycle.js'
import type { EntityRef } from './model.js'
import type { FieldBlockDefinitionSnapshot, FieldGroup, FieldStorageValue, FieldsTransaction } from './schema.js'
import { createFieldTypeRegistry, type FieldsContext } from './types.js'
import { validateFieldValue, type FieldValueMap } from './values.js'
import type { FieldsReader, FieldsValueVersion } from './options.js'

export type BlockAttributeValue = string | number | boolean | null | readonly BlockAttributeValue[]
export interface BlockFieldGroupValue { readonly groupKey: string; readonly definitionRevision: number; readonly values: FieldValueMap }
export interface InlineBlockNode {
  readonly kind: 'inline'
  readonly id: string
  readonly type: string
  readonly blockDefinitionRevision: number
  readonly fieldGroups: readonly BlockFieldGroupValue[]
  readonly attributes?: Readonly<Record<string, BlockAttributeValue>>
  readonly children?: readonly BlockNode[]
}
export interface ReusableBlockNode { readonly kind: 'reusable'; readonly id: string; readonly reusableId: string }
export type BlockNode = InlineBlockNode | ReusableBlockNode
export interface BlockDocument { readonly version: 1; readonly roots: readonly BlockNode[] }
export interface FieldBlockDefinition extends Omit<FieldBlockDefinitionSnapshot, 'template'> { readonly template?: readonly BlockNode[] }
export interface CodeFieldBlock extends FieldBlockDefinition { readonly origin: 'code' }
export interface ResolvedFieldBlock extends FieldBlockDefinition {
  readonly origin: 'code' | 'db'
  readonly version: number
  readonly active: boolean
  readonly shadowedDbVersion?: number
}
export interface FieldBlockDefinitionVersion {
  readonly blockKey: string
  readonly revision: number
  readonly canonicalHash: string
  readonly definition: FieldBlockDefinition
  readonly createdAt: Date
  readonly origin: 'code' | 'db' | 'import'
}
export interface FieldBlockDefinitionResolver { resolve(blockKey: string, revision: number): Promise<FieldBlockDefinitionVersion | null> }
export interface ResolvedFieldBlockRegistry {
  readonly version: string
  readonly blocks: ReadonlyMap<string, ResolvedFieldBlock>
  readonly definitions: FieldBlockDefinitionResolver
}
export interface FieldBlockDefinitionImpact {
  readonly token: string
  readonly blockKey: string
  readonly expectedVersion: number
  readonly authorizationPolicyVersion: string
  readonly documentCorpusVersion: string
  readonly documentCorpusHash: string
  readonly affectedDocuments: number
  readonly affectedDefinitionVersions: number
  readonly incompatibilities: readonly string[]
}
export type FieldBlockDefinitionPatch = Partial<Omit<FieldBlockDefinition, 'key'>>
export type FieldBlockChangeStrategy =
  | { readonly kind: 'reject' }
  | { readonly kind: 'retainUnknown' }
  | { readonly kind: 'mapDefinition'; readonly replacementKey: string }
  | { readonly kind: 'purgeWithBackup'; readonly backupId: string }
export interface BlockPolicy { readonly maxBytes: number; readonly maxNodes: number; readonly maxDepth: number; readonly maxChildren: number; readonly maxAttributeBytes: number; readonly allowUnknownPreserved?: boolean }
export interface ReusableBlockResolver { resolve(id: string, context: FieldsContext): Promise<{ document: BlockDocument; version: number } | null> }
export interface BlockRenderer<R> { render(node: ValidatedInlineBlockNode, definition: FieldBlockDefinitionVersion, context: FieldsContext): Promise<R> | R }
export interface CurrentBlockDocument { readonly ref: EntityRef; readonly document: BlockDocument; readonly version: FieldsValueVersion; readonly definitionVersions: Readonly<Record<string, number>> }
export interface BlockDocumentMutationOptions { readonly expectedVersion: FieldsValueVersion; readonly operationId: string; readonly expectedTransactionIdentity: TransactionIdentity; readonly parentRevisionId: string }
export const EMPTY_BLOCK_DOCUMENT_VERSION: FieldsValueVersion = '0'

const validatedBlockNodeBrand: unique symbol = Symbol('validatedBlockNode') as never
const validatedBlockDocumentBrand: unique symbol = Symbol('validatedBlockDocument') as never
const validatedRegistryVersion: unique symbol = Symbol('validatedRegistryVersion') as never
export interface ValidatedInlineBlockNode extends Omit<InlineBlockNode, 'children'> { readonly children?: readonly ValidatedBlockNode[]; readonly [validatedBlockNodeBrand]: true }
export interface ValidatedReusableBlockNode extends ReusableBlockNode { readonly [validatedBlockNodeBrand]: true }
export type ValidatedBlockNode = ValidatedInlineBlockNode | ValidatedReusableBlockNode
export interface ValidatedBlockDocument extends Omit<BlockDocument, 'roots'> { readonly roots: readonly ValidatedBlockNode[]; readonly [validatedBlockDocumentBrand]: true; readonly [validatedRegistryVersion]: string }

interface CurrentBlockRow extends Record<string, unknown> {
  key: string; origin: 'code' | 'db' | 'import'; version: number | string; active: boolean | number; currentRevision: number | string
  canonicalHash: string; definition: unknown; shadowedDbVersion: number | string | null
}
interface BlockVersionRow extends Record<string, unknown> { blockKey: string; revision: number | string; canonicalHash: string; definition: unknown; createdAt: Date | string; origin: 'code' | 'db' | 'import' }
interface GroupCurrentRow extends Record<string, unknown> { key: string; currentRevision: number | string; active: boolean | number }
interface DocumentRow extends Record<string, unknown> { entityType: string; entityId: string; version: number | string; document: unknown; definitionVersions: unknown; parentRevisionId: string; updatedBy: string; updatedAt: Date | string }

function rowsOf<Row extends Record<string, unknown>>(value: unknown): readonly Row[] {
  if (Array.isArray(value)) return value as Row[]
  if (typeof value === 'object' && value !== null && Array.isArray((value as { rows?: unknown }).rows)) return (value as { rows: Row[] }).rows
  throw new FieldsLifecycleError('integrity', 'database adapter returned an invalid row result')
}
async function executeRows<Row extends Record<string, unknown>>(db: FieldsReader, query: SQLWrapper): Promise<readonly Row[]> { return rowsOf<Row>(await db.execute(query)) }
function parseJson(value: unknown, field: string): unknown { if (typeof value !== 'string') return value; try { return JSON.parse(value) as unknown } catch { throw new FieldsLifecycleError('integrity', `${field} contains invalid JSON`) } }
function object(value: unknown, field: string): Record<string, unknown> { if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) throw new FieldsLifecycleError('validation', 'must be a plain object', field); return value as Record<string, unknown> }
function string(value: unknown, field: string, max = 4096): string { if (typeof value !== 'string' || value.length === 0 || value.length > max) throw new FieldsLifecycleError('validation', 'must be a bounded non-empty string', field); return value }
function key(value: unknown, field: string): string { const result = string(value, field, 128); if (!/^[a-z][a-z0-9_-]*$/.test(result)) throw new FieldsLifecycleError('validation', 'must be a normalized key', field); return result }
function positiveInt(value: unknown, field: string): number { const result = Number(value); if (!Number.isSafeInteger(result) || result < 1) throw new FieldsLifecycleError('validation', 'must be a positive safe integer', field); return result }
function bool(value: unknown): boolean { return value === true || value === 1 }
function uniqueStrings(value: unknown, field: string, maxItems = 128): readonly string[] {
  if (!Array.isArray(value) || value.length > maxItems) throw new FieldsLifecycleError('validation', 'must be a bounded array', field)
  const result = value.map((item, index) => string(item, `${field}.${index}`, 256))
  if (new Set(result).size !== result.length) throw new FieldsLifecycleError('validation', 'must not contain duplicates', field)
  return Object.freeze(result)
}
function jsonValue(value: unknown, field: string, depth = 0, budget = { nodes: 0, bytes: 0 }): FieldStorageValue {
  if (depth > 20 || ++budget.nodes > 5000) throw new FieldsLifecycleError('validation', 'exceeds structural bounds', field)
  if (value === null || typeof value === 'boolean') return value
  if (typeof value === 'number') { if (!Number.isFinite(value)) throw new FieldsLifecycleError('validation', 'contains non-finite number', field); budget.bytes += 8; return value }
  if (typeof value === 'string') { budget.bytes += new TextEncoder().encode(value).byteLength; if (budget.bytes > 1024 * 1024) throw new FieldsLifecycleError('validation', 'exceeds byte bound', field); return value }
  if (Array.isArray(value)) return Object.freeze(value.map((item, index) => jsonValue(item, `${field}.${index}`, depth + 1, budget)))
  const source = object(value, field), output: Record<string, FieldStorageValue> = {}
  for (const name of Object.keys(source).sort()) { if (name === '__proto__' || name === 'prototype' || name === 'constructor') throw new FieldsLifecycleError('validation', 'contains forbidden key', `${field}.${name}`); output[name] = jsonValue(source[name], `${field}.${name}`, depth + 1, budget) }
  return Object.freeze(output)
}

function parseAttributes(value: unknown, policy: BlockPolicy, field: string): Readonly<Record<string, BlockAttributeValue>> | undefined {
  if (value === undefined) return undefined
  const source = object(value, field), output: Record<string, BlockAttributeValue> = {}
  let bytes = 0
  const parse = (input: unknown, path: string, depth = 0): BlockAttributeValue => {
    if (depth > 8) throw new FieldsLifecycleError('validation', 'attribute exceeds maximum depth', path)
    if (input === null || typeof input === 'boolean') return input
    if (typeof input === 'number') { if (!Number.isFinite(input)) throw new FieldsLifecycleError('validation', 'attribute contains non-finite number', path); bytes += 8; return input }
    if (typeof input === 'string') { bytes += new TextEncoder().encode(input).byteLength; if (bytes > policy.maxAttributeBytes) throw new FieldsLifecycleError('validation', 'attributes exceed byte limit', field); return input }
    if (Array.isArray(input)) return Object.freeze(input.map((item, index) => parse(item, `${path}.${index}`, depth + 1)))
    throw new FieldsLifecycleError('validation', 'block attributes permit only primitive trees', path)
  }
  for (const name of Object.keys(source).sort()) output[name] = parse(source[name], `${field}.${name}`)
  return Object.freeze(output)
}
function parseFieldGroups(value: unknown, field: string): readonly BlockFieldGroupValue[] {
  if (!Array.isArray(value) || value.length > 128) throw new FieldsLifecycleError('validation', 'must be a bounded array', field)
  const seen = new Set<string>(), output: BlockFieldGroupValue[] = []
  for (let index = 0; index < value.length; index++) {
    const row = object(value[index], `${field}.${index}`), allowed = new Set(['groupKey','definitionRevision','values'])
    for (const name of Object.keys(row)) if (!allowed.has(name)) throw new FieldsLifecycleError('validation', 'unknown block field-group property', `${field}.${index}.${name}`)
    const groupKey = key(row.groupKey, `${field}.${index}.groupKey`)
    if (seen.has(groupKey)) throw new FieldsLifecycleError('validation', 'duplicate field group', `${field}.${index}.groupKey`)
    seen.add(groupKey)
    const values = jsonValue(row.values, `${field}.${index}.values`) as unknown as FieldValueMap
    output.push(Object.freeze({ groupKey, definitionRevision: positiveInt(row.definitionRevision, `${field}.${index}.definitionRevision`), values }))
  }
  return Object.freeze(output)
}
function parseNode(value: unknown, policy: BlockPolicy, field: string, depth = 0): BlockNode {
  if (depth > policy.maxDepth) throw new FieldsLifecycleError('validation', 'block document exceeds maximum depth', field)
  const row = object(value, field), kind = row.kind
  if (kind === 'reusable') {
    const allowed = new Set(['kind','id','reusableId']); for (const name of Object.keys(row)) if (!allowed.has(name)) throw new FieldsLifecycleError('validation', 'unknown reusable node property', `${field}.${name}`)
    return Object.freeze({ kind: 'reusable', id: string(row.id, `${field}.id`, 256), reusableId: string(row.reusableId, `${field}.reusableId`, 256) })
  }
  if (kind !== 'inline') throw new FieldsLifecycleError('validation', 'node kind must be inline or reusable', `${field}.kind`)
  const allowed = new Set(['kind','id','type','blockDefinitionRevision','fieldGroups','attributes','children']); for (const name of Object.keys(row)) if (!allowed.has(name)) throw new FieldsLifecycleError('validation', 'unknown inline node property', `${field}.${name}`)
  const children = row.children === undefined ? undefined : (() => { if (!Array.isArray(row.children) || row.children.length > policy.maxChildren) throw new FieldsLifecycleError('validation', 'children exceed maximum size', `${field}.children`); return Object.freeze(row.children.map((child, index) => parseNode(child, policy, `${field}.children.${index}`, depth + 1))) })()
  return Object.freeze({ kind: 'inline', id: string(row.id, `${field}.id`, 256), type: key(row.type, `${field}.type`), blockDefinitionRevision: positiveInt(row.blockDefinitionRevision, `${field}.blockDefinitionRevision`), fieldGroups: parseFieldGroups(row.fieldGroups, `${field}.fieldGroups`), ...(row.attributes === undefined ? {} : { attributes: parseAttributes(row.attributes, policy, `${field}.attributes`)! }), ...(children === undefined ? {} : { children }) })
}
function normalizeDocument(input: unknown, policy: BlockPolicy): BlockDocument {
  const source = object(input, 'document'), allowed = new Set(['version','roots']); for (const name of Object.keys(source)) if (!allowed.has(name)) throw new FieldsLifecycleError('validation', 'unknown block document property', `document.${name}`)
  if (source.version !== 1) throw new FieldsLifecycleError('validation', 'block document version must be 1', 'document.version')
  if (!Array.isArray(source.roots) || source.roots.length > policy.maxChildren) throw new FieldsLifecycleError('validation', 'roots exceed maximum size', 'document.roots')
  return Object.freeze({ version: 1, roots: Object.freeze(source.roots.map((node, index) => parseNode(node, policy, `document.roots.${index}`))) })
}

function normalizeDefinition(input: FieldBlockDefinition): FieldBlockDefinition {
  const blockKey = key(input.key, 'key')
  const title = string(input.title, 'title')
  const fieldGroupKeys = uniqueStrings(input.fieldGroupKeys, 'fieldGroupKeys').map((item, index) => key(item, `fieldGroupKeys.${index}`))
  if (fieldGroupKeys.length === 0) throw new FieldsLifecycleError('validation', 'block must reference at least one field group', 'fieldGroupKeys')
  const keywords = input.keywords === undefined ? undefined : uniqueStrings(input.keywords, 'keywords', 64)
  const align = input.align === undefined ? undefined : (() => {
    if (!Array.isArray(input.align) || input.align.length > 5) throw new FieldsLifecycleError('validation', 'align must be bounded', 'align')
    const allowed = new Set(['left','center','right','wide','full'])
    const output = input.align.map((item, index) => {
      if (!allowed.has(item)) throw new FieldsLifecycleError('validation', 'invalid alignment', `align.${index}`)
      return item
    })
    if (new Set(output).size !== output.length) throw new FieldsLifecycleError('validation', 'align must not contain duplicates', 'align')
    return Object.freeze(output)
  })()
  if (input.mode !== undefined && !['preview','edit','auto'].includes(input.mode)) throw new FieldsLifecycleError('validation', 'invalid block mode', 'mode')
  if (input.templateLock !== undefined && input.templateLock !== false && !['insert','all','contentOnly'].includes(input.templateLock)) throw new FieldsLifecycleError('validation', 'invalid template lock', 'templateLock')
  const supports = input.supports === undefined ? undefined : (() => {
    const source = object(input.supports, 'supports')
    const allowed = new Set(['anchor','className','multiple','reusable','innerBlocks','jsx'])
    const out: Record<string, boolean> = {}
    for (const name of Object.keys(source)) {
      if (!allowed.has(name)) throw new FieldsLifecycleError('validation', 'unknown supports flag', `supports.${name}`)
      if (typeof source[name] !== 'boolean') throw new FieldsLifecycleError('validation', 'supports flag must be boolean', `supports.${name}`)
      out[name] = source[name] as boolean
    }
    return Object.freeze(out)
  })()
  const parent = input.parent === undefined ? undefined : uniqueStrings(input.parent, 'parent', 64).map((item, index) => key(item, `parent.${index}`))
  const ancestor = input.ancestor === undefined ? undefined : uniqueStrings(input.ancestor, 'ancestor', 64).map((item, index) => key(item, `ancestor.${index}`))
  const templatePolicy: BlockPolicy = Object.freeze({ maxBytes: 256 * 1024, maxNodes: 1000, maxDepth: 12, maxChildren: 128, maxAttributeBytes: 64 * 1024, allowUnknownPreserved: false })
  const template = input.template === undefined ? undefined : normalizeDocument({ version: 1, roots: input.template }, templatePolicy).roots
  return Object.freeze({
    key: blockKey,
    title,
    ...(input.description === undefined ? {} : { description: string(input.description, 'description') }),
    ...(input.category === undefined ? {} : { category: string(input.category, 'category', 256) }),
    ...(input.icon === undefined ? {} : { icon: string(input.icon, 'icon', 512) }),
    ...(keywords === undefined ? {} : { keywords }),
    fieldGroupKeys: Object.freeze(fieldGroupKeys),
    ...(input.mode === undefined ? {} : { mode: input.mode }),
    ...(align === undefined ? {} : { align }),
    ...(supports === undefined ? {} : { supports }),
    ...(parent === undefined ? {} : { parent: Object.freeze(parent) }),
    ...(ancestor === undefined ? {} : { ancestor: Object.freeze(ancestor) }),
    ...(template === undefined ? {} : { template }),
    ...(input.templateLock === undefined ? {} : { templateLock: input.templateLock }),
  })
}
function parseDefinition(value: unknown): FieldBlockDefinition {
  const raw = parseJson(value, 'block definition')
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) throw new FieldsLifecycleError('integrity', 'block definition is malformed')
  return normalizeDefinition(raw as unknown as FieldBlockDefinition)
}
function resolvedCurrent(row: CurrentBlockRow): ResolvedFieldBlock {
  const definition = parseDefinition(row.definition)
  if (definition.key !== row.key) throw new FieldsLifecycleError('integrity', 'block definition key does not match persisted key')
  return Object.freeze({ ...definition, origin: row.origin === 'code' ? 'code' : 'db', version: Number(row.version), active: bool(row.active), ...(row.shadowedDbVersion === null ? {} : { shadowedDbVersion: Number(row.shadowedDbVersion) }) })
}
async function readCurrentRows(db: FieldsReader): Promise<readonly CurrentBlockRow[]> {
  return executeRows<CurrentBlockRow>(db, sql`SELECT key, origin, version, active, current_revision AS "currentRevision", canonical_hash AS "canonicalHash", definition, shadowed_db_version AS "shadowedDbVersion" FROM field_block_definitions ORDER BY key`)
}
async function readBlockVersion(db: FieldsReader, blockKey: string, revision: number): Promise<FieldBlockDefinitionVersion | null> {
  const rows = await executeRows<BlockVersionRow>(db, sql`SELECT block_key AS "blockKey", revision, canonical_hash AS "canonicalHash", definition, created_at AS "createdAt", origin FROM field_block_definition_versions WHERE block_key = ${blockKey} AND revision = ${revision} LIMIT 1`)
  const row = rows[0]
  if (!row) return null
  const definition = parseDefinition(row.definition)
  const canonicalHash = await fieldsRequestHash(definition)
  if (definition.key !== row.blockKey || canonicalHash !== row.canonicalHash) throw new FieldsLifecycleError('integrity', 'pinned block definition is corrupt')
  const createdAt = row.createdAt instanceof Date ? new Date(row.createdAt) : new Date(row.createdAt)
  if (Number.isNaN(createdAt.getTime())) throw new FieldsLifecycleError('integrity', 'pinned block definition timestamp is invalid')
  return Object.freeze({ blockKey: row.blockKey, revision: Number(row.revision), canonicalHash, definition, createdAt, origin: row.origin })
}

function mergeCurrent(rows: readonly CurrentBlockRow[], codeBlocks: readonly CodeFieldBlock[]): { blocks: Map<string, ResolvedFieldBlock>; codeRevisions: Map<string, FieldBlockDefinitionVersion> } {
  const blocks = new Map<string, ResolvedFieldBlock>()
  const rowByKey = new Map(rows.map((row) => [row.key, row]))
  const codeRevisions = new Map<string, FieldBlockDefinitionVersion>()
  for (const row of rows) blocks.set(row.key, resolvedCurrent(row))
  const seen = new Set<string>()
  for (const raw of codeBlocks) {
    const definition = normalizeDefinition(raw)
    if (seen.has(definition.key)) throw new FieldsLifecycleError('validation', 'duplicate code block key', definition.key)
    seen.add(definition.key)
    const previousRow = rowByKey.get(definition.key)
    const previous = previousRow ? resolvedCurrent(previousRow) : undefined
    const revision = previousRow?.origin === 'code' ? Number(previousRow.currentRevision) : 1
    blocks.set(definition.key, Object.freeze({
      ...definition,
      origin: 'code',
      version: previous?.version ?? 1,
      active: true,
      ...(previous?.origin === 'db' ? { shadowedDbVersion: previous.version } : previous?.shadowedDbVersion === undefined ? {} : { shadowedDbVersion: previous.shadowedDbVersion }),
    }))
    codeRevisions.set(`${definition.key}@${revision}`, Object.freeze({ blockKey: definition.key, revision, canonicalHash: '', definition, createdAt: new Date(0), origin: 'code' }))
  }
  return { blocks, codeRevisions }
}
function templateTypes(nodes: readonly BlockNode[] | undefined, out = new Set<string>()): Set<string> {
  for (const node of nodes ?? []) if (node.kind === 'inline') { out.add(node.type); templateTypes(node.children, out) }
  return out
}
function validateDefinitionGraph(blocks: ReadonlyMap<string, ResolvedFieldBlock>): void {
  for (const block of blocks.values()) {
    for (const parent of block.parent ?? []) if (!blocks.has(parent)) throw new FieldsLifecycleError('validation', 'block parent references missing definition', `${block.key}.parent`)
    for (const ancestor of block.ancestor ?? []) if (!blocks.has(ancestor)) throw new FieldsLifecycleError('validation', 'block ancestor references missing definition', `${block.key}.ancestor`)
    for (const type of templateTypes(block.template)) if (!blocks.has(type)) throw new FieldsLifecycleError('validation', 'block template references missing definition', `${block.key}.template`)
  }
  const visiting = new Set<string>(), visited = new Set<string>()
  const visit = (blockKey: string) => {
    if (visiting.has(blockKey)) throw new FieldsLifecycleError('validation', 'block templates form a recursive cycle', blockKey)
    if (visited.has(blockKey)) return
    visiting.add(blockKey)
    for (const child of templateTypes(blocks.get(blockKey)?.template)) visit(child)
    visiting.delete(blockKey)
    visited.add(blockKey)
  }
  for (const blockKey of blocks.keys()) visit(blockKey)
}
export function defineFieldBlock(definition: FieldBlockDefinition): CodeFieldBlock { return Object.freeze({ ...normalizeDefinition(definition), origin: 'code' }) }
export async function resolveFieldBlocks(db: FieldsReader, input: { codeBlocks?: readonly CodeFieldBlock[] } = {}): Promise<ResolvedFieldBlockRegistry> {
  const rows = await readCurrentRows(db)
  const merged = mergeCurrent(rows, input.codeBlocks ?? [])
  validateDefinitionGraph(merged.blocks)
  const snapshot = [...merged.blocks.values()].map((block) => ({ ...block })).sort((a, b) => a.key.localeCompare(b.key))
  const version = await fieldsRequestHash(snapshot)
  const registry: ResolvedFieldBlockRegistry = Object.freeze({
    version,
    blocks: new Map([...merged.blocks.entries()].sort(([left], [right]) => left.localeCompare(right))),
    definitions: Object.freeze({
      resolve: async (blockKey: string, revision: number) => {
        const persisted = await readBlockVersion(db, blockKey, revision)
        if (persisted) return persisted
        const code = merged.codeRevisions.get(`${blockKey}@${revision}`)
        return code ? Object.freeze({ ...code, canonicalHash: await fieldsRequestHash(code.definition) }) : null
      },
    }),
  })
  return attachRegistryReader(registry, db)
}
async function readCurrentBlock(db: FieldsReader, blockKey: string): Promise<{ row: CurrentBlockRow; block: ResolvedFieldBlock } | null> {
  const rows = await executeRows<CurrentBlockRow>(db, sql`SELECT key, origin, version, active, current_revision AS "currentRevision", canonical_hash AS "canonicalHash", definition, shadowed_db_version AS "shadowedDbVersion" FROM field_block_definitions WHERE key = ${blockKey} LIMIT 1`)
  return rows[0] ? { row: rows[0], block: resolvedCurrent(rows[0]) } : null
}
async function assertGroupDependencies(db: FieldsReader, definition: FieldBlockDefinition): Promise<void> {
  const rows = await executeRows<GroupCurrentRow>(db, sql`SELECT key, current_revision AS "currentRevision", active FROM field_group_definitions WHERE key IN (${sql.join(definition.fieldGroupKeys.map((groupKey) => sql`${groupKey}`), sql`, `)}) ORDER BY key`)
  const active = new Set(rows.filter((row) => bool(row.active)).map((row) => row.key))
  for (const groupKey of definition.fieldGroupKeys) if (!active.has(groupKey)) throw new FieldsLifecycleError('validation', 'block references missing or inactive field group', groupKey)
}
function definitionRef(blockKey: string): EntityRef { return Object.freeze({ entityType: 'fields-block-definition', entityId: blockKey }) }

export async function createFieldBlock(tx: FieldsTransaction, definition: FieldBlockDefinition, context: FieldsContext, input: { codeBlocks?: readonly CodeFieldBlock[]; operationId: string }): Promise<ResolvedFieldBlock> {
  assertActiveFieldsTransaction(tx)
  const normalized = normalizeDefinition(definition)
  const authorized = await assertFieldsAuthorized(context, 'manageDefinitions', definitionRef(normalized.key))
  if ((input.codeBlocks ?? []).some((block) => block.key === normalized.key)) throw new FieldsLifecycleError('validation', 'effective block key is owned by code', normalized.key)
  await assertGroupDependencies(tx, normalized)
  const canonicalHash = await fieldsRequestHash(normalized)
  const requestHash = await fieldsRequestHash({ kind: 'create-field-block', canonicalHash, codeKeys: (input.codeBlocks ?? []).map((block) => block.key).sort() })
  const replay = await readFieldsReceipt(tx, input.operationId)
  if (replay) {
    assertReceiptReplay(replay, { kind: 'fields:create-field-block', context: authorized, requestHash })
    const current = await readCurrentBlock(tx, normalized.key)
    if (!current) throw new FieldsLifecycleError('integrity', 'committed block receipt has no definition')
    return current.block
  }
  const currentRows = await readCurrentRows(tx)
  if (currentRows.some((row) => row.key === normalized.key)) throw new FieldsLifecycleError('validation', 'block key already exists', normalized.key)
  const candidateRow: CurrentBlockRow = { key: normalized.key, origin: 'db', version: 1, active: true, currentRevision: 1, canonicalHash, definition: normalized, shadowedDbVersion: null }
  const merged = mergeCurrent([...currentRows, candidateRow], input.codeBlocks ?? [])
  validateDefinitionGraph(merged.blocks)
  const now = new Date().toISOString()
  await tx.execute(sql`INSERT INTO field_block_definitions (key, origin, version, active, current_revision, canonical_hash, definition, created_at, updated_at) VALUES (${normalized.key}, 'db', 1, true, 1, ${canonicalHash}, ${JSON.stringify(normalized)}, ${now}, ${now})`)
  await tx.execute(sql`INSERT INTO field_block_definition_versions (block_key, revision, canonical_hash, definition, origin, created_at) VALUES (${normalized.key}, 1, ${canonicalHash}, ${JSON.stringify(normalized)}, 'db', ${now})`)
  if (normalized.template) {
    const registry = await resolveFieldBlocks(tx, { codeBlocks: input.codeBlocks })
    await validateBlockDocument({ version: 1, roots: normalized.template }, registry, { resolve: async () => null }, context, DEFAULT_BLOCK_POLICY)
  }
  const result: ResolvedFieldBlock = Object.freeze({ ...normalized, origin: 'db', version: 1, active: true })
  await writeFieldsReceipt(tx, { operationId: input.operationId, kind: 'fields:create-field-block', context: authorized, requestHash, result: Object.freeze({ key: normalized.key, version: 1 }) })
  return result
}

export const DEFAULT_BLOCK_POLICY: BlockPolicy = Object.freeze({ maxBytes: 1024 * 1024, maxNodes: 2000, maxDepth: 16, maxChildren: 256, maxAttributeBytes: 64 * 1024, allowUnknownPreserved: false })
function validatePolicy(policy: BlockPolicy): void {
  for (const [name, value, max] of [
    ['maxBytes', policy.maxBytes, 16 * 1024 * 1024],
    ['maxNodes', policy.maxNodes, 100_000],
    ['maxDepth', policy.maxDepth, 128],
    ['maxChildren', policy.maxChildren, 10_000],
    ['maxAttributeBytes', policy.maxAttributeBytes, 1024 * 1024],
  ] as const) if (!Number.isSafeInteger(value) || value < 1 || value > max) throw new FieldsLifecycleError('validation', 'block policy value is out of bounds', `policy.${name}`)
}
function plainRecord(value: unknown, field: string): Record<string, unknown> {
  const serialized = jsonValue(value, field)
  if (typeof serialized !== 'object' || serialized === null || Array.isArray(serialized)) throw new FieldsLifecycleError('validation', 'must be an object', field)
  return serialized as Record<string, unknown>
}
function hasPassword(fields: readonly import('./schema.js').AnyFieldDefinition[]): boolean {
  for (const field of fields) {
    if (field.type === 'password') return true
    if (field.type === 'group' || field.type === 'repeater') { if (hasPassword((field.settings as { fields: readonly import('./schema.js').AnyFieldDefinition[] }).fields)) return true }
    if (field.type === 'flexible') for (const layout of (field.settings as { layouts: readonly { fields: readonly import('./schema.js').AnyFieldDefinition[] }[] }).layouts) if (hasPassword(layout.fields)) return true
  }
  return false
}
async function validateBlockFieldMap(fields: readonly import('./schema.js').AnyFieldDefinition[], input: unknown, context: FieldsContext, field: string): Promise<FieldValueMap> {
  const source = plainRecord(input, field), definitions = new Map(fields.map((definition) => [definition.key, definition])), output: Record<string, unknown> = {}
  for (const name of Object.keys(source)) if (!definitions.has(name)) throw new FieldsLifecycleError('validation', 'block value names an unknown field', `${field}.${name}`)
  const registry = context.fieldTypeRegistry ?? createFieldTypeRegistry()
  const auth = authorizedFieldsContext(context)
  const valueContext = {
    sanitizer: context.sanitizer,
    ...(context.cloneResolver ? { cloneResolver: { resolve: (definition: import('./schema.js').AnyFieldDefinition) => context.cloneResolver!.resolve(definition) } } : {}),
    authorization: {
      assertTarget: async (target: { entityType: string; id: string }, action: 'readTarget' | 'writeReverseTarget') => {
        try { await auth.authorization.assert(auth.principal, action, { entityType: target.entityType, entityId: target.id }) }
        catch { throw new FieldsLifecycleError('access-denied', 'block field target is unavailable') }
      },
    },
  }
  for (const definition of fields) {
    if (definition.type === 'message' || definition.type === 'accordion' || definition.type === 'tab') continue
    const present = Object.prototype.hasOwnProperty.call(source, definition.key)
    if (!present) { if (definition.required) throw new FieldsLifecycleError('validation', 'required block field is missing', `${field}.${definition.key}`); continue }
    const value = source[definition.key]
    if (definition.type === 'password') {
      const password = plainRecord(value, `${field}.${definition.key}`)
      if (Object.keys(password).length !== 1 || typeof password.configured !== 'boolean') throw new FieldsLifecycleError('validation', 'block documents may contain only the redacted password configured marker', `${field}.${definition.key}`)
      output[definition.key] = Object.freeze({ configured: password.configured })
      continue
    }
    if (definition.type === 'group') {
      output[definition.key] = await validateBlockFieldMap((definition.settings as { fields: readonly import('./schema.js').AnyFieldDefinition[] }).fields, value, context, `${field}.${definition.key}`)
      continue
    }
    if (definition.type === 'repeater') {
      if (!Array.isArray(value)) throw new FieldsLifecycleError('validation', 'repeater block value must be an array', `${field}.${definition.key}`)
      const settings = definition.settings as { fields: readonly import('./schema.js').AnyFieldDefinition[]; min?: number; max?: number }
      if (settings.min !== undefined && value.length < settings.min || settings.max !== undefined && value.length > settings.max) throw new FieldsLifecycleError('validation', 'repeater row count violates definition', `${field}.${definition.key}`)
      const rowIds = new Set<string>(), rows = []
      for (let index = 0; index < value.length; index++) {
        const row = plainRecord(value[index], `${field}.${definition.key}.${index}`), rowId = string(row.rowId, `${field}.${definition.key}.${index}.rowId`, 256)
        if (rowIds.has(rowId)) throw new FieldsLifecycleError('validation', 'repeater row id is duplicated', `${field}.${definition.key}.${index}.rowId`)
        rowIds.add(rowId)
        rows.push(Object.freeze({ rowId, values: await validateBlockFieldMap(settings.fields, row.values, context, `${field}.${definition.key}.${index}.values`) }))
      }
      output[definition.key] = Object.freeze(rows)
      continue
    }
    if (definition.type === 'flexible') {
      if (!Array.isArray(value)) throw new FieldsLifecycleError('validation', 'flexible block value must be an array', `${field}.${definition.key}`)
      const settings = definition.settings as { layouts: readonly { key: string; fields: readonly import('./schema.js').AnyFieldDefinition[] }[]; min?: number; max?: number }
      if (settings.min !== undefined && value.length < settings.min || settings.max !== undefined && value.length > settings.max) throw new FieldsLifecycleError('validation', 'flexible row count violates definition', `${field}.${definition.key}`)
      const rowIds = new Set<string>(), rows = []
      for (let index = 0; index < value.length; index++) {
        const row = plainRecord(value[index], `${field}.${definition.key}.${index}`), rowId = string(row.rowId, `${field}.${definition.key}.${index}.rowId`, 256), layoutKey = key(row.layoutKey, `${field}.${definition.key}.${index}.layoutKey`)
        if (rowIds.has(rowId)) throw new FieldsLifecycleError('validation', 'flexible row id is duplicated', `${field}.${definition.key}.${index}.rowId`)
        rowIds.add(rowId)
        const layout = settings.layouts.find((candidate) => candidate.key === layoutKey)
        if (!layout) throw new FieldsLifecycleError('validation', 'flexible row references unknown layout', `${field}.${definition.key}.${index}.layoutKey`)
        rows.push(Object.freeze({ rowId, layoutKey, values: await validateBlockFieldMap(layout.fields, row.values, context, `${field}.${definition.key}.${index}.values`) }))
      }
      output[definition.key] = Object.freeze(rows)
      continue
    }
    if (definition.type === 'clone') {
      if (!context.cloneResolver) throw new FieldsLifecycleError('capability-unavailable', 'clone resolver is required for block field validation')
      output[definition.key] = await validateBlockFieldMap(context.cloneResolver.resolve(definition), value, context, `${field}.${definition.key}`)
      continue
    }
    output[definition.key] = await validateFieldValue(definition, value, registry, valueContext)
  }
  return Object.freeze(output) as FieldValueMap
}
function structureSignature(nodes: readonly BlockNode[] | undefined): readonly FieldStorageValue[] {
  return Object.freeze((nodes ?? []).map((node) => node.kind === 'reusable'
    ? Object.freeze({ kind: 'reusable', reusableId: node.reusableId })
    : Object.freeze({ kind: 'inline', type: node.type, children: structureSignature(node.children) })))
}
function checkTemplateLock(definition: FieldBlockDefinition, children: readonly BlockNode[] | undefined): void {
  if (!definition.template || definition.templateLock === undefined || definition.templateLock === false) return
  const expected = structureSignature(definition.template), actual = structureSignature(children)
  if (definition.templateLock === 'insert') {
    if (actual.length < expected.length || JSON.stringify(actual.slice(0, expected.length)) !== JSON.stringify(expected)) throw new FieldsLifecycleError('validation', 'children violate insert template lock', definition.key)
    return
  }
  if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new FieldsLifecycleError('validation', `children violate ${definition.templateLock} template lock`, definition.key)
}
function allowedAttributeKeys(definition: FieldBlockDefinition): ReadonlySet<string> {
  const allowed = new Set<string>()
  if (definition.supports?.anchor) allowed.add('anchor')
  if (definition.supports?.className) allowed.add('className')
  if ((definition.align?.length ?? 0) > 0) allowed.add('align')
  return allowed
}

export async function validateBlockDocument(document: BlockDocument, registry: ResolvedFieldBlockRegistry, resolver: ReusableBlockResolver, context: FieldsContext, policy: BlockPolicy): Promise<ValidatedBlockDocument> {
  validatePolicy(policy)
  const normalized = normalizeDocument(document, policy)
  const byteCount = new TextEncoder().encode(JSON.stringify(normalized)).byteLength
  if (byteCount > policy.maxBytes) throw new FieldsLifecycleError('validation', 'block document exceeds byte limit')
  authorizedFieldsContext(context)
  const expanded = { nodes: 0 }
  const typeCounts = new Map<string, number>()
  const reusableStack = new Set<string>()

  const validateDocument = async (input: BlockDocument, ancestors: readonly string[], localIds: Set<string>, depth: number): Promise<readonly ValidatedBlockNode[]> => {
    const result: ValidatedBlockNode[] = []
    for (let index = 0; index < input.roots.length; index++) result.push(await validateNode(input.roots[index]!, ancestors, localIds, depth, `roots.${index}`))
    return Object.freeze(result)
  }
  const validateNode = async (node: BlockNode, ancestors: readonly string[], localIds: Set<string>, depth: number, field: string): Promise<ValidatedBlockNode> => {
    if (depth > policy.maxDepth) throw new FieldsLifecycleError('validation', 'expanded block document exceeds maximum depth', field)
    if (++expanded.nodes > policy.maxNodes) throw new FieldsLifecycleError('validation', 'expanded block document exceeds maximum node count', field)
    if (localIds.has(node.id)) throw new FieldsLifecycleError('validation', 'block node id is duplicated', `${field}.id`)
    localIds.add(node.id)
    if (node.kind === 'reusable') {
      await assertFieldsAuthorized(context, 'read', { entityType: 'reusableBlock', entityId: node.reusableId })
      let resolved: { document: BlockDocument; version: number } | null
      try { resolved = await resolver.resolve(node.reusableId, context) } catch { throw new FieldsLifecycleError('access-denied', 'reusable block is unavailable') }
      if (!resolved) throw new FieldsLifecycleError('access-denied', 'reusable block is unavailable')
      const cycleKey = `${node.reusableId}@${resolved.version}`
      if (reusableStack.has(cycleKey)) throw new FieldsLifecycleError('validation', 'reusable block cycle detected', field)
      reusableStack.add(cycleKey)
      const nested = normalizeDocument(resolved.document, policy)
      await validateDocument(nested, ancestors, new Set<string>(), depth + 1)
      reusableStack.delete(cycleKey)
      return Object.freeze({ ...node, [validatedBlockNodeBrand]: true }) as ValidatedReusableBlockNode
    }

    const version = await registry.definitions.resolve(node.type, node.blockDefinitionRevision)
    if (!version) throw new FieldsLifecycleError('integrity', 'pinned block definition is unavailable', node.type)
    if (version.blockKey !== node.type || version.definition.key !== node.type) throw new FieldsLifecycleError('integrity', 'pinned block definition identity mismatch', node.type)
    const current = registry.blocks.get(node.type)
    if ((!current || !current.active) && policy.allowUnknownPreserved !== true) throw new FieldsLifecycleError('validation', 'block type is not active in the current registry', node.type)
    const parentType = ancestors.at(-1)
    if ((version.definition.parent?.length ?? 0) > 0 && (!parentType || !version.definition.parent!.includes(parentType))) throw new FieldsLifecycleError('validation', 'block violates parent restriction', node.type)
    if ((version.definition.ancestor?.length ?? 0) > 0 && !ancestors.some((ancestor) => version.definition.ancestor!.includes(ancestor))) throw new FieldsLifecycleError('validation', 'block violates ancestor restriction', node.type)
    const count = (typeCounts.get(node.type) ?? 0) + 1
    typeCounts.set(node.type, count)
    if (version.definition.supports?.multiple === false && count > 1) throw new FieldsLifecycleError('validation', 'block definition does not allow multiple instances', node.type)
    if ((node.children?.length ?? 0) > 0 && version.definition.supports?.innerBlocks !== true) throw new FieldsLifecycleError('validation', 'block definition does not allow children', node.type)
    checkTemplateLock(version.definition, node.children)

    const expectedGroups = [...version.definition.fieldGroupKeys].sort()
    const actualGroups = node.fieldGroups.map((group) => group.groupKey).sort()
    if (JSON.stringify(expectedGroups) !== JSON.stringify(actualGroups)) throw new FieldsLifecycleError('validation', 'block must contain exactly one value namespace per configured field group', node.type)
    const fieldGroups: BlockFieldGroupValue[] = []
    for (const groupValue of node.fieldGroups) {
      const groupVersion = await resolveFieldDefinitionVersion(registryReader(registry), groupValue.groupKey, groupValue.definitionRevision)
      if (hasPassword(groupVersion.definition.fields)) {
        // Passwords are accepted only as redacted read markers by validateBlockFieldMap; raw writes are impossible here.
      }
      const values = await validateBlockFieldMap(groupVersion.definition.fields, groupValue.values, context, `${field}.fieldGroups.${groupValue.groupKey}`)
      fieldGroups.push(Object.freeze({ groupKey: groupValue.groupKey, definitionRevision: groupValue.definitionRevision, values }))
    }
    const allowedAttributes = allowedAttributeKeys(version.definition)
    for (const name of Object.keys(node.attributes ?? {})) {
      if (!allowedAttributes.has(name)) throw new FieldsLifecycleError('validation', 'block attribute is not registered by the definition', `${field}.attributes.${name}`)
      if (name === 'align' && typeof node.attributes?.align === 'string' && !(version.definition.align ?? []).includes(node.attributes.align as never)) throw new FieldsLifecycleError('validation', 'block alignment is not allowed', `${field}.attributes.align`)
    }
    const children: ValidatedBlockNode[] = []
    for (let index = 0; index < (node.children?.length ?? 0); index++) children.push(await validateNode(node.children![index]!, [...ancestors, node.type], localIds, depth + 1, `${field}.children.${index}`))
    return Object.freeze({ ...node, fieldGroups: Object.freeze(fieldGroups), ...(children.length ? { children: Object.freeze(children) } : node.children ? { children: Object.freeze([]) } : {}), [validatedBlockNodeBrand]: true }) as ValidatedInlineBlockNode
  }

  // Exact immutable group versions use the reader captured by resolveFieldBlocks.
  const roots = await validateDocument(normalized, Object.freeze([]), new Set<string>(), 1)
  return Object.freeze({ version: 1, roots, [validatedBlockDocumentBrand]: true, [validatedRegistryVersion]: registry.version }) as ValidatedBlockDocument
}

const registryReaders = new WeakMap<object, FieldsReader>()
function registryReader(registry: ResolvedFieldBlockRegistry): FieldsReader {
  const reader = registryReaders.get(registry as object)
  if (!reader) throw new FieldsLifecycleError('capability-unavailable', 'block registry has no immutable field-definition reader')
  return reader
}
function attachRegistryReader(registry: ResolvedFieldBlockRegistry, reader: FieldsReader): ResolvedFieldBlockRegistry {
  registryReaders.set(registry as object, reader)
  return registry
}

export function parseBlockDocument(input: string, policy: BlockPolicy): BlockDocument {
  validatePolicy(policy)
  const bytes = new TextEncoder().encode(input).byteLength
  if (bytes > policy.maxBytes) throw new FieldsLifecycleError('validation', 'block document exceeds byte limit')
  let parsed: unknown
  try { parsed = JSON.parse(input) as unknown } catch { throw new FieldsLifecycleError('validation', 'block document is not valid JSON') }
  return normalizeDocument(parsed, policy)
}
function isValidatedDocument(document: ValidatedBlockDocument): boolean { return (document as unknown as Record<PropertyKey, unknown>)[validatedBlockDocumentBrand] === true }
export function serializeBlockDocument(document: ValidatedBlockDocument): string {
  if (!isValidatedDocument(document)) throw new FieldsLifecycleError('validation', 'block document has not passed validation')
  return JSON.stringify({ version: 1, roots: document.roots })
}
export async function validateBlockPublication(document: ValidatedBlockDocument, registry: ResolvedFieldBlockRegistry, resolver: ReusableBlockResolver, context: FieldsContext, policy: BlockPolicy): Promise<void> {
  if (!isValidatedDocument(document)) throw new FieldsLifecycleError('validation', 'block document has not passed validation')
  await validateBlockDocument(parseBlockDocument(serializeBlockDocument(document), policy), registry, resolver, context, { ...policy, allowUnknownPreserved: false })
}
export async function renderBlockDocument<R>(document: ValidatedBlockDocument, registry: ResolvedFieldBlockRegistry, resolver: ReusableBlockResolver, renderer: BlockRenderer<R>, context: FieldsContext): Promise<R[]> {
  if (!isValidatedDocument(document)) throw new FieldsLifecycleError('validation', 'block document has not passed validation')
  const output: R[] = []
  const walk = async (nodes: readonly ValidatedBlockNode[]): Promise<void> => {
    for (const node of nodes) {
      if (node.kind === 'reusable') {
        await assertFieldsAuthorized(context, 'read', { entityType: 'reusableBlock', entityId: node.reusableId })
        const resolved = await resolver.resolve(node.reusableId, context)
        if (!resolved) throw new FieldsLifecycleError('access-denied', 'reusable block is unavailable')
        const validated = await validateBlockDocument(resolved.document, registry, resolver, context, DEFAULT_BLOCK_POLICY)
        await walk(validated.roots)
        continue
      }
      const definition = await registry.definitions.resolve(node.type, node.blockDefinitionRevision)
      if (!definition) throw new FieldsLifecycleError('integrity', 'pinned block definition is unavailable', node.type)
      output.push(await renderer.render(node, definition, context))
      if (node.children) await walk(node.children)
    }
  }
  await walk(document.roots)
  return output
}

function parseDefinitionVersions(value: unknown): Readonly<Record<string, number>> {
  const source = parseJson(value, 'definitionVersions')
  if (typeof source !== 'object' || source === null || Array.isArray(source)) throw new FieldsLifecycleError('integrity', 'definition version map is malformed')
  const output: Record<string, number> = {}
  for (const [name, raw] of Object.entries(source as Record<string, unknown>)) output[name] = positiveInt(raw, `definitionVersions.${name}`)
  return Object.freeze(output)
}
function collectDefinitionVersions(document: ValidatedBlockDocument): Readonly<Record<string, number>> {
  const output: Record<string, number> = {}
  const set = (name: string, revision: number) => {
    const previous = output[name]
    if (previous !== undefined && previous !== revision) throw new FieldsLifecycleError('integrity', 'one current document cannot mix revisions for the same definition identity', name)
    output[name] = revision
  }
  const walk = (nodes: readonly ValidatedBlockNode[]) => {
    for (const node of nodes) {
      if (node.kind === 'reusable') continue
      set(`block:${node.type}`, node.blockDefinitionRevision)
      for (const group of node.fieldGroups) set(`group:${group.groupKey}`, group.definitionRevision)
      if (node.children) walk(node.children)
    }
  }
  walk(document.roots)
  return Object.freeze(output)
}
async function readDocumentRow(db: FieldsReader, ref: EntityRef): Promise<DocumentRow | null> {
  const rows = await executeRows<DocumentRow>(db, sql`SELECT entity_type AS "entityType", entity_id AS "entityId", version, document, definition_versions AS "definitionVersions", parent_revision_id AS "parentRevisionId", updated_by AS "updatedBy", updated_at AS "updatedAt" FROM field_block_documents WHERE entity_type = ${ref.entityType} AND entity_id = ${ref.entityId} LIMIT 1`)
  return rows[0] ?? null
}
async function currentDocumentFromRow(row: DocumentRow): Promise<CurrentBlockDocument> {
  const document = normalizeDocument(parseJson(row.document, 'block document'), DEFAULT_BLOCK_POLICY)
  const definitionVersions = parseDefinitionVersions(row.definitionVersions)
  const version = await fieldsRequestHash({ rowVersion: Number(row.version), document, definitionVersions })
  return Object.freeze({ ref: Object.freeze({ entityType: row.entityType, entityId: row.entityId }), document, version, definitionVersions })
}
export async function getBlockDocument(db: FieldsReader, ref: EntityRef, context: FieldsContext): Promise<CurrentBlockDocument | null> {
  await assertFieldsAuthorized(context, 'read', ref)
  const row = await readDocumentRow(db, ref)
  return row ? currentDocumentFromRow(row) : null
}
export async function setBlockDocument(tx: FieldsTransaction, ref: EntityRef, document: ValidatedBlockDocument, mutation: BlockDocumentMutationOptions, registry: ResolvedFieldBlockRegistry, context: FieldsContext): Promise<CurrentBlockDocument> {
  assertActiveFieldsTransaction(tx, mutation.expectedTransactionIdentity)
  const authorized = await assertFieldsAuthorized(context, 'write', ref)
  if (!isValidatedDocument(document)) throw new FieldsLifecycleError('validation', 'block document has not passed validation')
  if (document[validatedRegistryVersion] !== registry.version) throw new FieldsLifecycleError('stale-version', 'block registry changed after document validation')
  if (!mutation.parentRevisionId || mutation.parentRevisionId.length > 256) throw new FieldsLifecycleError('validation', 'parentRevisionId is required', 'parentRevisionId')
  const definitionVersions = collectDefinitionVersions(document)
  const serialized = serializeBlockDocument(document)
  const requestHash = await fieldsRequestHash({ ref, expectedVersion: mutation.expectedVersion, document: JSON.parse(serialized), definitionVersions, parentRevisionId: mutation.parentRevisionId, registryVersion: registry.version })
  const receipt = await readFieldsReceipt(tx, mutation.operationId)
  if (receipt) {
    assertReceiptReplay(receipt, { kind: 'fields:set-block-document', context: authorized, requestHash })
    const replayRow = await readDocumentRow(tx, ref)
    if (!replayRow) throw new FieldsLifecycleError('integrity', 'committed block document receipt has no document')
    return currentDocumentFromRow(replayRow)
  }
  const existingRow = await readDocumentRow(tx, ref)
  const existing = existingRow ? await currentDocumentFromRow(existingRow) : null
  const currentVersion = existing?.version ?? EMPTY_BLOCK_DOCUMENT_VERSION
  if (currentVersion !== mutation.expectedVersion) throw new FieldsLifecycleError('stale-version', 'block document changed')
  const numericVersion = Number(existingRow?.version ?? 0) + 1
  const now = new Date().toISOString()
  await tx.execute(sql`INSERT INTO field_block_documents (entity_type, entity_id, version, document, definition_versions, parent_revision_id, updated_by, updated_at) VALUES (${ref.entityType}, ${ref.entityId}, ${numericVersion}, ${serialized}, ${JSON.stringify(definitionVersions)}, ${mutation.parentRevisionId}, ${authorized.principal.id}, ${now}) ON CONFLICT (entity_type, entity_id) DO UPDATE SET version = EXCLUDED.version, document = EXCLUDED.document, definition_versions = EXCLUDED.definition_versions, parent_revision_id = EXCLUDED.parent_revision_id, updated_by = EXCLUDED.updated_by, updated_at = EXCLUDED.updated_at`)
  const row: DocumentRow = { entityType: ref.entityType, entityId: ref.entityId, version: numericVersion, document: serialized, definitionVersions, parentRevisionId: mutation.parentRevisionId, updatedBy: authorized.principal.id, updatedAt: now }
  const result = await currentDocumentFromRow(row)
  await writeFieldsReceipt(tx, { operationId: mutation.operationId, kind: 'fields:set-block-document', context: authorized, requestHash, result: Object.freeze({ version: result.version }) })
  return result
}
export async function deleteBlockDocument(tx: FieldsTransaction, ref: EntityRef, mutation: BlockDocumentMutationOptions, context: FieldsContext): Promise<void> {
  assertActiveFieldsTransaction(tx, mutation.expectedTransactionIdentity)
  const authorized = await assertFieldsAuthorized(context, 'write', ref)
  if (!mutation.parentRevisionId || mutation.parentRevisionId.length > 256) throw new FieldsLifecycleError('validation', 'parentRevisionId is required', 'parentRevisionId')
  const requestHash = await fieldsRequestHash({ ref, expectedVersion: mutation.expectedVersion, parentRevisionId: mutation.parentRevisionId })
  const receipt = await readFieldsReceipt(tx, mutation.operationId)
  if (receipt) { assertReceiptReplay(receipt, { kind: 'fields:delete-block-document', context: authorized, requestHash }); return }
  const existingRow = await readDocumentRow(tx, ref)
  const existing = existingRow ? await currentDocumentFromRow(existingRow) : null
  const currentVersion = existing?.version ?? EMPTY_BLOCK_DOCUMENT_VERSION
  if (currentVersion !== mutation.expectedVersion) throw new FieldsLifecycleError('stale-version', 'block document changed')
  await tx.execute(sql`DELETE FROM field_block_documents WHERE entity_type = ${ref.entityType} AND entity_id = ${ref.entityId}`)
  await writeFieldsReceipt(tx, { operationId: mutation.operationId, kind: 'fields:delete-block-document', context: authorized, requestHash, result: Object.freeze({ deleted: existing !== null }) })
}

export function createReusableBlockResolver(db: FieldsReader): ReusableBlockResolver {
  return Object.freeze({
    resolve: async (id: string) => {
      const rows = await executeRows<Record<string, unknown>>(db, sql`SELECT version, document FROM field_reusable_blocks WHERE id = ${id} AND deleted_at IS NULL LIMIT 1`)
      const row = rows[0]
      if (!row) return null
      return Object.freeze({ document: normalizeDocument(parseJson(row.document, 'reusable block document'), DEFAULT_BLOCK_POLICY), version: positiveInt(row.version, 'reusableBlock.version') })
    },
  })
}

interface BlockCorpusEntry extends Record<string, unknown> { section: 'current' | 'reusable' | 'revision'; identity: string; entityType: string; payload: unknown }
interface BlockCorpus {
  readonly version: string
  readonly hash: string
  readonly entries: readonly BlockCorpusEntry[]
  readonly templateOwners: readonly string[]
  readonly affectedDefinitionVersions: number
  readonly entityTypes: readonly string[]
}
function documentReferencesType(document: BlockDocument, blockKey: string): boolean {
  const walk = (nodes: readonly BlockNode[]): boolean => nodes.some((node) => node.kind === 'inline' && (node.type === blockKey || walk(node.children ?? [])))
  return walk(document.roots)
}
function transformDocumentType(document: BlockDocument, from: string, to: string, revision: number): BlockDocument {
  const walk = (nodes: readonly BlockNode[]): readonly BlockNode[] => Object.freeze(nodes.map((node) => {
    if (node.kind === 'reusable') return node
    const children = node.children ? walk(node.children) : undefined
    return Object.freeze({ ...node, ...(node.type === from ? { type: to, blockDefinitionRevision: revision } : {}), ...(children ? { children } : {}) })
  }))
  return Object.freeze({ version: 1, roots: walk(document.roots) })
}
async function blockCorpus(db: FieldsReader, blockKey: string): Promise<BlockCorpus> {
  const currentRows = await executeRows<Record<string, unknown>>(db, sql`SELECT entity_type AS "entityType", entity_id AS "entityId", version, document, definition_versions AS "definitionVersions", parent_revision_id AS "parentRevisionId" FROM field_block_documents ORDER BY entity_type, entity_id`)
  const reusableRows = await executeRows<Record<string, unknown>>(db, sql`SELECT id, version, document, definition_versions AS "definitionVersions", deleted_at AS "deletedAt" FROM field_reusable_blocks ORDER BY id`)
  const revisionRows = await executeRows<Record<string, unknown>>(db, sql`SELECT id, entity_type AS "entityType", entity_id AS "entityId", kind, block_document AS "blockDocument", definition_versions AS "definitionVersions", source_version AS "sourceVersion" FROM field_revisions WHERE block_document IS NOT NULL ORDER BY entity_type, entity_id, id`)
  const definitionRows = await readCurrentRows(db)
  const versionRows = await executeRows<Record<string, unknown>>(db, sql`SELECT revision, canonical_hash AS "canonicalHash" FROM field_block_definition_versions WHERE block_key = ${blockKey} ORDER BY revision`)
  const entries: BlockCorpusEntry[] = [], entityTypes = new Set<string>(), templateOwners: string[] = []
  for (const row of currentRows) {
    const document = normalizeDocument(parseJson(row.document, 'block document'), DEFAULT_BLOCK_POLICY)
    if (!documentReferencesType(document, blockKey)) continue
    const entityType = String(row.entityType)
    entityTypes.add(entityType)
    entries.push({ section: 'current', identity: `${entityType}:${String(row.entityId)}`, entityType, payload: { entityId: row.entityId, version: row.version, document, definitionVersions: parseJson(row.definitionVersions, 'definitionVersions'), parentRevisionId: row.parentRevisionId } })
  }
  for (const row of reusableRows) {
    const document = normalizeDocument(parseJson(row.document, 'reusable block document'), DEFAULT_BLOCK_POLICY)
    if (!documentReferencesType(document, blockKey)) continue
    entityTypes.add('reusableBlock')
    entries.push({ section: 'reusable', identity: String(row.id), entityType: 'reusableBlock', payload: { version: row.version, document, definitionVersions: parseJson(row.definitionVersions, 'definitionVersions'), deletedAt: row.deletedAt ?? null } })
  }
  for (const row of revisionRows) {
    const document = normalizeDocument(parseJson(row.blockDocument, 'revision block document'), DEFAULT_BLOCK_POLICY)
    if (!documentReferencesType(document, blockKey)) continue
    const entityType = String(row.entityType)
    entityTypes.add(entityType)
    entries.push({ section: 'revision', identity: String(row.id), entityType, payload: { entityId: row.entityId, kind: row.kind, document, definitionVersions: parseJson(row.definitionVersions, 'definitionVersions'), sourceVersion: row.sourceVersion } })
  }
  for (const row of definitionRows) if (row.key !== blockKey && templateTypes(resolvedCurrent(row).template).has(blockKey)) templateOwners.push(row.key)
  entries.sort((left, right) => left.section.localeCompare(right.section) || left.identity.localeCompare(right.identity))
  templateOwners.sort()
  const identity = entries.map((entry) => ({ section: entry.section, identity: entry.identity, entityType: entry.entityType }))
  const version = await fieldsRequestHash({ identity, templateOwners, definitionVersions: versionRows.map((row) => row.revision) })
  const hash = await fieldsRequestHash({ entries, templateOwners, definitionVersions: versionRows })
  return Object.freeze({ version, hash, entries: Object.freeze(entries), templateOwners: Object.freeze(templateOwners), affectedDefinitionVersions: versionRows.length, entityTypes: Object.freeze([...entityTypes].sort()) })
}
function blockTokenEncode(payload: Record<string, FieldStorageValue>): Promise<string> {
  const bytes = new TextEncoder().encode(JSON.stringify(payload)), hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('')
  return fieldsRequestHash(payload).then((digest) => `${hex}.${digest}`)
}
async function blockTokenDecode(token: string): Promise<Record<string, unknown>> {
  const match = /^([a-f0-9]+)\.([a-f0-9]{64})$/i.exec(token)
  if (!match || match[1]!.length % 2 || match[1]!.length > 128_000) throw new FieldsLifecycleError('validation', 'block impact token is invalid')
  let parsed: unknown
  try { parsed = JSON.parse(new TextDecoder().decode(new Uint8Array(match[1]!.match(/../g)!.map((pair) => Number.parseInt(pair, 16))))) as unknown } catch { throw new FieldsLifecycleError('validation', 'block impact token is invalid') }
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) || await fieldsRequestHash(parsed) !== match[2]!.toLowerCase()) throw new FieldsLifecycleError('validation', 'block impact token integrity check failed')
  return parsed as Record<string, unknown>
}
function strategyStorage(strategy: FieldBlockChangeStrategy): FieldStorageValue { return jsonValue(strategy, 'strategy') }
function strategyEqual(left: FieldBlockChangeStrategy, right: unknown): boolean { try { return JSON.stringify(strategyStorage(left)) === JSON.stringify(jsonValue(right, 'strategy')) } catch { return false } }
function blockChangeIncompatibilities(current: ResolvedFieldBlock, proposed: FieldBlockDefinition | null, corpus: BlockCorpus, strategy: FieldBlockChangeStrategy, deactivate: boolean, deleting: boolean, replacement?: ResolvedFieldBlock): readonly string[] {
  const output: string[] = []
  if (strategy.kind === 'reject' && (deactivate || deleting) && corpus.entries.length > 0) output.push('affected documents require an explicit retaining, mapping, or purge strategy')
  if (proposed) {
    const removedGroups = current.fieldGroupKeys.filter((groupKey) => !proposed.fieldGroupKeys.includes(groupKey))
    if (removedGroups.length && corpus.entries.length) output.push(`field groups removed while referenced: ${removedGroups.join(',')}`)
    if (current.supports?.innerBlocks === true && proposed.supports?.innerBlocks !== true && corpus.entries.length) output.push('innerBlocks support removed while documents remain')
    if (current.supports?.multiple !== false && proposed.supports?.multiple === false && corpus.entries.length) output.push('multiple support narrowed while documents remain')
  }
  if (strategy.kind === 'mapDefinition') {
    if (!replacement || !replacement.active) output.push('replacement block is missing or inactive')
    else if (JSON.stringify([...replacement.fieldGroupKeys].sort()) !== JSON.stringify([...current.fieldGroupKeys].sort())) output.push('replacement block field groups do not match the affected document namespaces')
    if (corpus.templateOwners.length) output.push('template references require an explicit template definition migration')
  }
  if (strategy.kind === 'purgeWithBackup' && corpus.templateOwners.length) output.push('template references must be removed or mapped before purge')
  return Object.freeze(output.sort())
}

function normalizeBlockStrategy(strategy: FieldBlockChangeStrategy): FieldBlockChangeStrategy {
  if (strategy.kind === 'reject' || strategy.kind === 'retainUnknown') return Object.freeze({ kind: strategy.kind })
  if (strategy.kind === 'mapDefinition') return Object.freeze({ kind: 'mapDefinition', replacementKey: key(strategy.replacementKey, 'strategy.replacementKey') })
  if (strategy.kind === 'purgeWithBackup') return Object.freeze({ kind: 'purgeWithBackup', backupId: string(strategy.backupId, 'strategy.backupId', 512) })
  throw new FieldsLifecycleError('validation', 'unknown block change strategy')
}
export async function previewFieldBlockChange(
  db: FieldsReader,
  context: FieldsContext,
  input: { key: string; expectedVersion: number; patch?: FieldBlockDefinitionPatch; deactivate?: boolean; delete?: boolean; strategy: FieldBlockChangeStrategy },
): Promise<FieldBlockDefinitionImpact> {
  const blockKey = key(input.key, 'key')
  await assertFieldsAuthorized(context, 'manageDefinitions', definitionRef(blockKey))
  const current = await readCurrentBlock(db, blockKey)
  if (!current) throw new FieldsLifecycleError('not-found', 'block definition is unavailable')
  if (current.block.origin === 'code') throw new FieldsLifecycleError('validation', 'code-owned block changes require registry reconciliation')
  if (current.block.version !== input.expectedVersion) throw new FieldsLifecycleError('stale-version', 'block definition version changed')
  const deactivate = input.deactivate === true, deleting = input.delete === true
  if (!input.patch && !deactivate && !deleting) throw new FieldsLifecycleError('validation', 'block change must patch, deactivate, or delete')
  if (deactivate && deleting) throw new FieldsLifecycleError('validation', 'block change cannot deactivate and delete simultaneously')
  const proposed = input.patch ? normalizeDefinition({ ...current.block, ...input.patch, key: blockKey }) : null
  if (proposed) await assertGroupDependencies(db, proposed)
  const strategy = normalizeBlockStrategy(input.strategy)
  const corpus = await blockCorpus(db, blockKey)
  const migration = await assertDefinitionMigration(context, corpus.entityTypes.length ? corpus.entityTypes : ['fields-definition'])
  const replacementCurrent = strategy.kind === 'mapDefinition' ? await readCurrentBlock(db, strategy.replacementKey) : null
  const incompatibilities = [...blockChangeIncompatibilities(current.block, proposed, corpus, strategy, deactivate, deleting, replacementCurrent?.block)]
  if (strategy.kind === 'mapDefinition' && replacementCurrent) {
    if (JSON.stringify(replacementCurrent.block.parent ?? []) !== JSON.stringify(current.block.parent ?? [])) incompatibilities.push('replacement parent constraints differ from source')
    if (JSON.stringify(replacementCurrent.block.ancestor ?? []) !== JSON.stringify(current.block.ancestor ?? [])) incompatibilities.push('replacement ancestor constraints differ from source')
    if (replacementCurrent.block.supports?.innerBlocks !== current.block.supports?.innerBlocks) incompatibilities.push('replacement inner-block support differs from source')
  }
  const normalizedIncompatibilities = Object.freeze([...new Set(incompatibilities)].sort())
  const payload: Record<string, FieldStorageValue> = Object.freeze({
    kind: 'field-block-change',
    blockKey,
    expectedVersion: current.block.version,
    authorizationPolicyVersion: migration.policyVersion,
    documentCorpusVersion: corpus.version,
    documentCorpusHash: corpus.hash,
    affectedDocuments: corpus.entries.length,
    affectedDefinitionVersions: corpus.affectedDefinitionVersions,
    entityTypes: Object.freeze(corpus.entityTypes),
    templateOwners: Object.freeze(corpus.templateOwners),
    proposedDefinition: proposed ? jsonValue(proposed, 'proposedDefinition') : null,
    deactivate,
    delete: deleting,
    strategy: strategyStorage(strategy),
    incompatibilities: Object.freeze(normalizedIncompatibilities),
  })
  return Object.freeze({
    token: await blockTokenEncode(payload),
    blockKey,
    expectedVersion: current.block.version,
    authorizationPolicyVersion: migration.policyVersion,
    documentCorpusVersion: corpus.version,
    documentCorpusHash: corpus.hash,
    affectedDocuments: corpus.entries.length,
    affectedDefinitionVersions: corpus.affectedDefinitionVersions,
    incompatibilities: normalizedIncompatibilities,
  })
}

function definitionVersionMapAfterMap(value: unknown, from: string, to: string, revision: number): Readonly<Record<string, number>> {
  const source = parseDefinitionVersions(value), output: Record<string, number> = { ...source }
  if (output[`block:${from}`] !== undefined) delete output[`block:${from}`]
  const prior = output[`block:${to}`]
  if (prior !== undefined && prior !== revision) throw new FieldsLifecycleError('integrity', 'mapping would mix replacement block revisions')
  output[`block:${to}`] = revision
  return Object.freeze(output)
}
async function applyBlockMap(tx: FieldsTransaction, corpus: BlockCorpus, from: string, replacement: { block: ResolvedFieldBlock; row: CurrentBlockRow }): Promise<void> {
  const revision = Number(replacement.row.currentRevision)
  for (const entry of corpus.entries) {
    const payload = entry.payload as Record<string, unknown>
    const document = normalizeDocument(payload.document, DEFAULT_BLOCK_POLICY)
    const mapped = transformDocumentType(document, from, replacement.block.key, revision)
    const versions = definitionVersionMapAfterMap(payload.definitionVersions, from, replacement.block.key, revision)
    if (entry.section === 'current') {
      const entityId = String(payload.entityId)
      await tx.execute(sql`UPDATE field_block_documents SET document = ${JSON.stringify(mapped)}, definition_versions = ${JSON.stringify(versions)}, version = version + 1, updated_at = ${new Date().toISOString()} WHERE entity_type = ${entry.entityType} AND entity_id = ${entityId}`)
    } else if (entry.section === 'reusable') {
      await tx.execute(sql`UPDATE field_reusable_blocks SET document = ${JSON.stringify(mapped)}, definition_versions = ${JSON.stringify(versions)}, version = version + 1, updated_at = ${new Date().toISOString()} WHERE id = ${entry.identity}`)
    } else {
      // Historical revisions/autosaves are immutable. They remain pinned to the
      // source block definition and retained definition version after mapping.
      continue
    }
  }
}
async function purgeBlockCorpus(tx: FieldsTransaction, corpus: BlockCorpus): Promise<void> {
  for (const entry of corpus.entries) {
    const payload = entry.payload as Record<string, unknown>
    if (entry.section === 'current') await tx.execute(sql`DELETE FROM field_block_documents WHERE entity_type = ${entry.entityType} AND entity_id = ${String(payload.entityId)}`)
    else if (entry.section === 'reusable') await tx.execute(sql`DELETE FROM field_reusable_blocks WHERE id = ${entry.identity}`)
    else await tx.execute(sql`DELETE FROM field_revisions WHERE id = ${entry.identity}`)
  }
}
function parsedTokenDefinition(value: unknown, blockKey: string): FieldBlockDefinition | null {
  if (value === null) return null
  if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new FieldsLifecycleError('validation', 'block impact token contains invalid proposed definition')
  const definition = normalizeDefinition(value as unknown as FieldBlockDefinition)
  if (definition.key !== blockKey) throw new FieldsLifecycleError('validation', 'block impact token definition key mismatch')
  return definition
}

export async function applyFieldBlockChange(
  tx: FieldsTransaction,
  context: FieldsContext,
  input: { impactToken: string; operationId: string; strategy: FieldBlockChangeStrategy },
  options: { codeBlocks?: readonly CodeFieldBlock[]; destructive?: FieldsDestructiveDependencies },
): Promise<ResolvedFieldBlock | null> {
  assertActiveFieldsTransaction(tx)
  const token = await blockTokenDecode(input.impactToken)
  if (token.kind !== 'field-block-change' || typeof token.blockKey !== 'string') {
    throw new FieldsLifecycleError('validation', 'block impact token has wrong operation kind')
  }
  const blockKey = key(token.blockKey, 'blockKey')
  const strategy = normalizeBlockStrategy(input.strategy)
  if (!strategyEqual(strategy, token.strategy)) throw new FieldsLifecycleError('operation-conflict', 'block strategy differs from preview')

  const authorized = await assertFieldsAuthorized(context, 'manageDefinitions', definitionRef(blockKey))
  const entityTypes = Array.isArray(token.entityTypes)
    ? token.entityTypes.map((value, index) => string(value, `entityTypes.${index}`, 256))
    : []
  const migration = await assertDefinitionMigration(context, entityTypes.length ? entityTypes : ['fields-definition'])
  if (token.authorizationPolicyVersion !== migration.policyVersion) throw new FieldsLifecycleError('corpus-drift', 'authorization policy changed since preview')

  const requestHash = await fieldsRequestHash({
    impactToken: input.impactToken,
    strategy,
    codeKeys: (options.codeBlocks ?? []).map((block) => block.key).sort(),
  })
  const replay = await readFieldsReceipt(tx, input.operationId)
  if (replay) {
    assertReceiptReplay(replay, { kind: 'fields:apply-block-change', context: authorized, requestHash })
    const current = await readCurrentBlock(tx, blockKey)
    return current?.block ?? null
  }

  const current = await readCurrentBlock(tx, blockKey)
  if (!current) throw new FieldsLifecycleError('not-found', 'block definition is unavailable')
  if (current.block.origin === 'code' || (options.codeBlocks ?? []).some((block) => block.key === blockKey)) {
    throw new FieldsLifecycleError('validation', 'code-owned block changes require registry reconciliation')
  }
  if (Number(token.expectedVersion) !== current.block.version) throw new FieldsLifecycleError('stale-version', 'block definition version changed')

  const corpus = await blockCorpus(tx, blockKey)
  if (
    token.documentCorpusVersion !== corpus.version
    || token.documentCorpusHash !== corpus.hash
    || Number(token.affectedDocuments) !== corpus.entries.length
    || Number(token.affectedDefinitionVersions) !== corpus.affectedDefinitionVersions
  ) throw new FieldsLifecycleError('corpus-drift', 'block document corpus changed since preview')
  if (JSON.stringify([...corpus.entityTypes].sort()) !== JSON.stringify([...entityTypes].sort())) {
    throw new FieldsLifecycleError('corpus-drift', 'block corpus authorization scope changed since preview')
  }

  const proposed = parsedTokenDefinition(token.proposedDefinition, blockKey)
  const deactivate = token.deactivate === true
  const deleting = token.delete === true
  const replacement = strategy.kind === 'mapDefinition' ? await readCurrentBlock(tx, strategy.replacementKey) : null
  const incompatibilities = [...blockChangeIncompatibilities(current.block, proposed, corpus, strategy, deactivate, deleting, replacement?.block)]
  if (strategy.kind === 'mapDefinition' && replacement) {
    if (JSON.stringify(replacement.block.parent ?? []) !== JSON.stringify(current.block.parent ?? [])) incompatibilities.push('replacement parent constraints differ from source')
    if (JSON.stringify(replacement.block.ancestor ?? []) !== JSON.stringify(current.block.ancestor ?? [])) incompatibilities.push('replacement ancestor constraints differ from source')
    if (replacement.block.supports?.innerBlocks !== current.block.supports?.innerBlocks) incompatibilities.push('replacement inner-block support differs from source')
  }
  const normalizedIncompatibilities = [...new Set(incompatibilities)].sort()
  const tokenIncompatibilities = Array.isArray(token.incompatibilities) ? token.incompatibilities.map(String).sort() : []
  if (JSON.stringify(normalizedIncompatibilities) !== JSON.stringify(tokenIncompatibilities)) {
    throw new FieldsLifecycleError('corpus-drift', 'block compatibility facts changed since preview')
  }
  if (normalizedIncompatibilities.length > 0) throw new FieldsLifecycleError('validation', normalizedIncompatibilities.join('; '))

  if (strategy.kind === 'purgeWithBackup') {
    if (!options.destructive) throw new FieldsLifecycleError('backup-invalid', 'destructive dependencies are required')
    const counts = Object.freeze({ documents: corpus.entries.length, definitionVersions: corpus.affectedDefinitionVersions })
    const scopeHash = await fieldsRequestHash({
      definitionKind: 'block', definitionKey: blockKey, strategy,
      authorizationPolicyVersion: migration.policyVersion,
      corpusVersion: corpus.version, corpusHash: corpus.hash,
      previewToken: input.impactToken, affectedCounts: counts,
    })
    const backup = await verifyFieldsBackup(tx, {
      backupId: strategy.backupId,
      expectedScopeHash: scopeHash,
      corpusVersion: corpus.version,
      corpusHash: corpus.hash,
      counts,
    }, options.destructive.backups)
    const event = Object.freeze({
      operationId: input.operationId,
      principalId: authorized.principal.id,
      definitionKind: 'block' as const,
      definitionKey: blockKey,
      strategy: strategyStorage(strategy),
      authorizationPolicyVersion: migration.policyVersion,
      corpusVersion: corpus.version,
      corpusHash: corpus.hash,
      previewToken: input.impactToken,
      affectedCounts: counts,
      backup,
    })
    await options.destructive.effects.audit.record(tx, event)
    await options.destructive.effects.outbox.enqueue(tx, event)
    await purgeBlockCorpus(tx, corpus)
  } else if (strategy.kind === 'mapDefinition') {
    if (!replacement) throw new FieldsLifecycleError('validation', 'replacement block is unavailable')
    await applyBlockMap(tx, corpus, blockKey, replacement)
  }

  let result: ResolvedFieldBlock | null
  if (deleting) {
    await tx.execute(sql`DELETE FROM field_block_definitions WHERE key = ${blockKey} AND version = ${current.block.version}`)
    if (strategy.kind === 'purgeWithBackup') await tx.execute(sql`DELETE FROM field_block_definition_versions WHERE block_key = ${blockKey}`)
    result = null
  } else {
    const nextVersion = current.block.version + 1
    let currentRevision = Number(current.row.currentRevision)
    let canonicalHash = current.row.canonicalHash
    let definition = current.block as FieldBlockDefinition
    if (proposed) {
      await assertGroupDependencies(tx, proposed)
      canonicalHash = await fieldsRequestHash(proposed)
      if (canonicalHash !== current.row.canonicalHash) {
        currentRevision += 1
        await tx.execute(sql`INSERT INTO field_block_definition_versions (block_key, revision, canonical_hash, definition, origin, created_at) VALUES (${blockKey}, ${currentRevision}, ${canonicalHash}, ${JSON.stringify(proposed)}, 'db', ${new Date().toISOString()})`)
        definition = proposed
      }
    }
    const active = deactivate ? false : current.block.active
    await tx.execute(sql`UPDATE field_block_definitions SET version = ${nextVersion}, active = ${active}, current_revision = ${currentRevision}, canonical_hash = ${canonicalHash}, definition = ${JSON.stringify(definition)}, updated_at = ${new Date().toISOString()} WHERE key = ${blockKey} AND version = ${current.block.version}`)
    result = Object.freeze({
      ...definition,
      origin: 'db',
      version: nextVersion,
      active,
      ...(current.block.shadowedDbVersion === undefined ? {} : { shadowedDbVersion: current.block.shadowedDbVersion }),
    })
  }
  await writeFieldsReceipt(tx, {
    operationId: input.operationId,
    kind: 'fields:apply-block-change',
    context: authorized,
    requestHash,
    result: result ? Object.freeze({ key: result.key, version: result.version }) : Object.freeze({ deleted: true }),
  })
  return result
}

/** Internal composition seam for definition reconciliation; intentionally not re-exported from package index. */
export const __fieldBlockLifecycleInternals = Object.freeze({
  normalizeDefinition,
  readCurrentRows,
  readCurrentBlock,
  mergeCurrent,
  validateDefinitionGraph,
  assertGroupDependencies,
  blockCorpus,
  blockChangeIncompatibilities,
  applyBlockMap,
  purgeBlockCorpus,
  validateBlockFieldMap,
  readBlockVersion,
})
