import { z } from 'zod'
import { OntologyIdSchema, parseOntologyId } from '../ontology'
import { JsonValueSchema, SystemInterfaceRefSchema, SystemPathSchema } from './systems'
import { ModelIdSchema } from './shared'
import { ResourceIdSchema, type ResourceEntry } from './resources'

// ---------------------------------------------------------------------------
// Topology domain
// ---------------------------------------------------------------------------
//
// Topology captures durable operational wiring. It stays separate from
// ontology, which describes semantic meaning.

const SecretLikeMetadataKeySchema = /(?:secret|password|passwd|token|api[-_]?key|credential|private[-_]?key)/i
const SecretLikeMetadataValueSchema =
  /(?:sk-[A-Za-z0-9_-]{12,}|pk_live_[A-Za-z0-9_-]{12,}|eyJ[A-Za-z0-9_-]{20,}|-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----)/

export const OmTopologyNodeKindSchema = z.enum([
  'system',
  'resource',
  'ontology',
  'policy',
  'role',
  'trigger',
  'humanCheckpoint',
  'externalResource'
])

export const OmTopologyRelationshipKindSchema = z.enum(['triggers', 'uses', 'approval'])

export const OmTopologyNodeRefSchema = z.discriminatedUnion('kind', [
  z.object({ kind: z.literal('system'), id: ModelIdSchema }),
  z.object({ kind: z.literal('resource'), id: ResourceIdSchema }),
  z.object({ kind: z.literal('ontology'), id: OntologyIdSchema }),
  z.object({ kind: z.literal('policy'), id: ModelIdSchema }),
  z.object({ kind: z.literal('role'), id: ModelIdSchema }),
  z.object({ kind: z.literal('trigger'), id: ResourceIdSchema }),
  z.object({ kind: z.literal('humanCheckpoint'), id: ResourceIdSchema }),
  z.object({ kind: z.literal('externalResource'), id: ResourceIdSchema })
])

export const OmTopologyMetadataSchema = z
  .record(z.string().trim().min(1).max(120), JsonValueSchema)
  .superRefine((metadata, ctx) => {
    function visit(value: unknown, path: Array<string | number>): void {
      if (typeof value === 'string' && SecretLikeMetadataValueSchema.test(value)) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          path,
          message: 'Topology metadata must not contain secret-like values'
        })
        return
      }

      if (Array.isArray(value)) {
        value.forEach((entry, index) => visit(entry, [...path, index]))
        return
      }

      if (typeof value !== 'object' || value === null) return

      Object.entries(value).forEach(([key, entry]) => {
        if (SecretLikeMetadataKeySchema.test(key)) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            path: [...path, key],
            message: `Topology metadata key "${key}" looks secret-like`
          })
        }
        visit(entry, [...path, key])
      })
    }

    visit(metadata, [])
  })

export const OmTopologySystemInterfaceGrantSchema = z
  .object({
    consumer: SystemInterfaceRefSchema,
    provider: SystemInterfaceRefSchema,
    resourceIds: z.array(ResourceIdSchema).default([]),
    ontologyIds: z.array(OntologyIdSchema).default([])
  })
  .strict()

export const OmTopologySystemInterfaceGrantMetadataSchema = z
  .object({
    systemInterfaceGrant: OmTopologySystemInterfaceGrantSchema
  })
  .strict()

export const OmTopologyRelationshipSchema = z.object({
  from: OmTopologyNodeRefSchema,
  kind: OmTopologyRelationshipKindSchema,
  to: OmTopologyNodeRefSchema,
  systemPath: SystemPathSchema.optional(),
  required: z.boolean().optional(),
  metadata: OmTopologyMetadataSchema.optional()
})

export const OmTopologyDomainSchema = z
  .object({
    version: z.literal(1).default(1),
    relationships: z.record(z.string().trim().min(1).max(255), OmTopologyRelationshipSchema).default({})
  })
  .default({ version: 1, relationships: {} })

export const DEFAULT_ORGANIZATION_MODEL_TOPOLOGY: z.infer<typeof OmTopologyDomainSchema> = {
  version: 1,
  relationships: {}
}

export type OmTopologyNodeKind = z.infer<typeof OmTopologyNodeKindSchema>
export type OmTopologyRelationshipKind = z.infer<typeof OmTopologyRelationshipKindSchema>
export type OmTopologyNodeRef = z.infer<typeof OmTopologyNodeRefSchema>
export type OmTopologyMetadata = z.infer<typeof OmTopologyMetadataSchema>
export type OmTopologySystemInterfaceGrant = z.infer<typeof OmTopologySystemInterfaceGrantSchema>
export type OmTopologySystemInterfaceGrantMetadata = z.infer<typeof OmTopologySystemInterfaceGrantMetadataSchema>
export type OmTopologyRelationship = z.infer<typeof OmTopologyRelationshipSchema>
export type OmTopologyDomain = z.infer<typeof OmTopologyDomainSchema>

type TypedIdObject = { id: string }
type TopologyRefFactoryInput = string | TypedIdObject
type TopologyRelationshipInput = Omit<OmTopologyRelationship, 'from' | 'to'> & {
  from: OmTopologyNodeInput
  to: OmTopologyNodeInput
}
type TopologyRelationshipOptions = Omit<Partial<OmTopologyRelationship>, 'from' | 'kind' | 'to'>
export type OmTopologyNodeInput = OmTopologyNodeRef | ResourceEntry

