// Copyright (c) 2026 Patrick Motsch
// All rights reserved.
/**
 * UdbSourcesProvider — TreeNodeProvider for the UDB Sources tab.
 *
 * Single responsibility: translate the generic UDB backend tree contract
 * (POST /api/udb/tree/children -> nodesByParent map) into the generic
 * TreeNode shape that FormGeneratorTree consumes, and forward flag
 * mutations to the single generic UDB endpoint
 * (POST /api/udb/node/{key}/flag/{flag}).
 *
 * No effective-value computation, no inheritance logic, no mixed-state math:
 * the backend is the single source of truth. The provider only:
 *   1. caches the most recently loaded backend node payload per id so the
 *      drag/settings handlers have direct access to coordinates,
 *   2. emits stable display ordering via `displayOrder`,
 *   3. hides flag affordances on synthetic container nodes (synthRoot,
 *      mandateGroup) by leaving the corresponding TreeNode field undefined.
 *
 * The UDB API endpoints are intentionally feature-agnostic; they do not
 * carry an `instanceId` in the path. The legacy `instanceId` argument is
 * still accepted by `createUdbSourcesProvider` because:
 *   - the drag payloads for "create personal DataSource" / "create FDS"
 *     hit feature-instance-scoped helper endpoints under /api/workspace/
 *     to keep ingestion/RAG bound to the caller's workspace,
 *   - the rootKey is namespaced per instance so two UDBs on the same
 *     screen never collide.
 */

import React from 'react';
import {
  FaGoogle, FaMicrosoft, FaTasks, FaCloud, FaLink, FaFolder, FaFile, FaEnvelope,
  FaCloudUploadAlt, FaCalendarAlt, FaComments, FaUser, FaTable, FaDatabase,
  FaBuilding,
} from 'react-icons/fa';
import { SiJira } from 'react-icons/si';
import api from '../../api';
import type { TreeNode, TreeNodeProvider, ScopeValue } from '../FormGenerator/FormGeneratorTree';

// ---------------------------------------------------------------------------
// Backend contract types
// ---------------------------------------------------------------------------

export type UdbBackendKind =
  | 'synthRoot'
  | 'connection' | 'service' | 'folder' | 'file'
  | 'mandateGroup' | 'featureNode' | 'fdsTable' | 'fdsRecord' | 'fdsField';

export interface UdbBackendNode {
  key: string;
  kind: UdbBackendKind;
  parentKey: string | null;
  label: string;
  icon?: string;
  hasChildren: boolean;
  dataSourceId: string | null;
  modelType: 'DataSource' | 'FeatureDataSource' | null;
  effectiveNeutralize: boolean | 'mixed';
  effectiveScope: string;
  effectiveRagIndexEnabled: boolean | 'mixed';
  supportsRag: boolean;
  canBeAdded: boolean;
  displayOrder?: number;
  defaultExpanded?: boolean;
  authority?: string;
  connectionId?: string;
  service?: string;
  sourceType?: string;
  path?: string;
  featureInstanceId?: string;
  featureCode?: string;
  mandateId?: string;
  tableName?: string;
  fieldName?: string;
  objectKey?: string;
  displayPath?: string;
  /** fdsTable-only: persisted list of column names to neutralize (PII mask). */
  neutralizeFields?: string[];
}

/** Kinds that represent the *root* of a data source (one DataSource record per
 *  node). Settings are only meaningful here; folders/files/services/tables
 *  inherit settings from the root and don't get their own gear icon. */
const _DATA_SOURCE_ROOT_KINDS = new Set<UdbBackendKind>([
  'connection',
  'featureNode',
]);

// ---------------------------------------------------------------------------
// Icon resolution (kept inline; UDB-domain mapping, not Tree-generic)
// ---------------------------------------------------------------------------

const _AUTHORITY_ICONS: Record<string, React.ReactNode> = {
  msft: <FaMicrosoft style={{ color: '#00a4ef', fontSize: 12 }} />,
  google: <FaGoogle style={{ color: '#4285f4', fontSize: 12 }} />,
  clickup: <FaTasks style={{ color: '#7b68ee', fontSize: 12 }} />,
  infomaniak: <FaCloud style={{ color: '#0098db', fontSize: 12 }} />,
  'local:ftp': <FaLink style={{ color: '#795548', fontSize: 12 }} />,
  'local:jira': <SiJira style={{ color: '#0052CC', fontSize: 12 }} />,
};

