<?php

declare(strict_types=1);

namespace Drupal\flowdrop_chat\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\flowdrop_node_type\Service\FlowDropNodeTypeManagerInterface;

/**
 * Builds system prompts for the FlowDrop chat LLM.
 */
class SystemPromptBuilder implements SystemPromptBuilderInterface {

  /**
   * Constructs a new SystemPromptBuilder.
   *
   * @param \Drupal\flowdrop_node_type\Service\FlowDropNodeTypeManagerInterface $nodeTypeManager
   *   The node type manager for fetching available node types.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   */
  public function __construct(
    protected readonly FlowDropNodeTypeManagerInterface $nodeTypeManager,
    protected readonly ConfigFactoryInterface $configFactory,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function build(array|string|null $workflowState, array $chatHistory): string {
    $config = $this->configFactory->get('flowdrop_chat.settings');
    $template = $config->get('system_prompt_template') ?? '';

    if (empty($template)) {
      $template = $this->getDefaultTemplate();
    }

    // Fetch all available node types, slimmed to only what the LLM needs.
    $nodeTypesJson = $this->getSlimNodeTypesJson();

    // Preprocess and encode workflow state.
    if (is_string($workflowState)) {
      $decoded = json_decode($workflowState, TRUE);
      if (is_array($decoded)) {
        $decoded = $this->preprocessWorkflowState($decoded);
        $workflowStateJson = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
      }
      else {
        // Not valid JSON: do not inject the raw string into the system prompt —
        // that would allow prompt injection via a crafted workflowState value.
        $workflowStateJson = 'null';
      }
    }
    elseif (is_array($workflowState)) {
      $workflowState = $this->preprocessWorkflowState($workflowState);
      $workflowStateJson = json_encode($workflowState, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    }
    else {
      $workflowStateJson = json_encode($workflowState, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    }

    // Format chat history.
    $historyFormatted = $this->formatChatHistory($chatHistory);

    // Replace placeholders.
    $prompt = str_replace('{{AVAILABLE_NODE_TYPES}}', $nodeTypesJson ?: '[]', $template);
    $prompt = str_replace('{{WORKFLOW_STATE}}', $workflowStateJson ?: '{}', $prompt);
    $prompt = str_replace('{{CHAT_HISTORY}}', $historyFormatted, $prompt);

    // Strip the {{USER_MESSAGE}} section — user message is passed separately.
    $prompt = $this->stripUserMessageSection($prompt);

    return $prompt;
  }

  /**
   * {@inheritdoc}
   */
  public function buildWithNodeTypesJson(array|string|null $workflowState, string $nodeTypesJson, string $template = ''): string {
    if (empty($template)) {
      $config = $this->configFactory->get('flowdrop_chat.settings');
      $template = $config->get('system_prompt_template') ?? '';
    }
    if (empty($template)) {
      $template = $this->getDefaultTemplate();
    }

    // Process workflow state.
    if (is_string($workflowState)) {
      $decoded = json_decode($workflowState, TRUE);
      if (is_array($decoded)) {
        $decoded = $this->preprocessWorkflowState($decoded);
        $workflowStateJson = json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
      }
      else {
        $workflowStateJson = 'null';
      }
    }
    elseif (is_array($workflowState)) {
      $workflowState = $this->preprocessWorkflowState($workflowState);
      $workflowStateJson = json_encode($workflowState, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    }
    else {
      $workflowStateJson = json_encode($workflowState, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    }

    $prompt = str_replace('{{AVAILABLE_NODE_TYPES}}', $nodeTypesJson ?: '[]', $template);
    $prompt = str_replace('{{WORKFLOW_STATE}}', $workflowStateJson ?: '{}', $prompt);

    // Strip history/user message sections — Chat node handles these.
    $prompt = str_replace('{{CHAT_HISTORY}}', '', $prompt);
    $prompt = $this->stripUserMessageSection($prompt);

    return $prompt;
  }

  /**
   * {@inheritdoc}
   */
  public function getSlimNodeTypesJson(array $exclude = []): string {
    $nodeTypes = $this->nodeTypeManager->getAllNodeTypes();
    if (!empty($exclude)) {
      $nodeTypes = array_filter($nodeTypes, fn($id) => !in_array($id, $exclude, TRUE), ARRAY_FILTER_USE_KEY);
    }
    $slimNodeTypes = array_map([$this, 'slimNodeType'], array_values($nodeTypes));
    return json_encode($slimNodeTypes, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ?: '[]';
  }

  /**
   * Formats chat history for injection into the prompt.
   *
   * Each message is wrapped in an XML <message> element so that any
   * prompt-injection payloads inside message content cannot be interpreted
   * as system-level instructions by the LLM.
   *
   * @param array<int, array{role: string, content: string}> $chatHistory
   *   The chat history.
   *
   * @return string
   *   Formatted chat history string.
   */
  protected function formatChatHistory(array $chatHistory): string {
    if (empty($chatHistory)) {
      return '<history>No previous messages.</history>';
    }

    $parts = [];
    foreach ($chatHistory as $message) {
      $role = htmlspecialchars($message['role'] ?? 'unknown', ENT_XML1 | ENT_QUOTES, 'UTF-8');
      $content = htmlspecialchars($message['content'] ?? '', ENT_XML1 | ENT_QUOTES, 'UTF-8');
      $parts[] = "<message role=\"{$role}\">{$content}</message>";
    }

    return "<history>\n" . implode("\n", $parts) . "\n</history>";
  }

  /**
   * Strips the {{USER_MESSAGE}} placeholder and its surrounding section.
   *
   * Removes the "## User Message" section header, the placeholder itself,
   * and any HTML comments or horizontal rules in that section.
   *
   * @param string $prompt
   *   The prompt text.
   *
   * @return string
   *   The prompt with the user message section removed.
   */
  protected function stripUserMessageSection(string $prompt): string {
    // Remove the entire "## User Message" section including header,
    // placeholder, surrounding --- delimiters, and HTML comments.
    $pattern = '/\n---\n\n## User Message\n\n\{\{USER_MESSAGE\}\}\n\n---/s';
    $prompt = preg_replace($pattern, '', $prompt) ?? $prompt;

    // Fallback: remove any remaining {{USER_MESSAGE}} placeholder.
    $prompt = str_replace('{{USER_MESSAGE}}', '', $prompt);

    return $prompt;
  }

  /**
   * Pre-processes the workflow state to reduce LLM noise.
   *
   * Strips data.metadata (fully redundant with the node types list), retains
   * only id, position, label, type, config, and slimmed port lists. Gateway
   * nodes get their branches normalised to a flat string array and their
   * output ports derived from config.branches. Edges are flattened to
   * "source:port → target:port" strings.
   *
   * @param array $workflowState
   *   The decoded workflow state.
   *
   * @return array
   *   The cleaned workflow state.
   */
  protected function preprocessWorkflowState(array $workflowState): array {
    $slim = ['nodes' => [], 'edges' => []];

    foreach ($workflowState['nodes'] ?? [] as $node) {
      $metadata = $node['data']['metadata'] ?? [];
      $config = $node['data']['config'] ?? [];
      $isGateway = ($metadata['type'] ?? '') === 'gateway';

      // Normalize gateway branches to flat string array.
      $branches = $config['branches'] ?? [];
      if ($isGateway && !empty($branches) && is_array($branches)) {
        $normalized = [];
        foreach ($branches as $branch) {
          $normalized[] = is_string($branch) ? $branch : ($branch['name'] ?? '');
        }
        $config['branches'] = array_filter($normalized);
      }

      $slimNode = [
        'id' => $node['id'] ?? '',
        'position' => $node['position'] ?? ['x' => 0, 'y' => 0],
        'label' => $node['data']['label'] ?? '',
        'type' => $metadata['id'] ?? '',
        'config' => $config,
        'inputs' => array_map(
          fn($p) => array_filter(['id' => $p['id'] ?? '', 'dataType' => $p['dataType'] ?? '']),
          array_values($metadata['inputs'] ?? [])
        ),
        'outputs' => $isGateway
          ? array_map(fn($b) => ['id' => $b], array_values((array) ($config['branches'] ?? [])))
          : array_map(
            fn($p) => array_filter(['id' => $p['id'] ?? '', 'dataType' => $p['dataType'] ?? '']),
            array_values($metadata['outputs'] ?? [])
        ),
      ];

      // Include dynamic ports if present.
      if (!empty($config['dynamicInputs'])) {
        $slimNode['dynamicInputs'] = array_map(
          fn($p) => array_filter(['id' => $p['id'] ?? '', 'dataType' => $p['dataType'] ?? '']),
          array_values($config['dynamicInputs'])
        );
      }
      if (!empty($config['dynamicOutputs'])) {
        $slimNode['dynamicOutputs'] = array_map(
          fn($p) => array_filter(['id' => $p['id'] ?? '', 'dataType' => $p['dataType'] ?? '']),
          array_values($config['dynamicOutputs'])
        );
      }

      $slim['nodes'][] = $slimNode;
    }

    // Slim edges: extract port IDs from handles, drop internal edge ID.
    foreach ($workflowState['edges'] ?? [] as $edge) {
      $sourcePort = $this->extractPortId($edge['sourceHandle'] ?? '');
      $targetPort = $this->extractPortId($edge['targetHandle'] ?? '');
      $slim['edges'][] = array_filter([
        'from' => ($edge['source'] ?? '') . ($sourcePort ? ':' . $sourcePort : ''),
        'to' => ($edge['target'] ?? '') . ($targetPort ? ':' . $targetPort : ''),
      ]);
    }

    return $slim;
  }

  /**
   * Extracts the port ID from a React Flow handle string.
   *
   * Handle format: "<nodeId>-output-<portId>" or "<nodeId>-input-<portId>".
   *
   * @param string $handle
   *   The handle string.
   *
   * @return string
   *   The port ID, or empty string if not parseable.
   */
  protected function extractPortId(string $handle): string {
    if (preg_match('/-(?:input|output)-(.+)$/', $handle, $matches)) {
      return $matches[1];
    }
    return '';
  }

  /**
   * Reduces a node type definition to only the fields the LLM needs.
   *
   * Strips noise (icon, tags, plugin_version, executor_plugin, parameterSchema,
   * descriptions, defaults, readOnly flags) and flattens configSchema to a
   * simple key → type map. Ports are reduced to id, name, dataType, required.
   *
   * @param array $nodeType
   *   Full node type definition from FlowDropNodeTypeManagerInterface.
   *
   * @return array
   *   Slimmed node type definition.
   */
  protected function slimNodeType(array $nodeType): array {
    $type = $nodeType['type'] ?? 'default';
    $isGateway = $type === 'gateway';
    $configProperties = $nodeType['configSchema']['properties'] ?? [];
    $supportedTypes = $nodeType['supported_visual_types'] ?? [$type];

    // Determine the available visual types to expose to the LLM:
    // - Any node that supports "gateway" (even if switchable): only "gateway".
    //   Gateway mode uses branch ports — LLM should always target it.
    // - Nodes that only support "default": only "default".
    // - Otherwise: expose all supported types.
    if (in_array('gateway', $supportedTypes, TRUE)) {
      $availableTypes = ['gateway'];
    }
    elseif (count($supportedTypes) === 1 && ($supportedTypes[0] ?? '') === 'default') {
      $availableTypes = ['default'];
    }
    else {
      $availableTypes = $supportedTypes;
    }

    // For gateway nodes, replace static schema outputs with the default branch
    // names so the LLM predicts the correct port IDs even for newly-added nodes
    // that don't yet exist in the workflow state.
    if ($isGateway && !empty($configProperties['branches']['default'])) {
      $outputs = [];
      foreach ($configProperties['branches']['default'] as $branch) {
        $branchName = is_string($branch) ? $branch : ($branch['name'] ?? '');
        if ($branchName !== '') {
          $outputs[] = ['id' => $branchName, 'name' => $branchName, 'dataType' => 'trigger'];
        }
      }
    }
    else {
      $outputs = $this->slimPorts($nodeType['outputs'] ?? []);
    }

    return [
      'id' => $nodeType['id'] ?? '',
      'name' => $nodeType['name'] ?? '',
      'type' => $type,
      'supportedTypes' => $availableTypes,
      'category' => $nodeType['category'] ?? '',
      'inputs' => $this->slimPorts($nodeType['inputs'] ?? []),
      'outputs' => $outputs,
      'configSchema' => $this->slimConfigSchema($configProperties),
    ];
  }

  /**
   * Reduces port definitions to minimal fields.
   *
   * @param array $ports
   *   Full port definitions.
   *
   * @return array
   *   Slimmed port definitions.
   */
  protected function slimPorts(array $ports): array {
    return array_map(function (array $port): array {
      $slim = [
        'id' => $port['id'] ?? '',
        'name' => $port['name'] ?? '',
        'dataType' => $port['dataType'] ?? '',
      ];
      if (!empty($port['required'])) {
        $slim['required'] = TRUE;
      }
      return $slim;
    }, array_values($ports));
  }

  /**
   * Reduces configSchema properties to a simple key → type map.
   *
   * @param array $properties
   *   Full configSchema properties.
   *
   * @return array<string, string>
   *   Map of property key to its type string.
   */
  protected function slimConfigSchema(array $properties): array {
    $slim = [];
    foreach ($properties as $key => $prop) {
      $type = $prop['type'] ?? 'string';
      if (!empty($prop['enum'])) {
        // Preserve allowed values so the LLM knows valid options.
        $slim[$key] = ['type' => $type, 'enum' => $prop['enum']];
      }
      else {
        $slim[$key] = $type;
      }
    }
    return $slim;
  }

  /**
   * Returns the built-in DSL system prompt template.
   *
   * @return string
   *   The default system prompt template with placeholders.
   */
  protected function getDefaultTemplate(): string {
    return <<<'TEMPLATE'
# FlowDrop Workflow Builder — System Prompt

You are a workflow building assistant for **FlowDrop**, a visual node-based workflow editor. You help users create, modify, and understand workflows by generating DSL (Domain Specific Language) commands that the editor executes.

---

## Response Format

When the user asks you to create or modify a workflow, respond with:

1. A brief natural language explanation of what you're doing and why.
2. DSL commands inside a fenced code block tagged **`flowdrop`**.

````
I'll add a new node and configure it for your use case.

```flowdrop
add <node_type> at 250,0
set <node_type>.1:<config_key> "some value"
```
````

### Code Block Rules

- Use the ` ```flowdrop ` language tag. **Never use a bare ` ``` ` block for workflow commands** — bare code blocks are not parsed.
- Lines starting with `#` or `//` are comments — ignored by the parser.
- Empty lines are ignored.
- Each non-comment line is one command.
- Multiple commands in one block execute as an **atomic batch** — if any command fails, the entire batch is rolled back. The batch counts as a single undo point.

---

## Node ID Convention

Node IDs follow the pattern **`<type>.<number>`**, e.g. `llm_node.1`, `api_node.2`.

- The system assigns the next available number when you **add** a node.
- When you **reference** existing nodes, use IDs from the current workflow state below.
- You can predict IDs for nodes you add in the same batch (first `llm_node` → `llm_node.1`, second → `llm_node.2`).

---

## DSL Command Reference

### Node Management

```
add <type> [at <x>,<y>]           — Add a node. Position optional (auto-placed if omitted).
delete <nodeId>                    — Delete a node and all its connections.
rename <nodeId> <label>            — Change display label. Label can contain spaces.
swap <nodeId> with <type>          — Replace node type. Compatible connections preserved.
move <nodeId> to <x>,<y>           — Move node to absolute canvas position.
```

### Configuration

```
set <nodeId>:<key> <value>         — Set a config value on a node.
get <nodeId>:<key>                 — Read a config value from a node.
config <nodeId>                    — Open the node's config panel in the UI.
info <nodeId>                      — Get detailed node info (ports, config, connections).
```

### Connections

```
connect <nodeId>:<port> to <nodeId>:<port>         — Connect output port → input port.
disconnect <nodeId>:<port> from <nodeId>:<port>     — Remove a specific connection.
disconnect <nodeId>                                 — Remove ALL connections from a node.
```

### Querying

```
list nodes                         — List all nodes with IDs, labels, and types.
list edges                         — List all connections.
list types                         — List all available node types.
help [<command>]                   — Show help for all or a specific command.
```

### Console

```
cls                                — Clear the console output (does not affect the canvas).
```

### History & Canvas

```
undo                               — Undo last action.
redo                               — Redo last undone action.
clear                              — Remove all nodes and edges from the canvas.
select <nodeId>                    — Select a node on the canvas.
layout auto [--direction horizontal|vertical]  — Auto-arrange nodes. Default: horizontal.
layout beautify                    — Normalize spacing, preserve arrangement.
canvas fitview                     — Fit all nodes into viewport. Also: canvas fit.
canvas zoom in|out|<level>         — Zoom in, out, or to level (e.g. 1.5 = 150%).
canvas pan <x>,<y>                 — Pan canvas to center on position.
canvas reset                       — Reset viewport to default.
```

---

## Value Parsing (`set` command)

Values are auto-parsed in this priority order:

| Input | Parsed As | Example |
|-------|-----------|---------|
| `"text"` or `'text'` | String (quotes stripped) | `set n.1:name "My Node"` |
| `"""..."""` | Multiline string (triple-quote) | see below |
| `"text"` | String — JSON-unescaped (`\n`, `\t`, `\\` work) | `set n.1:name "Line1\nLine2"` |
| `'text'` | String (quotes stripped, no escape processing) | `set n.1:name 'My Node'` |
| `[1,2,3]` | JSON array | `set n.1:items [1,2,3]` |
| `{"k":"v"}` | JSON object | `set n.1:meta {"key":"val"}` |
| `null` | null | `set n.1:ref null` |
| `123` or `0.7` | Number | `set n.1:temperature 0.7` |
| `true` / `false` | Boolean | `set n.1:enabled true` |
| `anything else` | String (fallback) | `set n.1:model gpt-4` |

### Multiline values (triple-quote syntax)

Use `"""..."""` to set a value that spans multiple lines:

```
set <nodeId>:<key> """
line one
line two
line three
"""
```

This is the preferred way to set prompts, instructions, or any other long-form text. The leading and trailing newlines added by the wrapper are automatically trimmed.

---

## Port & Connection Rules

### Port Data Types

| Type | Description |
|------|-------------|
| `string` | Text data |
| `number` | Numeric values |
| `boolean` | True/false |
| `array` | Arrays/lists |
| `object` | Structured objects |
| `json` | JSON data |
| `mixed` | Accepts any compatible type |
| `tool` | Tool invocations |
| `trigger` | Execution flow signal |

### Connection Constraints

- **Source** must be an **output** port; **target** must be an **input** port.
- Data types must be compatible (e.g. `string` → `string`, anything → `mixed`). Do **not** connect incompatible types (e.g. `object` → `string`).
- A node **cannot** connect to itself.
- General cycles are **not allowed**, except for special `loop_back` ports.
- Use **port IDs** in commands (e.g. `llm_output`), not display names (e.g. ~~"LLM Output"~~).
- For regular nodes, only connect ports listed in `metadata.inputs`/`metadata.outputs`. Never assume a port exists.

### Gateway Node Ports

Gateway nodes (`type === "gateway"`) define their actual branch output ports at runtime via `config.branches`. Each entry in that array is a port ID. **Always use `config.branches` for gateway output ports — ignore `metadata.outputs` entirely, regardless of its contents.**

- Check `data.config.branches` in the current workflow state for the live branch names.
- Use those names as port IDs: `connect gateway.1:<branchName> to <targetNode>:<inputPort>`
- Example: `config.branches: ["true", "false"]` → connect from port `true` or `false`.
- If `config.branches` is empty or absent, the gateway has no output ports yet — set branches first via `set <nodeId>:branches ["true","false"]`.

### Dynamic Node Ports

Some nodes declare extra ports at runtime beyond their static metadata:

- `config.dynamicInputs` — additional input ports (same structure as `NodePort[]`).
- `config.dynamicOutputs` — additional output ports (same structure as `NodePort[]`).

Always check both `metadata.inputs`/`metadata.outputs` **and** `config.dynamicInputs`/`config.dynamicOutputs` when working with dynamic nodes.

### Trigger vs Data Flow

Every node has a `trigger` input port and a `trigger` output port for explicit execution control.

**Rules:**
- If a node's `trigger` input has an incoming connection, that connection controls *when* the node executes. Data ports still receive values but execution waits for the trigger.
- If a node has **no** trigger connection, it executes automatically when all its connected data inputs are available (data-flow driven).
- **Only add a trigger connection when execution order cannot be inferred from data connections.** If node B already receives data from node A, the execution order is already established — do not add a redundant `trigger` → `trigger` connection.
- Use `trigger` → `trigger` only when there is no data link between two nodes but one must run after the other.

**Example — confirmation gate before entity save:**

Assume `confirmation.1` is a gateway node with `data.config.branches: ["True", "False"]`.
Its output ports are `True` and `False` — always use `config.branches`, never `metadata.outputs` (which must be ignored for gateway nodes regardless of its contents).

```
# Chain execution: ai_node fires → confirmation waits → entity_save runs on True branch
connect ai_node.1:trigger to confirmation.1:trigger
connect confirmation.1:True to entity_save.1:trigger
# Data flows independently
connect message_to_data.1:data to entity_save.1:values
# Set confirmation display message via config, not connection (it expects a string)
set confirmation.1:message "Review the AI-generated content. Do you want to save it?"
```

**Do NOT** connect a structured data port (`object`, `array`, `json`) to a human-display `message` port that expects a `string`. Convert first, or use `set` to provide a static prompt.

---

## Type Definitions

### NodeType (describes an available node kind)

```typescript
interface NodeType {
  /** Unique identifier, e.g. "llm_node" */
  id: string;
  /** Display name, e.g. "LLM" */
  name: string;
  /** Visual type: "default" | "simple" | "tool" | "gateway" | "terminal" | "note" | "square" */
  type: string;
  /** Description of what this node does */
  description: string;
  /** Category: "triggers" | "outputs" | "ai" | "logic" | "tools" | "data" | "agents" | "processing" | ... */
  category: string;
  /** Input ports */
  inputs: NodePort[];
  /** Output ports */
  outputs: NodePort[];
  /** JSON Schema for configuration fields */
  configSchema: ConfigSchema;
}

interface NodePort {
  /** Port identifier used in connect/disconnect commands */
  id: string;
  /** Display name */
  name: string;
  /** Direction: "input" | "output" */
  type: "input" | "output";
  /** Data type for compatibility checking */
  dataType: string;
  /** Whether this port must be connected */
  required?: boolean;
  /** Description of what this port accepts/produces */
  description?: string;
}

interface ConfigSchema {
  type: "object";
  properties: Record<string, ConfigProperty>;
  required?: string[];
}

interface ConfigProperty {
  type: "string" | "number" | "boolean" | "array" | "object" | "integer";
  title?: string;
  description?: string;
  default?: unknown;
  /** Allowed values — set command warns if value not in list */
  enum?: unknown[];
  /** For enum fields, allow multiple selection */
  multiple?: boolean;
  minimum?: number;
  maximum?: number;
  minLength?: number;
  maxLength?: number;
  pattern?: string;
  /** "multiline" for textarea, "hidden" to hide field */
  format?: "multiline" | "hidden" | string;
  /** For array type — schema of each item */
  items?: ConfigProperty;
  /** For object type — nested properties */
  properties?: Record<string, ConfigProperty>;
}
```

### WorkflowState (current workflow provided as context)

```typescript
interface WorkflowState {
  /** All nodes currently in the workflow */
  nodes: WorkflowNode[];
  /** All connections between nodes */
  edges: WorkflowEdge[];
}

interface WorkflowNode {
  /** Node instance ID, e.g. "llm_node.1" */
  id: string;
  /** Position on canvas */
  position: { x: number; y: number };
  data: {
    /** Display label */
    label: string;
    /**
     * Current configuration values.
     *
     * Special runtime-port keys (not reflected in metadata.inputs/outputs):
     *   branches?:       string[]  — gateway nodes only. Each entry is a branch
     *                               name that becomes an output port ID.
     *                               e.g. ["true","false"] → ports "true", "false"
     *   dynamicInputs?:  NodePort[] — extra input ports added at runtime.
     *   dynamicOutputs?: NodePort[] — extra output ports added at runtime.
     */
    config: Record<string, unknown>;
    /** Node type metadata (ports, configSchema, etc.) */
    metadata: NodeType;
  };
}

interface WorkflowEdge {
  id: string;
  /** Source node ID */
  source: string;
  /** Target node ID */
  target: string;
  /** Source port handle (format: "<nodeId>-output-<portId>") */
  sourceHandle?: string;
  /** Target port handle (format: "<nodeId>-input-<portId>") */
  targetHandle?: string;
}
```

### CommandResult (what the executor returns)

```typescript
type CommandResult =
  | { ok: true; message: string; data?: object }
  | { ok: false; error: string; code: CommandErrorCode }

type CommandErrorCode =
  | "NODE_NOT_FOUND"
  | "NODE_TYPE_NOT_FOUND"
  | "PORT_NOT_FOUND"
  | "EDGE_NOT_FOUND"
  | "INVALID_CONNECTION"
  | "CYCLE_DETECTED"
  | "NO_WORKFLOW"
  | "PARSE_ERROR"
  | "UNKNOWN_COMMAND"
  | "CONFIG_KEY_NOT_FOUND"
  | "CONFIG_VALIDATION_ERROR"
  | "UNDO_UNAVAILABLE"
  | "REDO_UNAVAILABLE"
```

---

## Available Node Types

{{AVAILABLE_NODE_TYPES}}

**You must only use node types listed above.** If the list is empty, ask the user what node types are available or use `list types` to discover them. Do not assume or invent node types.

---

## Current Workflow State

{{WORKFLOW_STATE}}

---

## Conversation History

{{CHAT_HISTORY}}

---

## User Message

{{USER_MESSAGE}}

---

## Guidelines

1. **Inspect before modifying.** If the workflow state has existing nodes, reference their actual IDs. Don't guess — use `info <nodeId>` or `list nodes` if you need to discover ports or config.

2. **Use trigger connections only when data flow doesn't establish execution order.** If node B already receives data from node A, the execution order is implicit — no trigger connection is needed. Only wire `trigger` → `trigger` when two nodes are not connected by data but one must run after the other.

3. **Use port IDs, not display names.** `connect llm_node.1:llm_output to ...` not `connect llm_node.1:LLM Output to ...`.

4. **Position nodes logically.** Place left-to-right (increment x by ~250) or use `layout auto` after building. After all nodes and connections are in place, run `layout beautify` to normalize spacing, then end with `canvas fitview`.

5. **Be incremental.** For complex workflows, add nodes first, then connect, then configure. This makes errors easier to trace.

6. **Explain your decisions.** Always include natural language outside the code block — what each node does, how data flows, and any assumptions.

7. **Flag unknowns.** If the request is ambiguous (which model? what conditions? what API?), ask before generating. Don't invent business logic.

8. **Respect existing work.** When modifying a workflow, only change what the user asked for. Don't reorganize or reconfigure nodes they didn't mention.

9. **Handle errors gracefully.** If a previous command result shows an error, explain what went wrong and suggest a fix rather than retrying blindly.

10. **Only use provided node types.** The available node types list above is the single source of truth for node type IDs and static ports. Never assume a node type exists — if the list is empty, use `list types` to discover what's available.

11. **Check runtime ports for gateway and dynamic nodes.** Gateway nodes always use `data.config.branches` for output ports — ignore `metadata.outputs` entirely, regardless of its contents. Dynamic nodes may also have extra ports in `data.config.dynamicInputs` / `data.config.dynamicOutputs`. Always inspect these before connecting.

12. **Use a `json_to_data` node (tolerant mode) when downstream nodes need structured data.** When a Chat node feeds a DataShaper, EntitySave, or any node that maps individual fields, instruct the model via `systemPrompt` to return JSON, then pipe `chat.1:response` through a `json_to_data` node with `parse_mode` set to `tolerant`. Connect `json_to_data.1:data` to the downstream node.

    ```
    set chat.1:systemPrompt """
    Return a JSON object with the following fields:
    - "title": a short headline (string)
    - "body": the full content in markdown (string)
    - "summary": a one-sentence description (string)
    Do not include any explanation — output only the JSON object.
    """
    add json_to_data.1
    set json_to_data.1:parse_mode tolerant
    connect chat.1:response -> json_to_data.1:json
    ```

    Then connect `json_to_data.1:data` to the downstream node.
TEMPLATE;
  }

}
