import { z } from 'zod'

export const OntologyKindSchema = z.enum([
  'object',
  'link',
  'action',
  'catalog',
  'event',
  'interface',
  'value-type',
  'property',
  'group',
  'endpoint'
])

export type OntologyKind = z.infer<typeof OntologyKindSchema>

const SYSTEM_PATH_PATTERN = '[a-z0-9][a-z0-9-]*(?:\\.[a-z0-9][a-z0-9-]*)*'
const LOCAL_ID_PATTERN = '[a-z0-9][a-z0-9._-]*'
const ONTOLOGY_ID_PATTERN = `^(global|${SYSTEM_PATH_PATTERN}):(${OntologyKindSchema.options.join('|')})\\/(${LOCAL_ID_PATTERN})$`
const ONTOLOGY_ID_REGEX = new RegExp(ONTOLOGY_ID_PATTERN)

export const OntologyIdSchema = z
  .string()
  .trim()
  .min(1)
  .max(300)
  .regex(ONTOLOGY_ID_REGEX, 'Ontology IDs must use <system-path>:<kind>/<local-id> or global:<kind>/<local-id>')

export type OntologyId = z.infer<typeof OntologyIdSchema>

export type ParsedOntologyId = {
  id: OntologyId
  scope: string
  kind: OntologyKind
  localId: string
  isGlobal: boolean
}

export function parseOntologyId(id: string): ParsedOntologyId {
  const normalized = OntologyIdSchema.parse(id)
  const match = ONTOLOGY_ID_REGEX.exec(normalized)
  if (match === null) {
    throw new Error(`Invalid ontology ID "${id}"`)
  }

  return {
    id: normalized,
    scope: match[1],
    kind: match[2] as OntologyKind,
    localId: match[3],
    isGlobal: match[1] === 'global'
  }
}

export function formatOntologyId(input: { scope: string; kind: OntologyKind; localId: string }): OntologyId {
  return OntologyIdSchema.parse(`${input.scope}:${input.kind}/${input.localId}`)
}

const OntologyReferenceListSchema = z.array(OntologyIdSchema).default([]).optional()

const OntologyRecordBaseSchema = z
  .object({
    id: OntologyIdSchema,
    label: z.string().trim().min(1).max(160).optional(),
    description: z.string().trim().min(1).max(2000).optional(),
    ownerSystemId: z.string().trim().min(1).max(200).optional(),
    aliases: z.array(OntologyIdSchema).optional()
  })
  .passthrough()

export const OntologyObjectTypeSchema = OntologyRecordBaseSchema.extend({
  properties: z.record(z.string().trim().min(1).max(200), z.unknown()).optional(),
  storage: z.record(z.string(), z.unknown()).optional()
})

export const OntologyLinkTypeSchema = OntologyRecordBaseSchema.extend({
  from: OntologyIdSchema,
  to: OntologyIdSchema,
  cardinality: z.string().trim().min(1).max(80).optional(),
  via: z.string().trim().min(1).max(255).optional()
})

export const OntologyActionTypeSchema = OntologyRecordBaseSchema.extend({
  actsOn: OntologyReferenceListSchema,
  input: z.record(z.string().trim().min(1).max(200), z.unknown()).optional(),
  effects: z.array(z.record(z.string(), z.unknown())).optional()
})

export const OntologyCatalogTypeSchema = OntologyRecordBaseSchema.extend({
  kind: z.string().trim().min(1).max(120).optional(),
  appliesTo: OntologyIdSchema.optional(),
  entries: z.record(z.string().trim().min(1).max(200), z.unknown()).optional()
})

export const OntologyEventTypeSchema = OntologyRecordBaseSchema.extend({
  payload: z.record(z.string().trim().min(1).max(200), z.unknown()).optional()
})

export const OntologyInterfaceTypeSchema = OntologyRecordBaseSchema.extend({
  properties: z.record(z.string().trim().min(1).max(200), z.unknown()).optional()
})

export const OntologyValueTypeSchema = OntologyRecordBaseSchema.extend({
  primitive: z.string().trim().min(1).max(120).optional()
})

export const OntologySharedPropertySchema = OntologyRecordBaseSchema.extend({
  valueType: OntologyIdSchema.optional(),
  searchable: z.boolean().optional(),
  pii: z.boolean().optional()
})

