import type { JSONSchema7 } from 'json-schema'
import type { JsonImportReport, InMemorySchema } from './JsonSchemaImporter.js'
import { JsonSchemaImporter } from './JsonSchemaImporter.js'
import {
  SchemaFilteringStrategy,
  type SchemaFilteringOptions,
  FILTERING_STRATEGIES,
} from './SchemaFilteringStrategy.js'
import type { DataDomain } from '../DataDomain.js'
import type { DomainEntity } from '../DomainEntity.js'
import type { ImportMessage } from './JsonSchemaImporter.js'

/**
 * Extended JsonSchemaImporter that supports filtering out non-structural schemas.
 * This version can eliminate schemas that don't contribute meaningful structure
 * to the data model, such as empty objects or conceptual marker types.
 */
export class FilteringJsonSchemaImporter extends JsonSchemaImporter {
  private filteringStrategy: SchemaFilteringStrategy

  constructor(domain: DataDomain, filteringOptions: SchemaFilteringOptions = {}) {
    super(domain)
    this.filteringStrategy = new SchemaFilteringStrategy(filteringOptions)
  }

  /**
   * Creates an importer with a predefined filtering strategy.
   */
  static withStrategy(
    domain: DataDomain,
    strategyName: keyof typeof FILTERING_STRATEGIES
  ): FilteringJsonSchemaImporter {
    const strategy = FILTERING_STRATEGIES[strategyName]
    // Access private options through a public method (would need to be added to SchemaFilteringStrategy)
    const options = strategy['options'] as SchemaFilteringOptions
    return new FilteringJsonSchemaImporter(domain, options)
  }

  /**
   * Creates an importer optimized for API development.
   * Excludes schemas that don't contribute to API structure.
   */
  static forApiModeling(domain: DataDomain): FilteringJsonSchemaImporter {
    return new FilteringJsonSchemaImporter(domain, {
      excludeEmptyObjects: true,
      excludeConceptualMarkers: true,
      excludePatterns: [
        /.*Enumeration$/, // Exclude *Enumeration schemas
        /.*Type$/, // Exclude conceptual *Type schemas that are just markers
      ],
    })
  }

  /**
   * Creates an importer optimized for database schema generation.
   * Very strict filtering - only includes schemas with concrete properties.
   */
  static forDatabaseModeling(domain: DataDomain): FilteringJsonSchemaImporter {
    return new FilteringJsonSchemaImporter(domain, {
      excludeEmptyObjects: true,
      excludeConceptualMarkers: true,
      customExclusionFilter: (schema: JSONSchema7) => {
        // For database modeling, exclude anything that doesn't have properties or clear structure
        return (
          schema.type === 'object' &&
          !schema.properties &&
          !schema.additionalProperties &&
          !schema.allOf?.some(
            (subSchema) => typeof subSchema === 'object' && 'properties' in subSchema && subSchema.properties
          )
        )
      },
    })
  }

  /**
   * Override the base import method to apply filtering.
   */
  override async import(schemas: InMemorySchema[], modelName: string): Promise<JsonImportReport> {
    // Apply pre-filtering to the schemas before import
    const filteredSchemas = this.preFilterSchemas(schemas)

    // Get the raw import result with filtered schemas
    const rawResult = await super.import(filteredSchemas.schemas, modelName)

    // Apply post-filtering to clean up any remaining issues
    const finalReport = this.postFilterReport(rawResult, filteredSchemas.messages)

    return finalReport
  }

  /**
   * Pre-filters schemas before they are processed by the base importer.
   */
  private preFilterSchemas(schemas: InMemorySchema[]): {
    schemas: InMemorySchema[]
    messages: ImportMessage[]
  } {
    const filteredSchemas: InMemorySchema[] = []
    const messages: ImportMessage[] = []

    for (const schema of schemas) {
      const schemaId = schema.contents.$id || schema.path

      if (this.filteringStrategy.shouldExcludeSchema(schema.contents, schemaId)) {
        const reason = this.filteringStrategy.getExclusionReason(schema.contents, schemaId)
        messages.push({
          level: 'info',
          message: `Pre-filtered schema '${schemaId}': ${reason}`,
          location: schema.path,
        })
      } else {
        filteredSchemas.push(schema)
      }
    }

    if (messages.length > 0) {
      messages.push({
        level: 'info',
        message: `Pre-filtering removed ${messages.length} non-structural schema(s)`,
      })
    }

    return { schemas: filteredSchemas, messages }
  }

