import { z } from 'zod'
import type { OrganizationModel, OrganizationModelSystemEntry } from './types'
import type { JsonValue } from './domains/systems'
import type { ResourceEntry } from './domains/resources'
import type { OmTopologyRelationship } from './domains/topology'

/**
 * Generic domain-record factory. Validates every entry through the given Zod
 * schema (throws `ZodError` on invalid input) and returns an id-keyed map.
 *
 * Each entry MUST carry an `id` field — validated at runtime via the parsed
 * result. Use the per-domain `defineX` / `defineXs` wrappers at call sites for
 * schema-specific type inference; this function is the shared implementation.
 *
 * @example
 * const actions = defineDomainRecord(ActionSchema, [
 *   { id: 'my-action', order: 10, label: 'My Action', invocations: [] }
 * ])
 * // → { 'my-action': { id: 'my-action', order: 10, label: 'My Action', invocations: [], ... } }
 */
export function defineDomainRecord<TSchema extends z.ZodTypeAny>(
  schema: TSchema,
  entries: readonly z.input<TSchema>[]
): Record<string, z.infer<TSchema>> {
  return Object.fromEntries(
    entries.map((entry) => {
      const parsed = schema.parse(entry) as z.infer<TSchema> & { id: string }
      return [parsed.id, parsed]
    })
  )
}

// Phase 4: removed locally-scoped inferred types for deleted compound-domain schemas
// (SalesPipeline, ProspectingBuildTemplate, etc.). migration-helpers.ts imports them directly.

/**
 * SystemEntry extended with the recursive tree slots (`systems`, `subsystems`).
 * `OrganizationModelSystemEntry` is inferred from `SystemEntrySchema: ZodType<SystemEntry>`
 * so it already carries these fields; this alias re-exports it under the tree-walker name
 * for clarity at call sites.
 */
export type SystemEntryWithTree = OrganizationModelSystemEntry

function childSystemsOf(system: SystemEntryWithTree): Record<string, SystemEntryWithTree> {
  return system.systems ?? system.subsystems ?? {}
}

export function defaultPathFor(id: string): string {
  return `/${id.replaceAll('.', '/')}`
}

export function parentIdOf(id: string): string | undefined {
  const index = id.lastIndexOf('.')
  return index === -1 ? undefined : id.slice(0, index)
}

/**
 * Return all entries of an OM domain map sorted by their declared `order` field.
 * Use this whenever iteration order matters for UI or deterministic output.
 */
export function listDomain<T extends { id: string; order: number }>(record: Record<string, T>): T[] {
  return Object.values(record).sort((a, b) => a.order - b.order)
}

export function listClientProfiles(model: OrganizationModel): OrganizationModel['clients'][string][] {
  return Object.values(model.clients ?? {}).sort((a, b) => a.slug.localeCompare(b.slug))
}

export function getClientProfile(model: OrganizationModel, id: string): OrganizationModel['clients'][string] | undefined {
  return model.clients?.[id]
}

export function getClientProfileBySlug(
  model: OrganizationModel,
  slug: string
): OrganizationModel['clients'][string] | undefined {
  return Object.values(model.clients ?? {}).find((client) => client.slug === slug)
}

export function findById(
  systems: Record<string, OrganizationModelSystemEntry>,
  id: string
): OrganizationModelSystemEntry | undefined {
  return systems[id]
}

export function findByPath(
  systems: Record<string, OrganizationModelSystemEntry>,
  path: string
): OrganizationModelSystemEntry | undefined {
  return Object.values(systems).find((system) => (system.path ?? system.ui?.path ?? defaultPathFor(system.id)) === path)
}

export function childrenOf(
  systems: Record<string, OrganizationModelSystemEntry>,
  id: string
): OrganizationModelSystemEntry[] {
  const prefix = `${id}.`
  return Object.values(systems).filter(
    (system) => system.id.startsWith(prefix) && !system.id.slice(prefix.length).includes('.')
  )
}

export function topLevel(systems: Record<string, OrganizationModelSystemEntry>): OrganizationModelSystemEntry[] {
  return Object.values(systems).filter((system) => !system.id.includes('.'))
}

export function ancestorsOf(
  systems: Record<string, OrganizationModelSystemEntry>,
  id: string
): OrganizationModelSystemEntry[] {
  const segments = id.split('.')
  const ids = segments.map((_, index) => segments.slice(0, index + 1).join('.'))
  return ids
    .map((ancestorId) => findById(systems, ancestorId))
    .filter((system): system is OrganizationModelSystemEntry => Boolean(system))
}