export const OntologyGroupSchema = OntologyRecordBaseSchema.extend({
  members: OntologyReferenceListSchema
})

export const OntologyEndpointTypeSchema = OntologyRecordBaseSchema.extend({
  route: z.string().trim().min(1).max(500).optional()
})

export const OntologyScopeSchema = z
  .object({
    objectTypes: z.record(OntologyIdSchema, OntologyObjectTypeSchema).default({}).optional(),
    linkTypes: z.record(OntologyIdSchema, OntologyLinkTypeSchema).default({}).optional(),
    actionTypes: z.record(OntologyIdSchema, OntologyActionTypeSchema).default({}).optional(),
    catalogTypes: z.record(OntologyIdSchema, OntologyCatalogTypeSchema).default({}).optional(),
    eventTypes: z.record(OntologyIdSchema, OntologyEventTypeSchema).default({}).optional(),
    interfaceTypes: z.record(OntologyIdSchema, OntologyInterfaceTypeSchema).default({}).optional(),
    valueTypes: z.record(OntologyIdSchema, OntologyValueTypeSchema).default({}).optional(),
    sharedProperties: z.record(OntologyIdSchema, OntologySharedPropertySchema).default({}).optional(),
    groups: z.record(OntologyIdSchema, OntologyGroupSchema).default({}).optional(),
    endpoints: z.record(OntologyIdSchema, OntologyEndpointTypeSchema).default({}).optional()
  })
  .default({})

export const DEFAULT_ONTOLOGY_SCOPE: OntologyScope = {
  valueTypes: {
    'global:value-type/uuid': {
      id: 'global:value-type/uuid',
      label: 'UUID',
      primitive: 'string'
    },
    'global:value-type/text': {
      id: 'global:value-type/text',
      label: 'Text',
      primitive: 'string'
    },
    'global:value-type/url': {
      id: 'global:value-type/url',
      label: 'URL',
      primitive: 'string'
    },
    'global:value-type/email': {
      id: 'global:value-type/email',
      label: 'Email',
      primitive: 'string'
    }
  }
}

export type OntologyObjectType = z.infer<typeof OntologyObjectTypeSchema>
export type OntologyLinkType = z.infer<typeof OntologyLinkTypeSchema>
export type OntologyActionType = z.infer<typeof OntologyActionTypeSchema>
export type OntologyCatalogType = z.infer<typeof OntologyCatalogTypeSchema>
export type OntologyEventType = z.infer<typeof OntologyEventTypeSchema>
export type OntologyInterfaceType = z.infer<typeof OntologyInterfaceTypeSchema>
export type OntologyValueType = z.infer<typeof OntologyValueTypeSchema>
export type OntologySharedProperty = z.infer<typeof OntologySharedPropertySchema>
export type OntologyGroup = z.infer<typeof OntologyGroupSchema>
export type OntologyEndpointType = z.infer<typeof OntologyEndpointTypeSchema>
export type OntologyScope = z.infer<typeof OntologyScopeSchema>

export type ResolvedOntologyIndex = {
  objectTypes: Record<OntologyId, ResolvedOntologyRecord<OntologyObjectType>>
  linkTypes: Record<OntologyId, ResolvedOntologyRecord<OntologyLinkType>>
  actionTypes: Record<OntologyId, ResolvedOntologyRecord<OntologyActionType>>
  catalogTypes: Record<OntologyId, ResolvedOntologyRecord<OntologyCatalogType>>
  eventTypes: Record<OntologyId, ResolvedOntologyRecord<OntologyEventType>>
  interfaceTypes: Record<OntologyId, ResolvedOntologyRecord<OntologyInterfaceType>>
  valueTypes: Record<OntologyId, ResolvedOntologyRecord<OntologyValueType>>
  sharedProperties: Record<OntologyId, ResolvedOntologyRecord<OntologySharedProperty>>
  groups: Record<OntologyId, ResolvedOntologyRecord<OntologyGroup>>
  endpoints: Record<OntologyId, ResolvedOntologyRecord<OntologyEndpointType>>
}

export type OntologyRecordOrigin = {
  kind: 'authored' | 'projected'
  source: string
  path: Array<string | number>
  systemPath?: string
  legacyId?: string
}

export type ResolvedOntologyRecord<TRecord> = TRecord & { origin: OntologyRecordOrigin }

