import {
	assert,
	exhaustiveSwitchError,
	getOwnProperty,
	isEqual,
	objectMapEntries,
	Result,
	structuredClone,
} from '@tldraw/utils'
import { UnknownRecord } from './BaseRecord'
import { devFreeze } from './devFreeze'
import {
	Migration,
	MigrationFailureReason,
	MigrationId,
	MigrationResult,
	MigrationSequence,
	parseMigrationId,
	sortMigrations,
	SynchronousStorage,
	validateMigrations,
} from './migrate'
import { RecordType } from './RecordType'
import { SerializedStore, Store, StoreSnapshot } from './Store'

/**
 * Version 1 format for serialized store schema information.
 *
 * This is the legacy format used before schema version 2. Version 1 schemas
 * separate store-level versioning from record-level versioning, and support
 * subtypes for complex record types like shapes.
 *
 * @example
 * ```ts
 * const schemaV1: SerializedSchemaV1 = {
 *   schemaVersion: 1,
 *   storeVersion: 2,
 *   recordVersions: {
 *     book: { version: 3 },
 *     shape: {
 *       version: 2,
 *       subTypeVersions: { rectangle: 1, circle: 2 },
 *       subTypeKey: 'type'
 *     }
 *   }
 * }
 * ```
 *
 * @public
 */
export interface SerializedSchemaV1 {
	/** Schema version is the version for this type you're looking at right now */
	schemaVersion: 1
	/**
	 * Store version is the version for the structure of the store. e.g. higher level structure like
	 * removing or renaming a record type.
	 */
	storeVersion: number
	/** Record versions are the versions for each record type. e.g. adding a new field to a record */
	recordVersions: Record<
		string,
		| {
				version: number
		  }
		| {
				// subtypes are used for migrating shape and asset props
				version: number
				subTypeVersions: Record<string, number>
				subTypeKey: string
		  }
	>
}

/**
 * Version 2 format for serialized store schema information.
 *
 * This is the current format that uses a unified sequence-based approach
 * for tracking versions across all migration sequences. Each sequence ID
 * maps to the latest version number for that sequence.
 *
 * @example
 * ```ts
 * const schemaV2: SerializedSchemaV2 = {
 *   schemaVersion: 2,
 *   sequences: {
 *     'com.tldraw.store': 3,
 *     'com.tldraw.book': 2,
 *     'com.tldraw.shape': 4,
 *     'com.tldraw.shape.rectangle': 1
 *   }
 * }
 * ```
 *
 * @public
 */
export interface SerializedSchemaV2 {
	schemaVersion: 2
	sequences: {
		[sequenceId: string]: number
	}
}

/**
 * Union type representing all supported serialized schema formats.
 *
 * This type allows the store to handle both legacy (V1) and current (V2)
 * schema formats during deserialization and migration.
 *
 * @example
 * ```ts
 * function handleSchema(schema: SerializedSchema) {
 *   if (schema.schemaVersion === 1) {
 *     // Handle V1 format
 *     console.log('Store version:', schema.storeVersion)
 *   } else {
 *     // Handle V2 format
 *     console.log('Sequences:', schema.sequences)
 *   }
 * }
 * ```
 *
 * @public
 */
export type SerializedSchema = SerializedSchemaV1 | SerializedSchemaV2

/**
 * Upgrades a serialized schema from version 1 to version 2 format.
 *
 * Version 1 schemas use separate `storeVersion` and `recordVersions` fields,
 * while version 2 schemas use a unified `sequences` object with sequence IDs.
 *
 * @param schema - The serialized schema to upgrade
 * @returns A Result containing the upgraded schema or an error message
 *
 * @example
 * ```ts
 * const v1Schema = {
 *   schemaVersion: 1,
 *   storeVersion: 1,
 *   recordVersions: {
 *     book: { version: 2 },
 *     author: { version: 1, subTypeVersions: { fiction: 1 }, subTypeKey: 'genre' }
 *   }
 * }
 *
 * const result = upgradeSchema(v1Schema)
 * if (result.ok) {
 *   console.log(result.value.sequences)
 *   // { 'com.tldraw.store': 1, 'com.tldraw.book': 2, 'com.tldraw.author': 1, 'com.tldraw.author.fiction': 1 }
 * }
 * ```
 *
 * @public
 */