  /**
   * Post-processes the import report to clean up any remaining filtered entities.
   */
  private postFilterReport(report: JsonImportReport, preFilterMessages: ImportMessage[]): JsonImportReport {
    const { model, messages } = report
    const allMessages = [...preFilterMessages, ...messages]
    let removedCount = 0

    // Get entities as array for easier processing
    const entities = Array.from(model.listEntities())
    const entitiesToRemove: string[] = []

    // Check each entity to see if it should be filtered out
    for (const entity of entities) {
      const shouldFilter = this.shouldFilterEntity(entity)

      if (shouldFilter) {
        entitiesToRemove.push(entity.key)
        const reason = this.getEntityFilterReason(entity)
        allMessages.push({
          level: 'info',
          message: `Post-filtered entity '${entity.info.name}': ${reason}`,
          location: entity.key,
        })
        removedCount++
      }
    }

    // Remove the filtered entities
    for (const entityKey of entitiesToRemove) {
      try {
        model.removeEntity(entityKey)
      } catch (error) {
        allMessages.push({
          level: 'warning',
          message: `Failed to remove entity '${entityKey}': ${error}`,
          location: entityKey,
        })
      }
    }

    // Clean up orphaned associations
    this.cleanupOrphanedAssociations(entities, entitiesToRemove, allMessages)

    if (removedCount > 0) {
      allMessages.push({
        level: 'info',
        message: `Post-filtering removed ${removedCount} additional non-structural entity/entities`,
      })
    }

    return {
      model,
      messages: allMessages,
    }
  }

  /**
   * Determines if an entity should be filtered out based on its characteristics.
   */
  private shouldFilterEntity(entity: DomainEntity): boolean {
    // Check if entity has no properties and no meaningful associations
    const hasProperties = Array.from(entity.listProperties()).length > 0
    const hasAssociations = Array.from(entity.listAssociations()).length > 0

    // If it has neither properties nor associations, it's likely a conceptual marker
    if (!hasProperties && !hasAssociations) {
      return this.filteringStrategy.shouldExcludeSchema(
        {
          type: 'object',
          title: entity.info.name,
          description: entity.info.description,
        } as JSONSchema7,
        entity.info.name
      )
    }

    return false
  }

  /**
   * Gets a human-readable reason for filtering an entity.
   */
  private getEntityFilterReason(entity: DomainEntity): string {
    const hasProperties = Array.from(entity.listProperties()).length > 0
    const hasAssociations = Array.from(entity.listAssociations()).length > 0

    if (!hasProperties && !hasAssociations) {
      return 'Entity has no properties or associations (likely a conceptual marker)'
    }

    return 'Entity matches exclusion pattern'
  }

  /**
   * Removes associations that reference entities that were filtered out.
   */
  private cleanupOrphanedAssociations(
    entities: DomainEntity[],
    removedEntityKeys: string[],
    messages: ImportMessage[]
  ): void {
    for (const entity of entities) {
      const associationsToRemove: string[] = []

      for (const association of entity.listAssociations()) {
        const hasOrphanedTargets = association.targets?.some((target) => removedEntityKeys.includes(target.key))

        if (hasOrphanedTargets) {
          associationsToRemove.push(association.key)
          messages.push({
            level: 'info',
            message: `Removed association '${association.info.name}' from '${entity.info.name}' due to filtered target entity`,
            location: `${entity.key}.${association.key}`,
          })
        }
      }

      for (const associationKey of associationsToRemove) {
        try {
          entity.removeAssociation(associationKey)
        } catch (error) {
          messages.push({
            level: 'warning',
            message: `Failed to remove association '${associationKey}' from '${entity.info.name}': ${error}`,
            location: `${entity.key}.${associationKey}`,
          })
        }
      }
    }
  }
}

/**
 * Utility functions for working with filtered imports.
 */
export const SchemaFilteringUtils = {
  /**
   * Analyzes a JSON schema to determine if it would be filtered by the given strategy.
   */
  wouldBeFiltered(schema: JSONSchema7, strategy: SchemaFilteringStrategy): boolean {
    return strategy.shouldExcludeSchema(schema)
  },

  /**
   * Gets statistics about what would be filtered from a set of schemas.
   */
  getFilteringStats(
    schemas: { id: string; schema: JSONSchema7 }[],
    strategy: SchemaFilteringStrategy
  ): {
    total: number
    filtered: number
    remaining: number
    filteredSchemas: { id: string; reason: string }[]
  } {
    const filteredSchemas: { id: string; reason: string }[] = []

    for (const { id, schema } of schemas) {
      if (strategy.shouldExcludeSchema(schema, id)) {
        filteredSchemas.push({
          id,
          reason: strategy.getExclusionReason(schema, id),
        })
      }
    }

    return {
      total: schemas.length,
      filtered: filteredSchemas.length,
      remaining: schemas.length - filteredSchemas.length,
      filteredSchemas,
    }
  },

  /**
   * Creates a report showing what would be filtered from a schema collection.
   */
  createFilteringReport(schemas: { id: string; schema: JSONSchema7 }[], strategy: SchemaFilteringStrategy): string {
    const stats = this.getFilteringStats(schemas, strategy)

    let report = `Schema Filtering Report\n`
    report += `======================\n\n`
    report += `Total schemas: ${stats.total}\n`
    report += `Would be filtered: ${stats.filtered}\n`
    report += `Would remain: ${stats.remaining}\n\n`

    if (stats.filteredSchemas.length > 0) {
      report += `Schemas that would be filtered:\n`
      report += `-------------------------------\n`
      for (const { id, reason } of stats.filteredSchemas) {
        report += `• ${id}: ${reason}\n`
      }
    }

    return report
  },
}
