import type { DataDomain } from './DataDomain.js'
import { DomainAssociationKind, DomainEntityKind, DomainPropertyKind } from '../models/kinds.js'
import { DomainElement, type DomainElementSchema } from './DomainElement.js'
import { nanoid } from '../nanoid.js'
import { type IThing, Thing } from '../models/Thing.js'
import { observed, retargetChange, toRaw } from '../decorators/observed.js'
import type { IShapeUnion } from '../amf/definitions/Shapes.js'
import { DomainProperty, type DomainPropertySchema } from './DomainProperty.js'
import { RemovePropertyException } from '../exceptions/remove_property_exception.js'
import { DomainAssociation, DomainAssociationSchema } from './DomainAssociation.js'
import type { AssociationAddOptions } from './types.js'
import type { FileBreadcrumb } from '../models/store/File.js'
import type { DomainModel } from './DomainModel.js'
import type { DomainNamespace } from './DomainNamespace.js'
import { ValidationError } from '../exceptions/validation_error.js'
import { ShapeGenerator } from './amf/ShapeGenerator.js'
import type { IShapeRenderOptions } from '../amf/shape/ShapeBase.js'
import { ApiSchemaGenerator } from '../amf/ApiSchemaGenerator.js'
import { removeGraphNode } from './GraphUtils.js'
import { DataSemantics, isEntitySemantic, type SemanticType, type AppliedDataSemantic } from './Semantics.js'

export interface EntityOrderedItem {
  /**
   * The type of the ordered item.
   */
  type: 'property' | 'association'

  /**
   * The key of the ordered item.
   */
  key: string
}

export interface DomainEntitySchema extends DomainElementSchema {
  kind: typeof DomainEntityKind
  /**
   * The domain entity description.
   */
  info: IThing

  /**
   * Optional general purpose tags for the UI.
   */
  tags?: string[]

  /**
   * The semantics applied to this entity.
   * This is a list of applied semantics that can be used to
   * describe the entity in more detail.
   */
  semantics?: AppliedDataSemantic[]

  /**
   * The ordered list of fields (properties and associations) in the schema.
   * These only keep references to the properties and associations.
   * The order of the fields is important as it defines the order of the
   * properties in the schema.
   */
  fields?: EntityOrderedItem[]

  /**
   * Whether this entity is deprecated.
   */
  deprecated?: boolean
}

/**
 * Represents an entity within a data domain model.
 *
 * A `DomainEntity` defines a structured data type,
 * containing properties and associations. It's a
 * fundamental building block for modeling complex data
 * structures.
 *
 * **Key Features:**
 *
 * - **Properties:** Defines the data elements (fields) within the entity.
 * - **Associations:** Defines relationships to other entities.
 * - **Inheritance:** Supports inheriting properties and associations from parent entities.
 * - **Metadata:** Supports tags, semantics, and deprecation status.
 * - **AMF Shape Generation:** Can be serialized to an AMF (API Modeling Framework) shape.
 * - **Example Generation:** Can generate example databased on its schema.
 *
 * **Usage:**
 *
 * The preferred way to create a `DomainEntity` is through
 * the `DomainModel.addEntity()` method.
 *
 * **Example:**
 *
 * ```typescript
 * const dataDomain = new DataDomain();
 * const userModel = dataDomain.addModel({ key: 'userModel' });
 * const userEntity = userModel.addEntity({ key: 'user' });
 * ```
 *
 * Entity Validation Rules:
 *
 * - **Primary Key:** An entity must have a primary key selected. This can come from a parent
 *   entity or be defined in the current entity.
 * - **Parent Entity:** The parent entity must exist in the graph.
 * - **Circular Dependency:** Adding a parent entity that creates a circular dependency is not allowed.
 * - **Unique Parent:** An entity cannot have the same parent more than once.
 * - **Property Shadowing:** A property can shadow a parent property. This means that if a property
 *   with the same key exists in both the parent and child entity, the child property will take precedence.
 * - **Association Shadowing:** An association can shadow a parent association. This means that if an
 *   association with the same key exists in both the parent and child entity,
 *   the child association will take precedence.
 * - **Property Resolution:** When the same property exists more than one parent and the child entity,
 *   doesn't explicitly define the property, then:
 *   - The last parent entity (iterating from the first entity to the last) that has the property
 *     will be used as the source of the property.
 * - **Association Resolution:** When the same association exists more than one parent and the child entity,
 *   doesn't explicitly define the association, then:
 *   - The last parent entity (iterating from the first entity to the last) that has the association
 *     will be used as the source of the association.
 * - **Minimum Required Properties:** An entity must have at least one property defined. Entities without properties
 *   are ignored in the system.
 * - **Entity Name**:
 *   - An entity must have a name defined.
 *   - The name has to follow the same rules as the names in a SQL database. This means that the name must be unique
 *     within the data domain and cannot contain special characters.
 * - **Association Targets:** An association must have a target entity defined.
 *   This means that the association must point to at least one entity.
 */