function idFrom(input: TopologyRefFactoryInput): string {
  return typeof input === 'string' ? input : input.id
}

function parseRef(kind: OmTopologyNodeKind, id: string): OmTopologyNodeRef {
  return OmTopologyNodeRefSchema.parse({ kind, id })
}

function isNodeRef(input: unknown): input is OmTopologyNodeRef {
  return OmTopologyNodeRefSchema.safeParse(input).success
}

function isResourceEntry(input: unknown): input is ResourceEntry {
  if (typeof input !== 'object' || input === null) return false
  const candidate = input as Partial<ResourceEntry>
  return (
    typeof candidate.id === 'string' &&
    typeof candidate.systemPath === 'string' &&
    typeof candidate.status === 'string' &&
    ['workflow', 'agent', 'integration', 'script'].includes(String(candidate.kind))
  )
}

export const topologyRef = {
  system: (system: TopologyRefFactoryInput) => parseRef('system', idFrom(system)),
  resource: (resource: TopologyRefFactoryInput) => parseRef('resource', idFrom(resource)),
  ontology: (record: TopologyRefFactoryInput) => parseRef('ontology', idFrom(record)),
  policy: (policy: TopologyRefFactoryInput) => parseRef('policy', idFrom(policy)),
  role: (role: TopologyRefFactoryInput) => parseRef('role', idFrom(role)),
  trigger: (trigger: TopologyRefFactoryInput) => parseRef('trigger', idFrom(trigger)),
  humanCheckpoint: (checkpoint: TopologyRefFactoryInput) => parseRef('humanCheckpoint', idFrom(checkpoint)),
  externalResource: (externalResource: TopologyRefFactoryInput) => parseRef('externalResource', idFrom(externalResource))
} as const

export const topologyRelationship = {
  triggers: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
    defineTopologyRelationship({
      ...options,
      from,
      kind: 'triggers',
      to
    }),
  uses: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
    defineTopologyRelationship({
      ...options,
      from,
      kind: 'uses',
      to
    }),
  approval: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
    defineTopologyRelationship({
      ...options,
      from,
      kind: 'approval',
      to
    }),
  usesIntegration: (
    from: OmTopologyNodeInput,
    integration: OmTopologyNodeInput,
    options: TopologyRelationshipOptions = {}
  ) =>
    defineTopologyRelationship({
      required: true,
      ...options,
      from,
      kind: 'uses',
      to: integration
    }),
  requestsApproval: (
    from: OmTopologyNodeInput,
    checkpoint: TopologyRefFactoryInput,
    options: TopologyRelationshipOptions = {}
  ) =>
    defineTopologyRelationship({
      required: true,
      ...options,
      from,
      kind: 'approval',
      to: topologyRef.humanCheckpoint(checkpoint)
    }),
  checkpointRoutesTo: (
    checkpoint: TopologyRefFactoryInput,
    to: OmTopologyNodeInput,
    options: TopologyRelationshipOptions = {}
  ) =>
    defineTopologyRelationship({
      required: true,
      ...options,
      from: topologyRef.humanCheckpoint(checkpoint),
      kind: 'triggers',
      to
    })
} as const

export function compileTopologyNodeRef(input: OmTopologyNodeInput): OmTopologyNodeRef {
  if (isNodeRef(input)) return input
  if (isResourceEntry(input)) return topologyRef.resource(input)

  throw new Error('Topology node refs must be typed node objects or serializable { kind, id } refs')
}

export function parseTopologyNodeRef(input: string | OmTopologyNodeRef): OmTopologyNodeRef {
  if (typeof input !== 'string') return OmTopologyNodeRefSchema.parse(input)

  const separatorIndex = input.indexOf(':')
  if (separatorIndex === -1) {
    throw new Error(`Topology node ref "${input}" must use <kind>:<id>`)
  }

  const kind = input.slice(0, separatorIndex)
  const id = input.slice(separatorIndex + 1)
  if (!OmTopologyNodeKindSchema.safeParse(kind).success) {
    throw new Error(`Topology node ref "${input}" has unsupported kind "${kind}"`)
  }

  return OmTopologyNodeRefSchema.parse({ kind, id })
}

export function defineTopologyRelationship(input: TopologyRelationshipInput): OmTopologyRelationship {
  return OmTopologyRelationshipSchema.parse({
    ...input,
    from: compileTopologyNodeRef(input.from),
    to: compileTopologyNodeRef(input.to)
  })
}

export function defineTopology(
  relationships: Record<string, TopologyRelationshipInput> | TopologyRelationshipInput[]
): OmTopologyDomain {
  const entries = Array.isArray(relationships)
    ? relationships.map((relationship, index) => [`relationship-${index + 1}`, relationship] as const)
    : Object.entries(relationships)

  return OmTopologyDomainSchema.parse({
    version: 1,
    relationships: Object.fromEntries(entries.map(([key, relationship]) => [key, defineTopologyRelationship(relationship)]))
  })
}

export function isOntologyTopologyRef(value: unknown): value is Extract<OmTopologyNodeRef, { kind: 'ontology' }> {
  if (!isNodeRef(value) || value.kind !== 'ontology') return false
  try {
    parseOntologyId(value.id)
    return true
  } catch {
    return false
  }
}
