import {
  DomainNamespaceKind,
  DomainEntityKind,
  DomainModelKind,
  DomainPropertyKind,
  DomainAssociationKind,
  DataDomainKind,
} from '../models/kinds.js'
import type { DataDomain } from './DataDomain.js'
import { AssociationValidation } from './validation/association_validation.js'
import { EntityValidation } from './validation/entity_validation.js'
import { PropertyValidation } from './validation/property_validation.js'

export type DomainImpactKinds =
  | typeof DomainNamespaceKind
  | typeof DomainEntityKind
  | typeof DomainModelKind
  | typeof DomainPropertyKind
  | typeof DomainAssociationKind
  | typeof DataDomainKind

/**
 * The impact analysis report
 */
export interface DomainImpactReport {
  /**
   * The key of the impacted data object.
   * This is the key of the object that is being changed.
   */
  key: string
  /**
   * The kind of the impacted data object.
   * This is the kind of the object that is being changed.
   */
  kind: DomainImpactKinds
  /**
   * The list of impacted data objects.
   */
  impact: DomainImpactItem[]
  /**
   * Whether it is possible to proceed with the change.
   * If the change is not possible, the reason will be in the impact list.
   */
  canProceed: boolean
}

export interface DomainImpactItem {
  /**
   * The key of the impacted data object.
   */
  key: string
  /**
   * The kind of the impacted data object.
   */
  kind: string
  /**
   * The type of the impact.
   *
   * - `delete` - The data object would be deleted.
   */
  type: 'delete' | 'publish'
  /**
   * The impact description.
   * Explains what will happen to the impacted data object.
   * This is a human-readable description of the impact.
   * It should be clear and concise.
   */
  impact: string
  /**
   * The severity of the impact.
   *
   * - `info` - The impact is informational.
   * - `warning` - The impact can potentially cause problems but is not a blocker.
   * - `error` - The impact is a blocker and needs to be resolved before proceeding.
   */
  severity: 'info' | 'warning' | 'error'
  /**
   * Whether the impact is blocking the operation.
   * If true, the operation cannot proceed.
   * @deprecated Use `severity` instead.
   */
  blocking: boolean
  /**
   * The type of the relationship between two impacted objects.
   */
  relationship?: 'child'
  /**
   * The resolution of the conflict if the change will be forced.
   */
  resolution?: string
  /**
   * The optional parent of the impacted data object.
   * For example, if the impacted item is a property, this will be the entity it belongs to.
   */
  parent?: string
}