export class DomainEntity extends DomainElement {
  /**
   * The kind of the domain element.
   */
  override kind: typeof DomainEntityKind

  /**
   * The description of the domain entity.
   */
  @retargetChange() accessor info: Thing

  /**
   * Optional general purpose tags for the UI.
   */
  @observed({ deep: true }) accessor tags: string[]

  /**
   * Semantics applied to this property.
   */
  @observed({ deep: true }) accessor semantics: AppliedDataSemantic[] = []

  /**
   * The ordered list of fields (properties and associations) in the schema.
   * These only keep references to the properties and associations.
   * The order of the fields is important as it defines the order of the
   * properties in the schema.
   */
  accessor fields: EntityOrderedItem[]

  /**
   * Whether this entity is deprecated.
   */
  @observed() accessor deprecated: boolean | undefined

  /**
   * Creates a full data entity schema with defaults.
   *
   * @param input The partial data entity schema.
   * @returns The data entity schema.
   */
  static createSchema(input: Partial<DomainEntitySchema> = {}): DomainEntitySchema {
    const { key = nanoid(), tags, semantics, fields, deprecated } = input
    const info = Thing.fromJSON(input.info, { name: 'new_entity' }).toJSON()
    const result: DomainEntitySchema = {
      kind: DomainEntityKind,
      key,
      info,
    }
    if (Array.isArray(tags)) {
      result.tags = [...tags]
    }
    if (Array.isArray(semantics)) {
      result.semantics = [...semantics]
    }
    if (Array.isArray(fields)) {
      result.fields = [...fields]
    }
    if (typeof deprecated === 'boolean') {
      result.deprecated = deprecated
    }
    return result
  }

  /**
   * Creates a new domain entity instance.
   *
   * You probably want to use the `DomainModel.addEntity()`
   * method instead.
   *
   * @param root A reference to the root DataDomain instance.
   * @param input The partial domain entity schema.
   */
  constructor(root: DataDomain, input?: Partial<DomainEntitySchema>) {
    const init = DomainEntity.createSchema(input)
    super(root, init.key)
    this.kind = DomainEntityKind
    this.info = new Thing(init.info)
    if (Array.isArray(init.tags)) {
      this.tags = [...init.tags]
    } else {
      this.tags = []
    }
    if (Array.isArray(init.semantics)) {
      this.semantics = init.semantics.map((item) => structuredClone(item))
    } else {
      this.semantics = []
    }
    if (Array.isArray(init.fields)) {
      this.fields = [...init.fields]
    } else {
      this.fields = []
    }
    if (typeof init.deprecated === 'boolean') {
      this.deprecated = init.deprecated
    }
  }

  /**
   * Generates a JSON representation of the entity.
   *
   * @returns A plain object representation of the `DomainEntity`.
   */
  toJSON(): DomainEntitySchema {
    const result: DomainEntitySchema = {
      kind: this.kind,
      key: this.key,
      info: this.info.toJSON(),
    }
    if (Array.isArray(this.fields) && this.fields.length) {
      result.fields = [...this.fields]
    }
    if (Array.isArray(this.semantics) && this.semantics.length) {
      result.semantics = toRaw(this, this.semantics)?.map((i) => structuredClone(i))
    }
    if (Array.isArray(this.tags) && this.tags.length) {
      result.tags = [...(toRaw(this, this.tags) as string[])]
    }
    if (typeof this.deprecated === 'boolean') {
      result.deprecated = this.deprecated
    }
    return result
  }

  /**
   * Returns a parent data entity where this data property
   * exist.
   *
   * @returns The parent `DomainModel` or undefined if not found.
   * @example
   * ```typescript
   * const parent = entity.getParentInstance();
   * if (parent) {
   *   console.log(parent.key);
   * }
   * ```
   */
  getParentInstance(): DomainModel | undefined {
    const parentKey = this.root.graph.parent(this.key)
    if (!parentKey) {
      return undefined
    }
    return this.root.graph.node(parentKey) as DomainModel | undefined
  }

