import { DomainEntityKind } from '../../models/kinds.js'
import type { DataDomain } from '../DataDomain.js'
import type { DomainEntity } from '../DomainEntity.js'
import { ReservedKeywords } from './postgresql.js'
import type { DomainValidation } from './rules.js'

/**
 * EntityValidation is a class that performs validation on entities in a data domain.
 *
 * @remarks
 * - We do not need to check for parent uniqueness here, because in the graph there can only be one edge
 *   between two nodes. Parent relationships are described by an edge in the graph.
 */
export class EntityValidation {
  constructor(protected domain: DataDomain) {}

  /**
   * Performs all the validation rules on the entity.
   * If you are interested in a specific rule, use the specific method.
   * @param target The target entity to validate. Can be a string with
   *               the entity key or a DomainEntity object.
   */
  validate(target: string | DomainEntity): DomainValidation[] {
    const results: DomainValidation[] = []
    let entity: DomainEntity | undefined
    if (typeof target === 'string') {
      entity = this.domain.findEntity(target)
    } else {
      entity = target
    }
    if (!entity) {
      const message = `The "${target}" entity does not exist.`
      const help = `The entity must be defined in the domain.`
      results.push({
        field: '*',
        rule: 'exists',
        message,
        help,
        key: target as string,
        kind: DomainEntityKind,
        severity: 'error',
      })
      return results
    }
    const primaryKey = this.validatePrimaryKey(entity)
    results.push(...primaryKey)
    const minimumProperties = this.minimumRequiredProperties(entity)
    results.push(...minimumProperties)
    const name = this.validateName(entity)
    results.push(...name)
    const unique = this.uniqueName(entity)
    results.push(...unique)
    return results
  }

  /**
   * Validates the entity primary key.
   * @param entity The entity to validate
   */
  validatePrimaryKey(entity: DomainEntity): DomainValidation[] {
    const results: DomainValidation[] = []
    const primary = entity.primaryKey()
    if (!primary) {
      const message = `The "${entity.info.getLabel()}" entity has no identifier.`
      const help = `An entity that can exists by itself must have identifier defined.`
      results.push({
        field: 'properties',
        rule: 'primary_key',
        message,
        help,
        severity: 'error',
        key: entity.key,
        kind: entity.kind,
      })
    }
    return results
  }

  /**
   * Checks if an entity has properties through its entire inheritance chain.
   * This includes direct properties and properties inherited from any level of parent entities.
   * @param entity The entity to check
   * @returns True if the entity has properties either directly or through inheritance
   */
  private hasPropertiesInherited(entity: DomainEntity): boolean {
    // Check direct properties first
    if (entity.hasProperties()) {
      return true
    }

    // Check all parents recursively
    for (const parent of entity.listParents()) {
      if (this.hasPropertiesInherited(parent)) {
        return true
      }
    }

    return false
  }

  /**
   * Checks if an entity has associations through its entire inheritance chain.
   * This includes direct associations and associations inherited from any level of parent entities.
   * @param entity The entity to check
   * @returns True if the entity has associations either directly or through inheritance
   */
  private hasAssociationsInherited(entity: DomainEntity): boolean {
    // Check direct associations first
    if (entity.hasAssociations()) {
      return true
    }

    // Check all parents recursively
    for (const parent of entity.listParents()) {
      if (this.hasAssociationsInherited(parent)) {
        return true
      }
    }

    return false
  }

  /**
   * Checks if the entity has the minimum required properties.
   * @param entity The entity to validate
   */
  minimumRequiredProperties(entity: DomainEntity): DomainValidation[] {
    const results: DomainValidation[] = []

    // Check if entity has properties or associations through entire inheritance chain
    const hasProperties = this.hasPropertiesInherited(entity)
    const hasAssociations = this.hasAssociationsInherited(entity)

    if (!hasProperties && !hasAssociations) {
      const message = `The "${entity.info.getLabel()}" entity has no properties. It will be ignored.`
      const help = `Entities that have no properties are ignored in the data domain. No schema will be generated for it.`
      results.push({
        field: 'properties',
        rule: 'required',
        message,
        help,
        severity: 'warning',
        key: entity.key,
        kind: entity.kind,
      })
    }
    return results
  }

