/**
 * Automation2 Flow Editor - Schema-based Data Picker.
 * Builds pickable paths from portTypeCatalog + node outputPorts, or from
 * outputPorts[n].dataPickOptions when the backend defines an explicit list (authoritative).
 * Resolves Transit chains to show the real upstream schema.
 * Includes a System Variables section.
 */

import React, { useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import { createRef, createSystemVar, type DataRef, type SystemVarRef, isCompatible } from './dataRef';
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
import type { DataPickOption, GraphDefinedSchemaRef, NodeType, PortField, PortSchema } from '../../../../api/workflowApi';
import { findLoopAncestorIds } from './scopeHelpers';
import styles from '../../editor/Automation2FlowEditor.module.css';

import { useLanguage } from '../../../../providers/language/LanguageContext';

interface DataPickerProps {
  open: boolean;
  onClose: () => void;
  onPick: (ref: DataRef | SystemVarRef) => void;
  availableSourceIds: string[];
  nodes: Array<{ id: string; title?: string; type?: string; parameters?: Record<string, unknown> }>;
  nodeOutputsPreview: Record<string, unknown>;
  getNodeLabel: (node: { id: string; title?: string }) => string;
  /** When set, the picker can hide incompatible candidates (strict toggle) and
   *  surfaces "Iterieren als Loop" affordances for List[X]→X candidates. */
  expectedParamType?: string;
}

interface PickablePath {
  path: (string | number)[];
  label: string;
  type?: string;
  /** True iff this path produces `List[X]` and the consumer expects `X` —
   *  picking with iterate=true appends the wildcard segment. */
  iterable?: boolean;
  /** Annotated after strict-filter pass: type exists but doesn't match the expected param type. */
  typeMismatch?: boolean;
  /** Surfaced at the top of the list as the most common / recommended pick. */
  recommended?: boolean;
  /** Tooltip (Katalog oder Backend-Hinweistext). */
  detail?: string;
}

const _LIST_INNER_RE = /^List\[(.+)\]$/;

function _fieldSegHuman(field: PortField): string {
  const picker = field.pickerLabel;
  if (typeof picker === 'string' && picker.trim()) return picker.trim();
  return field.name;
}

function _detailFromField(description: unknown): string | undefined {
  if (typeof description === 'string' && description.trim()) return description.trim();
  return undefined;
}

function _buildPathsFromSchema(
  schema: PortSchema | undefined,
  catalog: Record<string, PortSchema>,
  basePath: (string | number)[] = [],
  baseSegments: string[] = [],
  depth = 0,
): PickablePath[] {
  if (!schema || !schema.fields || depth > 8) return [];
  const result: PickablePath[] = [];

  // For form schemas (kind=fromGraph), expose the whole `payload` object as a
  // top-level pickable entry so the user can pass the entire form at once.
  if (depth === 0 && schema.name?.startsWith('FormPayload')) {
    result.push({
      path: ['payload'],
      label: 'Gesamtes Formular',
      type: 'object',
      recommended: true,
    });
  }

  for (const field of schema.fields) {
    const segHuman = _fieldSegHuman(field);
    const fieldPath = [...basePath, field.name];
    const label =
      baseSegments.length > 0
        ? `${baseSegments.join(' › ')} › ${segHuman}`
        : segHuman;
    const detail = _detailFromField(field.description);
    result.push({
      path: fieldPath,
      label,
      type: field.type,
      recommended: field.recommended ?? false,
      detail,
    });
    const m = typeof field.type === 'string' ? field.type.match(_LIST_INNER_RE) : null;
    const inner = m?.[1]?.trim();
    if (inner && catalog[inner]) {
      const pil = typeof field.pickerItemLabel === 'string' ? field.pickerItemLabel.trim() : '';
      const itemBridge = pil || '*';
      const nextSegments = [...baseSegments, segHuman, itemBridge];
      result.push(..._buildPathsFromSchema(catalog[inner], catalog, [...fieldPath, '*'], nextSegments, depth + 1));
    }
  }
  result.push({
    path: [...basePath, '_success'],
    label:
      baseSegments.length > 0 ? `${baseSegments.join(' › ')} › Erfolgskennzeichen` : '_success',
    type: 'bool',
  });
  result.push({
    path: [...basePath, '_error'],
    label:
      baseSegments.length > 0 ? `${baseSegments.join(' › ')} › Fehlermeldung` : '_error',
    type: 'str',
  });
  return result;
}
/** Annotate each candidate with `iterable=true` if it is `List[X]` and the
 *  consumer expects `X`. Used to render a "Iterieren als Loop"-Vorschlag. */
function _markIterableCandidates(paths: PickablePath[], expectedParamType?: string): PickablePath[] {
  if (!expectedParamType) return paths;
  return paths.map((p) => {
    if (!p.type) return p;
    const m = p.type.match(_LIST_INNER_RE);
    if (m && m[1].trim() === expectedParamType) return { ...p, iterable: true };
    return p;
  });
}

function _deriveFormPortSchemaFromParams(
  node: { parameters?: Record<string, unknown> },
  paramKey: string,
  formTypeToPort: Record<string, string> = {},
): PortSchema | undefined {
  const resolvePortType = (rawType: string) => formTypeToPort[rawType] ?? rawType;
  const raw = node.parameters?.[paramKey];
  if (!Array.isArray(raw)) return undefined;
  const fields: Array<{ name: string; type: string; description: string | Record<string, string>; required: boolean }> = [];
  for (const item of raw) {
    if (typeof item !== 'object' || item === null) continue;
    const rec = item as Record<string, unknown>;
    if (typeof rec.name !== 'string') continue;
    const lab = rec.label;
    let description: string | Record<string, string> = rec.name;
    if (typeof lab === 'string') description = lab;
    else if (lab && typeof lab === 'object') description = lab as Record<string, string>;
    const rawType = typeof rec.type === 'string' ? rec.type : 'str';
    if (rawType === 'group' && Array.isArray(rec.fields)) {
      for (const sub of rec.fields as Record<string, unknown>[]) {
        if (!sub || typeof sub.name !== 'string') continue;
        const sl = sub.label;
        let sdesc: string | Record<string, string> = `${rec.name}.${sub.name}`;
        if (typeof sl === 'string') sdesc = sl;
        else if (sl && typeof sl === 'object') sdesc = sl as Record<string, string>;
        fields.push({
          name: `${rec.name}.${sub.name}`,
          type: resolvePortType(typeof sub.type === 'string' ? sub.type : 'str'),
          description: sdesc,
          required: Boolean(sub.required),
        });
      }
      continue;
    }
    fields.push({
      name: rec.name,
      type: resolvePortType(rawType),
      description,
      required: Boolean(rec.required),
    });
  }
  return fields.length ? { name: 'FormPayload_dynamic', fields } : undefined;
}

function _buildPathsFromPreview(
  obj: unknown,
  basePath: (string | number)[] = [],
  wholeOutputLabel = '(ganze Ausgabe)',
): PickablePath[] {
  const pathLabel = basePath.length ? basePath.map(String).join(' → ') : wholeOutputLabel;
  if (obj == null || typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
    return [{ path: [...basePath], label: pathLabel }];
  }
  if (Array.isArray(obj)) {
    const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
    for (let i = 0; i < Math.min(obj.length, 5); i++) {
      result.push(..._buildPathsFromPreview(obj[i], [...basePath, i], wholeOutputLabel));
    }
    return result;
  }
  if (typeof obj === 'object') {
    const result: PickablePath[] = [{ path: [...basePath], label: pathLabel }];
    for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
      if (k.startsWith('_')) continue;
      result.push(..._buildPathsFromPreview(v, [...basePath, k], wholeOutputLabel));
    }
    return result;
  }
  return [{ path: [...basePath], label: pathLabel }];
}