  /**
   * Adds a property to this entity.
   *
   * @param property The partial property schema.
   * @returns The created `DomainProperty` instance.
   * @example
   * ```typescript
   * const property = entity.addProperty({
   *   key: 'name',
   *   type: 'string',
   *   required: true,
   * })
   * ```
   */
  addProperty(property?: Partial<DomainPropertySchema>): DomainProperty {
    if (property && property.key && this.root.graph.hasNode(property.key)) {
      throw new Error(`Trying to add a property ${property.key}, but it already exists`)
    }
    const instance = new DomainProperty(this.root, this.key, property)
    this.root.graph.setNode(instance.key, instance)
    this.root.graph.setEdge(this.key, instance.key, { type: 'property' })
    this.fields.push({
      type: 'property',
      key: instance.key,
    })
    this.root.notifyChange()
    return instance
  }

  /**
   * Removes a property from this entity.
   *
   * @param key The key of the property to remove.
   * @throws {RemovePropertyException} When the property does not exist.
   * @example
   * ```typescript
   * entity.removeProperty('name')
   * ```
   */
  removeProperty(key: string): void {
    if (!this.root.graph.hasNode(key)) {
      throw new RemovePropertyException(`Trying to remove the ${key} property, but it doesn't exist`)
    }
    const prop = this.root.graph.node(key) as DomainProperty
    if (prop.getParentInstance() !== this) {
      throw new RemovePropertyException(`Trying to remove the ${key} property, but it doesn't belong to this entity`)
    }
    removeGraphNode(this.root.graph, key)
    this.removeField(key)
    this.root.notifyChange()
  }

  /**
   * A shortcut to `listProperties()`.
   */
  get properties(): Generator<DomainProperty> {
    return this.listProperties()
  }

  /**
   * Lists all properties of this entity.
   *
   * Note, it accounts for the order of the properties as
   * defined in the `fields` array.
   *
   * @returns A generator that yields each `DomainProperty`.
   * @example
   * ```typescript
   * for (const property of entity.listProperties()) {
   *   console.log(property.key);
   * }
   * ```
   */
  *listProperties(): Generator<DomainProperty> {
    for (const { key, type } of this.fields) {
      if (type !== 'property') {
        continue
      }
      const node = this.root.graph.node(key)
      if (!node || node.kind !== DomainPropertyKind) {
        continue
      }
      yield node
    }
  }

  /**
   * Checks if this entity has any properties.
   *
   * @returns True if the entity has properties.
   * @example
   * ```typescript
   * if (entity.hasProperties()) {
   *   // ...
   * }
   * ```
   */
  hasProperties(): boolean {
    return this.fields.some((item) => item.type === 'property')
  }

  /**
   * A shortcut to `listParents()`.
   */
  get parents(): Generator<DomainEntity> {
    return this.listParents()
  }

  /**
   * Lists all parent entities of this entity.
   *
   * @returns A generator that yields each parent `DomainEntity`.
   * @example
   * ```typescript
   * for (const parent of entity.listParents()) {
   *   console.log(parent.key);
   * }
   * ```
   */
  *listParents(): Generator<DomainEntity> {
    for (const edge of this.root.graph.outEdges(this.key)) {
      const value = this.root.graph.edge(edge)
      if (!value || value.type !== 'parent') {
        continue
      }
      const parent = this.root.graph.node(edge.w) as DomainEntity
      if (parent) {
        yield parent
      }
    }
  }