/**
 * # DomainImpactAnalysis
 *
 * The `DomainImpactAnalysis` class is a tool for analyzing the consequences of deleting data domain elements
 * within a `DataDomain`. It helps developers understand the ripple effects of removing a namespace, data model,
 * entity, property, or association, ensuring data integrity and preventing unintended side effects.
 *
 * ## Core Concepts
 *
 * -   **Impact Report:** The central output of the `DomainImpactAnalysis` class is an `DomainImpactReport`.
 *     This report details the potential consequences of a deletion operation, including:
 *     -   The object being deleted (`key`, `kind`).
 *     -   A list of `DomainImpactItem` objects, each describing a specific consequence.
 *     -   Whether the deletion can proceed safely (`canProceed`).
 *
 * -   **DomainImpactItem:** Each `DomainImpactItem` describes a specific consequence of the deletion.
 *     Key properties include:
 *     -   `key`: The key of the impacted object.
 *     -   `kind`: The kind of the impacted object.
 *     -   `type`: The type of impact (currently only `delete`).
 *     -   `impact`: A human-readable description of the impact.
 *     -   `blocking`: Whether this impact prevents the deletion from proceeding.
 *     -   `relationship`: The type of relationship between the deleted object and the impacted object (e.g., `child`).
 *     -   `resolution`: A description of how the impact will be resolved if the deletion is forced.
 *
 * -   **Blocking Impacts:** Some impacts are considered "blocking," meaning they prevent the deletion from proceeding
 *     without manual intervention. For example, deleting an entity that is a parent to other entities is
 *     a blocking impact.
 *
 * -   **Non-Blocking Impacts:** Some impacts are informational and do not prevent the deletion. For example, deleting a
 *     property is not a blocking impact.
 *
 * ## Usage
 *
 * 1.  **Instantiation:** Create an instance of `DomainImpactAnalysis`, passing the root `DataDomain` as an argument.
 *
 *     ```typescript
 *     import { DataDomain } from './DataDomain';
 *     import { DomainImpactAnalysis } from './DomainImpactAnalysis';
 *
 *     const dataDomain = new DataDomain();
 *     // ... add some data to the domain
 *     const analyzer = new DomainImpactAnalysis(dataDomain);
 *     ```
 *
 * 2.  **Performing Analysis:** Use the `deleteAnalysis()` method to generate an `DomainImpactReport` for
 *     a specific deletion.
 *     Provide the `key` and `kind` of the object to be deleted.
 *
 *     ```typescript
 *     import { DomainEntityKind } from '../models/kinds.js';
 *     // ...
 *     const entityKey = 'some-entity-key';
 *     const report = analyzer.deleteAnalysis(entityKey, DomainEntityKind);
 *     ```
 *
 * 3.  **Interpreting the Report:** Examine the `DomainImpactReport` to understand the consequences of the deletion.
 *     -   Check `report.canProceed` to see if the deletion is safe.
 *     -   Iterate through `report.impact` to understand each consequence.
 *     -   Pay special attention to `impact.blocking` to identify impacts that require manual resolution.
 *
 *     ```typescript
 *     if (report.canProceed) {
 *       // Proceed with deletion
 *     } else {
 *       console.warn('Deletion cannot proceed due to the following impacts:');
 *       report.impact.forEach((item) => {
 *         console.warn(`- ${item.impact}`);
 *         if (item.blocking) {
 *           console.warn(`  - This impact is blocking.`);
 *           if (item.resolution) {
 *             console.warn(`  - Resolution: ${item.resolution}`);
 *           }
 *         }
 *       });
 *       // Handle blocking impacts (e.g., prompt the user, modify the data, etc.)
 *     }
 *     ```
 *
 * ## Supported Deletion Scenarios
 *
 * The `DomainImpactAnalysis` class supports analyzing the deletion of the following data domain element types:
 *
 * - **Namespaces (`DomainNamespaceKind`):** Deleting a namespace also impacts all its child namespaces,
 *   data models, entities, properties, and associations.
 * - **Data Models (`DomainModelKind`):** Deleting a data model impacts all its entities, properties, and associations.
 * - **Entities (`DomainEntityKind`):** Deleting an entity impacts its properties, associations, and any other
 *   entities that have it as a parent or target in an association.
 * - **Properties (`DomainPropertyKind`):** Deleting a property impacts the entity it belongs to.
 * - **Associations (`DomainAssociationKind`):** Deleting an association impacts the entity it belongs to.
 * - **Data Domain (`DataDomainKind`):** Deleting a data domain is not supported.
 *
 * ## Example: Deleting an Entity
 *
 * Consider the following scenario:
 *
 * ```
 * DataDomain
 *   - Namespace: `MyNamespace`
 *     -   Data Model: `ProductModel`
 *         -   Entity: `Product`
 *             -   Property: `name`
 *             -   Association: `category` (targets `Category` entity)
 *         -   Entity: `Category`
 *             - Property: `name`
 *         - Entity: `SpecialProduct` (parent: `Product`)
 * ```
 *
 * If you attempt to delete the `Product` entity, the `DomainImpactAnalysis` will generate a report similar to this:
 *
 * ```json
 * {
 *   "key": "Product",
 *   "kind": "DomainEntityKind",
 *   "impact": [
 *     {
 *       "key": "Product",
 *       "kind": "DomainEntityKind",
 *       "type": "delete",
 *       "impact": "The entity with key Product will be deleted.",
 *       "blocking": false
 *     },
 *     {
 *       "key": "SpecialProduct",
 *       "kind": "DomainEntityKind",
 *       "type": "delete",
 *       "impact": "The SpecialProduct entity will become an orphan because it is a child of Product.",
 *       "resolution": "The \"Product\" entity will be removed as the parent of the \"SpecialProduct\" entity.",
 *       "blocking": true,
 *       "relationship": "child"
 *     },
 *     {
 *       "key": "category",
 *       "kind": "DomainAssociationKind",
 *       "type": "delete",
 *       "impact": "The association with key category will be broken because it has a target to Product.",
 *       "resolution": "The association with key category will be removed from Product.",
 *       "blocking": true
 *     },
 *     {
 *       "key": "name",
 *       "kind": "DomainPropertyKind",
 *       "type": "delete",
 *       "impact": "The property with key name will be deleted.",
 *       "blocking": false
 *     }
 *   ],
 *   "canProceed": false
 * }
 * ```
 *
 * This report indicates that:
 *
 * -   The `Product` entity will be deleted.
 * -   The `SpecialProduct` entity will become an orphan (blocking).
 * -   The `category` association will be broken (blocking).
 * -   The `name` property will be deleted.
 * -   The deletion cannot proceed without addressing the blocking impacts.
 *
 * ## Types
 *
 * ### `DomainImpactKinds`
 *
 * -   **Description:** A type alias for the kinds of data domain elements that can be analyzed.
 * -   **Values:**
 *     - `DomainNamespaceKind`
 *     - `DomainEntityKind`
 *     - `DomainModelKind`
 *     - `DomainPropertyKind`
 *     - `DomainAssociationKind`
 *     - `DataDomainKind`
 *
 * ### `DomainImpactReport`
 *
 * -   **Description:** The structure of the impact analysis report.
 * -   **Properties:**
 *     -   `key` (`string`): The key of the object being deleted.
 *     -   `kind` (`DomainImpactKinds`): The kind of the object being deleted.
 *     -   `impact` (`DomainImpactItem[]`): The list of impacts.
 *     -   `canProceed` (`boolean`): Whether the deletion can proceed.
 *
 * ### `DomainImpactItem`
 *
 * -   **Description:** The structure of an individual impact item.
 * -   **Properties:**
 *     -   `key` (`string`): The key of the impacted object.
 *     -   `kind` (`string`): The kind of the impacted object.
 *     -   `type` (`'delete'`): The type of impact.
 *     -   `impact` (`string`): The impact description.
 *     -   `blocking` (`boolean`): Whether the impact is blocking.
 *     -   `relationship` (`'child'`, optional): The relationship type.
 *     -   `resolution` (`string`, optional): The resolution description.
 *
 * ## Error Handling
 *
 * The `DomainImpactAnalysis` class does not throw errors. Instead, it uses the `DomainImpactReport` to communicate
 * the results of the analysis, including any blocking impacts.
 *
 * ## Best Practices
 *
 * - **Always Analyze Before Deleting:** Before deleting any data domain element, always use `DomainImpactAnalysis`
 *   to understand the consequences.
 * - **Handle Blocking Impacts:** Pay close attention to `blocking` impacts and implement appropriate
 *   logic to handle them.
 * - **Inform the User:** If a deletion cannot proceed, inform the user about the blocking impacts and provide
 *   guidance on how to resolve them.
 * - **Consider Forced Deletion:** In some cases, you may want to allow users to force a deletion even if there are
 *   blocking impacts. In such cases, you should clearly communicate the risks to the user.
 *
 * ## Conclusion
 *
 * The `DomainImpactAnalysis` class is an essential tool for maintaining data integrity when working with complex
 * data domain models. By providing detailed impact reports, it empowers developers to make informed decisions
 * about data deletion and prevent unintended consequences.
 */
