/**
 * SourcesTab – Full data-source management inside the Unified Data Bar.
 *
 * Tree structure (Browse Sources):
 *   UserConnection (Level 1, loaded on mount)
 *     └─ Service (Level 2, loaded when connection expanded)
 *         └─ Folder / Site / File (Level 3+, loaded when service/folder expanded)
 *
 * Feature Data tree:
 *   MandateGroup
 *     └─ FeatureConnection (feature instance)
 *         └─ FeatureTable (tables exposed by that instance)
 *
 * Active Sources sections show scope-cycling and neutralize-toggle buttons.
 */

import React, { useEffect, useState, useCallback, useRef } from 'react';
import type { UdbContext } from './UnifiedDataBar';
import api from '../../api';
import { getPageIcon } from '../../config/pageRegistry';
import styles from './SourcesTab.module.css';

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

/* ─── Types (inline, no external imports) ────────────────────────────── */

interface UdbDataSource {
  id: string;
  connectionId: string;
  sourceType: string;
  path: string;
  label: string;
  displayPath?: string;
  scope: string;
  neutralize: boolean;
}

interface UdbFeatureDataSource {
  id: string;
  featureInstanceId: string;
  featureCode: string;
  tableName: string;
  objectKey: string;
  label: string;
  scope: string;
  neutralize: boolean;
  recordFilter?: Record<string, string>;
}

interface TreeNode {
  key: string;
  label: string;
  icon: string;
  type: 'connection' | 'service' | 'folder' | 'file';
  expanded: boolean;
  loading: boolean;
  children: TreeNode[] | null;
  connectionId: string;
  service?: string;
  path?: string;
  displayPath?: string;
  authority?: string;
}

interface FeatureConnectionNode {
  featureInstanceId: string;
  featureCode: string;
  mandateId?: string;
  label: string;
  icon: string;
  tableCount: number;
  expanded: boolean;
  loading: boolean;
  tables: FeatureTableNode[] | null;
  parentRecords: Record<string, ParentRecordNode[]>;
}

interface MandateGroupNode {
  mandateId: string;
  mandateLabel: string;
  expanded: boolean;
  featureConnections: FeatureConnectionNode[];
}

interface FeatureTableNode {
  objectKey: string;
  tableName: string;
  label: string;
  fields: string[];
  isParent?: boolean;
  parentTable?: string;
  parentKey?: string;
  displayFields?: string[];
}

interface ParentRecordNode {
  id: string;
  displayLabel: string;
  fields: Record<string, any>;
  tableName: string;
  expanded: boolean;
}

/* ─── Props ──────────────────────────────────────────────────────────── */

interface SourcesTabProps {
  context: UdbContext;
  onSourcesChanged?: () => void;
}

/* ─── Icons ──────────────────────────────────────────────────────────── */

const _AUTHORITY_ICONS: Record<string, string> = {
  msft: '\uD83D\uDFE6',
  google: '\uD83D\uDFE9',
  clickup: '\uD83D\uDCCB',
  'local:ftp': '\uD83D\uDD17',
  'local:jira': '\uD83D\uDD27',
};

const _SERVICE_ICONS: Record<string, string> = {
  sharepoint: '\uD83D\uDCC1',
  onedrive: '\u2601\uFE0F',
  outlook: '\uD83D\uDCE7',
  teams: '\uD83D\uDCAC',
  drive: '\uD83D\uDCC2',
  gmail: '\uD83D\uDCE8',
  files: '\uD83D\uDCC2',
};

/* ─── Source colors & icons ──────────────────────────────────────────── */

const _SOURCE_COLORS: Record<string, string> = {
  sharepointFolder: '#0078d4',
  sharepoint: '#0078d4',
  onedriveFolder: '#0078d4',
  onedrive: '#0078d4',
  outlookFolder: '#0078d4',
  outlook: '#0078d4',
  googleDriveFolder: '#34a853',
  drive: '#34a853',
  gmailFolder: '#ea4335',
  gmail: '#ea4335',
  ftpFolder: '#795548',
  files: '#795548',
  'local:ftp': '#795548',
  'local:jira': '#0052CC',
  clickup: '#7b68ee',
};

function _getSourceColor(sourceType: string): string {
  return _SOURCE_COLORS[sourceType] || '#F25843';
}

const _SOURCE_ICONS: Record<string, string> = {
  sharepointFolder: '\uD83D\uDCC1',
  sharepoint: '\uD83D\uDCC1',
  onedriveFolder: '\u2601\uFE0F',
  onedrive: '\u2601\uFE0F',
  outlookFolder: '\uD83D\uDCE7',
  outlook: '\uD83D\uDCE7',
  googleDriveFolder: '\uD83D\uDCC2',
  drive: '\uD83D\uDCC2',
  gmailFolder: '\uD83D\uDCE8',
  gmail: '\uD83D\uDCE8',
  ftpFolder: '\uD83D\uDD17',
  files: '\uD83D\uDD17',
  'local:ftp': '\uD83D\uDD17',
  'local:jira': '\uD83D\uDD27',
  clickup: '\uD83D\uDCCB',
};

function _getSourceIcon(sourceType: string): string {
  return _SOURCE_ICONS[sourceType] || '\uD83D\uDCC1';
}

/* ─── Scope / Neutralize constants ───────────────────────────────────── */

const _SCOPE_ORDER: string[] = ['personal', 'featureInstance', 'mandate'];

const _SCOPE_ICONS: Record<string, string> = {
  personal: '\uD83D\uDC64',
  featureInstance: '\uD83D\uDC65',
  mandate: '\uD83C\uDFE2',
  global: '\uD83C\uDF10',
};

function _nextScope(current: string): string {
  const idx = _SCOPE_ORDER.indexOf(current);
  if (idx === -1) return _SCOPE_ORDER[0];
  return _SCOPE_ORDER[(idx + 1) % _SCOPE_ORDER.length];
}

const _SERVICE_TO_SOURCE_TYPE: Record<string, string> = {
  sharepoint: 'sharepointFolder',
  onedrive: 'onedriveFolder',
  outlook: 'outlookFolder',
  drive: 'googleDriveFolder',
  gmail: 'gmailFolder',
  files: 'ftpFolder',
};

/* ─── Tree helpers ───────────────────────────────────────────────────── */

function _mapTree(nodes: TreeNode[], key: string, updater: (n: TreeNode) => TreeNode): TreeNode[] {
  return nodes.map(n => {
    if (n.key === key) return updater(n);
    if (n.children) return { ...n, children: _mapTree(n.children, key, updater) };
    return n;
  });
}

function _mapFeatureTreeUpdate(
  prev: MandateGroupNode[],
  featureInstanceId: string,
  updater: (n: FeatureConnectionNode) => FeatureConnectionNode,
): MandateGroupNode[] {
  return prev.map(g => ({
    ...g,
    featureConnections: g.featureConnections.map(n =>
      n.featureInstanceId === featureInstanceId ? updater(n) : n
    ),
  }));
}