  /**
   * Adds a parent to this entity.
   *
   * @param key The key of the parent entity.
   * @param domain The key of the foreign domain the parent belongs to.
   * @returns `this` for chaining.
   * @throws ValidationError When the parent does not exist or a circular dependency is detected.
   * @example
   * ```typescript
   * entity.addParent('baseEntity');
   * ```
   */
  addParent(key: string, domain?: string): this {
    const effectiveKey = this.domain.buildReferenceKey(key, domain)
    // Prevent adding self as parent
    if (effectiveKey === this.key) {
      const message = 'Entity cannot be a parent of itself'
      throw new ValidationError(
        [
          {
            field: 'parents',
            message,
            rule: 'circular',
          },
        ],
        { message }
      )
    }
    const { graph } = this.root
    if (!graph.hasNode(effectiveKey)) {
      let message = `Entity with key "${key}" not found`
      if (domain) {
        message += ` in domain "${domain}"`
      }
      throw new ValidationError(
        [
          {
            field: 'parents',
            message,
            rule: 'required',
          },
        ],
        { message }
      )
    }
    // Check for circular inheritance
    if (this.hasCircularParent(effectiveKey)) {
      const message = `Circular inheritance detected. Cannot add ${key} as a parent`
      throw new ValidationError(
        [
          {
            field: 'parents',
            message,
            rule: 'circular',
          },
        ],
        { message }
      )
    }
    if (graph.outEdges(this.key, effectiveKey).next().value) {
      const message = `Parent ${key} already exists`
      throw new ValidationError(
        [
          {
            field: 'parents',
            message,
            rule: 'unique',
          },
        ],
        { message }
      )
    }
    // Parents are associated through graph edges.
    // Otherwise it would collide with the parent model.
    if (domain) {
      graph.setEdge(this.key, effectiveKey, { type: 'parent', domain, foreign: true })
    } else {
      graph.setEdge(this.key, effectiveKey, { type: 'parent' })
    }
    this.root.notifyChange()
    return this
  }

  /**
   * Removes a parent from this entity.
   *
   * @param key The key of the parent entity to remove.
   * @param domain Optional domain key if the parent belongs to a foreign domain.
   * @returns `this` for chaining.
   * @throws Error When the parent does not exist.
   * @example
   * ```typescript
   * // Remove local parent
   * entity.removeParent('baseEntity');
   *
   * // Remove foreign domain parent
   * entity.removeParent('baseEntity', 'externalDomain');
   * ```
   */
  removeParent(key: string, domain?: string): this {
    const { graph } = this.root
    const effectiveKey = this.domain.buildReferenceKey(key, domain)

    if (!graph.hasEdge(this.key, effectiveKey)) {
      let message = `Trying to remove a parent ${key} from ${this.key}, but it doesn't exist`
      if (domain) {
        message = `Trying to remove a foreign parent ${key} from domain "${domain}" from ${this.key}, but it doesn't exist`
      }
      throw new Error(message)
    }

    // Verify it's actually a parent edge
    const edge = graph.edge(this.key, effectiveKey)
    if (!edge || edge.type !== 'parent') {
      let message = `Edge between ${this.key} and ${key} exists but is not a parent relationship`
      if (domain) {
        message = `Edge between ${this.key} and ${key} in domain "${domain}" exists but is not a parent relationship`
      }
      throw new Error(message)
    }

    graph.removeEdge(this.key, effectiveKey)
    this.root.notifyChange()
    return this
  }

  /**
   * Checks if this entity has any parents.
   *
   * @returns True if the entity has parents.
   * @example
   * ```typescript
   * if (entity.hasParents()) {
   *   // ...
   * }
   * ```
   */
  hasParents(): boolean {
    for (const edge of this.root.graph.outEdges(this.key)) {
      if (this.root.graph.edge(edge)?.type === 'parent') {
        return true
      }
    }
    return false
  }

  /**
   * Checks if the passed entity has a circular parent
   * relationship when attempting to add a new parent.
   *
   * @param key The key of the parent being added.
   * @returns True if adding this parent would create a
   *   circular relationship.
   */
  hasCircularParent(key: string): boolean {
    const { graph } = this.root

    // Check if new parent is one of the children.
    for (const edge of graph.outEdges(key, this.key)) {
      if (graph.edge(edge)?.type === 'parent') {
        return true
      }
    }

    // check if this entity is in parents chain.
    let current: DomainEntity | undefined = graph.node(key) as DomainEntity | undefined
    while (current) {
      if (current.key === this.key) {
        return true
      }
      const parents = [...current.listParents()]
      if (parents.length === 0) {
        break
      }
      // Assuming single inheritance
      // TODO: Update this to check for multiple parents.
      current = parents[0]
    }

    return false
  }