export type OntologyDiagnostic = {
  code: 'duplicate_ontology_id' | 'invalid_ontology_id' | 'ontology_kind_mismatch'
  message: string
  id: string
  path: Array<string | number>
  source: string
  origin: OntologyRecordOrigin
  existingSource?: string
  existingOrigin?: OntologyRecordOrigin
}

export type OntologyCompilation = {
  ontology: ResolvedOntologyIndex
  diagnostics: OntologyDiagnostic[]
}

export type ResolvedOntologyRecordEntry = {
  id: OntologyId
  kind: OntologyKind
  record: ResolvedOntologyRecord<
    | OntologyObjectType
    | OntologyLinkType
    | OntologyActionType
    | OntologyCatalogType
    | OntologyEventType
    | OntologyInterfaceType
    | OntologyValueType
    | OntologySharedProperty
    | OntologyGroup
    | OntologyEndpointType
  >
}

type ScopeKey = keyof ResolvedOntologyIndex

const SCOPE_KIND: Record<ScopeKey, OntologyKind> = {
  objectTypes: 'object',
  linkTypes: 'link',
  actionTypes: 'action',
  catalogTypes: 'catalog',
  eventTypes: 'event',
  interfaceTypes: 'interface',
  valueTypes: 'value-type',
  sharedProperties: 'property',
  groups: 'group',
  endpoints: 'endpoint'
}

const SCOPE_KEYS = Object.keys(SCOPE_KIND) as ScopeKey[]

export function ontologyGraphNodeId(id: OntologyId | string): string {
  return `ontology:${OntologyIdSchema.parse(id)}`
}

export function ontologyIdFromGraphNodeId(nodeId: string): OntologyId | undefined {
  if (!nodeId.startsWith('ontology:')) return undefined

  const result = OntologyIdSchema.safeParse(nodeId.slice('ontology:'.length))
  return result.success ? result.data : undefined
}

export function isOntologyGraphNodeId(nodeId: string): boolean {
  return ontologyIdFromGraphNodeId(nodeId) !== undefined
}

export function listResolvedOntologyRecords(index: ResolvedOntologyIndex): ResolvedOntologyRecordEntry[] {
  return SCOPE_KEYS.flatMap((scopeKey) => {
    const kind = SCOPE_KIND[scopeKey]
    return Object.entries(index[scopeKey])
      .sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
      .map(([id, record]) => ({
        id: id as OntologyId,
        kind,
        record
      }))
  })
}

type ModelLike = {
  ontology?: OntologyScope
  systems?: Record<string, SystemLike>
  entities?: Record<string, EntityLike>
  actions?: Record<string, ActionLike>
}

type SystemLike = {
  id: string
  label?: string
  title?: string
  description?: string
  ontology?: OntologyScope
  systems?: Record<string, SystemLike>
  subsystems?: Record<string, SystemLike>
}

type EntityLike = {
  id: string
  label: string
  description?: string
  ownedBySystemId: string
  table?: string
  rowSchema?: string
  stateCatalogId?: string
  links?: Array<{ toEntity: string; kind: string; via?: string; label?: string }>
}

type ActionLike = {
  id: string
  label: string
  description?: string
  resourceId?: string
  scope?: 'global' | { domain: string }
  affects?: string[]
  invocations?: unknown[]
  lifecycle?: string
}

type SourceContext = {
  source: string
  path: Array<string | number>
  kind: 'authored' | 'projected'
  systemPath?: string
  legacyId?: string
}

function originFromContext(context: SourceContext): OntologyRecordOrigin {
  return {
    kind: context.kind,
    source: context.source,
    path: context.path,
    ...(context.systemPath !== undefined ? { systemPath: context.systemPath } : {}),
    ...(context.legacyId !== undefined ? { legacyId: context.legacyId } : {})
  }
}

function createEmptyIndex(): ResolvedOntologyIndex {
  return {
    objectTypes: {},
    linkTypes: {},
    actionTypes: {},
    catalogTypes: {},
    eventTypes: {},
    interfaceTypes: {},
    valueTypes: {},
    sharedProperties: {},
    groups: {},
    endpoints: {}
  }
}

