/**
 * types.ts — wire types for the force-directed Architecture graph
 * widget (P2 of issue openova-io/openova#309).
 *
 * The graph widget is data-shape-agnostic: a higher-level adapter turns
 * the hierarchical infrastructure tree into these neutral
 * GraphNode / GraphEdge shapes, and the GraphCanvas only knows about
 * those.
 *
 * Per docs/INVIOLABLE-PRINCIPLES.md:
 *   #1 (waterfall, target shape) — these types are the FINAL contract
 *      between the page-level orchestrator and the canvas.
 *   #4 (never hardcode) — node radius, color and edge colour are all
 *      derived from the type field.
 */

/**
 * Architecture node types — drawn from the hierarchical infrastructure
 * tree. Region / Cluster / vCluster come straight from the spec; the
 * other shapes (LoadBalancer, NodePool, WorkerNode, Network, PVC,
 * Bucket, Volume, Service, Ingress) surface the leaves so the operator
 * can see the whole picture in one canvas.
 *
 * Per #348 item 2: the underlying graph data layer holds every node
 * type AND every relation regardless of which chips are active. The
 * adapter emits a node for every type listed below; chip add/remove is
 * pure visibility filtering on top of the full cache.
 */
export type ArchNodeType =
  | 'Cloud'
  | 'Region'
  | 'Cluster'
  | 'vCluster'
  | 'NodePool'
  | 'WorkerNode'
  | 'LoadBalancer'
  | 'Network'
  | 'PVC'
  | 'Bucket'
  | 'Volume'
  | 'Service'
  | 'Ingress'

/**
 * Canonical ordered list of every type the data layer + chip strip
 * knows about. Single source of truth — referenced by adapter,
 * page-level orchestrator, legend, type-bar, tests. No hardcoded
 * lists anywhere else.
 */
export const ALL_NODE_TYPES: ArchNodeType[] = [
  'Cloud',
  'Region',
  'Cluster',
  'vCluster',
  'NodePool',
  'WorkerNode',
  'LoadBalancer',
  'Network',
  'PVC',
  'Bucket',
  'Volume',
  'Service',
  'Ingress',
]

/**
 * Edge relationship types. Containment is just one of these — the
 * founder spec verbatim: "forget about the containment, just show it
 * as another type of relation."
 *
 * Per #348 item 4: each relation has an ArchiMate-derived rendering
 * (composition, aggregation, assignment, triggering, flow, used-by,
 * realization, association). See EDGE_MARKER_END / EDGE_DASHED below.
 */
export type ArchEdgeType =
  | 'contains'
  | 'member-of'
  | 'runs-on'
  | 'routes-to'
  | 'triggers'
  | 'flows-to'
  | 'attached-to'
  | 'depends-on'
  | 'used-by'
  | 'realizes'
  | 'peers-with'
  | 'associates'

export const ALL_EDGE_TYPES: ArchEdgeType[] = [
  'contains',
  'member-of',
  'runs-on',
  'routes-to',
  'triggers',
  'flows-to',
  'attached-to',
  'depends-on',
  'used-by',
  'realizes',
  'peers-with',
  'associates',
]

/**
 * Auto-100% density threshold (#348 item 1). Any node type with
 * total < SMALL_TYPE_THRESHOLD is unaffected by the global density
 * slider; always rendered fully unless its chip is explicitly
 * disabled. Per-type density popover for small types is hidden — only
 * the visibility toggle remains.
 *
 * Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode), this constant
 * is the single source of truth referenced by both the page-level
 * orchestrator and the test suite.
 */
export const SMALL_TYPE_THRESHOLD = 20

export type ArchStatus = 'healthy' | 'degraded' | 'failed' | 'unknown'

/** A node on the graph canvas — composite id, type-tagged, with status. */
export interface GraphNode {
  /** Composite id: `${type}:${elementId}`. Stable across renders. */
  id: string
  type: ArchNodeType
  label: string
  /** Optional one-line subtext (e.g. SKU, IP). */
  sublabel?: string
  status?: ArchStatus
  /** Free-form per-type metadata shown in the detail panel. */
  metadata?: Record<string, string>
}

export interface GraphEdge {
  id: string
  source: string
  target: string
  type: ArchEdgeType
}

/* ── Canvas runtime shapes ───────────────────────────────────────── */

/**
 * Internal D3-force-augmented node (mutable x/y/fx/fy). The canvas
 * keeps these in a Map keyed by id so positions persist across
 * prop-driven re-renders. Exported because the page tests inspect
 * the same shape.
 */
export interface LiveNode extends GraphNode {
  x: number
  y: number
  vx: number
  vy: number
  /** Pinned x — set by drag-to-pin. null = unpinned (D3 will move it). */
  fx: number | null
  fy: number | null
  /** Cached degree (incoming + outgoing edges). */
  degree: number
}

/**
 * Internal edge — D3-force mutates `source` / `target` from string ids
 * to LiveNode references after the first simulation tick. Use the
 * `edgeNodeId()` helper everywhere you read these fields. We omit
 * source/target from the GraphEdge structural extension because their
 * runtime shape is `string | LiveNode` (string only on the very first
 * tick; node ref afterward).
 */