export class DomainImpactAnalysis {
  private report: DomainImpactReport
  private root: DataDomain

  constructor(root: DataDomain) {
    this.root = root
    this.report = {
      key: '',
      kind: DataDomainKind,
      impact: [],
      canProceed: false,
    }
  }

  /**
   * Generates a report of how the data domain will be impacted by the deletion of a data domain object.
   * @param key The key of the impacted data domain object.
   * @param kind The kind of the impacted data object.
   * @returns The delete impact analysis report.
   */
  deleteAnalysis(key: string, kind: DomainImpactKinds): DomainImpactReport {
    this.report = {
      key,
      kind,
      impact: [],
      canProceed: true,
    }
    this.createDeleteImpact(key, kind, key)
    return this.report
  }

  /**
   * Generates a report of how the data domain will be impacted by the removal of a foreign namespace.
   * @param key The key of the foreign namespace.
   * @returns The delete impact analysis report.
   */
  removeForeignNamespaceAnalysis(key: string): DomainImpactReport {
    this.report = {
      key,
      kind: DomainNamespaceKind,
      impact: [],
      canProceed: true,
    }
    this.createRemoveForeignNamespaceImpact(key)
    return this.report
  }

  /**
   * Analyzes the data domain for publishing. Essentially, it performs a validation of the data domain
   * and returns a report of the impact.
   * @returns The publish impact analysis report.
   */
  publishAnalysis(): DomainImpactReport {
    this.report = {
      key: '',
      kind: DataDomainKind,
      impact: [],
      canProceed: true,
    }
    const entityValidator = new EntityValidation(this.root)
    const propertyValidator = new PropertyValidation(this.root)
    const associationValidator = new AssociationValidation(this.root)
    for (const entity of this.root.listEntities()) {
      if (entity.domain.key !== this.root.key) {
        // we don't need to validate foreign entities
        continue
      }
      const report = entityValidator.validate(entity)
      for (const item of report) {
        const blocking = item.severity === 'error'
        this.report.canProceed = this.report.canProceed && !blocking
        this.report.impact.push({
          key: item.key,
          kind: item.kind,
          type: 'publish',
          impact: item.message,
          blocking,
          resolution: item.help,
          severity: item.severity,
          parent: item.parent,
        })
      }
      for (const property of entity.properties) {
        const report = propertyValidator.validate(property)
        for (const item of report) {
          const blocking = item.severity === 'error'
          this.report.canProceed = this.report.canProceed && !blocking
          this.report.impact.push({
            key: item.key,
            kind: item.kind,
            type: 'publish',
            impact: item.message,
            blocking,
            resolution: item.help,
            severity: item.severity,
            parent: item.parent,
          })
        }
      }
      for (const association of entity.associations) {
        const report = associationValidator.validate(association)
        for (const item of report) {
          const blocking = item.severity === 'error'
          this.report.canProceed = this.report.canProceed && !blocking
          this.report.impact.push({
            key: item.key,
            kind: item.kind,
            type: 'publish',
            impact: item.message,
            blocking,
            resolution: item.help,
            severity: item.severity,
            parent: item.parent,
          })
        }
      }
    }
    return this.report
  }