/** Gateway ``outputPorts[n].dataPickOptions`` — authoritative; no client-side catalog merge. */
function _pathsFromDataPickOptions(options: DataPickOption[]): PickablePath[] {
  return options.map((o) => ({
    path: [...o.path],
    label: o.pickerLabel,
    type: o.type,
    recommended: Boolean(o.recommended),
    iterable: Boolean(o.iterable),
    detail: typeof o.detail === 'string' ? o.detail.trim() : undefined,
  }));
}

function _resolveSchemaForNode(
  nodeId: string,
  nodes: Array<{ id: string; type?: string; parameters?: Record<string, unknown> }>,
  nodeTypes: NodeType[],
  connections: Array<{ source: string; target: string; sourceOutput?: number }>,
  catalog: Record<string, PortSchema>,
  visited: Set<string> = new Set(),
  formTypeToPort: Record<string, string> = {},
): PortSchema | undefined {
  if (visited.has(nodeId)) return undefined;
  visited.add(nodeId);

  const node = nodes.find((n) => n.id === nodeId);
  if (!node) return undefined;
  const typeDef = nodeTypes.find((nt) => nt.id === node.type);
  if (!typeDef?.outputPorts) return undefined;

  const port0 = typeDef.outputPorts[0] as {
    schema?: string | GraphDefinedSchemaRef;
    dynamic?: boolean;
    deriveFrom?: string;
  };
  if (!port0) return undefined;

  const schemaSpec = port0.schema;
  if (typeof schemaSpec === 'object' && schemaSpec !== null && schemaSpec.kind === 'fromGraph') {
    const paramKey = schemaSpec.parameter ?? 'fields';
    return _deriveFormPortSchemaFromParams(node, paramKey, formTypeToPort);
  }
  if (port0.dynamic && port0.deriveFrom) {
    return _deriveFormPortSchemaFromParams(node, port0.deriveFrom, formTypeToPort);
  }
  if (typeof schemaSpec === 'string' && schemaSpec !== 'Transit') {
    return catalog[schemaSpec];
  }

  // Transit: follow the incoming connection to find the real producer
  const incoming = connections.find((c) => c.target === nodeId);
  if (!incoming) return undefined;
  return _resolveSchemaForNode(incoming.source, nodes, nodeTypes, connections, catalog, visited, formTypeToPort);
}