const _SERVICE_ICONS: Record<string, React.ReactNode> = {
  sharepoint: <FaFolder style={{ color: '#0078d4', fontSize: 11 }} />,
  onedrive: <FaCloudUploadAlt style={{ color: '#0078d4', fontSize: 11 }} />,
  outlook: <FaEnvelope style={{ color: '#0078d4', fontSize: 11 }} />,
  teams: <FaComments style={{ color: '#6264a7', fontSize: 11 }} />,
  drive: <FaCloudUploadAlt style={{ color: '#4285f4', fontSize: 11 }} />,
  gmail: <FaEnvelope style={{ color: '#ea4335', fontSize: 11 }} />,
  files: <FaLink style={{ color: '#795548', fontSize: 11 }} />,
  kdrive: <FaCloudUploadAlt style={{ color: '#0098db', fontSize: 11 }} />,
  calendar: <FaCalendarAlt style={{ color: '#888', fontSize: 11 }} />,
  contact: <FaUser style={{ color: '#888', fontSize: 11 }} />,
};

const _KIND_FALLBACK_ICONS: Record<string, React.ReactNode> = {
  synthRoot: <FaDatabase style={{ color: '#666', fontSize: 12 }} />,
  connection: <FaLink style={{ color: '#888', fontSize: 12 }} />,
  service: <FaFolder style={{ color: '#888', fontSize: 11 }} />,
  folder: <FaFolder style={{ color: '#888', fontSize: 11 }} />,
  file: <FaFile style={{ color: '#888', fontSize: 11 }} />,
  mandateGroup: <FaBuilding style={{ color: '#7b1fa2', fontSize: 12 }} />,
  featureNode: <FaDatabase style={{ color: '#7b1fa2', fontSize: 11 }} />,
  fdsTable: <FaTable style={{ color: '#7b1fa2', fontSize: 11 }} />,
  fdsRecord: <FaFile style={{ color: '#7b1fa2', fontSize: 11 }} />,
  fdsField: <span style={{ color: '#9e9e9e', fontSize: 10, fontFamily: 'monospace' }}>{'\u22EE'}</span>,
};

function _renderIcon(node: UdbBackendNode): React.ReactNode {
  if (node.kind === 'connection') {
    return _AUTHORITY_ICONS[node.icon || ''] ?? _KIND_FALLBACK_ICONS.connection;
  }
  if (node.kind === 'service') {
    return _SERVICE_ICONS[node.icon || ''] ?? _KIND_FALLBACK_ICONS.service;
  }
  return _KIND_FALLBACK_ICONS[node.kind] ?? null;
}

// ---------------------------------------------------------------------------
// Domain rule: which kinds expose flag toggles
// ---------------------------------------------------------------------------

/** Synthetic / structural containers carry no DB record and have no flags.
 *  The provider hides scope/neutralize/ragIndexEnabled for them so the tree
 *  doesn't render dead buttons. */
function _isSyntheticContainer(kind: UdbBackendKind): boolean {
  return kind === 'synthRoot' || kind === 'mandateGroup';
}

/** FDS-family kinds (no `scope` attribute; visibility is feature RBAC). */
function _isFdsKind(kind: UdbBackendKind): boolean {
  return kind === 'featureNode' || kind === 'fdsTable' || kind === 'fdsRecord' || kind === 'fdsField';
}

// ---------------------------------------------------------------------------
// Mapping: backend payload -> generic TreeNode
// ---------------------------------------------------------------------------