export function parentOf(
  systems: Record<string, OrganizationModelSystemEntry>,
  id: string
): OrganizationModelSystemEntry | undefined {
  const parentId = parentIdOf(id)
  return parentId ? findById(systems, parentId) : undefined
}

function inheritedValue<T>(
  systems: Record<string, OrganizationModelSystemEntry>,
  id: string,
  getValue: (system: OrganizationModelSystemEntry) => T | undefined
): T | undefined {
  return ancestorsOf(systems, id)
    .slice()
    .reverse()
    .map(getValue)
    .find((value): value is T => value !== undefined)
}

export function uiPositionFor(systems: Record<string, OrganizationModelSystemEntry>, id: string) {
  return inheritedValue(systems, id, (system) => system.uiPosition)
}

export function requiresAdminFor(systems: Record<string, OrganizationModelSystemEntry>, id: string): boolean {
  return inheritedValue(systems, id, (system) => system.requiresAdmin) ?? false
}

export function devOnlyFor(systems: Record<string, OrganizationModelSystemEntry>, id: string): boolean {
  return inheritedValue(systems, id, (system) => system.devOnly || system.lifecycle === 'beta' || undefined) ?? false
}

// ---------------------------------------------------------------------------
// Recursive system-tree helpers (Wave 1B)
// These operate on the NEW recursive System shape introduced by W1A, where
// `model.systems` is `Record<LocalId, SystemEntry>` (nested via `subsystems`).
// `SystemEntry` here is the extended interface defined at the top of this file.
// ---------------------------------------------------------------------------

/**
 * Resolve a system by its dot-separated path string.
 *
 * `getSystem(model, 'sales.crm.dispositions')` walks:
 *   `model.systems['sales'] → .subsystems['crm'] → .subsystems['dispositions']`
 *
 * Returns `undefined` if any segment is missing.
 */
export function getSystem(model: OrganizationModel, path: string): SystemEntryWithTree | undefined {
  const flatMatch = (model.systems as Record<string, SystemEntryWithTree>)[path]
  if (flatMatch !== undefined) return flatMatch

  const segments = path.split('.')
  // model.systems is typed as Record<string, OrganizationModelSystemEntry>; cast
  // to SystemEntryWithTree (which extends it with optional content + subsystems).
  let current: Record<string, SystemEntryWithTree> = model.systems as Record<string, SystemEntryWithTree>
  let node: SystemEntryWithTree | undefined
  for (const seg of segments) {
    node = current[seg]
    if (node === undefined) return undefined
    current = childSystemsOf(node)
  }
  return node
}

/**
 * Return the root-first ancestor chain for a system path, including the system
 * itself as the last element.
 *
 * `getSystemAncestors(model, 'sales.crm')` → `[salesEntry, crmEntry]`
 *
 * Returns an empty array if the path cannot be resolved.
 */
export function getSystemAncestors(model: OrganizationModel, path: string): SystemEntryWithTree[] {
  const flatMatch = (model.systems as Record<string, SystemEntryWithTree>)[path]
  if (flatMatch !== undefined) {
    const ancestorIds = path
      .split('.')
      .map((_, index, segments) => segments.slice(0, index + 1).join('.'))
      .slice(0, -1)

    return [
      ...ancestorIds
        .map((ancestorId) => (model.systems as Record<string, SystemEntryWithTree>)[ancestorId])
        .filter((system): system is SystemEntryWithTree => system !== undefined),
      flatMatch
    ]
  }

  const segments = path.split('.')
  const result: SystemEntryWithTree[] = []
  let current: Record<string, SystemEntryWithTree> = model.systems as Record<string, SystemEntryWithTree>
  for (const seg of segments) {
    const node = current[seg]
    if (node === undefined) return []
    result.push(node)
    current = childSystemsOf(node)
  }
  return result
}

/**
 * Flat depth-first enumeration of every system in the model tree.
 * Each entry carries the computed full path and the system node.
 * Order: parent before children (pre-order DFS).
 *
 * Example result paths: `'sales'`, `'sales.crm'`, `'sales.crm.dispositions'`
 */