function _findFeatureInstanceMeta(
  groups: MandateGroupNode[],
  featureInstanceId: string,
): { mandateLabel: string; instanceLabel: string } | null {
  for (const g of groups) {
    const fc = g.featureConnections.find(f => f.featureInstanceId === featureInstanceId);
    if (fc) return { mandateLabel: g.mandateLabel, instanceLabel: fc.label };
  }
  return null;
}

function _personalDataSourceHoverTitle(connLabel: string, ds: UdbDataSource): string {
  const pathPart = (ds.displayPath && ds.displayPath.trim()) || ds.label || ds.path || '';
  return pathPart ? `${connLabel} / ${pathPart}` : connLabel;
}

function _featureDataSourceHoverTitle(
  meta: { mandateLabel: string; instanceLabel: string } | null,
  fds: UdbFeatureDataSource,
): string {
  const parts: string[] = [];
  if (meta) {
    parts.push(meta.mandateLabel, meta.instanceLabel);
  }
  const labelPart = fds.label && fds.tableName && fds.label !== fds.tableName
    ? `${fds.label} (${fds.tableName})`
    : (fds.label || fds.tableName);
  parts.push(labelPart);
  if (fds.objectKey && fds.objectKey !== labelPart && !labelPart.includes(fds.objectKey)) {
    parts.push(fds.objectKey);
  }
  return parts.join(' / ');
}

/* ─── Data fetching (module-level) ───────────────────────────────────── */

async function _loadServices(instanceId: string, connectionId: string): Promise<TreeNode[]> {
  const res = await api.get(`/api/workspace/${instanceId}/connections/${connectionId}/services`);
  const services = res.data.services || [];
  return services.map((s: any) => ({
    key: `svc-${connectionId}-${s.service}`,
    label: s.label || s.service,
    icon: _SERVICE_ICONS[s.service] || '\uD83D\uDCC2',
    type: 'service' as const,
    expanded: false,
    loading: false,
    children: null,
    connectionId,
    service: s.service,
    path: '/',
    displayPath: s.label || s.service,
  }));
}

async function _browseService(
  instanceId: string,
  connectionId: string,
  service: string,
  path: string,
  parentDisplayPath: string | undefined,
): Promise<TreeNode[]> {
  const res = await api.get(`/api/workspace/${instanceId}/connections/${connectionId}/browse`, {
    params: { service, path },
  });
  const items = res.data.items || [];
  return items.map((entry: any, idx: number) => {
    const seg = entry.name || '';
    const displayPath = parentDisplayPath
      ? `${parentDisplayPath} / ${seg}`
      : seg;
    return {
      key: `item-${connectionId}-${service}-${entry.path || idx}`,
      label: entry.name,
      icon: entry.isFolder ? '\uD83D\uDCC1' : _fileIcon(entry.name),
      type: entry.isFolder ? 'folder' as const : 'file' as const,
      expanded: false,
      loading: false,
      children: entry.isFolder ? null : [],
      connectionId,
      service,
      path: entry.path,
      displayPath,
    };
  });
}

function _fileIcon(name: string): string {
  const ext = name.split('.').pop()?.toLowerCase() || '';
  const map: Record<string, string> = {
    pdf: '\uD83D\uDCC4', doc: '\uD83D\uDCDD', docx: '\uD83D\uDCDD',
    xls: '\uD83D\uDCCA', xlsx: '\uD83D\uDCCA', csv: '\uD83D\uDCCA',
    ppt: '\uD83D\uDCC8', pptx: '\uD83D\uDCC8',
    txt: '\uD83D\uDCC4', json: '\uD83D\uDCCB',
    png: '\uD83D\uDDBC\uFE0F', jpg: '\uD83D\uDDBC\uFE0F', jpeg: '\uD83D\uDDBC\uFE0F',
    zip: '\uD83D\uDCE6', rar: '\uD83D\uDCE6',
    mp3: '\uD83C\uDFB5', wav: '\uD83C\uDFB5',
    mp4: '\uD83C\uDFAC', mov: '\uD83C\uDFAC',
  };
  return map[ext] || '\uD83D\uDCC4';
}

/* ─── Spinner (inline) ───────────────────────────────────────────────── */

function _Spinner(): React.ReactElement {
  return (
    <span style={{
      display: 'inline-block', width: 10, height: 10,
      border: '1.5px solid var(--border-color, #ccc)', borderTopColor: 'var(--primary-color, #F25843)',
      borderRadius: '50%',
      animation: 'spin 0.6s linear infinite',
    }} />
  );
}

/* ─── Component ──────────────────────────────────────────────────────── */