export const DataPicker: React.FC<DataPickerProps> = ({ open,
  onClose,
  onPick,
  availableSourceIds,
  nodes,
  nodeOutputsPreview,
  getNodeLabel,
  expectedParamType,
}) => {
  const { t } = useLanguage();
  const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
  const [showSystem, setShowSystem] = useState(false);
  // Default: when the consumer declares an expected type, show only compatible
  // candidates ("strict" mode). User can override per-session via the toggle.
  const [strictFilter, setStrictFilter] = useState<boolean>(Boolean(expectedParamType));
  const ctx = useAutomation2DataFlow();

  // NOTE: All hooks must be called unconditionally on every render to satisfy
  // the Rules of Hooks. The `if (!open) return null;` early-return therefore
  // has to live BELOW every hook in this component. Adding a useMemo (or any
  // other hook) below it would change the hook count when the picker toggles
  // open/closed and crash the whole tree (white screen).
  const connectionsRaw = ctx?.connections ?? [];
  const connections = useMemo(
    () =>
      connectionsRaw.map((c) => ({
        source: c.sourceId,
        target: c.targetId,
        sourceOutput: c.sourceHandle,
      })),
    [connectionsRaw],
  );
  const loopAncestorIds = useMemo(() => {
    const cid = ctx?.currentNodeId;
    if (!cid) return [] as string[];
    return findLoopAncestorIds(nodes, connections, cid);
  }, [ctx?.currentNodeId, nodes, connections]);

  if (!open) return null;

  const catalog = ctx?.portTypeCatalog ?? {};
  const systemVars = ctx?.systemVariables ?? {};
  const nodeTypes = ctx?.nodeTypes ?? [];
  const formTypeToPort: Record<string, string> = Object.fromEntries(
    (ctx?.formFieldTypes ?? []).map((f) => [f.id, f.portType])
  );

  const toggleExpand = (nodeId: string) => {
    setExpandedNodes((prev) => {
      const next = new Set(prev);
      if (next.has(nodeId)) next.delete(nodeId);
      else next.add(nodeId);
      return next;
    });
  };

  const handlePick = (nodeId: string, path: (string | number)[], expectedType?: string) => {
    onPick(createRef(nodeId, path, expectedType));
    onClose();
  };

  /** Loop-Vorschlag: for List[X]→X candidates, append the '*' wildcard so the
   *  engine maps the consumer over each element (executionEngine wildcard). */
  const handlePickIterate = (nodeId: string, path: (string | number)[], expectedType?: string) => {
    onPick(createRef(nodeId, [...path, '*'], expectedType));
    onClose();
  };

  const handlePickSystemVar = (variable: string) => {
    onPick(createSystemVar(variable));
    onClose();
  };

  const _dialog = (
    <div
      className={styles.dataPickerOverlay}
      onClick={onClose}
      onKeyDown={(e) => e.key === 'Escape' && onClose()}
      role="presentation"
    >
      <div
        className={styles.dataPickerModal}
        onClick={(e) => e.stopPropagation()}
        role="dialog"
        aria-modal="true"
        aria-labelledby="automation2DataPickerTitle"
      >
        <div className={styles.dataPickerHeader}>
          <h4 className={styles.dataPickerTitle} id="automation2DataPickerTitle">
            {t('Datenquelle wählen')}
            {expectedParamType && (
              <span
                className={styles.dataPickerTypeBadge}
                title={t('Erwarteter Typ')}
              >
                {expectedParamType}
              </span>
            )}
          </h4>
          <div className={styles.dataPickerHeaderControls}>
            {expectedParamType && (
              <label className={styles.dataPickerStrictLabel}>
                <input
                  type="checkbox"
                  checked={strictFilter}
                  onChange={(e) => setStrictFilter(e.target.checked)}
                />
                {t('Nur kompatible')}
              </label>
            )}
            <button type="button" className={styles.dataPickerClose} onClick={onClose} aria-label={t('Schließen')}>
              ×
            </button>
          </div>
        </div>
        <div className={styles.dataPickerBody}>
          {/* System Variables Section */}
          {loopAncestorIds.length > 0 && (
            <div className={styles.dataPickerNodeSection}>
              <div className={styles.dataPickerNodeHeader} style={{ cursor: 'default' }}>
                <span className={styles.dataPickerNodeLabel}>{t('Schleife (lexikalisch)')}</span>
              </div>
              <div className={styles.dataPickerTree}>
                {loopAncestorIds.map((loopId) => {
                  const loopNode = nodes.find((n) => n.id === loopId);
                  const loopLabel = loopNode ? getNodeLabel(loopNode as { id: string; title?: string }) : loopId;
                  const loopSchema = catalog.LoopItem;
                  const loopPaths = loopSchema
                    ? _buildPathsFromSchema(loopSchema, catalog, [], [], 0).filter((p) => !String(p.path[p.path.length - 1]).startsWith('_'))
                    : [
                        { path: ['currentItem'], label: 'currentItem', type: 'Any' },
                        { path: ['currentIndex'], label: 'currentIndex', type: 'int' },
                        { path: ['count'], label: 'count', type: 'int' },
                      ];
                  return (
                    <div key={loopId} style={{ marginBottom: 6 }}>
                      <div style={{ fontSize: 11, color: 'var(--text-secondary)', marginBottom: 2 }}>{loopLabel}</div>
                      {loopPaths.map((p, i) => {
                        const mismatch =
                          Boolean(expectedParamType) &&
                          Boolean(p.type) &&
                          isCompatible(p.type!, expectedParamType!) === 'mismatch';
                        return (
                          <button
                            key={`${loopId}-${p.path.join('.')}-${i}`}
                            type="button"
                            className={styles.dataPickerLeaf}
                            onClick={() => handlePick(loopId, p.path, p.type)}
                          >
                            {p.label}
                            {p.type && (
                              <span className={styles.dataPickerLeafType}>
                                ({p.type})
                              </span>
                            )}
                            {mismatch && (
                              <span
                                className={styles.dataPickerMismatchBadge}
                                title={t('Typ weicht ab — wird beim Ausführen konvertiert')}
                              >
                                ⚠
                              </span>
                            )}
                          </button>
                        );
                      })}
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          {Object.keys(systemVars).length > 0 && (
            <div className={styles.dataPickerNodeSection}>
              <button
                type="button"
                className={styles.dataPickerNodeHeader}
                onClick={() => setShowSystem(!showSystem)}
              >
                <span className={styles.dataPickerExpandIcon}>{showSystem ? '▼' : '▶'}</span>
                <span className={styles.dataPickerNodeLabel}>{t('System')}</span>
              </button>
              {showSystem && (
                <div className={styles.dataPickerTree}>
                  {Object.entries(systemVars).map(([key, info]) => (
                    <button
                      key={key}
                      type="button"
                      className={styles.dataPickerLeaf}
                      onClick={() => handlePickSystemVar(key)}
                      title={info.description}
                    >
                      {key} <span className={styles.dataPickerLeafType}>({info.type})</span>
                    </button>
                  ))}
                </div>
              )}
            </div>
          )}

          {/* Node outputs */}
          {(() => {
            const filteredIds = availableSourceIds.filter((nodeId) => {
              const node = nodes.find((n) => n.id === nodeId);
              return node?.type !== 'trigger.manual';
            });
            if (filteredIds.length === 0 && Object.keys(systemVars).length === 0) {
              return <p className={styles.dataPickerEmpty}>{t('Keine vorherigen Nodes verfügbar')}</p>;
            }
            return filteredIds.map((nodeId) => {
              const node = nodes.find((n) => n.id === nodeId);
              // User-defined step title (or node-type label as fallback)
              const stepTitle = node ? getNodeLabel(node) : nodeId;
              const nodeTypeDef = node?.type ? nodeTypes.find((nt) => nt.id === node.type) : undefined;
              // Human-readable type label (e.g. "Formular", "Web-Recherche")
              const typeLabel = nodeTypeDef?.label ?? node?.type ?? '';
              const isExpanded = expandedNodes.has(nodeId);

              const port0Def = nodeTypeDef?.outputPorts?.[0];
              const backendPick =
                port0Def?.dataPickOptions &&
                Array.isArray(port0Def.dataPickOptions) &&
                port0Def.dataPickOptions.length > 0;

              let schemaPaths: PickablePath[];
              if (backendPick) {
                schemaPaths = _pathsFromDataPickOptions(port0Def!.dataPickOptions!);
              } else {
                const resolvedSchema = _resolveSchemaForNode(
                  nodeId,
                  nodes,
                  nodeTypes,
                  connections,
                  catalog,
                  new Set(),
                  formTypeToPort,
                );
                schemaPaths = _buildPathsFromSchema(resolvedSchema, catalog);
              }
              const annotated = _markIterableCandidates(
                schemaPaths.length > 0
                  ? schemaPaths
                  : _buildPathsFromPreview(nodeOutputsPreview[nodeId], [], t('(ganze Ausgabe)')),
                expectedParamType,
              );
              const markedPaths = annotated.map((p) => ({
                ...p,
                typeMismatch:
                  strictFilter &&
                  Boolean(expectedParamType) &&
                  Boolean(p.type) &&
                  !p.iterable &&
                  isCompatible(p.type!, expectedParamType!) === 'mismatch',
              }));
              const orderedPaths = [
                ...markedPaths.filter((p) => p.recommended),
                ...markedPaths.filter((p) => !p.recommended),
              ];

              return (
                <div key={nodeId} className={styles.dataPickerNodeSection}>
                  <button
                    type="button"
                    className={styles.dataPickerNodeHeader}
                    onClick={() => toggleExpand(nodeId)}
                  >
                    <span className={styles.dataPickerExpandIcon}>{isExpanded ? '▼' : '▶'}</span>
                    <span className={styles.dataPickerNodeLabel}>{stepTitle}</span>
                    {typeLabel && (
                      <span className={styles.dataPickerNodeSchemaHint}>
                        {typeLabel}
                      </span>
                    )}
                  </button>
                  {isExpanded && (
                    <div className={styles.dataPickerTree}>
                      {orderedPaths.length === 0 && (
                        <div style={{ fontSize: 11, color: 'var(--text-secondary)', padding: '4px 8px' }}>
                          {t('(keine Felder verfügbar)')}
                        </div>
                      )}
                      {orderedPaths.map((p, i) => (
                        <div
                          key={`${p.path.join('.')}-${i}`}
                          style={{ display: 'flex', alignItems: 'center', gap: 4 }}
                        >
                          <button
                            type="button"
                            className={`${styles.dataPickerLeaf}${p.recommended ? ` ${styles.dataPickerLeafRecommended}` : ''}`}
                            style={{ flex: 1 }}
                            onClick={() => handlePick(nodeId, p.path, p.type)}
                            title={p.detail || p.label}
                          >
                            {p.label}
                            {p.recommended && (
                              <span className={styles.dataPickerRecommendedPill}>
                                {t('Empfohlen')}
                              </span>
                            )}
                            {p.type && (
                              <span className={styles.dataPickerLeafType}>
                                ({p.type})
                              </span>
                            )}
                            {p.typeMismatch && (
                              <span
                                className={styles.dataPickerMismatchBadge}
                                title={t('Typ weicht ab — wird beim Ausführen konvertiert')}
                              >
                                ⚠
                              </span>
                            )}
                          </button>
                          {p.iterable && (
                            <button
                              type="button"
                              className={`${styles.dataPickerLeaf} ${styles.dataPickerIterateBtn}`}
                              onClick={() => handlePickIterate(nodeId, p.path, expectedParamType)}
                              title={t('Pro Element der Liste iterieren (Loop)')}
                            >
                              {t('iterieren')}
                            </button>
                          )}
                        </div>
                      ))}
                    </div>
                  )}
                </div>
              );
            });
          })()}
        </div>
      </div>
    </div>
  );

  return createPortal(_dialog, document.body);
};
