/**
 * @module types/reservedFields
 * @description The canonical reserved-field contract — ONE place that defines
 * which keys belong to Brainy (top-level entity/relationship fields) and may
 * therefore never live inside a `metadata` bag.
 *
 * Three layers enforce the contract, all driven by the constants below:
 *
 * 1. **Compile time** — `AddParams.metadata`, `UpdateParams.metadata`,
 *    `RelateParams.metadata` and `UpdateRelationParams.metadata` are typed so
 *    a literal reserved key is a TypeScript error (see
 *    {@link EntityMetadataInput} / {@link RelationMetadataInput}).
 * 2. **Write time** — for untyped (JavaScript) callers that smuggle a
 *    reserved key past the compiler anyway, every write path normalizes the
 *    bag: user-mutable fields are remapped to their dedicated top-level
 *    param (top-level wins when both are supplied) and system-managed fields
 *    are dropped with a one-shot warning naming the correct write path.
 * 3. **Read time** — every read path splits the stored flat record through
 *    {@link splitNounMetadataRecord} / {@link splitVerbMetadataRecord}, so a
 *    reserved field is surfaced ONLY at top level and `entity.metadata` /
 *    `relation.metadata` contain ONLY custom fields, always — live reads,
 *    batch reads, and historical (`asOf`) reads alike.
 *
 * Documented for consumers in `docs/concepts/consistency-model.md`
 * ("Reserved fields").
 */

/**
 * @description Entity (noun) field names reserved by Brainy. These keys are
 * stored in the flat per-entity metadata record alongside custom fields, but
 * they belong to Brainy: every read path extracts them to top-level
 * `Entity` fields, and no write path accepts them inside `metadata`.
 *
 * | Key | Canonical write path |
 * |-----|----------------------|
 * | `noun` | the `type` param of `add()` / `update()` (stored under the key `noun`) |
 * | `subtype` | the `subtype` param |
 * | `visibility` | the `visibility` param (`'public'` \| `'internal'`; `'system'` is Brainy-only) |
 * | `createdAt` | system-managed — set once at `add()` time |
 * | `updatedAt` | system-managed — set on every write |
 * | `confidence` | the `confidence` param |
 * | `weight` | the `weight` param |
 * | `service` | the `service` param of `add()` (immutable afterwards) |
 * | `data` | the `data` param |
 * | `createdBy` | the `createdBy` param of `add()` (immutable afterwards) |
 * | `_rev` | system-managed revision counter — pass `ifRev` to `update()` for CAS |
 *
 * @example
 * import { RESERVED_ENTITY_FIELDS } from '@soulcraft/brainy'
 * const isReserved = (key: string) =>
 *   (RESERVED_ENTITY_FIELDS as readonly string[]).includes(key)
 */
export const RESERVED_ENTITY_FIELDS = [
  'noun',
  'subtype',
  'visibility',
  'createdAt',
  'updatedAt',
  'confidence',
  'weight',
  'service',
  'data',
  'createdBy',
  '_rev'
] as const

/**
 * @description Union of the entity field names reserved by Brainy — the
 * element type of {@link RESERVED_ENTITY_FIELDS}.
 */
export type ReservedEntityField = (typeof RESERVED_ENTITY_FIELDS)[number]

/**
 * @description Relationship (verb) field names reserved by Brainy — the verb
 * mirror of {@link RESERVED_ENTITY_FIELDS}. The stored flat record keys the
 * relationship type under `verb` (the public `Relation` field is `type`);
 * everything else matches the entity list.
 *
 * | Key | Canonical write path |
 * |-----|----------------------|
 * | `verb` | the `type` param of `relate()` / `updateRelation()` (stored under the key `verb`) |
 * | `subtype` | the `subtype` param |
 * | `visibility` | the `visibility` param (`'public'` \| `'internal'`; `'system'` is Brainy-only) |
 * | `createdAt` | system-managed — set once at `relate()` time |
 * | `updatedAt` | system-managed — set on every write |
 * | `confidence` | the `confidence` param |
 * | `weight` | the `weight` param |
 * | `service` | the `service` param of `relate()` (immutable afterwards) |
 * | `data` | the `data` param |
 * | `createdBy` | system-managed |
 * | `_rev` | system-managed |
 */
export const RESERVED_RELATION_FIELDS = [
  'verb',
  'subtype',
  'visibility',
  'createdAt',
  'updatedAt',
  'confidence',
  'weight',
  'service',
  'data',
  'createdBy',
  '_rev'
] as const

/**
 * @description Union of the relationship field names reserved by Brainy —
 * the element type of {@link RESERVED_RELATION_FIELDS}.
 */
export type ReservedRelationField = (typeof RESERVED_RELATION_FIELDS)[number]

/**
 * @description `true` when `T` is exactly `any` (the classic
 * `0 extends 1 & T` probe — only `any` absorbs the impossible intersection).
 * Used to keep the reserved-key guard active for untyped brains, where a
 * plain `T & guard` intersection would collapse to `any` and check nothing.
 */
type IsAny<T> = 0 extends 1 & T ? true : false

/**
 * @description Compile-time tripwire: marks every reserved entity key as
 * `never` so an object literal carrying one fails to type-check. Keys that
 * `T` itself declares (including via an index signature, where
 * `keyof T = string`) are exempted — a consumer who *explicitly* types a
 * reserved key into their metadata shape keeps a working (if unwise) type,
 * and index-signature metadata types remain assignable.
 */