function _mapBackendNode(
  n: UdbBackendNode,
  onSettingsClick: (n: UdbBackendNode) => Promise<void> | void,
): TreeNode<UdbBackendNode> {
  const isSynthetic = _isSyntheticContainer(n.kind);
  const isFolderLike = n.hasChildren;

  const node: TreeNode<UdbBackendNode> = {
    id: n.key,
    name: n.label,
    type: isFolderLike ? 'folder' : 'file',
    parentId: n.parentKey,
    ownership: 'own',
    icon: _renderIcon(n),
    displayOrder: n.displayOrder,
    defaultExpanded: n.defaultExpanded,
    data: n,
  };

  if (!isSynthetic) {
    if (n.kind === 'fdsField') {
      // Fields expose ONLY neutralize (mapped to parent table's
      // neutralizeFields list). Scope and RAG are not field-level concepts.
      node.neutralize = n.effectiveNeutralize;
    } else if (_isFdsKind(n.kind)) {
      // FDS records have neutralize + ragIndexEnabled, but no scope.
      node.neutralize = n.effectiveNeutralize;
      if (n.supportsRag) {
        node.ragIndexEnabled = n.effectiveRagIndexEnabled;
      }
    } else {
      // DataSource family carries the full three-flag set.
      node.scope = n.effectiveScope as ScopeValue | 'mixed';
      node.neutralize = n.effectiveNeutralize;
      if (n.supportsRag) {
        node.ragIndexEnabled = n.effectiveRagIndexEnabled;
      }
    }
  }

  if (_DATA_SOURCE_ROOT_KINDS.has(n.kind)) {
    node.extraActions = [{
      key: 'settings',
      icon: '\u2699\uFE0F',
      tooltip: 'Einstellungen',
      onClick: () => onSettingsClick(n),
    }];
  }

  return node;
}

// ---------------------------------------------------------------------------
// Provider factory
// ---------------------------------------------------------------------------

export interface UdbSourcesProviderHandle extends TreeNodeProvider<UdbBackendNode> {
  /** Resolve the DataSource UUID for a node, creating a record if needed. */
  ensureDataSourceId(node: UdbBackendNode): Promise<string | null>;
  /** Test/diagnostic hook only -- exposes the latest cached backend payloads
   *  so consumers can inspect data flow without round-tripping through the
   *  network. Not part of the contract used at runtime. */
  _diagnosticGetCacheSize(): number;
}