export function upgradeSchema(schema: SerializedSchema): Result<SerializedSchemaV2, string> {
	if (schema.schemaVersion > 2 || schema.schemaVersion < 1) return Result.err('Bad schema version')
	if (schema.schemaVersion === 2) return Result.ok(schema as SerializedSchemaV2)
	const result: SerializedSchemaV2 = {
		schemaVersion: 2,
		sequences: {
			'com.tldraw.store': schema.storeVersion,
		},
	}

	for (const [typeName, recordVersion] of Object.entries(schema.recordVersions)) {
		result.sequences[`com.tldraw.${typeName}`] = recordVersion.version
		if ('subTypeKey' in recordVersion) {
			for (const [subType, version] of Object.entries(recordVersion.subTypeVersions)) {
				result.sequences[`com.tldraw.${typeName}.${subType}`] = version
			}
		}
	}
	return Result.ok(result)
}

/**
 * Information about a record validation failure that occurred in the store.
 *
 * This interface provides context about validation errors, including the failed
 * record, the store state, and the operation phase where the failure occurred.
 * It's used by validation failure handlers to implement recovery strategies.
 *
 * @example
 * ```ts
 * const schema = StoreSchema.create(
 *   { book: Book },
 *   {
 *     onValidationFailure: (failure: StoreValidationFailure<Book>) => {
 *       console.error(`Validation failed during ${failure.phase}:`, failure.error)
 *       console.log('Failed record:', failure.record)
 *       console.log('Previous record:', failure.recordBefore)
 *
 *       // Return a corrected version of the record
 *       return { ...failure.record, title: failure.record.title || 'Untitled' }
 *     }
 *   }
 * )
 * ```
 *
 * @public
 */
export interface StoreValidationFailure<R extends UnknownRecord> {
	error: unknown
	store: Store<R>
	record: R
	phase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests'
	recordBefore: R | null
}

/**
 * Configuration options for creating a StoreSchema.
 *
 * These options control migration behavior, validation error handling,
 * and integrity checking for the store schema.
 *
 * @example
 * ```ts
 * const options: StoreSchemaOptions<MyRecord, MyProps> = {
 *   migrations: [bookMigrations, authorMigrations],
 *   onValidationFailure: (failure) => {
 *     // Log the error and return a corrected record
 *     console.error('Validation failed:', failure.error)
 *     return sanitizeRecord(failure.record)
 *   },
 *   createIntegrityChecker: (store) => {
 *     // Set up integrity checking logic
 *     return setupIntegrityChecks(store)
 *   }
 * }
 * ```
 *
 * @public
 */
export interface StoreSchemaOptions<R extends UnknownRecord, P> {
	migrations?: MigrationSequence[]
	/** @public */
	onValidationFailure?(data: StoreValidationFailure<R>): R
	/** @internal */
	createIntegrityChecker?(store: Store<R, P>): void
}

/**
 * Manages the schema definition, validation, and migration system for a Store.
 *
 * StoreSchema coordinates record types, handles data migrations between schema
 * versions, validates records, and provides the foundational structure for
 * reactive stores. It acts as the central authority for data consistency
 * and evolution within the store system.
 *
 * @example
 * ```ts
 * // Define record types
 * const Book = createRecordType<Book>('book', { scope: 'document' })
 * const Author = createRecordType<Author>('author', { scope: 'document' })
 *
 * // Create schema with migrations
 * const schema = StoreSchema.create(
 *   { book: Book, author: Author },
 *   {
 *     migrations: [bookMigrations, authorMigrations],
 *     onValidationFailure: (failure) => {
 *       console.warn('Validation failed, using default:', failure.error)
 *       return failure.record // or return a corrected version
 *     }
 *   }
 * )
 *
 * // Use with store
 * const store = new Store({ schema })
 * ```
 *
 * @public
 */