export function listAllSystems(model: OrganizationModel): Array<{ path: string; system: SystemEntryWithTree }> {
  const results: Array<{ path: string; system: SystemEntryWithTree }> = []

  function walk(map: Record<string, SystemEntryWithTree>, prefix: string): void {
    for (const [localId, system] of Object.entries(map)) {
      const fullPath = prefix ? `${prefix}.${localId}` : localId
      results.push({ path: fullPath, system })
      const childSystems = childSystemsOf(system)
      if (Object.keys(childSystems).length > 0) {
        walk(childSystems, fullPath)
      }
    }
  }

  walk(model.systems as Record<string, SystemEntryWithTree>, '')
  return results
}

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

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

  for (const [key, value] of Object.entries(override)) {
    const existing = result[key]
    result[key] = isPlainJsonObject(existing) && isPlainJsonObject(value) ? mergeJsonConfig(existing, value) : value
  }

  return result
}

/** Resolve a system's effective local config from first-class `System.config`. */
export function resolveSystemConfig(model: OrganizationModel, path: string): Record<string, JsonValue> {
  const system = getSystem(model, path)
  if (system === undefined) return {}

  return mergeJsonConfig({}, system.config ?? {})
}

/**
 * Return all resources whose `systemPath` belongs to the given system.
 *
 * When `includeDescendants` is false (default), only resources whose
 * `systemPath` exactly matches `systemPath` are returned.
 *
 * When `includeDescendants` is true, resources attached to any descendant
 * system are also included. Descendant matching is segment-aware: the path
 * `'sales'` does NOT match `'salesforce.foo'` because segment boundaries are
 * enforced by splitting on `'.'`.
 *
 * @example
 * getResourcesForSystem(model, 'sales')
 * // → resources where systemPath === 'sales'
 *
 * getResourcesForSystem(model, 'sales', { includeDescendants: true })
 * // → resources where systemPath === 'sales' OR systemPath starts with 'sales.'
 */
export function getResourcesForSystem(
  model: OrganizationModel,
  systemPath: string,
  options: { includeDescendants?: boolean } = {}
): ResourceEntry[] {
  const { includeDescendants = false } = options
  const prefix = systemPath + '.'
  return Object.values(model.resources ?? {}).filter(
    (r) => r.systemPath === systemPath || (includeDescendants && r.systemPath.startsWith(prefix))
  )
}

export interface SystemTopologyEdge {
  id: string
  relationship: OmTopologyRelationship
}

export interface SystemDeprecationDependents {
  resources: ResourceEntry[]
  topologyEdges: SystemTopologyEdge[]
}

function systemPathIsInScope(candidate: string, systemPath: string, includeDescendants: boolean): boolean {
  return candidate === systemPath || (includeDescendants && candidate.startsWith(`${systemPath}.`))
}

function topologyRelationshipTouchesSystem(
  relationship: OmTopologyRelationship,
  systemPath: string,
  resourceIds: Set<string>,
  includeDescendants: boolean
): boolean {
  if (
    relationship.systemPath !== undefined &&
    systemPathIsInScope(relationship.systemPath, systemPath, includeDescendants)
  ) {
    return true
  }

  for (const ref of [relationship.from, relationship.to]) {
    if (ref.kind === 'system' && systemPathIsInScope(ref.id, systemPath, includeDescendants)) return true
    if (ref.kind === 'resource' && resourceIds.has(ref.id)) return true
  }

  return false
}

export function getTopologyEdgesForSystem(
  model: OrganizationModel,
  systemPath: string,
  options: { includeDescendants?: boolean; resourceIds?: Iterable<string> } = {}
): SystemTopologyEdge[] {
  const { includeDescendants = false } = options
  const resourceIds = new Set(
    options.resourceIds ?? getResourcesForSystem(model, systemPath, { includeDescendants }).map((r) => r.id)
  )

  return Object.entries(model.topology?.relationships ?? {})
    .filter(([, relationship]) =>
      topologyRelationshipTouchesSystem(relationship, systemPath, resourceIds, includeDescendants)
    )
    .map(([id, relationship]) => ({ id, relationship }))
}

export function getSystemDeprecationDependents(
  model: OrganizationModel,
  systemPath: string,
  options: { includeDescendants?: boolean } = {}
): SystemDeprecationDependents {
  const resources = getResourcesForSystem(model, systemPath, options).filter((resource) => resource.status === 'active')
  const resourceIds = new Set(resources.map((resource) => resource.id))

  return {
    resources,
    topologyEdges: getTopologyEdgesForSystem(model, systemPath, { ...options, resourceIds })
  }
}