export interface LiveEdge extends Omit<GraphEdge, 'source' | 'target'> {
  source: string | LiveNode
  target: string | LiveNode
}

/**
 * D3-force-link mutates link.source / link.target from their initial
 * string id values to node-object references after the first tick. Any
 * code that reads either field MUST go through this helper to support
 * both shapes.
 *
 * Critical: the canonical bug pattern (and the reason this helper exists
 * as a single export) is `link.source === id` — that comparison is true
 * pre-tick and false post-tick. Always read via `edgeNodeId(link.source)`.
 */
export function edgeNodeId(v: string | { id: string }): string {
  return typeof v === 'object' ? v.id : v
}

/* ── Visual mapping ──────────────────────────────────────────────── */

/**
 * Per-type color palette. Each type is visually distinct on the
 * canvas. Per INVIOLABLE-PRINCIPLES #4 (never hardcode visible-only
 * tokens): these map onto the project's CSS variables where one
 * exists, and fall back to literal hex for the type-distinctive
 * accents the palette lacks.
 */
export const NODE_FILL: Record<ArchNodeType, string> = {
  Cloud: '#7048e8', // violet — provider tenant anchor
  Region: '#1c7ed6', // blue
  Cluster: '#0ca678', // teal — control-plane group
  vCluster: '#37b24d', // green — isolation scope
  NodePool: '#f59f00', // amber
  WorkerNode: '#fab005', // yellow
  LoadBalancer: '#e8590c', // orange
  Network: '#868e96', // grey
  PVC: '#9775fa', // light violet — persistent volume claim
  Bucket: '#5c7cfa', // indigo — object storage
  Volume: '#22b8cf', // cyan — block storage
  Service: '#20c997', // mint — k8s service
  Ingress: '#e64980', // pink — k8s ingress
}

export const EDGE_STROKE: Record<ArchEdgeType, string> = {
  contains: '#4c6ef5', // solid blue
  'member-of': '#7950f2', // solid violet — aggregation
  'runs-on': '#15aabf', // solid cyan — assignment
  'routes-to': '#fa5252', // solid red — triggering
  triggers: '#fa5252', // solid red — triggering
  'flows-to': '#fd7e14', // dashed orange — flow
  'attached-to': '#868e96', // dashed grey
  'depends-on': '#fd7e14', // dashed orange — used-by
  'used-by': '#fd7e14', // dashed orange — used-by
  realizes: '#15aabf', // dashed cyan — realization
  'peers-with': '#7950f2', // dashed violet — association
  associates: '#868e96', // solid grey — association (plain)
}

/**
 * Edges that render dashed instead of solid (#348 item 4 — ArchiMate
 * notation). Used-by, flow, realization and association-with-direction
 * are rendered dashed; composition / aggregation / assignment /
 * triggering are solid.
 */
export const EDGE_DASHED: Record<ArchEdgeType, boolean> = {
  contains: false,
  'member-of': false,
  'runs-on': false,
  'routes-to': false,
  triggers: false,
  'flows-to': true,
  'attached-to': true,
  'depends-on': true,
  'used-by': true,
  realizes: true,
  'peers-with': false,
  associates: false,
}

/**
 * ArchiMate marker kinds. The renderer emits one SVG <marker> per
 * value here; the per-edge mapping below picks which sits at each end.
 *
 * Mapping rules per #348 item 4:
 *   • contains          → composition: filled diamond at PARENT (source) end
 *   • member-of         → aggregation: hollow diamond at PARENT (target) end
 *   • runs-on           → assignment: filled circles at BOTH ends
 *   • routes-to/triggers→ triggering: filled triangle at TARGET
 *   • flows-to          → flow: filled triangle at TARGET, dashed line
 *   • depends-on/used-by→ used-by: open triangle at TARGET, dashed line
 *   • realizes          → realization: hollow triangle at TARGET, dashed
 *   • peers-with/associates → association: plain (no markers)
 *   • attached-to       → association-attached: small open circle at TARGET, dashed
 */
export type EdgeMarker =
  | 'composition'
  | 'aggregation'
  | 'assignment-dot'
  | 'triggering'
  | 'used-by'
  | 'realization'
  | 'attached'
  | null

export const EDGE_MARKER_START: Record<ArchEdgeType, EdgeMarker> = {
  contains: 'composition', // filled diamond at parent (source) end
  'member-of': null,
  'runs-on': 'assignment-dot',
  'routes-to': null,
  triggers: null,
  'flows-to': null,
  'attached-to': null,
  'depends-on': null,
  'used-by': null,
  realizes: null,
  'peers-with': null,
  associates: null,
}

export const EDGE_MARKER_END: Record<ArchEdgeType, EdgeMarker> = {
  contains: null,
  'member-of': 'aggregation', // hollow diamond at parent (target) end
  'runs-on': 'assignment-dot',
  'routes-to': 'triggering',
  triggers: 'triggering',
  'flows-to': 'triggering',
  'attached-to': 'attached',
  'depends-on': 'used-by',
  'used-by': 'used-by',
  realizes: 'realization',
  'peers-with': null,
  associates: null,
}