function sortResolvedOntologyIndex(index: ResolvedOntologyIndex): ResolvedOntologyIndex {
  const sorted = createEmptyIndex()

  for (const scopeKey of SCOPE_KEYS) {
    const target = sorted[scopeKey] as Record<string, unknown>
    for (const [id, record] of Object.entries(index[scopeKey]).sort(([leftId], [rightId]) =>
      leftId.localeCompare(rightId)
    )) {
      target[id] = record
    }
  }

  return sorted
}

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

function addRecord<TRecord extends { id: string }>(
  index: ResolvedOntologyIndex,
  diagnostics: OntologyDiagnostic[],
  sourcesById: Map<string, SourceContext>,
  scopeKey: ScopeKey,
  record: TRecord,
  context: SourceContext
): void {
  let parsed: ParsedOntologyId
  try {
    parsed = parseOntologyId(record.id)
  } catch {
    diagnostics.push({
      code: 'invalid_ontology_id',
      message: `Invalid ontology ID "${record.id}" from ${context.source} at ${context.path.join('.')}`,
      id: record.id,
      path: context.path,
      source: context.source,
      origin: originFromContext(context)
    })
    return
  }

  const expectedKind = SCOPE_KIND[scopeKey]
  if (parsed.kind !== expectedKind) {
    diagnostics.push({
      code: 'ontology_kind_mismatch',
      message: `Ontology ID "${record.id}" has kind "${parsed.kind}" but was authored in ${scopeKey} (${expectedKind}) at ${context.path.join('.')}`,
      id: record.id,
      path: context.path,
      source: context.source,
      origin: originFromContext(context)
    })
    return
  }

  const existing = sourcesById.get(parsed.id)
  if (existing !== undefined) {
    diagnostics.push({
      code: 'duplicate_ontology_id',
      message: `Duplicate ontology ID "${parsed.id}" from ${context.source} at ${context.path.join('.')} conflicts with ${existing.source} at ${existing.path.join('.')}`,
      id: parsed.id,
      path: context.path,
      source: context.source,
      origin: originFromContext(context),
      existingSource: existing.source,
      existingOrigin: originFromContext(existing)
    })
    return
  }

  sourcesById.set(parsed.id, context)
  ;(index[scopeKey] as Record<string, ResolvedOntologyRecord<TRecord>>)[parsed.id] = {
    ...record,
    origin: originFromContext(context)
  }
}

function addScope(
  index: ResolvedOntologyIndex,
  diagnostics: OntologyDiagnostic[],
  sourcesById: Map<string, SourceContext>,
  scope: OntologyScope | undefined,
  source: string,
  path: Array<string | number>
): void {
  if (scope === undefined) return

  for (const scopeKey of SCOPE_KEYS) {
    const records = scope[scopeKey] ?? {}
    for (const [key, record] of Object.entries(records)) {
      addRecord(index, diagnostics, sourcesById, scopeKey, record, {
        source,
        path: [...path, scopeKey, key],
        kind: 'authored',
        systemPath: source.startsWith('system:') ? source.slice('system:'.length, -'.ontology'.length) : undefined
      })
    }
  }
}

function legacyObjectId(entity: EntityLike): OntologyId {
  return formatOntologyId({ scope: entity.ownedBySystemId, kind: 'object', localId: entity.id })
}

function legacyActionOwner(action: ActionLike, entities: Record<string, EntityLike>): string {
  const firstAffectedEntityId = action.affects?.find((entityId) => entities[entityId] !== undefined)
  if (firstAffectedEntityId !== undefined) {
    return entities[firstAffectedEntityId].ownedBySystemId
  }
  if (typeof action.scope === 'object') {
    return action.scope.domain
  }
  return 'global'
}

