export type DefinitionGraphPosition = {
  x: number;
  y: number;
};

export type DefinitionGraphPort = {
  id: string;
  label: string;
  required: boolean;
  multiple: boolean;
  minimum_connections: number;
};

export type DefinitionGraphConfigField = {
  id: string;
  label: string;
  kind: string;
  required: boolean;
  description?: string | null;
  options: [string, string][];
};

export type DefinitionGraphNodeType = {
  type: string;
  category: string;
  category_label: string;
  label: string;
  description: string;
  icon: string;
  input_ports: DefinitionGraphPort[];
  output_ports: DefinitionGraphPort[];
  config_fields: DefinitionGraphConfigField[];
  default_config: Record<string, unknown>;
  metadata?: Record<string, unknown>;
};

export type DefinitionGraphNode = {
  id: string;
  type: string;
  label: string;
  position: DefinitionGraphPosition;
  config: Record<string, unknown>;
};

export type DefinitionGraphEdge = {
  id: string;
  source: string;
  target: string;
  source_port?: string;
  target_port?: string;
};

export type DefinitionGraph = {
  schema_version: number;
  nodes: DefinitionGraphNode[];
  edges: DefinitionGraphEdge[];
};

export type DefinitionGraphConnection = {
  source: string;
  target: string;
  sourcePort?: string | null;
  targetPort?: string | null;
};

export function createDefinitionGraphNode<TNode extends DefinitionGraphNode = DefinitionGraphNode>(
  type: string,
  position: DefinitionGraphPosition,
  library: DefinitionGraphNodeType[]
): TNode {
  const definition = library.find((item) => item.type === type);
  return {
    id: `${type.replace(/\./g, "-")}-${crypto.randomUUID()}`,
    type,
    label: definition?.label ?? type,
    position,
    config: structuredClone(definition?.default_config ?? {})
  } as TNode;
}

export function definitionConnectionError(
  graph: DefinitionGraph,
  library: DefinitionGraphNodeType[],
  connection: DefinitionGraphConnection,
  options: { allowCycles?: boolean } = {}
): string | null {
  if (!connection.source || !connection.target) return "Both endpoints are required.";
  if (connection.source === connection.target) return "A node cannot connect to itself.";
  const source = graph.nodes.find((node) => node.id === connection.source);
  const target = graph.nodes.find((node) => node.id === connection.target);
  if (!source || !target) return "The connection references an unknown node.";
  const sourceDefinition = library.find((item) => item.type === source.type);
  const targetDefinition = library.find((item) => item.type === target.type);
  if (!sourceDefinition || !targetDefinition) return "The node type is not in this library.";
  const sourcePort = connection.sourcePort ?? "output";
  const targetPort = connection.targetPort ?? "input";
  if (!sourceDefinition.output_ports.some((port) => port.id === sourcePort)) {
    return "The source port is not available.";
  }
  const port = targetDefinition.input_ports.find((item) => item.id === targetPort);
  if (!port) return "The target port is not available.";
  if (graph.edges.some(
    (edge) =>
      edge.source === connection.source
      && edge.target === connection.target
      && (edge.source_port ?? "output") === sourcePort
      && (edge.target_port ?? "input") === targetPort
  )) {
    return "This connection already exists.";
  }
  if (!port.multiple && graph.edges.some(
    (edge) =>
      edge.target === connection.target
      && (edge.target_port ?? "input") === targetPort
  )) {
    return "The target port accepts only one connection.";
  }
  if (!options.allowCycles && wouldCreateDefinitionCycle(
    graph,
    connection.source,
    connection.target
  )) {
    return "This connection would create a cycle.";
  }
  return null;
}

export function wouldCreateDefinitionCycle(
  graph: DefinitionGraph,
  source: string,
  target: string
): boolean {
  const outgoing = new Map<string, string[]>();
  graph.edges.forEach((edge) => {
    outgoing.set(edge.source, [...(outgoing.get(edge.source) ?? []), edge.target]);
  });
  const pending = [target];
  const visited = new Set<string>();
  while (pending.length) {
    const nodeId = pending.pop();
    if (!nodeId || visited.has(nodeId)) continue;
    if (nodeId === source) return true;
    visited.add(nodeId);
    pending.push(...(outgoing.get(nodeId) ?? []));
  }
  return false;
}