export type NoReservedEntityKeys<T> = {
  readonly [K in ReservedEntityField as K extends keyof T ? never : K]?: never
}

/**
 * @description Relationship mirror of {@link NoReservedEntityKeys}.
 */
export type NoReservedRelationKeys<T> = {
  readonly [K in ReservedRelationField as K extends keyof T ? never : K]?: never
}

/**
 * @description The metadata bag shape for untyped brains (`T = any`): an
 * open index signature (any custom key, any value — exactly the pre-8.0
 * latitude) intersected with the reserved-key guard, whose declared
 * `?: never` properties take precedence over the index signature so a
 * literal reserved key is still a compile error.
 */
type OpenBag<Guard> = { [key: string]: any } & Guard

/**
 * @description The type of `AddParams.metadata`: the consumer's metadata
 * shape `T` with reserved entity keys forbidden at compile time. For untyped
 * brains (`T = any`) the bag stays open ({@link OpenBag}), so arbitrary
 * custom fields remain legal while literal reserved keys still error.
 */
export type EntityMetadataInput<T> = IsAny<T> extends true
  ? OpenBag<NoReservedEntityKeys<object>>
  : T & NoReservedEntityKeys<T>

/**
 * @description The type of `UpdateParams.metadata`: a partial patch of the
 * consumer's metadata shape with reserved entity keys forbidden at compile
 * time. Same `T = any` handling as {@link EntityMetadataInput}.
 */
export type EntityMetadataPatch<T> = IsAny<T> extends true
  ? OpenBag<NoReservedEntityKeys<object>>
  : Partial<T> & NoReservedEntityKeys<T>

/**
 * @description The type of `RelateParams.metadata`: the consumer's edge
 * metadata shape with reserved relationship keys forbidden at compile time.
 */
export type RelationMetadataInput<T> = IsAny<T> extends true
  ? OpenBag<NoReservedRelationKeys<object>>
  : T & NoReservedRelationKeys<T>

/**
 * @description The type of `UpdateRelationParams.metadata`: a partial patch
 * of the consumer's edge metadata shape with reserved relationship keys
 * forbidden at compile time.
 */
export type RelationMetadataPatch<T> = IsAny<T> extends true
  ? OpenBag<NoReservedRelationKeys<object>>
  : Partial<T> & NoReservedRelationKeys<T>

/**
 * @description Result of splitting a stored flat metadata record into its
 * reserved (Brainy-owned) and custom (consumer-owned) halves.
 */
export interface SplitMetadataRecord<F extends string> {
  /** The reserved fields present in the record, keyed by reserved name. */
  reserved: Partial<Record<F, unknown>>
  /** Every other key — the consumer's custom metadata, and nothing else. */
  custom: Record<string, unknown>
}

const RESERVED_ENTITY_SET: ReadonlySet<string> = new Set(RESERVED_ENTITY_FIELDS)
const RESERVED_RELATION_SET: ReadonlySet<string> = new Set(RESERVED_RELATION_FIELDS)

/**
 * @description Shared splitter — partitions a record's keys against a
 * reserved-name set. `null`/`undefined` records split to two empty objects.
 * @param record - The stored flat metadata record (reserved + custom keys mixed).
 * @param reservedSet - The reserved-name set to partition against.
 * @returns The `{ reserved, custom }` halves.
 */
function splitRecord<F extends string>(
  record: Record<string, unknown> | null | undefined,
  reservedSet: ReadonlySet<string>
): SplitMetadataRecord<F> {
  const reserved: Record<string, unknown> = {}
  const custom: Record<string, unknown> = {}
  if (record && typeof record === 'object') {
    for (const [key, value] of Object.entries(record)) {
      if (reservedSet.has(key)) {
        reserved[key] = value
      } else {
        custom[key] = value
      }
    }
  }
  return { reserved: reserved as Partial<Record<F, unknown>>, custom }
}

/**
 * @description Split a stored entity (noun) flat metadata record into
 * reserved fields and custom metadata — THE canonical read-side split. Every
 * entity read path (live `get()`, batch reads, paginated listings, and
 * historical `asOf()` materialization) goes through this function, so the
 * reserved list can never drift between read paths.
 * @param record - The stored flat metadata record.
 * @returns `reserved` (Brainy-owned fields) and `custom` (the consumer's metadata bag).
 * @example
 * const { reserved, custom } = splitNounMetadataRecord(stored)
 * // reserved.noun → entity.type, reserved.confidence → entity.confidence, …
 * // custom → entity.metadata (custom fields only, always)
 */
export function splitNounMetadataRecord(
  record: Record<string, unknown> | null | undefined
): SplitMetadataRecord<ReservedEntityField> {
  return splitRecord(record, RESERVED_ENTITY_SET)
}

/**
 * @description Split a stored relationship (verb) flat metadata record into
 * reserved fields and custom metadata — the verb mirror of
 * {@link splitNounMetadataRecord}, used by every relationship read path.
 * @param record - The stored flat metadata record.
 * @returns `reserved` (Brainy-owned fields) and `custom` (the consumer's metadata bag).
 */
export function splitVerbMetadataRecord(
  record: Record<string, unknown> | null | undefined
): SplitMetadataRecord<ReservedRelationField> {
  return splitRecord(record, RESERVED_RELATION_SET)
}