function addLegacyEntityProjections(
  index: ResolvedOntologyIndex,
  diagnostics: OntologyDiagnostic[],
  sourcesById: Map<string, SourceContext>,
  entities: Record<string, EntityLike>
): void {
  for (const entity of Object.values(entities)) {
    const objectType: OntologyObjectType = {
      id: legacyObjectId(entity),
      label: entity.label,
      description: entity.description,
      ownerSystemId: entity.ownedBySystemId,
      ...(entity.table !== undefined
        ? {
            storage: {
              kind: 'table',
              table: entity.table
            }
          }
        : {}),
      ...(entity.rowSchema !== undefined ? { rowSchema: entity.rowSchema } : {}),
      ...(entity.stateCatalogId !== undefined ? { stateCatalogId: entity.stateCatalogId } : {})
    }

    addRecord(index, diagnostics, sourcesById, 'objectTypes', objectType, {
      source: 'legacy.entities',
      path: ['entities', entity.id],
      kind: 'projected',
      systemPath: entity.ownedBySystemId,
      legacyId: entity.id
    })

    entity.links?.forEach((link, linkIndex) => {
      const targetEntity = entities[link.toEntity]
      if (targetEntity === undefined) return

      const linkType: OntologyLinkType = {
        id: formatOntologyId({
          scope: entity.ownedBySystemId,
          kind: 'link',
          localId: `${entity.id}-${link.toEntity}-${linkIndex}`
        }),
        label: link.label ?? link.kind,
        ownerSystemId: entity.ownedBySystemId,
        from: legacyObjectId(entity),
        to: legacyObjectId(targetEntity),
        cardinality: link.kind,
        ...(link.via !== undefined ? { via: link.via } : {})
      }

      addRecord(index, diagnostics, sourcesById, 'linkTypes', linkType, {
        source: 'legacy.entities.links',
        path: ['entities', entity.id, 'links', linkIndex],
        kind: 'projected',
        systemPath: entity.ownedBySystemId,
        legacyId: `${entity.id}.links.${linkIndex}`
      })
    })
  }
}

function addLegacyActionProjections(
  index: ResolvedOntologyIndex,
  diagnostics: OntologyDiagnostic[],
  sourcesById: Map<string, SourceContext>,
  actions: Record<string, ActionLike>,
  entities: Record<string, EntityLike>
): void {
  for (const action of Object.values(actions)) {
    const ownerSystemId = legacyActionOwner(action, entities)
    const actionType: OntologyActionType = {
      id: formatOntologyId({ scope: ownerSystemId, kind: 'action', localId: action.id }),
      label: action.label,
      description: action.description,
      ownerSystemId,
      actsOn: action.affects
        ?.map((entityId) => (entities[entityId] ? legacyObjectId(entities[entityId]) : undefined))
        .filter((id): id is OntologyId => id !== undefined),
      ...(action.resourceId !== undefined ? { resourceId: action.resourceId } : {}),
      ...(action.invocations !== undefined ? { invocations: action.invocations } : {}),
      ...(action.lifecycle !== undefined ? { lifecycle: action.lifecycle } : {}),
      legacyActionId: action.id
    }

    addRecord(index, diagnostics, sourcesById, 'actionTypes', actionType, {
      source: 'legacy.actions',
      path: ['actions', action.id],
      kind: 'projected',
      systemPath: ownerSystemId,
      legacyId: action.id
    })
  }
}

function addSystemScopes(
  index: ResolvedOntologyIndex,
  diagnostics: OntologyDiagnostic[],
  sourcesById: Map<string, SourceContext>,
  systems: Record<string, SystemLike>,
  prefix: string,
  schemaPath: Array<string | number>
): void {
  for (const [key, system] of Object.entries(systems)) {
    const systemPath = prefix ? `${prefix}.${key}` : key
    const currentPath = [...schemaPath, key]
    addScope(index, diagnostics, sourcesById, system.ontology, `system:${systemPath}.ontology`, [
      ...currentPath,
      'ontology'
    ])
    addSystemScopes(index, diagnostics, sourcesById, childSystemsOf(system), systemPath, [
      ...currentPath,
      system.systems !== undefined ? 'systems' : 'subsystems'
    ])
  }
}

export function compileOrganizationOntology(model: ModelLike): OntologyCompilation {
  const ontology = createEmptyIndex()
  const diagnostics: OntologyDiagnostic[] = []
  const sourcesById = new Map<string, SourceContext>()

  addScope(ontology, diagnostics, sourcesById, model.ontology, 'organization.ontology', ['ontology'])
  addSystemScopes(ontology, diagnostics, sourcesById, model.systems ?? {}, '', ['systems'])
  addLegacyEntityProjections(ontology, diagnostics, sourcesById, model.entities ?? {})
  addLegacyActionProjections(ontology, diagnostics, sourcesById, model.actions ?? {}, model.entities ?? {})

  return { ontology: sortResolvedOntologyIndex(ontology), diagnostics }
}

export function getOntologyDiagnostics(model: ModelLike): OntologyDiagnostic[] {
  return compileOrganizationOntology(model).diagnostics
}