  protected createDeleteImpact(key: string, kind: DomainImpactKinds, rootKey: string): void {
    switch (kind) {
      case DomainNamespaceKind:
        this.deleteNamespaceAnalysis(key, rootKey)
        break
      case DomainModelKind:
        this.deleteDataModelAnalysis(key, rootKey)
        break
      case DomainEntityKind:
        this.deleteEntityAnalysis(key, rootKey)
        break
      case DomainPropertyKind:
        this.deletePropertyAnalysis(key)
        break
      case DomainAssociationKind:
        this.deleteAssociationAnalysis(key)
        break
      default:
      // ignore unknown kinds
    }
  }

  protected deleteNamespaceAnalysis(key: string, rootKey: string): void {
    const ns = this.root.findNamespace(key)
    if (!ns) {
      return
    }
    this.report.impact.push({
      key: ns.key,
      kind: ns.kind,
      type: 'delete',
      impact: `The ${ns.info.getLabel()} ${this.kindToLabel(DomainNamespaceKind)} will be deleted.`,
      blocking: false,
      severity: 'info',
    })
    for (const child of ns.listNamespaces()) {
      this.deleteNamespaceAnalysis(child.key, rootKey)
    }
    for (const child of ns.listModels()) {
      this.deleteDataModelAnalysis(child.key, rootKey)
    }
  }

  protected deleteDataModelAnalysis(key: string, rootKey: string): void {
    const model = this.root.findModel(key)
    if (!model) {
      return
    }
    this.report.impact.push({
      key: model.key,
      kind: model.kind,
      type: 'delete',
      impact: `The ${model.info.getLabel()} ${this.kindToLabel(DomainModelKind)} will be deleted.`,
      blocking: false,
      severity: 'info',
    })
    for (const child of model.listEntities()) {
      this.deleteEntityAnalysis(child.key, rootKey)
    }
  }

  protected deleteEntityAnalysis(key: string, rootKey: string): void {
    const entity = this.root.findEntity(key)
    if (!entity) {
      return
    }
    this.report.impact.push({
      key: entity.key,
      kind: entity.kind,
      type: 'delete',
      impact: `The ${entity.info.getLabel()} ${this.kindToLabel(DomainEntityKind)} will be deleted.`,
      blocking: false,
      severity: 'info',
    })

    // We need to know whether the entity is a parent of another entity
    for (const info of this.root.graph.inEdges(key)) {
      const edge = this.root.graph.edge(info)
      if (!edge || edge.type !== 'parent') {
        continue
      }
      const childEntity = this.root.findEntity(info.v)
      if (!childEntity) {
        continue
      }
      if (childEntity.isChildOf(rootKey)) {
        // No need to include this child as it itself is being deleted
        continue
      }
      const pLabel = entity.info.getLabel()
      const cLabel = childEntity.info.getLabel()
      this.report.impact.push({
        key: childEntity.key,
        kind: childEntity.kind,
        type: 'delete',
        impact: `The "${cLabel}" ${this.kindToLabel(DomainEntityKind)} will become an orphan because it is a child of the "${pLabel}" entity.`,
        resolution: `The "${pLabel}" entity will be removed as the parent of the "${cLabel}" entity.`,
        blocking: false,
        relationship: 'child',
        severity: 'error',
      })
      this.report.canProceed = false
    }
    // We need to know whether there's another entity that has an association to this entity.
    for (const edgeInfo of this.root.graph.inEdges(entity.key)) {
      const edge = this.root.graph.edge(edgeInfo)
      if (!edge || edge.type !== 'association') {
        continue
      }
      const association = this.root.findAssociation(edgeInfo.v)
      if (!association) {
        continue
      }
      const targetEntity = association.getParentInstance()
      if (!targetEntity) {
        continue
      }
      if (targetEntity.isChildOf(rootKey)) {
        // No need to include this association as the target entity itself is being deleted
        continue
      }

      const aLabel = association.info.getLabel()
      const eLabel = entity.info.getLabel()
      this.report.impact.push({
        key: association.key,
        kind: association.kind,
        type: 'delete',
        impact: `The ${aLabel} ${this.kindToLabel(DomainAssociationKind)} will be broken because it has a target to ${eLabel}.`,
        resolution: `The ${aLabel} ${this.kindToLabel(DomainAssociationKind)} will be removed from ${eLabel}.`,
        blocking: true,
        severity: 'error',
      })
      this.report.canProceed = false
    }
    for (const child of entity.listProperties()) {
      this.deletePropertyAnalysis(child.key)
    }
    for (const child of entity.listAssociations()) {
      this.deleteAssociationAnalysis(child.key)
    }
  }