  /**
   * Creates breadcrumbs from this entity to the root
   * namespace.
   *
   * @returns An array of `FileBreadcrumb` instances.
   * @example
   * ```typescript
   * const breadcrumbs = entity.breadcrumbs();
   * console.log(breadcrumbs);
   * ```
   */
  breadcrumbs(): FileBreadcrumb[] {
    const result: FileBreadcrumb[] = []
    result.push({
      key: this.key,
      name: this.info.getLabel('Unnamed entity'),
      kind: DomainEntityKind,
    })
    const model = this.getParentInstance()
    if (model) {
      result.push({
        key: model.key,
        kind: model.kind,
        name: model.info.getLabel('Unnamed model'),
      })
      let parent: DataDomain | DomainNamespace = model.getParentInstance()
      while (parent && parent !== this.root) {
        result.push({
          key: parent.key,
          kind: parent.kind,
          name: parent.info.getLabel('Unnamed namespace'),
        })
        const typed = parent as DomainNamespace
        parent = typed.getParentInstance()
      }
    }
    result.push({
      key: this.root.key,
      name: this.root.info.getLabel('Unnamed data domain'),
      kind: this.root.kind,
    })
    return result.reverse()
  }

  /**
   * Adds an association to this entity.
   *
   * @param init The association options.
   * @param input The partial association schema.
   * @returns The created `DomainAssociation` instance.
   * @throws Error When the target entity does not exist.
   * @example
   * ```typescript
   * const association = entity.addAssociation({
   *   key: 'address',
   * });
   * ```
   */
  addAssociation(init: AssociationAddOptions = {}, input?: Partial<DomainAssociationSchema>): DomainAssociation {
    const { graph } = this.root
    if (init.domain && init.key) {
      // target is foreign
      const foreignKey = `${init.domain}:${init.key}`
      if (!graph.hasNode(foreignKey)) {
        throw new Error(`Foreign entity ${foreignKey} not found`)
      }
    } else if (init.key) {
      if (!graph.hasNode(init.key)) {
        throw new Error(`Target entity ${init.key} not found`)
      }
    }
    const item = new DomainAssociation(this.root, this.key, input)
    graph.setNode(item.key, item)
    graph.setEdge(this.key, item.key, { type: 'association' })
    if (init.key) {
      item.addTarget(init.key, init.domain)
    }
    this.fields.push({ type: 'association', key: item.key })
    this.root.notifyChange()
    return item
  }

  /**
   * Removes an association from this entity.
   *
   * @param key The key of the association to remove.
   * @throws Error When the association does not exist.
   * @example
   * ```typescript
   * entity.removeAssociation('address');
   * ```
   */
  removeAssociation(key: string): void {
    if (!this.root.graph.hasEdge(this.key, key)) {
      throw new Error(`Unable to find a connection between this entity and the ${key} association`)
    }
    const edge = this.root.graph.edge(this.key, key)
    if (!edge || edge.type !== 'association') {
      throw new Error(`Association ${key} does not exist`)
    }
    removeGraphNode(this.root.graph, key)
    this.removeField(key)
    this.root.notifyChange()
  }

  /**
   * A shortcut to `listAssociations()`.
   */
  get associations(): Generator<DomainAssociation> {
    return this.listAssociations()
  }

  /**
   * Lists all associations of this entity.
   *
   * The order of the associations is defined in the `fields` array.
   *
   * @returns A generator that yields each
   *   `DomainAssociation`.
   * @example
   * ```typescript
   * for (const association of entity.listAssociations()) {
   *   console.log(association.key);
   * }
   * ```
   */
  *listAssociations(): Generator<DomainAssociation> {
    for (const { key, type } of this.fields) {
      if (type !== 'association') {
        continue
      }
      const node = this.root.graph.node(key)
      if (!node || node.kind !== DomainAssociationKind) {
        continue
      }
      yield node
    }
  }

  /**
   * Checks if this entity has any associations.
   *
   * @returns True if the entity has associations.
   * @example
   * ```typescript
   * if (entity.hasAssociations()) {
   *   // ...
   * }
   * ```
   */
  hasAssociations(): boolean {
    return this.fields.some((item) => item.type === 'association')
  }

  /**
   * Lists all fields (properties and associations) of this entity.
   *
   * @returns A generator that yields each `DomainAssociation` or `DomainProperty`.
   * @example
   * ```typescript
   * for (const field of entity.listFields()) {
   *   console.log(field.key);
   * }
   * ```
   */
  *listFields(): Generator<DomainAssociation | DomainProperty> {
    for (const { key } of this.fields) {
      const node = this.root.graph.node(key)
      if (!node) {
        continue
      }
      if (node.kind === DomainAssociationKind || node.kind === DomainPropertyKind) {
        yield node
      }
    }
  }

  /**
   * Checks if this entity has any fields (properties or associations).
   *
   * @returns True if the entity has fields.
   * @example
   * ```typescript
   * if (entity.hasFields()) {
   *   // ...
   * }
   * ```
   */
  hasFields(): boolean {
    return this.fields.length > 0
  }