export function createUdbSourcesProvider(
  instanceId: string,
  onOpenSettings: (dataSourceId: string, label: string) => void,
): UdbSourcesProviderHandle {
  // Per-id cache of the most recent backend payload. Used by the
  // settings/drag handlers (NOT by the generic flag PATCH path; that
  // identifies the node purely by its tree key).
  const nodeCache = new Map<string, UdbBackendNode>();

  async function _ensureRecordForSettings(node: UdbBackendNode): Promise<string | null> {
    if (node.dataSourceId) return node.dataSourceId;
    try {
      if (node.kind === 'connection' || node.kind === 'service'
          || node.kind === 'folder' || node.kind === 'file') {
        const sourceType = node.sourceType
          || (node.kind === 'connection' ? node.authority : '')
          || '';
        const res = await api.post(`/api/workspace/${instanceId}/datasources`, {
          connectionId: node.connectionId || '',
          sourceType,
          path: node.path || '/',
          label: node.label,
          displayPath: node.displayPath || node.label,
        });
        const newId: string | null = res.data?.id ?? null;
        if (newId) {
          nodeCache.set(node.key, { ...node, dataSourceId: newId, modelType: 'DataSource' });
        }
        return newId;
      }
      if (node.kind === 'featureNode' || node.kind === 'fdsTable' || node.kind === 'fdsRecord') {
        const tableName = node.tableName || (node.kind === 'featureNode' ? '*' : '');
        const objectKey = node.objectKey
          || (node.kind === 'featureNode' ? `data.feature.${node.featureCode}.*` : '');
        const res = await api.post(`/api/workspace/${instanceId}/feature-datasources`, {
          featureInstanceId: node.featureInstanceId || '',
          featureCode: node.featureCode || '',
          tableName,
          objectKey,
          label: node.label,
        });
        const newId: string | null = res.data?.id ?? null;
        if (newId) {
          nodeCache.set(node.key, { ...node, dataSourceId: newId, modelType: 'FeatureDataSource' });
        }
        return newId;
      }
    } catch (err) {
      console.error('[UdbSourcesProvider] ensureRecordForSettings failed', err);
    }
    return null;
  }

  async function _onSettingsClick(node: UdbBackendNode): Promise<void> {
    const dsId = await _ensureRecordForSettings(node);
    if (!dsId) {
      console.warn('[UdbSourcesProvider] settings click: cannot ensure record', node.key);
      return;
    }
    onOpenSettings(dsId, node.label);
  }

  /** Forward a flag mutation to the generic UDB endpoint. The backend
   *  resolves the node from `nodeKey`, runs the polymorphic `canEdit`
   *  permission check, and applies the cascade-reset. */
  async function _patchFlag(
    ids: string[],
    flag: 'neutralize' | 'scope' | 'ragIndexEnabled',
    value: unknown,
  ): Promise<void> {
    for (const nodeKey of ids) {
      try {
        await api.post(`/api/udb/node/${encodeURIComponent(nodeKey)}/flag/${flag}`, { value });
      } catch (err) {
        console.error('[UdbSourcesProvider] patch failed', { nodeKey, flag, err });
        throw err;
      }
    }
  }

  return {
    rootKey: `udb-sources-${instanceId}`,

    async loadChildren(parentId, _ownership) {
      const res = await api.post(`/api/udb/tree/children`, {
        parents: [parentId],
      });
      const nodesByParent = res.data?.nodesByParent || {};
      const lookupKey = parentId ?? '__root__';
      const list: UdbBackendNode[] = nodesByParent[lookupKey] || [];
      for (const n of list) nodeCache.set(n.key, n);
      return list.map((n) => _mapBackendNode(n, _onSettingsClick));
    },

    canPatchScope(node) {
      const data = node.data;
      // Scope only exists on DataSource family; FDS / synthetic containers / fields hide it.
      return !!data && !_isSyntheticContainer(data.kind) && !_isFdsKind(data.kind);
    },

    canPatchNeutralize(node) {
      const data = node.data;
      return !!data && !_isSyntheticContainer(data.kind);
    },

    canPatchRagIndex(node) {
      const data = node.data;
      // RAG exists at the data-source level (DS root / FDS table+rows), never on fields.
      return !!data && data.supportsRag === true && data.kind !== 'fdsField';
    },

    async patchScope(ids, scope, _cascadeChildren) {
      // Backend cascades NULL on descendants automatically based on the
      // existence of explicit child records; the cascadeChildren flag is the
      // FilesTab convention and is irrelevant here.
      await _patchFlag(ids, 'scope', scope);
    },

    async patchNeutralize(ids, neutralize) {
      await _patchFlag(ids, 'neutralize', neutralize);
    },

    async patchRagIndex(ids, ragIndexEnabled) {
      await _patchFlag(ids, 'ragIndexEnabled', ragIndexEnabled);
    },

    customizeDragData(node, dataTransfer) {
      const data = node.data as UdbBackendNode | undefined;
      if (!data || _isSyntheticContainer(data.kind)) return;

      if (data.kind === 'connection' || data.kind === 'service'
          || data.kind === 'folder' || data.kind === 'file') {
        const sourceType = data.sourceType
          || (data.kind === 'connection' ? data.authority : '') || '';
        const payload = {
          connectionId: data.connectionId || '',
          sourceType,
          path: data.path || '/',
          label: data.label,
          displayPath: data.displayPath || data.label,
        };
        dataTransfer.setData('application/datasource', JSON.stringify(payload));
      } else if (data.kind === 'featureNode' || data.kind === 'fdsTable' || data.kind === 'fdsRecord') {
        const tableName = data.tableName || (data.kind === 'featureNode' ? '*' : '');
        const objectKey = data.objectKey
          || (data.kind === 'featureNode' ? `data.feature.${data.featureCode}.*` : '');
        const payload = {
          featureInstanceId: data.featureInstanceId || '',
          featureCode: data.featureCode || '',
          tableName,
          objectKey,
          label: data.label,
        };
        dataTransfer.setData('application/feature-source', JSON.stringify(payload));
      }
    },

    async ensureDataSourceId(node: UdbBackendNode): Promise<string | null> {
      return _ensureRecordForSettings(node);
    },

    _diagnosticGetCacheSize() {
      return nodeCache.size;
    },
  };
}