const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => {
  const { t } = useLanguage();
  const _scopeLabel = (scope: string) => ({
    personal: t('Persönlich'),
    featureInstance: t('Feature-Instanz'),
    mandate: t('Mandant'),
    global: t('Global'),
  } as Record<string, string>)[scope] || scope;
  const _scopeCycleTitle = (scope: string) =>
    `${t('Bereich')}: ${_scopeLabel(scope)} → ${_scopeLabel(_nextScope(scope))}`;
  const instanceId = context.instanceId;

  /* ── Active sources (fetched internally) ── */
  const [dataSources, setDataSources] = useState<UdbDataSource[]>([]);
  const [featureDataSources, setFeatureDataSources] = useState<UdbFeatureDataSource[]>([]);

  /* ── Browse tree state ── */
  const [tree, setTree] = useState<TreeNode[]>([]);
  const [loadingRoot, setLoadingRoot] = useState(false);
  const [addingPath, setAddingPath] = useState<string | null>(null);

  /* ── Feature tree state ── */
  const [featureTree, setFeatureTree] = useState<MandateGroupNode[]>([]);
  const [loadingFeatures, setLoadingFeatures] = useState(false);
  const [addingFeatureKey, setAddingFeatureKey] = useState<string | null>(null);

  const mountedRef = useRef(true);
  useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []);

  /* ── Fetch active personal data sources ── */
  const _fetchDataSources = useCallback(() => {
    if (!instanceId) return;
    api.get(`/api/workspace/${instanceId}/datasources`)
      .then(res => {
        if (!mountedRef.current) return;
        const list: UdbDataSource[] = (res.data.dataSources || res.data || []).map((d: any) => ({
          id: d.id,
          connectionId: d.connectionId,
          sourceType: d.sourceType,
          path: d.path,
          label: d.label,
          displayPath: d.displayPath,
          scope: d.scope || 'personal',
          neutralize: d.neutralize ?? false,
        }));
        setDataSources(list);
      })
      .catch(() => { if (mountedRef.current) setDataSources([]); });
  }, [instanceId]);

  /* ── Fetch active feature data sources ── */
  const _fetchFeatureDataSources = useCallback(() => {
    if (!instanceId) return;
    api.get(`/api/workspace/${instanceId}/feature-datasources`)
      .then(res => {
        if (!mountedRef.current) return;
        const list: UdbFeatureDataSource[] = (res.data.featureDataSources || res.data || []).map((d: any) => ({
          id: d.id,
          featureInstanceId: d.featureInstanceId,
          featureCode: d.featureCode,
          tableName: d.tableName,
          objectKey: d.objectKey,
          label: d.label,
          scope: d.scope || 'personal',
          neutralize: d.neutralize ?? false,
          recordFilter: d.recordFilter || undefined,
        }));
        setFeatureDataSources(list);
      })
      .catch(() => { if (mountedRef.current) setFeatureDataSources([]); });
  }, [instanceId]);

  useEffect(() => { _fetchDataSources(); }, [_fetchDataSources]);
  useEffect(() => { _fetchFeatureDataSources(); }, [_fetchFeatureDataSources]);

  /* ── Load Level 1: UserConnections ── */
  const _loadConnections = useCallback(() => {
    if (!instanceId) return;
    setLoadingRoot(true);
    api.get(`/api/workspace/${instanceId}/connections`)
      .then(res => {
        if (!mountedRef.current) return;
        const conns = res.data.connections || [];
        const nodes: TreeNode[] = conns
          .filter((c: any) => c.status === 'active')
          .map((c: any) => ({
            key: `conn-${c.id}`,
            label: c.externalEmail || c.externalUsername || c.authority,
            icon: _AUTHORITY_ICONS[c.authority] || '\uD83D\uDD17',
            type: 'connection' as const,
            expanded: false,
            loading: false,
            children: null,
            connectionId: c.id,
            authority: c.authority,
          }));
        setTree(nodes);
      })
      .catch(() => { if (mountedRef.current) setTree([]); })
      .finally(() => { if (mountedRef.current) setLoadingRoot(false); });
  }, [instanceId]);

  useEffect(() => { _loadConnections(); }, [_loadConnections]);

  /* ── Generic tree update helper ── */
  const _updateNode = useCallback((key: string, updater: (node: TreeNode) => TreeNode) => {
    setTree(prev => _mapTree(prev, key, updater));
  }, []);

  /* ── Toggle expand/collapse ── */
  const _toggleNode = useCallback(async (node: TreeNode) => {
    if (node.expanded) {
      _updateNode(node.key, n => ({ ...n, expanded: false }));
      return;
    }

    if (node.children !== null) {
      _updateNode(node.key, n => ({ ...n, expanded: true }));
      return;
    }

    _updateNode(node.key, n => ({ ...n, loading: true, expanded: true }));

    try {
      let children: TreeNode[] = [];

      if (node.type === 'connection') {
        children = await _loadServices(instanceId, node.connectionId);
      } else if (node.type === 'service' || node.type === 'folder') {
        children = await _browseService(
          instanceId,
          node.connectionId,
          node.service!,
          node.path || '/',
          node.displayPath || node.label,
        );
      }

      if (mountedRef.current) {
        _updateNode(node.key, n => ({ ...n, loading: false, children }));
      }
    } catch {
      if (mountedRef.current) {
        _updateNode(node.key, n => ({ ...n, loading: false, children: [] }));
      }
    }
  }, [instanceId, _updateNode]);

  /* ── Add as DataSource ── */
  const _addAsDataSource = useCallback(async (node: TreeNode) => {
    if (!node.service || !node.connectionId) return;
    setAddingPath(node.key);
    try {
      await api.post(`/api/workspace/${instanceId}/datasources`, {
        connectionId: node.connectionId,
        sourceType: _SERVICE_TO_SOURCE_TYPE[node.service] || node.service,
        path: node.path || '/',
        label: node.label,
        displayPath: node.displayPath || node.label,
      });
      _fetchDataSources();
      onSourcesChanged?.();
    } catch (err) {
      console.error('Failed to add data source:', err);
    } finally {
      if (mountedRef.current) setAddingPath(null);
    }
  }, [instanceId, _fetchDataSources]);

  /* ── Remove DataSource ── */
  const _removeDatasource = useCallback(async (dsId: string) => {
    try {
      await api.delete(`/api/workspace/${instanceId}/datasources/${dsId}`);
      _fetchDataSources();
      onSourcesChanged?.();
    } catch (err) {
      console.error('Failed to remove data source:', err);
    }
  }, [instanceId, _fetchDataSources]);

  /* ── Check if a path is already added ── */
  const _isAdded = useCallback((connectionId: string, service: string | undefined, path: string | undefined): boolean => {
    const expectedSourceType = service ? (_SERVICE_TO_SOURCE_TYPE[service] || service) : undefined;
    return dataSources.some(ds =>
      ds.connectionId === connectionId &&
      ds.path === (path || '/') &&
      (!expectedSourceType || ds.sourceType === expectedSourceType),
    );
  }, [dataSources]);

  /* ── Scope change (personal data source, optimistic) ── */
  const _cyclePersonalScope = useCallback(async (ds: UdbDataSource) => {
    const newScope = _nextScope(ds.scope);
    setDataSources(prev => prev.map(d => d.id === ds.id ? { ...d, scope: newScope } : d));
    try {
      await api.patch(`/api/datasources/${ds.id}/scope`, { scope: newScope });
    } catch {
      setDataSources(prev => prev.map(d => d.id === ds.id ? { ...d, scope: ds.scope } : d));
    }
  }, []);

  /* ── Neutralize toggle (personal data source, optimistic) ── */
  const _togglePersonalNeutralize = useCallback(async (ds: UdbDataSource) => {
    const newValue = !ds.neutralize;
    setDataSources(prev => prev.map(d => d.id === ds.id ? { ...d, neutralize: newValue } : d));
    try {
      await api.patch(`/api/datasources/${ds.id}/neutralize`, { neutralize: newValue });
    } catch {
      setDataSources(prev => prev.map(d => d.id === ds.id ? { ...d, neutralize: ds.neutralize } : d));
    }
  }, []);

  /* ── Scope change (feature data source, optimistic) ── */
  const _cycleFeatureScope = useCallback(async (fds: UdbFeatureDataSource) => {
    const newScope = _nextScope(fds.scope);
    setFeatureDataSources(prev => prev.map(d => d.id === fds.id ? { ...d, scope: newScope } : d));
    try {
      await api.patch(`/api/datasources/${fds.id}/scope`, { scope: newScope });
    } catch {
      setFeatureDataSources(prev => prev.map(d => d.id === fds.id ? { ...d, scope: fds.scope } : d));
    }
  }, []);

  /* ── Neutralize toggle (feature data source, optimistic) ── */
  const _toggleFeatureNeutralize = useCallback(async (fds: UdbFeatureDataSource) => {
    const newValue = !fds.neutralize;
    setFeatureDataSources(prev => prev.map(d => d.id === fds.id ? { ...d, neutralize: newValue } : d));
    try {
      await api.patch(`/api/datasources/${fds.id}/neutralize`, { neutralize: newValue });
    } catch {
      setFeatureDataSources(prev => prev.map(d => d.id === fds.id ? { ...d, neutralize: fds.neutralize } : d));
    }
  }, []);

  /* ── Feature Connections: Load Level 1 ── */
  const _loadFeatureConnections = useCallback(() => {
    if (!instanceId) return;
    setLoadingFeatures(true);
    api.get(`/api/workspace/${instanceId}/feature-connections`)
      .then(res => {
        if (!mountedRef.current) return;
        const groups = res.data.featureConnectionsByMandate || [];
        setFeatureTree(groups.map((g: any) => ({
          mandateId: g.mandateId,
          mandateLabel: g.mandateLabel || g.mandateId,
          expanded: true,
          featureConnections: (g.featureConnections || []).map((c: any) => ({
            featureInstanceId: c.featureInstanceId,
            featureCode: c.featureCode,
            mandateId: c.mandateId,
            label: c.label,
            icon: c.icon || '\uD83D\uDDC3\uFE0F',
            tableCount: c.tableCount || 0,
            expanded: false,
            loading: false,
            tables: null,
            parentRecords: {},
          })),
        })));
      })
      .catch(() => { if (mountedRef.current) setFeatureTree([]); })
      .finally(() => { if (mountedRef.current) setLoadingFeatures(false); });
  }, [instanceId]);

  useEffect(() => { _loadFeatureConnections(); }, [_loadFeatureConnections]);

  /* ── Feature Connections: Toggle mandate group ── */
  const _toggleMandateGroup = useCallback((mandateId: string) => {
    setFeatureTree(prev => prev.map(g =>
      g.mandateId === mandateId ? { ...g, expanded: !g.expanded } : g
    ));
  }, []);

  /* ── Feature Connections: Toggle expand ── */
  const _toggleFeatureNode = useCallback(async (node: FeatureConnectionNode) => {
    if (node.expanded) {
      setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ ...n, expanded: false })));
      return;
    }

    if (node.tables !== null) {
      setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({ ...n, expanded: true })));
      return;
    }

    setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
      ...n, loading: true, expanded: true,
    })));

    try {
      const res = await api.get(`/api/workspace/${instanceId}/feature-connections/${node.featureInstanceId}/tables`);
      const tables: FeatureTableNode[] = (res.data.tables || []).map((t: any) => ({
        objectKey: t.objectKey,
        tableName: t.tableName,
        label: t.label || {},
        fields: t.fields || [],
        isParent: t.isParent || false,
        parentTable: t.parentTable || undefined,
        parentKey: t.parentKey || undefined,
        displayFields: t.displayFields || undefined,
      }));
      if (mountedRef.current) {
        setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
          ...n, loading: false, tables,
        })));
      }
    } catch {
      if (mountedRef.current) {
        setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
          ...n, loading: false, tables: [],
        })));
      }
    }
  }, [instanceId]);

  /* ── Feature: Add table as FeatureDataSource ── */
  const _addFeatureTable = useCallback(async (node: FeatureConnectionNode, table: FeatureTableNode) => {
    const key = `${node.featureInstanceId}-${table.tableName}`;
    setAddingFeatureKey(key);
    try {
      await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
        featureInstanceId: node.featureInstanceId,
        featureCode: node.featureCode,
        tableName: table.tableName,
        objectKey: table.objectKey,
        label: table.label || table.tableName,
      });
      _fetchFeatureDataSources();
      onSourcesChanged?.();
    } catch (err) {
      console.error('Failed to add feature data source:', err);
    } finally {
      if (mountedRef.current) setAddingFeatureKey(null);
    }
  }, [instanceId, _fetchFeatureDataSources]);

  /* ── Feature: Remove FeatureDataSource ── */
  const _removeFeatureDataSource = useCallback(async (fdsId: string) => {
    try {
      await api.delete(`/api/workspace/${instanceId}/feature-datasources/${fdsId}`);
      _fetchFeatureDataSources();
      onSourcesChanged?.();
    } catch (err) {
      console.error('Failed to remove feature data source:', err);
    }
  }, [instanceId, _fetchFeatureDataSources]);

  /* ── Feature: check if table already added ── */
  const _isFeatureTableAdded = useCallback((featureInstanceId: string, tableName: string): boolean => {
    return featureDataSources.some(fds =>
      fds.featureInstanceId === featureInstanceId && fds.tableName === tableName,
    );
  }, [featureDataSources]);

  /* ── Parent groups: expand/collapse + load records ── */
  const [expandedParentGroups, setExpandedParentGroups] = useState<Set<string>>(new Set());
  const [loadingParentGroup, setLoadingParentGroup] = useState<string | null>(null);
  const [addingParentKey, setAddingParentKey] = useState<string | null>(null);

  const _toggleParentGroup = useCallback(async (node: FeatureConnectionNode, parentTableName: string) => {
    const groupKey = `${node.featureInstanceId}-${parentTableName}`;

    if (expandedParentGroups.has(groupKey)) {
      setExpandedParentGroups(prev => { const next = new Set(prev); next.delete(groupKey); return next; });
      return;
    }

    setExpandedParentGroups(prev => new Set(prev).add(groupKey));

    if (node.parentRecords[parentTableName]) return;

    setLoadingParentGroup(groupKey);
    try {
      const res = await api.get(
        `/api/workspace/${instanceId}/feature-connections/${node.featureInstanceId}/parent-objects/${parentTableName}`,
      );
      const records: ParentRecordNode[] = (res.data.parentObjects || []).map((r: any) => ({
        id: r.id,
        displayLabel: r.displayLabel || r.id,
        fields: r.fields || {},
        tableName: parentTableName,
        expanded: false,
      }));
      if (mountedRef.current) {
        setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
          ...n,
          parentRecords: { ...n.parentRecords, [parentTableName]: records },
        })));
      }
    } catch {
      if (mountedRef.current) {
        setFeatureTree(prev => _mapFeatureTreeUpdate(prev, node.featureInstanceId, n => ({
          ...n,
          parentRecords: { ...n.parentRecords, [parentTableName]: [] },
        })));
      }
    } finally {
      if (mountedRef.current) setLoadingParentGroup(null);
    }
  }, [instanceId, expandedParentGroups]);

  const _toggleParentRecord = useCallback((featureInstanceId: string, parentTableName: string, recordId: string) => {
    setFeatureTree(prev => _mapFeatureTreeUpdate(prev, featureInstanceId, n => ({
      ...n,
      parentRecords: {
        ...n.parentRecords,
        [parentTableName]: (n.parentRecords[parentTableName] || []).map(r =>
          r.id === recordId ? { ...r, expanded: !r.expanded } : r,
        ),
      },
    })));
  }, []);

  /* ── Parent record: add parent + all children with recordFilter ── */
  const _addParentRecord = useCallback(async (
    node: FeatureConnectionNode,
    parentRecord: ParentRecordNode,
    allTables: FeatureTableNode[],
  ) => {
    const addKey = `${node.featureInstanceId}-parent-${parentRecord.id}`;
    setAddingParentKey(addKey);
    try {
      const parentTable = allTables.find(t => t.tableName === parentRecord.tableName && t.isParent);
      const childTables = allTables.filter(t => t.parentTable === parentRecord.tableName);

      if (parentTable) {
        const parentLabel = `${parentTable.label || parentTable.tableName}: ${parentRecord.displayLabel}`;
        await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
          featureInstanceId: node.featureInstanceId,
          featureCode: node.featureCode,
          tableName: parentTable.tableName,
          objectKey: parentTable.objectKey,
          label: parentLabel,
          recordFilter: { id: parentRecord.id },
        });
      }

      for (const child of childTables) {
        const childLabel = `${child.label || child.tableName}: ${parentRecord.displayLabel}`;
        await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
          featureInstanceId: node.featureInstanceId,
          featureCode: node.featureCode,
          tableName: child.tableName,
          objectKey: child.objectKey,
          label: childLabel,
          recordFilter: { [child.parentKey!]: parentRecord.id },
        });
      }

      _fetchFeatureDataSources();
      onSourcesChanged?.();
    } catch (err) {
      console.error('Failed to add parent record sources:', err);
    } finally {
      if (mountedRef.current) setAddingParentKey(null);
    }
  }, [instanceId, _fetchFeatureDataSources]);

  /* ── Check if a parent record is already added ── */
  const _isParentRecordAdded = useCallback((featureInstanceId: string, parentTableName: string, recordId: string): boolean => {
    return featureDataSources.some(fds =>
      fds.featureInstanceId === featureInstanceId &&
      fds.tableName === parentTableName &&
      fds.recordFilter?.id === recordId,
    );
  }, [featureDataSources]);

  /* ── Render ── */

  return (
    <div className={styles.sourcesTab} style={{ padding: 8, fontSize: 13 }}>
      {/* ── Active Personal Sources ── */}
      {dataSources.length > 0 && (
        <div style={{ marginBottom: 8 }}>
          <div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
            {t('Aktive persönliche Quellen')}
          </div>
          {[...dataSources].sort((a, b) => {
            const aKey = `${a.sourceType}|${a.label || a.path || ''}`;
            const bKey = `${b.sourceType}|${b.label || b.path || ''}`;
            return aKey.localeCompare(bKey);
          }).map(ds => {
            const connColor = _getSourceColor(ds.sourceType);
            const connNode = tree.find(n => n.connectionId === ds.connectionId);
            const connLabel = connNode?.label || ds.connectionId;
            const folder = ds.label || ds.path || ds.id;
            return (
              <div key={ds.id} style={{
                display: 'flex', alignItems: 'center', gap: 6,
                padding: '4px 6px', borderRadius: 4, marginBottom: 2,
                background: `${connColor}18`,
                borderLeft: `3px solid ${connColor}`,
                fontSize: 12,
              }} title={_personalDataSourceHoverTitle(connLabel, ds)}>
                <span style={{ fontSize: 12, flexShrink: 0 }}>{_getSourceIcon(ds.sourceType)}</span>
                <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {connLabel} – {folder}
                </span>
                <button
                  onClick={() => _cyclePersonalScope(ds)}
                  style={{
                    background: 'none', border: 'none', cursor: 'pointer',
                    fontSize: 13, padding: '0 2px', lineHeight: 1,
                  }}
                  title={_scopeCycleTitle(ds.scope)}
                >
                  {_SCOPE_ICONS[ds.scope] || _SCOPE_ICONS.personal}
                </button>
                <button
                  onClick={() => _togglePersonalNeutralize(ds)}
                  style={{
                    background: 'none', border: 'none', cursor: 'pointer',
                    fontSize: 13, padding: '0 2px', lineHeight: 1,
                    opacity: ds.neutralize ? 1 : 0.35,
                  }}
                  title={ds.neutralize ? t('Neutralisierung: Klick zum Deaktivieren') : t('Neutralisierung aus: Klick zum Aktivieren')}
                >
                  {'\uD83D\uDD12'}
                </button>
                <button
                  onClick={() => _removeDatasource(ds.id)}
                  style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
                  title={t('Entfernen')}
                >
                  {'\u2715'}
                </button>
              </div>
            );
          })}
          <div style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '8px 0' }} />
        </div>
      )}

      {/* ── Browse Sources header ── */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
        <span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
          {t('Quellen durchsuchen')}
        </span>
        <button
          onClick={_loadConnections}
          disabled={loadingRoot}
          style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: 'var(--primary-color, #F25843)' }}
        >
          {loadingRoot ? '...' : '\u21BB'}
        </button>
      </div>

      {/* ── Browse Sources tree ── */}
      {loadingRoot && tree.length === 0 && (
        <div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
          {t('Verbindungen werden geladen…')}
        </div>
      )}

      {!loadingRoot && tree.length === 0 && (
        <div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
          {t('Keine aktiven Verbindungen.')}
        </div>
      )}

      {tree.map(node => (
        <_TreeNodeView
          key={node.key}
          node={node}
          depth={0}
          onToggle={_toggleNode}
          onAdd={_addAsDataSource}
          isAdded={_isAdded}
          addingPath={addingPath}
        />
      ))}

      {/* ── Divider ── */}
      <div style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '12px 0 8px' }} />

      {/* ── Active Feature Sources (grouped by parent record) ── */}
      {featureDataSources.length > 0 && (
        <div style={{ marginBottom: 8 }}>
          <div style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase', marginBottom: 4 }}>
            {t('Aktive Feature-Quellen')}
          </div>
          {(() => {
            const sorted = [...featureDataSources].sort((a, b) => (a.label || a.tableName || '').localeCompare(b.label || b.tableName || ''));
            const grouped: { key: string; label: string; items: UdbFeatureDataSource[] }[] = [];
            const standalone: UdbFeatureDataSource[] = [];

            for (const fds of sorted) {
              if (fds.recordFilter && Object.keys(fds.recordFilter).length > 0) {
                const filterKey = `${fds.featureInstanceId}|${JSON.stringify(fds.recordFilter)}`;
                let group = grouped.find(g => g.key === filterKey);
                if (!group) {
                  const parentLabel = fds.label.includes(':') ? fds.label.split(':')[1]?.trim() : fds.label;
                  const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
                  group = { key: filterKey, label: `${meta?.instanceLabel || fds.featureCode} – ${parentLabel}`, items: [] };
                  grouped.push(group);
                }
                group.items.push(fds);
              } else {
                standalone.push(fds);
              }
            }

            return (
              <>
                {grouped.map(group => (
                  <div key={group.key} style={{ marginBottom: 4 }}>
                    <div style={{
                      fontSize: 11, fontWeight: 600, color: '#7b1fa2',
                      padding: '2px 6px', marginBottom: 1,
                      display: 'flex', alignItems: 'center', gap: 4,
                    }}>
                      <span>{'\uD83D\uDCCB'}</span>
                      <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                        {group.label}
                      </span>
                      <button
                        onClick={() => { group.items.forEach(fds => _removeFeatureDataSource(fds.id)); }}
                        style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
                        title={t('Alle Tabellen für diese Quelle entfernen')}
                      >
                        {'\u2715'}
                      </button>
                    </div>
                    {group.items.map(fds => {
                      const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
                      return (
                        <div key={fds.id} style={{
                          display: 'flex', alignItems: 'center', gap: 6,
                          padding: '3px 6px 3px 22px', borderRadius: 4, marginBottom: 1,
                          background: '#7b1fa210',
                          fontSize: 11,
                        }} title={_featureDataSourceHoverTitle(meta, fds)}>
                          <span style={{ fontSize: 11, flexShrink: 0, color: '#7b1fa2' }}>
                            {getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDCC4'}
                          </span>
                          <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                            {fds.tableName}
                          </span>
                          <button
                            onClick={() => _cycleFeatureScope(fds)}
                            style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1 }}
                            title={_scopeCycleTitle(fds.scope)}
                          >
                            {_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
                          </button>
                          <button
                            onClick={() => _toggleFeatureNeutralize(fds)}
                            style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }}
                            title={fds.neutralize ? t('Neutralisierung an') : t('Neutralisierung aus')}
                          >
                            {'\uD83D\uDD12'}
                          </button>
                          <button
                            onClick={() => _removeFeatureDataSource(fds.id)}
                            style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
                            title={t('Entfernen')}
                          >
                            {'\u2715'}
                          </button>
                        </div>
                      );
                    })}
                  </div>
                ))}
                {standalone.map(fds => {
                  const meta = _findFeatureInstanceMeta(featureTree, fds.featureInstanceId);
                  const fdsConnLabel = meta?.instanceLabel || fds.tableName;
                  return (
                    <div key={fds.id} style={{
                      display: 'flex', alignItems: 'center', gap: 6,
                      padding: '4px 6px', borderRadius: 4, marginBottom: 2,
                      background: '#7b1fa218',
                      borderLeft: '3px solid #7b1fa2',
                      fontSize: 12,
                    }} title={_featureDataSourceHoverTitle(meta, fds)}>
                      <span style={{ fontSize: 12, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
                        {getPageIcon(`feature.${fds.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
                      </span>
                      <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                        {fdsConnLabel} – {fds.tableName}
                      </span>
                      <button
                        onClick={() => _cycleFeatureScope(fds)}
                        style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1 }}
                        title={_scopeCycleTitle(fds.scope)}
                      >
                        {_SCOPE_ICONS[fds.scope] || _SCOPE_ICONS.personal}
                      </button>
                      <button
                        onClick={() => _toggleFeatureNeutralize(fds)}
                        style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }}
                        title={fds.neutralize ? t('Neutralisierung an') : t('Neutralisierung aus')}
                      >
                        {'\uD83D\uDD12'}
                      </button>
                      <button
                        onClick={() => _removeFeatureDataSource(fds.id)}
                        style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
                        title={t('Entfernen')}
                      >
                        {'\u2715'}
                      </button>
                    </div>
                  );
                })}
              </>
            );
          })()}
          <div style={{ height: 1, background: 'var(--border-color, #e0e0e0)', margin: '8px 0' }} />
        </div>
      )}

      {/* ── Feature Data header ── */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
        <span style={{ fontSize: 11, fontWeight: 600, color: '#666', textTransform: 'uppercase' }}>
          {t('Feature-Daten')}
        </span>
        <button
          onClick={_loadFeatureConnections}
          disabled={loadingFeatures}
          style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#7b1fa2' }}
        >
          {loadingFeatures ? '...' : '\u21BB'}
        </button>
      </div>

      {/* ── Feature Data tree ── */}
      {loadingFeatures && featureTree.length === 0 && (
        <div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
          {t('Feature-Instanzen werden geladen…')}
        </div>
      )}

      {!loadingFeatures && featureTree.length === 0 && (
        <div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
          {t('Keine Feature-Instanzen gefunden.')}
        </div>
      )}

      {featureTree.map(g => (
        <_MandateGroupView
          key={g.mandateId}
          group={g}
          onToggleGroup={_toggleMandateGroup}
          onToggleFeature={_toggleFeatureNode}
          onAddTable={_addFeatureTable}
          isTableAdded={_isFeatureTableAdded}
          addingKey={addingFeatureKey}
          onToggleParentGroup={_toggleParentGroup}
          onToggleParentRecord={_toggleParentRecord}
          onAddParentRecord={_addParentRecord}
          isParentRecordAdded={_isParentRecordAdded}
          expandedParentGroups={expandedParentGroups}
          loadingParentGroup={loadingParentGroup}
          addingParentKey={addingParentKey}
        />
      ))}
    </div>
  );
};

/* ─── TreeNodeView (recursive) ───────────────────────────────────────── */

interface _TreeNodeViewProps {
  node: TreeNode;
  depth: number;
  onToggle: (node: TreeNode) => void;
  onAdd: (node: TreeNode) => void;
  isAdded: (connectionId: string, service: string | undefined, path: string | undefined) => boolean;
  addingPath: string | null;
}

const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
  node, depth, onToggle, onAdd, isAdded, addingPath,
}) => {
  const { t } = useLanguage();
  const [hovered, setHovered] = useState(false);
  const hasChildren = node.type !== 'file';
  const chevron = hasChildren
    ? (node.expanded ? '\u25BE' : '\u25B8')
    : '\u00A0\u00A0';
  const canAdd = node.type === 'folder' || node.type === 'service';
  const alreadyAdded = canAdd && isAdded(node.connectionId, node.service, node.path);
  const isAdding = addingPath === node.key;

  return (
    <div>
      <div
        onClick={() => { if (hasChildren) onToggle(node); }}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: 4,
          paddingLeft: depth * 16 + 4,
          paddingRight: 4,
          paddingTop: 3,
          paddingBottom: 3,
          cursor: hasChildren ? 'pointer' : 'default',
          borderRadius: 3,
          background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent',
          transition: 'background 0.1s',
          userSelect: 'none',
        }}
      >
        <span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
          {node.loading ? _Spinner() : chevron}
        </span>
        <span style={{ fontSize: 14, flexShrink: 0 }}>{node.icon}</span>
        <span style={{
          flex: 1, minWidth: 0, overflow: 'hidden',
          textOverflow: 'ellipsis', whiteSpace: 'nowrap',
          fontSize: 12,
          fontWeight: node.type === 'connection' ? 600 : 400,
        }}>
          {node.label}
        </span>
        {canAdd && hovered && !alreadyAdded && (
          <button
            onClick={e => { e.stopPropagation(); onAdd(node); }}
            disabled={isAdding}
            style={{
              background: 'none', border: '1px solid var(--primary-color, #F25843)', borderRadius: 3,
              cursor: isAdding ? 'not-allowed' : 'pointer',
              fontSize: 10, color: 'var(--primary-color, #F25843)', padding: '1px 5px',
              opacity: isAdding ? 0.5 : 1,
              flexShrink: 0,
            }}
            title={t('Als Datenquelle hinzufügen')}
          >
            {isAdding ? '...' : `+ ${t('Hinzufügen')}`}
          </button>
        )}
        {canAdd && alreadyAdded && (
          <span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('Bereits hinzugefügt')}>
            {'\u2713'}
          </span>
        )}
      </div>

      {node.expanded && node.children && node.children.length > 0 && (
        <div>
          {node.children.map(child => (
            <_TreeNodeView
              key={child.key}
              node={child}
              depth={depth + 1}
              onToggle={onToggle}
              onAdd={onAdd}
              isAdded={isAdded}
              addingPath={addingPath}
            />
          ))}
        </div>
      )}

      {node.expanded && node.children && node.children.length === 0 && !node.loading && (
        <div style={{ paddingLeft: (depth + 1) * 16 + 20, fontSize: 11, color: '#bbb', padding: '2px 0 2px ' + ((depth + 1) * 16 + 20) + 'px' }}>
          {t('(leer)')}
        </div>
      )}
    </div>
  );
};

/* ─── MandateGroupView (mandate + feature instances) ─────────────────── */

interface _MandateGroupViewProps {
  group: MandateGroupNode;
  onToggleGroup: (mandateId: string) => void;
  onToggleFeature: (node: FeatureConnectionNode) => void;
  onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
  isTableAdded: (featureInstanceId: string, tableName: string) => boolean;
  addingKey: string | null;
  onToggleParentGroup: (node: FeatureConnectionNode, parentTableName: string) => void;
  onToggleParentRecord: (featureInstanceId: string, parentTableName: string, recordId: string) => void;
  onAddParentRecord: (node: FeatureConnectionNode, record: ParentRecordNode, allTables: FeatureTableNode[]) => void;
  isParentRecordAdded: (featureInstanceId: string, parentTableName: string, recordId: string) => boolean;
  expandedParentGroups: Set<string>;
  loadingParentGroup: string | null;
  addingParentKey: string | null;
}

const _MandateGroupView: React.FC<_MandateGroupViewProps> = ({
  group, onToggleGroup, onToggleFeature, onAddTable, isTableAdded, addingKey,
  onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded,
  expandedParentGroups, loadingParentGroup, addingParentKey,
}) => {
  const [hovered, setHovered] = useState(false);
  const chevron = group.expanded ? '\u25BE' : '\u25B8';

  return (
    <div>
      <div
        onClick={() => onToggleGroup(group.mandateId)}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        style={{
          display: 'flex', alignItems: 'center', gap: 4,
          paddingLeft: 4, paddingRight: 4, paddingTop: 3, paddingBottom: 3,
          cursor: 'pointer', borderRadius: 3,
          background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent',
          transition: 'background 0.1s', userSelect: 'none',
        }}
      >
        <span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
          {chevron}
        </span>
        <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12, fontWeight: 700, color: '#555' }}>
          {group.mandateLabel}
        </span>
      </div>

      {group.expanded && (
        <div style={{ paddingLeft: 10 }}>
          {group.featureConnections.map(fNode => (
            <_FeatureNodeView
              key={fNode.featureInstanceId}
              node={fNode}
              onToggle={onToggleFeature}
              onAddTable={onAddTable}
              isTableAdded={isTableAdded}
              addingKey={addingKey}
              onToggleParentGroup={onToggleParentGroup}
              onToggleParentRecord={onToggleParentRecord}
              onAddParentRecord={onAddParentRecord}
              isParentRecordAdded={isParentRecordAdded}
              expandedParentGroups={expandedParentGroups}
              loadingParentGroup={loadingParentGroup}
              addingParentKey={addingParentKey}
            />
          ))}
        </div>
      )}
    </div>
  );
};

/* ─── FeatureNodeView (feature instance + tables) ────────────────────── */

interface _FeatureNodeViewProps {
  node: FeatureConnectionNode;
  onToggle: (node: FeatureConnectionNode) => void;
  onAddTable: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
  isTableAdded: (featureInstanceId: string, tableName: string) => boolean;
  addingKey: string | null;
  onToggleParentGroup: (node: FeatureConnectionNode, parentTableName: string) => void;
  onToggleParentRecord: (featureInstanceId: string, parentTableName: string, recordId: string) => void;
  onAddParentRecord: (node: FeatureConnectionNode, record: ParentRecordNode, allTables: FeatureTableNode[]) => void;
  isParentRecordAdded: (featureInstanceId: string, parentTableName: string, recordId: string) => boolean;
  expandedParentGroups: Set<string>;
  loadingParentGroup: string | null;
  addingParentKey: string | null;
}

const _FeatureNodeView: React.FC<_FeatureNodeViewProps> = ({
  node, onToggle, onAddTable, isTableAdded, addingKey,
  onToggleParentGroup, onToggleParentRecord, onAddParentRecord, isParentRecordAdded,
  expandedParentGroups, loadingParentGroup, addingParentKey,
}) => {
  const { t } = useLanguage();
  const [hovered, setHovered] = useState(false);
  const chevron = node.expanded ? '\u25BE' : '\u25B8';

  const parentTables = (node.tables || []).filter(t => t.isParent);
  const standaloneTables = (node.tables || []).filter(t => !t.isParent && !t.parentTable);

  return (
    <div>
      <div
        onClick={() => onToggle(node)}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        style={{
          display: 'flex', alignItems: 'center', gap: 4,
          paddingLeft: 4, paddingRight: 4, paddingTop: 3, paddingBottom: 3,
          cursor: 'pointer', borderRadius: 3,
          background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent',
          transition: 'background 0.1s', userSelect: 'none',
        }}
      >
        <span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
          {node.loading ? _Spinner() : chevron}
        </span>
        <span style={{ fontSize: 13, flexShrink: 0, display: 'flex', alignItems: 'center', color: '#7b1fa2' }}>
          {getPageIcon(`feature.${node.featureCode}`) || '\uD83D\uDDC3\uFE0F'}
        </span>
        <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12, fontWeight: 600 }}>
          {node.label}
        </span>
        <span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
          {node.tableCount} {t('Tabellen')}
        </span>
      </div>

      {node.expanded && node.tables && node.tables.length > 0 && (
        <div>
          {/* Parent table groups (hierarchical) */}
          {parentTables.map(pt => {
            const groupKey = `${node.featureInstanceId}-${pt.tableName}`;
            const isGroupExpanded = expandedParentGroups.has(groupKey);
            const isGroupLoading = loadingParentGroup === groupKey;
            const records = node.parentRecords[pt.tableName];
            const childTables = (node.tables || []).filter(t => t.parentTable === pt.tableName);
            const ptLabel = pt.label || pt.tableName;

            return (
              <_ParentGroupView
                key={groupKey}
                featureNode={node}
                parentTable={pt}
                label={ptLabel}
                expanded={isGroupExpanded}
                loading={isGroupLoading}
                records={records || null}
                childTables={childTables}
                allTables={node.tables!}
                onToggleGroup={() => onToggleParentGroup(node, pt.tableName)}
                onToggleRecord={(recordId) => onToggleParentRecord(node.featureInstanceId, pt.tableName, recordId)}
                onAddRecord={(record) => onAddParentRecord(node, record, node.tables!)}
                isRecordAdded={(recordId) => isParentRecordAdded(node.featureInstanceId, pt.tableName, recordId)}
                addingParentKey={addingParentKey}
              />
            );
          })}

          {/* Standalone tables (not part of any hierarchy) */}
          {standaloneTables.map(table => (
            <_FeatureTableRow
              key={table.objectKey}
              featureNode={node}
              table={table}
              onAdd={onAddTable}
              isAdded={isTableAdded(node.featureInstanceId, table.tableName)}
              isAdding={addingKey === `${node.featureInstanceId}-${table.tableName}`}
            />
          ))}
        </div>
      )}

      {node.expanded && node.tables && node.tables.length === 0 && !node.loading && (
        <div style={{ paddingLeft: 36, fontSize: 11, color: '#bbb', padding: '2px 0 2px 36px' }}>
          {t('(keine Tabellen)')}
        </div>
      )}
    </div>
  );
};

/* ─── FeatureTableRow ────────────────────────────────────────────────── */

interface _FeatureTableRowProps {
  featureNode: FeatureConnectionNode;
  table: FeatureTableNode;
  onAdd: (node: FeatureConnectionNode, table: FeatureTableNode) => void;
  isAdded: boolean;
  isAdding: boolean;
}

const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
  featureNode, table, onAdd, isAdded, isAdding,
}) => {
  const { t } = useLanguage();
  const [hovered, setHovered] = useState(false);
  const tableLabel = table.label || table.tableName;

  return (
    <div
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{
        display: 'flex', alignItems: 'center', gap: 4,
        paddingLeft: 36, paddingRight: 4, paddingTop: 3, paddingBottom: 3,
        borderRadius: 3,
        background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent',
        transition: 'background 0.1s', userSelect: 'none',
      }}
      title={`${table.tableName}: ${table.fields.join(', ')}`}
    >
      <span style={{ fontSize: 14, flexShrink: 0 }}>{'\uD83D\uDCC1'}</span>
      <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12 }}>
        {tableLabel}
      </span>
      {hovered && !isAdded && (
        <button
          onClick={() => onAdd(featureNode, table)}
          disabled={isAdding}
          style={{
            background: 'none', border: '1px solid #7b1fa2', borderRadius: 3,
            cursor: isAdding ? 'not-allowed' : 'pointer',
            fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
            opacity: isAdding ? 0.5 : 1, flexShrink: 0,
          }}
          title={t('Als Feature-Datenquelle hinzufügen')}
        >
          {isAdding ? '...' : `+ ${t('Hinzufügen')}`}
        </button>
      )}
      {isAdded && (
        <span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('Bereits hinzugefügt')}>
          {'\u2713'}
        </span>
      )}
    </div>
  );
};

/* ─── ParentGroupView (parent table → parent records) ────────────────── */

interface _ParentGroupViewProps {
  featureNode: FeatureConnectionNode;
  parentTable: FeatureTableNode;
  label: string;
  expanded: boolean;
  loading: boolean;
  records: ParentRecordNode[] | null;
  childTables: FeatureTableNode[];
  allTables: FeatureTableNode[];
  onToggleGroup: () => void;
  onToggleRecord: (recordId: string) => void;
  onAddRecord: (record: ParentRecordNode) => void;
  isRecordAdded: (recordId: string) => boolean;
  addingParentKey: string | null;
}

const _ParentGroupView: React.FC<_ParentGroupViewProps> = ({
  featureNode, parentTable: _parentTable, label, expanded, loading, records, childTables, allTables,
  onToggleGroup, onToggleRecord, onAddRecord, isRecordAdded, addingParentKey,
}) => {
  const { t } = useLanguage();
  const [hovered, setHovered] = useState(false);
  const chevron = expanded ? '\u25BE' : '\u25B8';

  return (
    <div>
      <div
        onClick={onToggleGroup}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        style={{
          display: 'flex', alignItems: 'center', gap: 4,
          paddingLeft: 24, paddingRight: 4, paddingTop: 3, paddingBottom: 3,
          cursor: 'pointer', borderRadius: 3,
          background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent',
          transition: 'background 0.1s', userSelect: 'none',
        }}
      >
        <span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
          {loading ? _Spinner() : chevron}
        </span>
        <span style={{ fontSize: 13, flexShrink: 0 }}>{'\uD83D\uDCC2'}</span>
        <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12, fontWeight: 600, color: '#555' }}>
          {label}
        </span>
        {childTables.length > 0 && (
          <span style={{ fontSize: 10, color: '#999', flexShrink: 0 }}>
            +{childTables.length} {t('Tabellen')}
          </span>
        )}
      </div>

      {expanded && records && records.length > 0 && (
        <div>
          {records.map(record => (
            <_ParentRecordRow
              key={record.id}
              featureNode={featureNode}
              record={record}
              childTables={childTables}
              allTables={allTables}
              onToggle={() => onToggleRecord(record.id)}
              onAdd={() => onAddRecord(record)}
              isAdded={isRecordAdded(record.id)}
              isAdding={addingParentKey === `${featureNode.featureInstanceId}-parent-${record.id}`}
            />
          ))}
        </div>
      )}

      {expanded && records && records.length === 0 && !loading && (
        <div style={{ paddingLeft: 52, fontSize: 11, color: '#bbb', padding: '2px 0 2px 52px' }}>
          {t('(keine Einträge)')}
        </div>
      )}
    </div>
  );
};

/* ─── ParentRecordRow (single parent record + child tables info) ─────── */

interface _ParentRecordRowProps {
  featureNode: FeatureConnectionNode;
  record: ParentRecordNode;
  childTables: FeatureTableNode[];
  allTables: FeatureTableNode[];
  onToggle: () => void;
  onAdd: () => void;
  isAdded: boolean;
  isAdding: boolean;
}

const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
  featureNode: _featureNode, record, childTables, allTables: _allTables,
  onToggle, onAdd, isAdded, isAdding,
}) => {
  const { t } = useLanguage();
  const [hovered, setHovered] = useState(false);
  const chevron = record.expanded ? '\u25BE' : '\u25B8';

  return (
    <div>
      <div
        onClick={onToggle}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        style={{
          display: 'flex', alignItems: 'center', gap: 4,
          paddingLeft: 44, paddingRight: 4, paddingTop: 3, paddingBottom: 3,
          cursor: 'pointer', borderRadius: 3,
          background: hovered ? 'var(--hover-bg, #f5f5f5)' : 'transparent',
          transition: 'background 0.1s', userSelect: 'none',
        }}
        title={Object.entries(record.fields).map(([k, v]) => `${k}: ${v}`).join(', ')}
      >
        <span style={{ fontSize: 10, color: '#888', width: 12, textAlign: 'center', flexShrink: 0 }}>
          {chevron}
        </span>
        <span style={{ fontSize: 13, flexShrink: 0 }}>{'\uD83D\uDCCB'}</span>
        <span style={{ flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', fontSize: 12 }}>
          {record.displayLabel}
        </span>
        {hovered && !isAdded && (
          <button
            onClick={e => { e.stopPropagation(); onAdd(); }}
            disabled={isAdding}
            style={{
              background: 'none', border: '1px solid #7b1fa2', borderRadius: 3,
              cursor: isAdding ? 'not-allowed' : 'pointer',
              fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
              opacity: isAdding ? 0.5 : 1, flexShrink: 0,
            }}
            title={t('Alle Tabellen für diese Quelle hinzufügen')}
          >
            {isAdding ? '...' : `+ ${t('Hinzufügen')}`}
          </button>
        )}
        {isAdded && (
          <span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('Bereits hinzugefügt')}>
            {'\u2713'}
          </span>
        )}
      </div>

      {record.expanded && (
        <div style={{ paddingLeft: 64 }}>
          {childTables.map(ct => {
            const ctLabel = ct.label || ct.tableName;
            return (
              <div key={ct.objectKey} style={{
                display: 'flex', alignItems: 'center', gap: 4,
                paddingTop: 2, paddingBottom: 2, fontSize: 11, color: '#888',
              }}>
                <span style={{ fontSize: 12 }}>{'\uD83D\uDCC4'}</span>
                <span>{ctLabel}</span>
                <span style={{ fontSize: 10, color: '#bbb' }}>({ct.parentKey})</span>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};

export default SourcesTab;