  protected deletePropertyAnalysis(key: string): void {
    const property = this.root.findProperty(key)
    if (!property) {
      return
    }
    this.report.impact.push({
      key: property.key,
      kind: property.kind,
      type: 'delete',
      impact: `The ${property.info.getLabel()} ${this.kindToLabel(DomainPropertyKind)} will be deleted.`,
      blocking: false,
      severity: 'info',
    })
  }

  protected deleteAssociationAnalysis(key: string): void {
    const association = this.root.findAssociation(key)
    if (!association) {
      return
    }
    this.report.impact.push({
      key: association.key,
      kind: association.kind,
      type: 'delete',
      impact: `The ${association.info.getLabel()} ${this.kindToLabel(DomainAssociationKind)} will be deleted.`,
      blocking: false,
      severity: 'info',
    })
  }

  protected kindToLabel(kind: DomainImpactKinds): string {
    switch (kind) {
      case DomainNamespaceKind:
        return 'namespace'
      case DomainEntityKind:
        return 'entity'
      case DomainModelKind:
        return 'data model'
      case DomainPropertyKind:
        return 'property'
      case DomainAssociationKind:
        return 'association'
      default:
        return 'unknown'
    }
  }

  protected createRemoveForeignNamespaceImpact(key: string): void {
    const foreignNamespace = this.root.dependencies.get(key)
    if (!foreignNamespace) {
      return
    }
    // Check for parent relationships to foreign entities
    for (const entity of this.root.listEntities()) {
      for (const info of this.root.graph.outEdges(entity.key)) {
        const edge = this.root.graph.edge(info)
        if (!edge) {
          continue
        }
        if (edge.type === 'parent' && edge.domain === key) {
          // the parent is in the foreign namespace
          const parentEntity = this.root.findEntity(info.w)
          if (!parentEntity) {
            continue
          }
          const eLabel = entity.info.getLabel()
          const pLabel = parentEntity.info.getLabel()
          this.report.impact.push({
            key: entity.key,
            kind: entity.kind,
            type: 'delete',
            impact: `The "${eLabel}" ${this.kindToLabel(DomainEntityKind)} will become an orphan because its parent "${pLabel}" is in the foreign namespace "${foreignNamespace.key}".`,
            resolution: `The "${pLabel}" entity will be removed as the parent of the "${eLabel}" entity.`,
            blocking: true,
            relationship: 'child',
            severity: 'error',
          })
          this.report.canProceed = false
        } else if (edge.type === 'association') {
          const association = this.root.findAssociation(info.w)
          if (!association) {
            continue
          }
          const targets = association.targets.filter((t) => t.domain === key)
          if (targets.length) {
            for (const info of targets) {
              const targetEntity = this.root.findForeignEntity(info.key, info.domain as string)
              if (!targetEntity) {
                continue
              }
              const aLabel = association.info.getLabel()
              const eLabel = entity.info.getLabel()
              const tLabel = targetEntity.info.getLabel()
              this.report.impact.push({
                key: association.key,
                kind: association.kind,
                type: 'delete',
                impact: `The "${aLabel}" ${this.kindToLabel(DomainAssociationKind)} from "${eLabel}" will be broken because it targets "${tLabel}" in the foreign namespace "${foreignNamespace.key}".`,
                resolution: `The "${aLabel}" ${this.kindToLabel(DomainAssociationKind)} will be removed from "${eLabel}".`,
                blocking: true,
                severity: 'error',
              })
              this.report.canProceed = false
            }
          }
        }
      }
    }
  }
}