  private removeField(key: string): void {
    this.fields = this.fields.filter((item) => item.key !== key)
  }

  /**
   * Creates an AMF's data Shape.
   *
   * The property itself is auto-generated. If the `schema` is defined then it is used
   * as the `range` of the property. Otherwise basic shape is generated for the range.
   *
   * This is a preferred way of reading the AMF shape as this synchronizes changed
   * data properties with the shape definition.
   *
   * @returns AMF property shape definition.
   */
  toApiShape(): IShapeUnion {
    const serializer = new ShapeGenerator()
    return serializer.entity(this)
  }

  /**
   * Reads the schema of the Entity and generates an example for it.
   *
   * @param mime The mime type of the example.
   * @param opts The example generation options.
   * @returns The generated example.
   * @example
   * ```typescript
   * const example = entity.toExample('application/json');
   * console.log(example);
   * ```
   */
  toExample(mime: string, opts: IShapeRenderOptions = {}): string | number | boolean | null | undefined {
    const shape = this.toApiShape()
    const generator = new ApiSchemaGenerator(mime, {
      renderExamples: opts.renderExamples ?? true,
      renderMocked: opts.renderMocked ?? true,
      renderOptional: opts.renderOptional ?? true,
      selectedUnions: opts.selectedUnions,
    })
    return generator.generate(shape)
  }

  /**
   * Checks whether the entity is a child of the given namespace or data model.
   * The relationship doesn't have to be direct, as long as the entity
   * is in the hierarchy it will return true.
   *
   * @param key The key of the parent to check.
   * @returns True if this entity is a child of the given namespace or data model.
   */
  isChildOf(key: string): boolean {
    if (this.key === key) {
      return false
    }
    const parent = this.getParentInstance()
    if (!parent) {
      return false
    }
    if (parent.key === key) {
      return true
    }
    return parent.isChildOf(key)
  }

  /**
   * Finds the primary key property of this entity.
   * @returns The primary key property of this entity or undefined if not found.
   */
  primaryKey(): DomainProperty | undefined {
    for (const property of this.properties) {
      if (property.primary) {
        return property
      }
    }
    let property: DomainProperty | undefined
    for (const parent of this.listParents()) {
      const prop = parent.primaryKey()
      if (prop) {
        // According to the validation rules, the last parent
        // that has the property will be used as the source of
        // the property.
        // In the future, we may add a schema definition to the entity that
        // will explicitly define which property to use.
        property = prop
      }
    }
    return property
  }

  /**
   * Adds or updates a semantic to the entity.
   * @param semantic The semantic to add to the entity.
   * @throws Error if the semantic is not an entity semantic.
   */
  addSemantic(semantic: AppliedDataSemantic): void {
    const sem = DataSemantics[semantic.id]
    if (!isEntitySemantic(sem)) {
      throw new Error(`Invalid semantic type: ${semantic.id}. Expected an entity semantic.`)
    }
    const index = this.semantics.findIndex((s) => s.id === semantic.id)
    if (index >= 0) {
      this.semantics[index] = semantic
    } else {
      this.semantics.push(semantic)
    }
    this.domain.notifyChange()
  }

  /**
   * Removes a semantic from the entity.
   * @param semanticId The ID of the semantic to remove.
   */
  removeSemantic(semanticId: SemanticType): void {
    const index = this.semantics.findIndex((s) => s.id === semanticId)
    if (index >= 0) {
      this.semantics.splice(index, 1)
      this.domain.notifyChange()
    }
  }

  /**
   * Checks if the entity has a specific semantic.
   * @param semanticId The ID of the semantic to check.
   * @returns True if the semantic is present, false otherwise.
   */
  hasSemantic(semanticId: SemanticType): boolean {
    return this.semantics.some((s) => s.id === semanticId)
  }

  /**
   * Generates a unique name by appending a number to the base name.
   * @param baseName The base name to make unique.
   * @returns A unique name within the current entity.
   */
  generateUniqueName(baseName: string): string {
    const existingNames = new Set<string>()
    for (const field of this.listFields()) {
      if (field.info.name) {
        existingNames.add(field.info.name)
      }
    }

    let counter = 2
    let newName = `${baseName}_copy`
    while (existingNames.has(newName)) {
      newName = `${baseName}_copy_${counter}`
      counter++
    }

    return newName
  }
}