  /**
   * Validates the entity name.
   *
   * @remarks
   * - An entity must have a name defined.
   * - The name has to follow the same rules as the names in a PostgreSQL database.
   *   - Table names must start with a letter (a-z) or underscore (_).
   *   - Subsequent characters can be letters, digits (0-9), or underscores (_).
   *   - Names are case-insensitive.
   *   - The maximum length of a table name is 31 characters
   *   - Should be snake case (it's a convention, not an error).
   *   - Should not be a reserved word (for example: "IN", "SELECT", "FROM", etc.).
   * @param entity The entity to validate
   */
  validateName(entity: DomainEntity): DomainValidation[] {
    const results: DomainValidation[] = []
    const label = entity.info.getLabel()
    if (!entity.info.name) {
      const message = `The "${label}" entity has no name.`
      const help = `An entity must have a name.`
      results.push({
        field: 'name',
        rule: 'required',
        message,
        help,
        severity: 'error',
        key: entity.key,
        kind: entity.kind,
      })
      return results
    }

    const name = entity.info.name
    if (name.length < 2) {
      const message = `The "${label}" entity name is too short.`
      const help = `The name must be at least 2 characters long.`
      results.push({
        field: 'name',
        rule: 'length',
        message,
        help,
        severity: 'error',
        key: entity.key,
        kind: entity.kind,
      })
    }
    if (name.length > 31) {
      const message = `The "${label}" entity name is too long.`
      const help = `The name must be at most 31 characters long.`
      results.push({
        field: 'name',
        rule: 'length',
        message,
        help,
        severity: 'error',
        key: entity.key,
        kind: entity.kind,
      })
    }
    if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
      const message = `The "${label}" entity name is invalid.`
      const help = `The name must start with a letter (a-z) or underscore (_). Subsequent characters can be letters, digits (0-9), or underscores (_).`
      results.push({
        field: 'name',
        rule: 'format',
        message,
        help,
        severity: 'error',
        key: entity.key,
        kind: entity.kind,
      })
    }
    if (/^[A-Z]/.test(name)) {
      const message = `The "${label}" entity name is not in snake case.`
      const help = `The name should be in snake case (lowercase letters and underscores).`
      results.push({
        field: 'name',
        rule: 'snake_case',
        message,
        help,
        severity: 'info',
        key: entity.key,
        kind: entity.kind,
      })
    }
    if (ReservedKeywords.has(name.toUpperCase())) {
      const message = `The "${label}" entity name is a reserved keyword.`
      const help = `The name should not be a reserved keyword.`
      results.push({
        field: 'name',
        rule: 'reserved',
        message,
        help,
        severity: 'error',
        key: entity.key,
        kind: entity.kind,
      })
    }
    return results
  }

  /**
   * Checks if the entity name is unique in the data domain.
   * @param entity The entity to validate
   */
  uniqueName(entity: DomainEntity): DomainValidation[] {
    const results: DomainValidation[] = []
    const name = entity.info.name?.toLowerCase()
    if (!name) {
      return results
    }
    // We need to check the unique names in all entities, including foreign ones.
    for (const other of this.domain.listEntities()) {
      if (other.info.name?.toLowerCase() === name && other.key !== entity.key) {
        const message = `The "${name}" entity name is already used in the data domain.`
        const help = `The name must be unique. This includes references to other data domains.`
        results.push({
          field: 'name',
          rule: 'unique',
          message,
          help,
          severity: 'error',
          key: other.key,
          kind: other.kind,
        })
      }
    }
    for (const other of this.domain.listAllForeignEntities()) {
      if (other.info.name?.toLowerCase() === name && other.key !== entity.key) {
        const message = `The "${name}" entity name is already used in the foreign data domain.`
        const help = `The name must be unique. This includes references to other data domains.`
        results.push({
          field: 'name',
          rule: 'unique',
          message,
          help,
          severity: 'error',
          key: other.key,
          kind: other.kind,
          parent: other.domain.key,
        })
      }
    }
    return results
  }
}