export class StoreSchema<R extends UnknownRecord, P = unknown> {
	/**
	 * Creates a new StoreSchema with the given record types and options.
	 *
	 * This static factory method is the recommended way to create a StoreSchema.
	 * It ensures type safety while providing a clean API for schema definition.
	 *
	 * @param types - Object mapping type names to their RecordType definitions
	 * @param options - Optional configuration for migrations, validation, and integrity checking
	 * @returns A new StoreSchema instance
	 *
	 * @example
	 * ```ts
	 * const Book = createRecordType<Book>('book', { scope: 'document' })
	 * const Author = createRecordType<Author>('author', { scope: 'document' })
	 *
	 * const schema = StoreSchema.create(
	 *   {
	 *     book: Book,
	 *     author: Author
	 *   },
	 *   {
	 *     migrations: [bookMigrations],
	 *     onValidationFailure: (failure) => failure.record
	 *   }
	 * )
	 * ```
	 *
	 * @public
	 */
	static create<R extends UnknownRecord, P = unknown>(
		// HACK: making this param work with RecordType is an enormous pain
		// let's just settle for making sure each typeName has a corresponding RecordType
		// and accept that this function won't be able to infer the record type from it's arguments
		types: { [TypeName in R['typeName']]: { createId: any } },
		options?: StoreSchemaOptions<R, P>
	): StoreSchema<R, P> {
		return new StoreSchema<R, P>(types as any, options ?? {})
	}

	readonly migrations: Record<string, MigrationSequence> = {}
	readonly sortedMigrations: readonly Migration[]
	private readonly migrationCache = new WeakMap<SerializedSchema, Result<Migration[], string>>()

	private constructor(
		public readonly types: {
			[Record in R as Record['typeName']]: RecordType<R, any>
		},
		private readonly options: StoreSchemaOptions<R, P>
	) {
		for (const m of options.migrations ?? []) {
			assert(!this.migrations[m.sequenceId], `Duplicate migration sequenceId ${m.sequenceId}`)
			validateMigrations(m)
			this.migrations[m.sequenceId] = m
		}
		const allMigrations = Object.values(this.migrations).flatMap((m) => m.sequence)
		this.sortedMigrations = sortMigrations(allMigrations)

		for (const migration of this.sortedMigrations) {
			if (!migration.dependsOn?.length) continue
			for (const dep of migration.dependsOn) {
				const depMigration = allMigrations.find((m) => m.id === dep)
				assert(depMigration, `Migration '${migration.id}' depends on missing migration '${dep}'`)
			}
		}
	}

	/**
	 * Validates a record using its corresponding RecordType validator.
	 *
	 * This method ensures that records conform to their type definitions before
	 * being stored. If validation fails and an onValidationFailure handler is
	 * provided, it will be called to potentially recover from the error.
	 *
	 * @param store - The store instance where validation is occurring
	 * @param record - The record to validate
	 * @param phase - The lifecycle phase where validation is happening
	 * @param recordBefore - The previous version of the record (for updates)
	 * @returns The validated record, potentially modified by validation failure handler
	 *
	 * @example
	 * ```ts
	 * try {
	 *   const validatedBook = schema.validateRecord(
	 *     store,
	 *     { id: 'book:1', typeName: 'book', title: '', author: 'Jane Doe' },
	 *     'createRecord',
	 *     null
	 *   )
	 * } catch (error) {
	 *   console.error('Record validation failed:', error)
	 * }
	 * ```
	 *
	 * @public
	 */
	validateRecord(
		store: Store<R>,
		record: R,
		phase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests',
		recordBefore: R | null
	): R {
		try {
			const recordType = getOwnProperty(this.types, record.typeName)
			if (!recordType) {
				throw new Error(`Missing definition for record type ${record.typeName}`)
			}
			return recordType.validate(record, recordBefore ?? undefined)
		} catch (error: unknown) {
			if (this.options.onValidationFailure) {
				return this.options.onValidationFailure({
					store,
					record,
					phase,
					recordBefore,
					error,
				})
			} else {
				throw error
			}
		}
	}

