import { DEFAULT_ORGANIZATION_MODEL } from './defaults'
import { listAllSystems, resolveSystemConfig } from './helpers'
import { OrganizationModelSchema } from './schema'
import type { DeepPartial, OrganizationModel, OrganizationModelSystemEntry } from './types'
import type { ResourceEntry } from './domains/resources'

/**
 * A system entry enriched with the list of resources that directly attach to
 * this system (i.e. `resource.systemPath` exactly matches the system's path).
 *
 * Descendant rollup is NOT included here — callers that need it should use
 * `getResourcesForSystem(model, path, { includeDescendants: true })` from helpers.
 */
export type ResolvedSystemEntry = OrganizationModelSystemEntry & {
  /** Resources whose `systemPath` exactly matches this system's path. */
  resources?: ResourceEntry[]
}

/**
 * An `OrganizationModel` where every flat-system entry in `systems` has been
 * enriched with its directly-attached `resources`.
 *
 * Produced by `resolveOrganizationModelWithResources`. The flat `systems` map
 * only covers top-level keys (same shape as `OrganizationModel.systems`);
 * hierarchical walk uses `listAllSystems` from helpers.
 */
export type ResolvedOrganizationModel = Omit<OrganizationModel, 'systems'> & {
  systems: Record<string, ResolvedSystemEntry>
}

export type ResolveOrganizationModelOptions = {
  /**
   * Tenant-style overrides merge against the generic default fixture by
   * default. Full canonical models can disable that merge to avoid inheriting
   * fixture-only Systems.
   */
  mergeDefaults?: boolean
}

function isPlainObject(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function collectNestedSystemPaths(systems: Record<string, unknown>, prefix = ''): Set<string> {
  const paths = new Set<string>()

  for (const [key, value] of Object.entries(systems)) {
    if (!isPlainObject(value)) continue

    const path = prefix ? `${prefix}.${key}` : key
    for (const childKey of ['systems', 'subsystems']) {
      const childSystems = value[childKey]
      if (!isPlainObject(childSystems)) continue

      for (const childPath of collectNestedSystemPaths(childSystems, path)) {
        paths.add(childPath)
      }
    }

    if (prefix !== '') {
      paths.add(path)
    }
  }

  return paths
}

function pruneFlatSystemDescendantCollisions(
  base: Record<string, unknown>,
  override: Record<string, unknown>
): Record<string, unknown> {
  const result: Record<string, unknown> = { ...base }

  for (const nestedPath of collectNestedSystemPaths(override)) {
    delete result[nestedPath]
  }

  return result
}

function deepMerge<T>(base: T, override: DeepPartial<T> | undefined): T {
  if (override === undefined) {
    return base
  }

  // Arrays are override-replaced, not concatenated. Flattened record domains get
  // additive-by-id object merging; see flatten-additive-merge.test.ts.
  if (Array.isArray(base)) {
    return (override as T) ?? base
  }

  if (!isPlainObject(base) || !isPlainObject(override)) {
    return (override as T) ?? base
  }

  const result: Record<string, unknown> = { ...base }

  for (const [key, value] of Object.entries(override)) {
    if (value === undefined) continue

    const existing = result[key]
    if (key === 'systems' && isPlainObject(existing) && isPlainObject(value)) {
      result[key] = deepMerge(pruneFlatSystemDescendantCollisions(existing, value), value)
      continue
    }

    result[key] = isPlainObject(existing) && isPlainObject(value) ? deepMerge(existing, value) : value
  }

  return result as T
}

export function defineOrganizationModel<T extends DeepPartial<OrganizationModel>>(model: T): T {
  return model
}

export function resolveOrganizationModel(
  override?: DeepPartial<OrganizationModel>,
  organizationIdOrOptions?: string | ResolveOrganizationModelOptions,
  options?: ResolveOrganizationModelOptions
): OrganizationModel {
  const resolvedOptions = typeof organizationIdOrOptions === 'object' ? organizationIdOrOptions : options
  const mergeDefaults = resolvedOptions?.mergeDefaults ?? true
  const merged = mergeDefaults ? deepMerge(DEFAULT_ORGANIZATION_MODEL, override) : (override ?? {})
  // Pipeline data lives in ontology catalogTypes; use getAllPipelines(model)
  // from migration-helpers to check pipeline presence when needed.
  return OrganizationModelSchema.parse(merged)
}

/**
 * Resolve the organization model and enrich each flat system entry in
 * `model.systems` with the list of resources whose `systemPath` exactly
 * matches that system's flat key.
 *
 * This is a pure additive view — no schema is changed. Descendant resource
 * rollup is intentionally excluded; consumers that need it can call
 * `getResourcesForSystem(model, path, { includeDescendants: true })`.
 *
 * @example
 * const resolved = resolveOrganizationModelWithResources(override)
 * resolved.systems['sys.lead-gen']?.resources
 * // → ResourceEntry[] | undefined
 */
export function resolveOrganizationModelWithResources(
  override?: DeepPartial<OrganizationModel>,
  organizationId?: string
): ResolvedOrganizationModel {
  const model = resolveOrganizationModel(override, organizationId)

  // Build a path → resource[] index from the flat resources map.
  // Only direct system-path matches (no descendant rollup).
  const byPath = new Map<string, ResourceEntry[]>()
  for (const resource of Object.values(model.resources ?? {})) {
    const existing = byPath.get(resource.systemPath)
    if (existing) {
      existing.push(resource)
    } else {
      byPath.set(resource.systemPath, [resource])
    }
  }

  // Enrich every system entry, including nested subsystems, with directly
  // attached resources keyed by computed system path.
  const enrichedSystems: Record<string, ResolvedSystemEntry> = {}
  for (const { path, system } of listAllSystems(model)) {
    const resources = byPath.get(path) ?? byPath.get(system.id)
    const config = resolveSystemConfig(model, path)
    enrichedSystems[path] =
      resources !== undefined ? { ...system, id: path, config, resources } : { ...system, id: path, config }
  }

  return { ...model, systems: enrichedSystems }
}