	/**
	 * Gets all migrations that need to be applied to upgrade from a persisted schema
	 * to the current schema version.
	 *
	 * This method compares the persisted schema with the current schema and determines
	 * which migrations need to be applied to bring the data up to date. It handles
	 * both regular migrations and retroactive migrations, and caches results for
	 * performance.
	 *
	 * @param persistedSchema - The schema version that was previously persisted
	 * @returns A Result containing the list of migrations to apply, or an error message
	 *
	 * @example
	 * ```ts
	 * const persistedSchema = {
	 *   schemaVersion: 2,
	 *   sequences: { 'com.tldraw.book': 1, 'com.tldraw.author': 0 }
	 * }
	 *
	 * const migrationsResult = schema.getMigrationsSince(persistedSchema)
	 * if (migrationsResult.ok) {
	 *   console.log('Migrations to apply:', migrationsResult.value.length)
	 *   // Apply each migration to bring data up to date
	 * }
	 * ```
	 *
	 * @public
	 */
	public getMigrationsSince(persistedSchema: SerializedSchema): Result<Migration[], string> {
		// Check cache first
		const cached = this.migrationCache.get(persistedSchema)
		if (cached) {
			return cached
		}

		const upgradeResult = upgradeSchema(persistedSchema)
		if (!upgradeResult.ok) {
			// Cache the error result
			this.migrationCache.set(persistedSchema, upgradeResult)
			return upgradeResult
		}
		const schema = upgradeResult.value
		const sequenceIdsToInclude = new Set(
			// start with any shared sequences
			Object.keys(schema.sequences).filter((sequenceId) => this.migrations[sequenceId])
		)

		// also include any sequences that are not in the persisted schema but are marked as postHoc
		for (const sequenceId in this.migrations) {
			if (schema.sequences[sequenceId] === undefined && this.migrations[sequenceId].retroactive) {
				sequenceIdsToInclude.add(sequenceId)
			}
		}

		if (sequenceIdsToInclude.size === 0) {
			const result = Result.ok([])
			// Cache the empty result
			this.migrationCache.set(persistedSchema, result)
			return result
		}

		const allMigrationsToInclude = new Set<MigrationId>()
		for (const sequenceId of sequenceIdsToInclude) {
			const theirVersion = schema.sequences[sequenceId]
			if (
				(typeof theirVersion !== 'number' && this.migrations[sequenceId].retroactive) ||
				theirVersion === 0
			) {
				for (const migration of this.migrations[sequenceId].sequence) {
					allMigrationsToInclude.add(migration.id)
				}
				continue
			}
			const theirVersionId = `${sequenceId}/${theirVersion}`
			const idx = this.migrations[sequenceId].sequence.findIndex((m) => m.id === theirVersionId)
			// todo: better error handling
			if (idx === -1) {
				const result = Result.err('Incompatible schema?')
				// Cache the error result
				this.migrationCache.set(persistedSchema, result)
				return result
			}
			for (const migration of this.migrations[sequenceId].sequence.slice(idx + 1)) {
				allMigrationsToInclude.add(migration.id)
			}
		}

		// collect any migrations
		const result = Result.ok(
			this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id))
		)
		// Cache the result
		this.migrationCache.set(persistedSchema, result)
		return result
	}

	/**
	 * Migrates a single persisted record to match the current schema version.
	 *
	 * This method applies the necessary migrations to transform a record from an
	 * older (or newer) schema version to the current version. It supports both
	 * forward ('up') and backward ('down') migrations.
	 *
	 * @param record - The record to migrate
	 * @param persistedSchema - The schema version the record was persisted with
	 * @param direction - Direction to migrate ('up' for newer, 'down' for older)
	 * @returns A MigrationResult containing the migrated record or an error
	 *
	 * @example
	 * ```ts
	 * const oldRecord = { id: 'book:1', typeName: 'book', title: 'Old Title', publishDate: '2020-01-01' }
	 * const oldSchema = { schemaVersion: 2, sequences: { 'com.tldraw.book': 1 } }
	 *
	 * const result = schema.migratePersistedRecord(oldRecord, oldSchema, 'up')
	 * if (result.type === 'success') {
	 *   console.log('Migrated record:', result.value)
	 *   // Record now has publishedYear instead of publishDate
	 * } else {
	 *   console.error('Migration failed:', result.reason)
	 * }
	 * ```
	 *
	 * @public
	 */
	migratePersistedRecord(
		record: R,
		persistedSchema: SerializedSchema,
		direction: 'up' | 'down' = 'up'
	): MigrationResult<R> {
		const migrations = this.getMigrationsSince(persistedSchema)
		if (!migrations.ok) {
			// TODO: better error
			console.error('Error migrating record', migrations.error)
			return { type: 'error', reason: MigrationFailureReason.MigrationError }
		}
		let migrationsToApply = migrations.value
		if (migrationsToApply.length === 0) {
			return { type: 'success', value: record }
		}

		if (!migrationsToApply.every((m) => m.scope === 'record')) {
			return {
				type: 'error',
				reason:
					direction === 'down'
						? MigrationFailureReason.TargetVersionTooOld
						: MigrationFailureReason.TargetVersionTooNew,
			}
		}

		if (direction === 'down') {
			if (!migrationsToApply.every((m) => m.scope === 'record' && m.down)) {
				return {
					type: 'error',
					reason: MigrationFailureReason.TargetVersionTooOld,
				}
			}
			migrationsToApply = migrationsToApply.slice().reverse()
		}

		record = structuredClone(record)
		try {
			for (const migration of migrationsToApply) {
				if (migration.scope === 'store') throw new Error(/* won't happen, just for TS */)
				if (migration.scope === 'storage') throw new Error(/* won't happen, just for TS */)
				const shouldApply = migration.filter ? migration.filter(record) : true
				if (!shouldApply) continue
				const result = migration[direction]!(record)
				if (result) {
					record = structuredClone(result) as any
				}
			}
		} catch (e) {
			console.error('Error migrating record', e)
			return { type: 'error', reason: MigrationFailureReason.MigrationError }
		}

		return { type: 'success', value: record }
	}

	migrateStorage(storage: SynchronousStorage<R>) {
		const schema = storage.getSchema()
		assert(schema, 'Schema is missing.')

		const migrations = this.getMigrationsSince(schema)
		if (!migrations.ok) {
			console.error('Error migrating store', migrations.error)
			throw new Error(migrations.error)
		}
		const migrationsToApply = migrations.value
		if (migrationsToApply.length === 0) {
			return
		}

		storage.setSchema(this.serialize())

		for (const migration of migrationsToApply) {
			if (migration.scope === 'record') {
				// Collect updates during iteration, then apply them after.
				// This avoids issues with live iterators (e.g., SQLite) where updating
				// records during iteration can cause them to be visited multiple times.
				const updates: [string, R][] = []
				for (const [id, state] of storage.entries()) {
					const shouldApply = migration.filter ? migration.filter(state) : true
					if (!shouldApply) continue
					const record = structuredClone(state)
					const result = migration.up!(record as any) ?? record
					if (!isEqual(result, state)) {
						updates.push([id, result as R])
					}
				}
				for (const [id, record] of updates) {
					storage.set(id, record)
				}
			} else if (migration.scope === 'store') {
				// legacy
				const prevStore = Object.fromEntries(storage.entries())
				let nextStore = structuredClone(prevStore)
				nextStore = (migration.up!(nextStore) as any) ?? nextStore
				for (const [id, state] of Object.entries(nextStore)) {
					if (!state) continue // these will be deleted in the next loop
					if (!isEqual(state, prevStore[id])) {
						storage.set(id, state)
					}
				}
				for (const id of Object.keys(prevStore)) {
					if (!nextStore[id]) {
						storage.delete(id)
					}
				}
			} else if (migration.scope === 'storage') {
				migration.up!(storage)
			} else {
				exhaustiveSwitchError(migration)
			}
		}
		// Clean up by filtering out any non-document records.
		// This is mainly legacy support for extremely early days tldraw.
		for (const [id, state] of storage.entries()) {
			if (this.getType(state.typeName).scope !== 'document') {
				storage.delete(id)
			}
		}
	}

	/**
	 * Migrates an entire store snapshot to match the current schema version.
	 *
	 * This method applies all necessary migrations to bring a persisted store
	 * snapshot up to the current schema version. It handles both record-level
	 * and store-level migrations, and can optionally mutate the input store
	 * for performance.
	 *
	 * @param snapshot - The store snapshot containing data and schema information
	 * @param opts - Options controlling migration behavior
	 *   - mutateInputStore - Whether to modify the input store directly (default: false)
	 * @returns A MigrationResult containing the migrated store or an error
	 *
	 * @example
	 * ```ts
	 * const snapshot = {
	 *   schema: { schemaVersion: 2, sequences: { 'com.tldraw.book': 1 } },
	 *   store: {
	 *     'book:1': { id: 'book:1', typeName: 'book', title: 'Old Book', publishDate: '2020-01-01' }
	 *   }
	 * }
	 *
	 * const result = schema.migrateStoreSnapshot(snapshot)
	 * if (result.type === 'success') {
	 *   console.log('Migrated store:', result.value)
	 *   // All records are now at current schema version
	 * }
	 * ```
	 *
	 * @public
	 */
	migrateStoreSnapshot(
		snapshot: StoreSnapshot<R>,
		opts?: { mutateInputStore?: boolean }
	): MigrationResult<SerializedStore<R>> {
		const migrations = this.getMigrationsSince(snapshot.schema)
		if (!migrations.ok) {
			// TODO: better error
			console.error('Error migrating store', migrations.error)
			return { type: 'error', reason: MigrationFailureReason.MigrationError }
		}
		const migrationsToApply = migrations.value
		if (migrationsToApply.length === 0) {
			return { type: 'success', value: snapshot.store }
		}
		const store = Object.assign(
			new Map<string, R>(objectMapEntries(snapshot.store).map(devFreeze)),
			{
				getSchema: () => snapshot.schema,
				setSchema: (_: SerializedSchema) => {},
			}
		)
		try {
			this.migrateStorage(store)
			if (opts?.mutateInputStore) {
				for (const [id, record] of store.entries()) {
					snapshot.store[id as keyof typeof snapshot.store] = record
				}
				for (const id of Object.keys(snapshot.store)) {
					if (!store.has(id)) {
						delete snapshot.store[id as keyof typeof snapshot.store]
					}
				}
				return { type: 'success', value: snapshot.store }
			} else {
				return {
					type: 'success',
					value: Object.fromEntries(store.entries()) as SerializedStore<R>,
				}
			}
		} catch (e) {
			console.error('Error migrating store', e)
			return { type: 'error', reason: MigrationFailureReason.MigrationError }
		}
	}

	/**
	 * Creates an integrity checker function for the given store.
	 *
	 * This method calls the createIntegrityChecker option if provided, allowing
	 * custom integrity checking logic to be set up for the store. The integrity
	 * checker is used to validate store consistency and catch data corruption.
	 *
	 * @param store - The store instance to create an integrity checker for
	 * @returns An integrity checker function, or undefined if none is configured
	 *
	 * @internal
	 */
	createIntegrityChecker(store: Store<R, P>): (() => void) | undefined {
		return this.options.createIntegrityChecker?.(store) ?? undefined
	}

	/**
	 * Serializes the current schema to a SerializedSchemaV2 format.
	 *
	 * This method creates a serialized representation of the current schema,
	 * capturing the latest version number for each migration sequence.
	 * The result can be persisted and later used to determine what migrations
	 * need to be applied when loading data.
	 *
	 * @returns A SerializedSchemaV2 object representing the current schema state
	 *
	 * @example
	 * ```ts
	 * const serialized = schema.serialize()
	 * console.log(serialized)
	 * // {
	 * //   schemaVersion: 2,
	 * //   sequences: {
	 * //     'com.tldraw.book': 3,
	 * //     'com.tldraw.author': 2
	 * //   }
	 * // }
	 *
	 * // Store this with your data for future migrations
	 * localStorage.setItem('schema', JSON.stringify(serialized))
	 * ```
	 *
	 * @public
	 */
	serialize(): SerializedSchemaV2 {
		return {
			schemaVersion: 2,
			sequences: Object.fromEntries(
				Object.values(this.migrations).map(({ sequenceId, sequence }) => [
					sequenceId,
					sequence.length ? parseMigrationId(sequence.at(-1)!.id).version : 0,
				])
			),
		}
	}

	/**
	 * Serializes a schema representing the earliest possible version.
	 *
	 * This method creates a serialized schema where all migration sequences
	 * are set to version 0, representing the state before any migrations
	 * have been applied. This is used in specific legacy scenarios.
	 *
	 * @returns A SerializedSchema with all sequences set to version 0
	 *
	 * @deprecated This is only here for legacy reasons, don't use it unless you have david's blessing!
	 * @internal
	 */
	serializeEarliestVersion(): SerializedSchema {
		return {
			schemaVersion: 2,
			sequences: Object.fromEntries(
				Object.values(this.migrations).map(({ sequenceId }) => [sequenceId, 0])
			),
		}
	}

	/**
	 * Gets the RecordType definition for a given type name.
	 *
	 * This method retrieves the RecordType associated with the specified
	 * type name, which contains the record's validation, creation, and
	 * other behavioral logic.
	 *
	 * @param typeName - The name of the record type to retrieve
	 * @returns The RecordType definition for the specified type
	 *
	 * @throws Will throw an error if the record type does not exist
	 *
	 * @internal
	 */
	getType(typeName: string) {
		const type = getOwnProperty(this.types, typeName)
		assert(type, 'record type does not exists')
		return type
	}
}
