/**
 * NOTE: @since versions (engine-v1.x.x) refer to internal template engine
 * development iterations, not G7 platform release versions.
 */
/**
 * 그누보드7 템플릿 엔진 - 데이터 소스 매니저
 *
 * data_sources를 처리하고 API 요청을 관리하는 매니저
 *
 * @package G7
 * @subpackage Template Engine
 * @since 1.0.0
 */

import { getApiClient } from '../api/ApiClient';
import { AuthManager } from '../auth/AuthManager';
import { resolveExpressionString, evaluateRenderCondition } from './helpers/RenderHelpers';
import { DataBindingEngine } from './DataBindingEngine';
import { getErrorHandlingResolver } from '../error';
import { getActionDispatcher } from './ActionDispatcher';
import type { ErrorHandlingMap, OnErrorHandler, OnSuccessHandler, ErrorContext, SuccessContext, ErrorCondition } from '../types/ErrorHandling';
import { createLogger } from '../utils/Logger';
import { webSocketManager } from '../websocket/WebSocketManager';
import type { G7DevToolsInterface } from './G7CoreGlobals';
import type { GlobalHeaderRule } from './LayoutLoader';

const logger = createLogger('DataSourceManager');

// DataBindingEngine 인스턴스 (표현식 평가용)
const bindingEngine = new DataBindingEngine();

/**
 * 파라미터 값을 해석합니다. 배열 등 원본 타입을 유지합니다.
 *
 * resolveExpressionString은 항상 문자열을 반환하므로,
 * 전체가 {{...}} 표현식인 경우 evaluateExpression을 사용하여
 * 배열, 객체 등 원본 타입을 유지합니다.
 *
 * @param value 파라미터 값 (문자열)
 * @param context 바인딩 컨텍스트
 * @returns 해석된 값 (원본 타입 유지)
 */
function resolveParamValue(value: string, context: Record<string, any>): any {
  // 전체가 하나의 {{...}} 표현식인지 확인
  // 예: "{{query['sales_status[]']}}" → true
  // 예: "page={{query.page}}" → false
  const singleExpressionMatch = value.match(/^\{\{(.+)\}\}$/);

  if (singleExpressionMatch) {
    // 단일 표현식인 경우 evaluateExpression으로 원본 타입 유지
    const expression = singleExpressionMatch[1].trim();
    try {
      return bindingEngine.evaluateExpression(expression, context);
    } catch (error) {
      logger.error('Failed to evaluate param expression:', expression, error);
      return value; // 실패 시 원본 반환
    }
  }

  // 복합 문자열인 경우 resolveExpressionString 사용 (문자열 보간)
  return resolveExpressionString(value, context, { skipCache: true });
}

/**
 * URLSearchParams를 배열 파라미터를 지원하는 객체로 변환
 *
 * URLSearchParams.entries()와 Object.fromEntries()를 사용하면
 * 중복 키(배열 파라미터)의 마지막 값만 남게 됩니다.
 * 이 함수는 getAll()을 사용하여 모든 값을 배열로 수집합니다.
 *
 * @param params URLSearchParams 인스턴스
 * @returns 배열 파라미터를 지원하는 객체
 *
 * @example
 * // sales_status[]=a&sales_status[]=b → { 'sales_status[]': ['a', 'b'] }
 * // page=1&sort=name → { page: '1', sort: 'name' }
 */
function parseQueryParams(params: URLSearchParams): Record<string, string | string[]> {
  const result: Record<string, string | string[]> = {};

  for (const key of params.keys()) {
    // 이미 처리한 키는 건너뛰기
    if (key in result) continue;

    const values = params.getAll(key);
    if (values.length > 1) {
      // 여러 값이 있으면 배열로 저장
      result[key] = values;
    } else if (values.length === 1) {
      // 단일 값이지만 key[]  형태면 배열로 저장 (일관성)
      if (key.endsWith('[]')) {
        result[key] = values;
      } else {
        result[key] = values[0];
      }
    }
  }

  return result;
}

/**
 * G7Core.devTools 인터페이스 가져오기
 *
 * G7DevToolsCore.getInstance() 직접 호출 대신 G7Core.devTools를 사용합니다.
 * DevTools가 비활성화되거나 초기화되지 않은 경우 안전하게 undefined 반환
 */
function getDevTools(): G7DevToolsInterface | undefined {
  try {
    const G7Core = (window as any).G7Core;
    return G7Core?.devTools;
  } catch {
    return undefined;
  }
}

/**
 * 데이터 소스 타입
 */
export type DataSourceType = 'api' | 'static' | 'route_params' | 'query_params' | 'websocket';

/**
 * WebSocket 채널 타입
 */
export type WebSocketChannelType = 'public' | 'private' | 'presence';

/**
 * HTTP 메서드 타입
 */
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

/**
 * 로딩 전략 타입
 *
 * - blocking: API 응답 완료 전까지 렌더링하지 않음
 * - progressive: 스켈레톤 UI 렌더링 후 데이터 로드되면 업데이트 (기본값)
 * - background: 일단 렌더링하고 백그라운드에서 fetch
 */
export type LoadingStrategy = 'blocking' | 'progressive' | 'background';

/**
 * 데이터 소스 로딩 상태
 */
export type DataSourceLoadingState = 'idle' | 'loading' | 'success' | 'error';

/**
 * 데이터 소스 인터페이스
 */
export interface DataSource {
  /**
   * 데이터 소스 고유 ID
   */
  id: string;

  /**
   * 데이터 소스 타입
   */
  type: DataSourceType;

  /**
   * API 엔드포인트 (type이 'api'인 경우)
   */
  endpoint?: string;

  /**
   * HTTP 메서드 (type이 'api'인 경우)
   */
  method?: HttpMethod;

  /**
   * 자동 fetch 여부
   */
  auto_fetch?: boolean;

  /**
   * 인증 필요 여부 (deprecated: auth_mode 사용 권장)
   *
   * @deprecated auth_mode를 사용하세요. auth_required: true는 auth_mode: 'required'와 동일합니다.
   */
  auth_required?: boolean;

  /**
   * 인증 모드
   *
   * - 'required': 토큰 필수 (ApiClient 사용, 토큰 없으면 요청 실패)
   * - 'optional': 토큰 있으면 전송, 없으면 일반 요청 (비회원/회원 공용 API)
   * - 'none': 토큰 안 보냄 (공개 API, 기본값)
   *
   * auth_required: true는 auth_mode: 'required'와 동일하게 동작합니다.
   */
  auth_mode?: 'required' | 'optional' | 'none';

  /**
   * 로딩 전략 (기본값: progressive)
   */
  loading_strategy?: LoadingStrategy;

  /**
   * 요청 Content-Type (기본값: 'application/json')
   *
   * 'multipart/form-data'로 설정하면 파일 업로드가 가능합니다.
   * File/Blob 객체를 params에 포함하여 FormData로 전송합니다.
   *
   * @example
   * { "id": "upload", "endpoint": "/api/upload", "method": "POST",
   *   "contentType": "multipart/form-data", "params": { "file": "{{_global.uploadFile}}" } }
   */
  contentType?: string;

  /**
   * 요청 파라미터
   */
  params?: Record<string, any>;

  /**
   * 정적 데이터 (type이 'static'인 경우)
   */
  data?: any;

  /**
   * 로컬 상태 초기화 설정
   *
   * API 응답 데이터를 _local[key]에 자동 복사합니다.
   * 폼 편집 시 초기 데이터를 로컬 상태로 관리할 때 사용합니다.
   *
   * 문자열 형태: 전체 응답 데이터를 저장
   * 객체 형태: path를 지정하여 응답의 특정 필드만 저장
   *
   * @example
   * // 문자열 형태 - 전체 응답 저장
   * { "id": "user", "endpoint": "/api/users/1", "initLocal": "form" }
   * // → response.data가 _local.form에 복사됨
   *
   * @example
   * // 객체 형태 - 특정 필드만 저장
   * { "id": "user", "endpoint": "/api/users/1", "initLocal": { "key": "form", "path": "data" } }
   * // → response.data.data가 _local.form에 복사됨
   *
   * @example
   * // 맵 형태 (engine-v1.7.0+) - 여러 필드를 각각 저장 (표현식 지원)
   * { "id": "cart", "endpoint": "/api/cart", "initLocal": { "selectedItems": "data.item_ids" } }
   * // → response.data.data.item_ids가 _local.selectedItems에 복사됨
   *
   * @example
   * // 맵 형태 + 표현식 (engine-v1.7.0+)
   * { "id": "cart", "initLocal": { "selectedItems": "{{data.items.map(i => i.id)}}" } }
   * // → response.data.data.items를 map하여 _local.selectedItems에 저장
   *
   * @example
   * // dot notation 타겟 경로 (engine-v1.8.0+)
   * { "id": "checkout", "initLocal": { "checkout.item_coupons": "data.promotions.item_coupons" } }
   * // → response.data.data.promotions.item_coupons가 _local.checkout.item_coupons에 복사됨
   *
   * @example
   * // 중첩 객체 표기법 (engine-v1.8.0+)
   * { "id": "checkout", "initLocal": {
   *     "checkout": {
   *       "item_coupons": "data.promotions.item_coupons",
   *       "use_points": "data.use_points"
   *     }
   *   }
   * }
   *
   * @example
   * // 병합 전략 옵션 (engine-v1.8.0+) - _merge: "deep" | "shallow" | "replace"
   * { "id": "checkout", "initLocal": {
   *     "_merge": "replace",
   *     "checkout.item_coupons": "data.promotions.item_coupons"
   *   }
   * }
   */
  initLocal?: string | { key: string; path: string } | Record<string, string | Record<string, string>>;

  /**
   * 전역 상태 초기화 설정
   *
   * API 응답 데이터를 _global[key]에 자동 복사합니다.
   * 여러 페이지에서 공유하는 데이터(현재 사용자, 앱 설정 등)에 사용합니다.
   *
   * 문자열 형태: 전체 응답 데이터를 저장
   * 객체 형태: path를 지정하여 응답의 특정 필드만 저장
   *
   * @example
   * // 문자열 형태 - 전체 응답 저장
   * { "id": "currentUser", "endpoint": "/api/me", "initGlobal": "currentUser" }
   * // → response.data가 _global.currentUser에 복사됨
   *
   * @example
   * // 객체 형태 - 특정 필드만 저장
   * { "id": "modules", "endpoint": "/api/modules", "initGlobal": { "key": "installedModules", "path": "data" } }
   * // → response.data.data가 _global.installedModules에 복사됨
   *
   * @example
   * // 배열 형태 - 여러 전역 상태 동시 초기화
   * { "id": "layout_files", "endpoint": "/api/layouts", "initGlobal": [
   *   "layoutFilesList",
   *   { "key": "selectedLayoutName", "path": "[0].name" }
   * ] }
   * // → response.data가 _global.layoutFilesList에 복사됨
   * // → response.data[0].name이 _global.selectedLayoutName에 복사됨
   *
   * @example
   * // 맵 형태 (engine-v1.7.0+) - 여러 필드를 각각 저장 (표현식 지원)
   * { "id": "config", "endpoint": "/api/config", "initGlobal": { "theme": "data.theme", "locale": "data.locale" } }
   * // → response.data.data.theme이 _global.theme에, response.data.data.locale이 _global.locale에 복사됨
   */
  initGlobal?: string | { key: string; path: string } | Array<string | { key: string; path: string }> | Record<string, string>;

  /**
   * 격리된 상태 초기화 설정
   *
   * API 응답 데이터를 _isolated[key]에 자동 복사합니다.
   * isolatedState 속성이 정의된 컴포넌트 내에서만 사용됩니다.
   * 해당 영역 내에서만 유효한 독립적인 상태가 필요할 때 사용합니다.
   *
   * 문자열 형태: 전체 응답 데이터를 저장
   * 객체 형태: { key: 저장할 키, path: 응답에서 추출할 경로 }
   *
   * @example
   * // 문자열 형태 - 전체 응답 저장
   * { "id": "categories", "endpoint": "/api/categories", "initIsolated": "categoryList" }
   * // → response.data가 _isolated.categoryList에 복사됨
   *
   * @example
   * // 객체 형태 - 특정 필드만 저장
   * { "id": "options", "endpoint": "/api/options", "initIsolated": { "key": "items", "path": "data.list" } }
   * // → response.data.data.list가 _isolated.items에 복사됨
   *
   * @example
   * // 맵 형태 (engine-v1.7.0+) - 여러 필드를 각각 저장 (표현식 지원)
   * { "id": "widget", "endpoint": "/api/widget", "initIsolated": { "items": "data.list", "total": "data.count" } }
   * // → response.data.data.list가 _isolated.items에, data.count가 _isolated.total에 복사됨
   */
  initIsolated?: string | { key: string; path: string } | Record<string, string>;

  /**
   * 마운트 시 다시 fetch 여부
   *
   * true로 설정하면 페이지에 진입할 때마다 데이터를 다시 fetch하고
   * initLocal/initGlobal 옵션이 있는 경우 로컬/전역 상태를 강제로 재초기화합니다.
   *
   * SPA에서 같은 페이지로 다시 이동할 때 (예: 목록 → 편집 → 목록 → 편집)
   * 이전에 수정한 폼 데이터가 남아있는 문제를 해결합니다.
   *
   * @example
   * // data_sources에서 사용
   * { "id": "board", "endpoint": "/api/boards/{{route.id}}", "initLocal": "formData", "refetchOnMount": true }
   * // → 페이지 진입 시 항상 board 데이터를 다시 fetch하고 _local.formData를 초기화
   *
   * @default false
   */
  refetchOnMount?: boolean;

  /**
   * 커스텀 HTTP 헤더
   *
   * API 요청 시 추가할 커스텀 헤더를 정의합니다.
   * 값에 표현식({{...}})을 사용하여 동적으로 헤더 값을 설정할 수 있습니다.
   *
   * 지원되는 컨텍스트:
   * - route: 라우트 파라미터 (예: {{route.id}})
   * - query: 쿼리 파라미터 (예: {{query.mode}})
   * - _global: 전역 상태 (예: {{_global.cartKey}})
   *
   * 주의:
   * - _local은 렌더링 전에 data_source가 처리되므로 사용할 수 없습니다.
   * - 값이 null, undefined, 빈 문자열인 헤더는 전송되지 않습니다.
   *
   * @example
   * // 장바구니 키 헤더 전송
   * {
   *   "id": "cart",
   *   "endpoint": "/api/cart",
   *   "headers": {
   *     "X-Cart-Key": "{{_global.cartKey}}"
   *   }
   * }
   *
   * @example
   * // 여러 커스텀 헤더
   * {
   *   "id": "data",
   *   "endpoint": "/api/data",
   *   "headers": {
   *     "X-Custom-Header": "custom-value",
   *     "X-User-Id": "{{_global.currentUser?.uuid}}"
   *   }
   * }
   */
  headers?: Record<string, string>;

  /**
   * 조건부 로딩 표현식
   *
   * 표현식이 truthy로 평가될 때만 이 데이터 소스를 fetch합니다.
   * 같은 id를 가진 여러 데이터 소스를 정의하고, 조건에 따라 하나만 실행되도록 할 수 있습니다.
   *
   * 지원되는 컨텍스트:
   * - route: 라우트 파라미터 (예: {{route.id}})
   * - query: 쿼리 파라미터 (예: {{query.mode}})
   * - _global: 전역 상태 (예: {{_global.isAdmin}})
   *
   * 주의: _local은 렌더링 전에 data_source가 처리되므로 사용할 수 없습니다.
   *
   * @example
   * // 수정 모드: route.id가 있을 때만 기존 데이터 로드
   * { "id": "user", "endpoint": "/api/users/{{route.id}}", "if": "{{route.id}}" }
   *
   * // 생성 모드: route.id가 없을 때만 템플릿 로드
   * { "id": "user", "endpoint": "/api/users/template", "if": "{{!route.id}}" }
   */
  if?: string;

  /**
   * 조건부 데이터 소스 로드 조건 (복합 조건)
   *
   * if 속성의 상위 호환으로 AND/OR 그룹 및 if-else 체인을 지원합니다.
   *
   * 주의:
   * - if와 conditions가 둘 다 있으면 if가 우선 평가됩니다
   * - _local은 렌더링 전에 data_source가 처리되므로 사용할 수 없습니다
   *
   * @example
   * // AND 그룹: route.id가 있고 권한이 있을 때만 로드
   * {
   *   "id": "product",
   *   "endpoint": "/api/products/{{route.id}}",
   *   "conditions": {
   *     "and": ["{{!!route.id}}", "{{_global.hasPermission('view_product')}}"]
   *   }
   * }
   *
   * @example
   * // OR 그룹: admin이거나 manager일 때 로드
   * {
   *   "id": "dashboard",
   *   "endpoint": "/api/admin/dashboard",
   *   "conditions": {
   *     "or": ["{{_global.user?.role === 'admin'}}", "{{_global.user?.role === 'manager'}}"]
   *   }
   * }
   *
   * @since engine-v1.10.0
   */
  conditions?: import('./helpers/ConditionEvaluator').ConditionsProperty;

  /**
   * WebSocket 채널명 (type이 'websocket'인 경우)
   *
   * @example "admin.dashboard"
   */
  channel?: string;

  /**
   * WebSocket 이벤트명 (type이 'websocket'인 경우)
   *
   * @example "dashboard.stats.updated"
   */
  event?: string;

  /**
   * WebSocket 채널 타입 (기본값: 'private')
   */
  channel_type?: WebSocketChannelType;

  /**
   * WebSocket 수신 시 업데이트할 대상 데이터 소스 ID
   *
   * 이 필드가 설정되면 WebSocket 이벤트 수신 시
   * 해당 ID의 데이터 소스 캐시를 업데이트합니다.
   *
   * @example "dashboard_stats"
   */
  target_source?: string;

  /**
   * WebSocket 이벤트 수신 시 실행할 액션 목록 (engine-v1.33.0+)
   *
   * WebSocket 이벤트 수신 시 각 액션이 순차적으로 실행됩니다.
   * 액션의 `$args[0]`로 수신 페이로드에 접근할 수 있습니다.
   *
   * 사용 예시: 수신 시 관련 데이터소스 refetch + 토스트 알림 표시
   *
   * @example
   * [
   *   { "handler": "refetchDataSource", "params": { "dataSourceId": "notifications" } },
   *   { "handler": "toast", "params": { "type": "info", "message": "{{$args[0].subject}}" } }
   * ]
   */
  onReceive?: any[];

  /**
   * API 호출 실패 시 사용할 기본 데이터
   *
   * API 요청이 실패하거나 endpoint가 존재하지 않을 때
   * 이 값이 데이터로 사용됩니다.
   * errorHandling이 함께 정의된 경우 에러 핸들러가 먼저 실행된 후
   * fallback 데이터가 렌더링에 사용됩니다.
   *
   * @example
   * ```json
   * // 객체 fallback
   * { "fallback": { "users": 0, "posts": 0 } }
   *
   * // 배열 fallback
   * { "fallback": [] }
   *
   * // 단순 값 fallback
   * { "fallback": 0 }
   * ```
   */
  fallback?: any;

  /**
   * HTTP 에러 코드별 핸들러 매핑
   *
   * 이 데이터 소스의 API 호출 실패 시 에러 코드에 따라 다른 핸들러를 실행합니다.
   * onError보다 우선순위가 높습니다.
   *
   * @example
   * ```json
   * {
   *   "errorHandling": {
   *     "403": { "handler": "showErrorPage", "params": { "target": "content" } },
   *     "404": { "handler": "toast", "params": { "type": "warning", "message": "$t:errors.not_found" } },
   *     "default": { "handler": "toast", "params": { "type": "error", "message": "{{error.message}}" } }
   *   }
   * }
   * ```
   */
  errorHandling?: ErrorHandlingMap;

  /**
   * 응답 조건부 에러 처리
   *
   * API가 200으로 응답하더라도 응답 데이터 기반으로 에러를 트리거합니다.
   * 조건이 truthy로 평가되면 errorHandling에 정의된 핸들러가 실행됩니다.
   *
   * 표현식에서 `response` 객체로 API 응답 데이터에 접근합니다.
   *
   * @since engine-v1.18.0
   *
   * @example
   * ```json
   * {
   *   "errorCondition": {
   *     "if": "{{response?.data?.abilities?.can_update !== true}}",
   *     "errorCode": 403
   *   },
   *   "errorHandling": {
   *     "403": { "handler": "showErrorPage", "params": { "target": "content" } }
   *   }
   * }
   * ```
   */
  errorCondition?: ErrorCondition;

  /**
   * 범용 에러 핸들러 (폴백)
   *
   * errorHandling에 해당 에러 코드가 정의되지 않은 경우 실행됩니다.
   * 단일 핸들러 또는 핸들러 배열을 지정할 수 있습니다.
   *
   * @example
   * ```json
   * // 단일 핸들러
   * { "onError": { "handler": "toast", "params": { "type": "error", "message": "{{error.message}}" } } }
   *
   * // 핸들러 배열 (순차 실행)
   * { "onError": [
   *   { "handler": "setState", "params": { "target": "global", "errorMessage": "{{error.message}}" } },
   *   { "handler": "openModal", "target": "error_modal" }
   * ] }
   * ```
   */
  onError?: OnErrorHandler;

  /**
   * 데이터 로드 성공 시 실행할 핸들러
   *
   * API 요청이 성공하고 데이터가 캐시에 저장된 후 실행됩니다.
   * params에서 {{response.xxx}} 형태로 API 응답에 접근할 수 있습니다.
   *
   * 주요 사용 사례:
   * - 데이터 로드 후 조건부 모달 표시
   * - 데이터 기반 추가 상태 설정
   * - 로드 완료 후 UI 업데이트
   *
   * @since engine-v1.17.0
   *
   * @example
   * ```json
   * // 단일 핸들러 - 조건부 모달 표시
   * {
   *   "onSuccess": {
   *     "handler": "conditions",
   *     "conditions": [
   *       {
   *         "if": "{{response.data.unavailable_items?.length > 0}}",
   *         "handler": "openModal",
   *         "target": "unavailable_modal"
   *       }
   *     ]
   *   }
   * }
   *
   * // 핸들러 배열 (순차 실행)
   * { "onSuccess": [
   *   { "handler": "setState", "params": { "target": "local", "loaded": true } },
   *   { "handler": "toast", "params": { "type": "success", "message": "Data loaded" } }
   * ] }
   *
   * // sequence로 여러 작업 수행
   * {
   *   "onSuccess": {
   *     "handler": "sequence",
   *     "actions": [
   *       { "handler": "setState", "params": { "target": "local", "resultData": "{{response.data}}" } },
   *       { "handler": "apiCall", "params": { "endpoint": "/api/log", "method": "POST" } }
   *     ]
   *   }
   * }
   * ```
   */
  onSuccess?: OnSuccessHandler;

  /**
   * 출처 메타 (편집 모드 전용 — engine-v1.50.0+)
   *
   * 레이아웃 편집기가 `with_source_meta=1` 로 요청할 때만 백엔드가 부여하는
   * 소유 확장 식별 메타입니다. 동일 `id` 를 가진 data_source 가 여러 출처
   * (템플릿/모듈/플러그인)에 정의되어 충돌할 때, 샘플 데이터 해소
   * (`sampleDataProvider`)가 자기 출처의 샘플을 우선 선택하도록 합니다.
   *
   * 일반 렌더 경로에는 부여되지 않으며 저장 시 sanitize 에서 제외되므로
   * 영속 레이아웃 JSON 에는 포함되지 않습니다.
   *
   * @since engine-v1.50.0
   */
  __source?: {
    kind: 'template' | 'module' | 'plugin' | 'core' | 'base' | 'route';
    identifier?: string | null;
  };
}

/**
 * 데이터 소스 결과 인터페이스
 */
export interface DataSourceResult {
  /**
   * 데이터 소스 ID
   */
  id: string;

  /**
   * 로딩 상태
   */
  state: DataSourceLoadingState;

  /**
   * 데이터 (성공 시)
   */
  data?: any;

  /**
   * 에러 (실패 시)
   */
  error?: Error;

  /**
   * HTTP 에러 코드 (실패 시)
   */
  errorCode?: number;

  /**
   * 에러가 핸들러에 의해 처리되었는지 여부
   */
  errorHandled?: boolean;
}

/**
 * 데이터 소스 매니저 옵션
 */
export interface DataSourceManagerOptions {
  /**
   * 401 응답 시 호출될 콜백
   */
  onUnauthorized?: () => void;

  /**
   * 에러 발생 시 호출될 콜백
   */
  onError?: (error: Error, source: DataSource) => void;

  /**
   * 샘플 데이터 프로바이더 (편집 모드 전용)
   *
   * 옵션이 주입되면 `fetchApiDataSource` 가 네트워크 fetch 대신 프로바이더의
   * `resolve()` 결과를 반환한다. 일반 렌더 모드(편집기 외) 부팅에서는 옵션을
   * 전달하지 않으므로 기존 동작 100% 보존. 계획서 참조.
   *
   * @since engine-v1.50.0
   */
  sampleProvider?: SampleDataProvider;

  /**
   * onSuccess 핸들러 실행에 사용할 ActionDispatcher (편집 모드 전용)
   *
   * 옵션이 주입되면 `executeOnSuccessHandler` 가 전역 `getActionDispatcher()` 대신
   * 본 dispatcher 를 사용한다. 일반 렌더 모드는 옵션 미전달 → 기존 동작 보존.
   *
   * 동기: PreviewCanvas 가 자체 DataSourceManager 인스턴스를 만들지만, 그 인스턴스
   * 의 onSuccess 핸들러가 호스트 ActionDispatcher 를 호출하면 호스트 `setGlobalState`
   * 가 발화되어 호스트 `#app` 이 통째로 unmount 되는 결함이 발생한다.
   *
   * @since engine-v1.50.0
   */
  actionDispatcher?: import('./ActionDispatcher').ActionDispatcher;
}

/**
 * 샘플 데이터 프로바이더 인터페이스 — 편집 모드 캔버스 전용
 *
 * `useSampleData` 가 병합 `editorSpec.sampleData`(byDataSourceId / bySource /
 * byEndpointPattern) 로 구성한 뒤 편집 모드 부팅 시 `DataSourceManager` 옵션으로
 * 주입한다. 미매칭 데이터소스는 도메인-중립 빈 응답으로 디그레이드한다
 * (코어 키워드 프리셋 폐기 —-v1.50.0).
 *
 * @since engine-v1.50.0
 */
export interface SampleDataProvider {
  /**
   * 데이터소스 ID 가 샘플 모드의 매칭 후보인지 확인.
   * `false` 면 일반 fetch 경로(네트워크) 로 폴백.
   */
  has(dataSourceId: string): boolean;

  /**
   * 데이터소스에 대해 샘플 데이터를 동기/비동기로 반환.
   * 반환값은 일반 API 응답과 같은 shape 이어야 한다(`{ data: [...] }` 등).
   */
  resolve(dataSource: DataSource): unknown | Promise<unknown>;
}

/**
 * 조건 평가용 컨텍스트 인터페이스
 */
export interface ConditionContext {
  /**
   * 라우트 파라미터
   */
  route?: Record<string, string>;

  /**
   * 쿼리 파라미터
   */
  query?: Record<string, string>;

  /**
   * 전역 상태
   */
  _global?: Record<string, any>;
}

/**
 * 데이터 캐시 엔트리 인터페이스
 */
interface DataCacheEntry {
  /**
   * 캐시된 데이터
   */
  data: any;

  /**
   * 캐시 생성 시간
   */
  timestamp: number;
}

/**
 * 데이터 소스 매니저 클래스
 *
 * data_sources를 처리하고 API 요청을 관리
 */
export class DataSourceManager {
  /**
   * 데이터 캐시 만료 시간 (밀리초)
   *
   * 5분으로 설정하여 메모리 누수를 방지합니다.
   */
  private static readonly DATA_CACHE_TTL = 300000; // 5분

  private options: DataSourceManagerOptions;
  private dataCache: Map<string, DataCacheEntry> = new Map();
  private bindingEngine: DataBindingEngine;

  /**
   * 전역 헤더 규칙 (레이아웃에서 설정)
   *
   * API 호출 시 패턴에 매칭되는 엔드포인트에 자동으로 헤더를 추가합니다.
   *
   * @since engine-v1.16.0
   */
  private globalHeaders: GlobalHeaderRule[] = [];

  constructor(options: DataSourceManagerOptions = {}) {
    this.options = options;
    this.bindingEngine = new DataBindingEngine();
  }

  /**
   * 전역 헤더 규칙을 설정합니다.
   *
   * 레이아웃 로드 시 호출되어 globalHeaders를 설정합니다.
   * 이후 모든 API 호출에서 패턴 매칭을 통해 자동으로 헤더가 적용됩니다.
   *
   * @param headers 전역 헤더 규칙 배열
   * @since engine-v1.16.0
   */
  public setGlobalHeaders(headers: GlobalHeaderRule[]): void {
    this.globalHeaders = headers || [];
  }

  /**
   * 엔드포인트가 패턴에 매칭되는지 확인합니다.
   *
   * @param endpoint API 엔드포인트 경로
   * @param pattern glob 스타일 패턴 ("*", "/api/shop/*" 등)
   * @returns 매칭 여부
   * @since engine-v1.16.0
   */
  private matchesPattern(endpoint: string, pattern: string): boolean {
    if (pattern === '*') return true;

    // glob 패턴을 정규식으로 변환 (* → .*)
    const regexPattern = pattern
      .replace(/[.+?^${}()|[\]\\]/g, '\\$&')  // 특수문자 이스케이프
      .replace(/\*/g, '.*');                   // * → .*

    return new RegExp(`^${regexPattern}$`).test(endpoint);
  }

  /**
   * 엔드포인트에 매칭되는 전역 헤더를 추출합니다.
   *
   * globalHeaders 배열을 순회하며 패턴이 매칭되는 모든 헤더를 병합합니다.
   * 나중에 정의된 규칙의 헤더가 먼저 정의된 규칙의 헤더를 덮어씁니다.
   *
   * @param endpoint API 엔드포인트 경로
   * @param context 표현식 평가를 위한 컨텍스트
   * @returns 병합된 헤더 객체
   * @since engine-v1.16.0
   */
  /**
   * params 객체를 FormData로 변환합니다.
   * File/Blob은 그대로 append, 기타 값은 문자열 변환하여 append합니다.
   */
  private toFormData(params: Record<string, any>): FormData {
    const formData = new FormData();
    for (const [key, value] of Object.entries(params)) {
      if (value instanceof File || value instanceof Blob) {
        formData.append(key, value);
      } else if (value !== null && value !== undefined) {
        formData.append(
          key,
          typeof value === 'object' ? JSON.stringify(value) : String(value)
        );
      }
    }
    return formData;
  }

  private getMatchingGlobalHeaders(
    endpoint: string,
    context: Record<string, any>,
  ): Record<string, string> {
    const result: Record<string, string> = {};

    for (const rule of this.globalHeaders) {
      if (this.matchesPattern(endpoint, rule.pattern)) {
        Object.entries(rule.headers).forEach(([key, value]) => {
          // 표현식 평가 ({{_global.xxx}} 등)
          const resolved = resolveParamValue(value, context);
          // null, undefined, 빈 문자열이 아닌 경우에만 헤더 추가
          if (resolved != null && resolved !== '') {
            result[key] = String(resolved);
          }
        });
      }
    }

    return result;
  }

  /**
   * 조건부 데이터 소스 필터링
   *
   * 같은 id를 가진 데이터 소스 중 조건을 만족하는 첫 번째 것만 선택합니다.
   * - if 속성이 없는 데이터 소스는 항상 선택됩니다.
   * - if 속성이 있는 데이터 소스는 조건이 truthy일 때만 선택됩니다.
   * - 같은 id가 여러 개인 경우, 조건을 만족하는 첫 번째 것만 선택됩니다.
   *
   * @param sources 데이터 소스 배열
   * @param conditionContext 조건 평가용 컨텍스트 (route, query, _global)
   * @returns 필터링된 데이터 소스 배열
   */
  filterByCondition(
    sources: DataSource[],
    conditionContext: ConditionContext = {}
  ): DataSource[] {
    // 이미 선택된 id를 추적
    const selectedIds = new Set<string>();
    const filteredSources: DataSource[] = [];

    for (const source of sources) {
      // 이미 같은 id가 선택되었으면 스킵
      if (selectedIds.has(source.id)) {
        continue;
      }

      // if와 conditions가 모두 없으면 항상 선택
      if (!source.if && !source.conditions) {
        selectedIds.add(source.id);
        filteredSources.push(source);
        continue;
      }

      // 조건 평가 (evaluateRenderCondition은 if와 conditions 모두 지원)
      const shouldInclude = evaluateRenderCondition(
        { if: source.if, conditions: source.conditions },
        conditionContext,
        this.bindingEngine,
        `data_source:${source.id}`
      );

      if (shouldInclude) {
        selectedIds.add(source.id);
        filteredSources.push(source);
      }
    }

    return filteredSources;
  }

  /**
   * errorCondition을 평가하여 응답 데이터 기반 에러 조건을 체크합니다.
   *
   * @param source 데이터 소스
   * @param responseData API 응답 데이터
   * @returns errorCondition이 truthy면 해당 errorCode, 아니면 null
   */
  private checkErrorCondition(source: DataSource, responseData: any): number | null {
    if (!source.errorCondition?.if || source.errorCondition?.errorCode === undefined) {
      return null;
    }

    try {
      const context = { response: responseData };
      const result = evaluateRenderCondition(
        { if: source.errorCondition.if },
        context,
        this.bindingEngine,
        `errorCondition:${source.id}`
      );

      if (result) {
        logger.log(`errorCondition matched for ${source.id}, triggering error ${source.errorCondition.errorCode}`);
        return source.errorCondition.errorCode;
      }
    } catch (error) {
      logger.error(`Failed to evaluate errorCondition for ${source.id}:`, error);
    }

    return null;
  }

  /**
   * 데이터 소스 fetch (blocking + progressive 전용)
   *
   * @param sources 데이터 소스 배열
   * @param routeParams 라우트 파라미터
   * @param queryParams 쿼리 파라미터
   * @param globalState _global 상태 (globalHeaders 표현식 평가용)
   * @param localState _local 상태 (globalHeaders 표현식 평가용)
   * @returns 데이터 맵 (key: 데이터 소스 ID, value: 데이터)
   */
  async fetchDataSources(
    sources: DataSource[],
    routeParams: Record<string, string> = {},
    queryParams: URLSearchParams = new URLSearchParams(),
    globalState?: Record<string, any>,
    localState?: Record<string, any>,
  ): Promise<Record<string, any>> {
    const results: Record<string, any> = {};

    // auto_fetch가 true인 데이터 소스만 처리 (websocket 타입 제외)
    //
    // 샘플 모드(편집기) 예외: auto_fetch:false 인 지연 소스(탭/모달 활성 시 런타임에
    // fetch)는 정적 시뮬레이션인 편집기 캔버스에서 활성화 액션을 실행할 수 없어, 페이지
    // 상태로 게이트를 열어도 데이터가 비어 본체가 미렌더된다(myComments
    // 내댓글 서브탭). 샘플 모드에서는 지연 소스도 샘플 주입 대상에 포함해 게이트 노출 시
    // 본체가 렌더되게 한다(런타임 동작과 정합 — 런타임은 탭 활성 시 동일 데이터를 fetch).
    // 일반 렌더 모드(sampleProvider 미주입)는 기존 auto_fetch 필터를 100% 보존한다.
    const inSampleMode = !!this.options.sampleProvider;
    const autoFetchSources = sources.filter(
      (source) =>
        (inSampleMode || source.auto_fetch !== false) && source.type !== 'websocket'
    );

    // 병렬 처리
    await Promise.all(
      autoFetchSources.map(async (source) => {
        try {
          const data = await this.fetchDataSource(source, routeParams, queryParams, globalState, localState);
          results[source.id] = data;
          this.dataCache.set(source.id, { data, timestamp: Date.now() });

          // errorCondition 체크: 200 응답이어도 조건 만족 시 에러로 처리
          const errorConditionCode = this.checkErrorCondition(source, data);
          if (errorConditionCode !== null) {
            const errorContext: ErrorContext = {
              status: errorConditionCode,
              message: `Error condition matched for ${source.id}`,
              data: data,
            };

            const resolver = getErrorHandlingResolver();
            const resolveResult = resolver.resolve(errorConditionCode, {
              errorHandling: source.errorHandling,
              onError: source.onError,
            });

            if (resolveResult.handler) {
              try {
                await resolver.execute(resolveResult.handler, errorContext);
              } catch (handlerError) {
                logger.error(`Error executing errorCondition handler for ${source.id}:`, handlerError);
              }
            }

            // fallback이 정의된 경우 렌더링용 데이터로 사용
            if (source.fallback !== undefined) {
              results[source.id] = source.fallback;
              this.dataCache.set(source.id, { data: source.fallback, timestamp: Date.now() });
              logger.log(`Using fallback data for ${source.id} (errorCondition)`);
            }

            return; // onSuccess 스킵
          }

          // onSuccess 핸들러 실행
          if (source.onSuccess) {
            try {
              const successContext: SuccessContext = {
                data,
                sourceId: source.id,
              };
              await this.executeOnSuccessHandler(source.onSuccess, successContext);
            } catch (handlerError) {
              logger.error(`Error executing onSuccess handler for ${source.id}:`, handlerError);
            }
          }
        } catch (error) {
          logger.error(`Failed to fetch data source: ${source.id}`, error);

          // HTTP 에러 코드 추출
          const errorCode = (error as any)?.response?.status ||
                           (error as any)?.status ||
                           500;

          // 에러 컨텍스트 생성
          // API 응답의 message를 우선 사용하고, 없으면 Axios 에러 메시지 사용
          const responseData = (error as any)?.response?.data;
          const errorMessage = responseData?.message || (error as Error).message || 'Unknown error';
          const errorContext: ErrorContext = {
            status: errorCode,
            message: errorMessage,
            errors: responseData?.errors,
            data: responseData,
            statusText: (error as any)?.response?.statusText,
          };

          // ErrorHandlingResolver를 통해 핸들러 결정 및 실행
          const resolver = getErrorHandlingResolver();
          const resolveResult = resolver.resolve(errorCode, {
            errorHandling: source.errorHandling,
            onError: source.onError,
          });

          if (resolveResult.handler) {
            try {
              await resolver.execute(resolveResult.handler, errorContext);
            } catch (handlerError) {
              logger.error(`Error executing error handler for ${source.id}:`, handlerError);
            }
          }

          // fallback이 정의된 경우 렌더링용 데이터로 사용
          if (source.fallback !== undefined) {
            results[source.id] = source.fallback;
            this.dataCache.set(source.id, { data: source.fallback, timestamp: Date.now() });
            logger.log(`Using fallback data for ${source.id}`);
          }

          // 기존 onError 콜백도 호출 (호환성 유지)
          if (this.options.onError) {
            this.options.onError(error as Error, source);
          }
        }
      }),
    );

    return results;
  }

  /**
   * 데이터 소스 fetch (with result tracking)
   *
   * @param sources 데이터 소스 배열
   * @param routeParams 라우트 파라미터
   * @param queryParams 쿼리 파라미터
   * @param globalState _global 상태 (선택적, refetchDataSource 시 endpoint 표현식에서 _global 접근 지원)
   * @param localState _local 상태 (선택적, refetchDataSource 시 params 표현식에서 _local 접근 지원)
   * @returns 데이터 소스 결과 배열
   * @param options fetch 옵션
   *   - ignoreAutoFetch: true면 auto_fetch: false인 소스도 강제 fetch (refetchDataSource 용)
   */
  async fetchDataSourcesWithResults(
    sources: DataSource[],
    routeParams: Record<string, string> = {},
    queryParams: URLSearchParams = new URLSearchParams(),
    globalState?: Record<string, any>,
    localState?: Record<string, any>,
    options?: { ignoreAutoFetch?: boolean },
  ): Promise<DataSourceResult[]> {
    // 계약: WebSocket 소스는 fetch 대상이 아님 — 호출자가 사전 필터링해야 함 (engine-v1.32.5)
    // 내부 filter는 safety net으로 유지하되, 호출자가 필터를 누락하면 dev 모드에서 경고
    // (이전에는 silent filter로 인해 호출자의 인덱스 기반 매핑이 조용히 어긋나는 버그 발생)
    const hasWebSocket = sources.some((source) => source.type === 'websocket');
    if (hasWebSocket) {
      const wsIds = sources
        .filter((source) => source.type === 'websocket')
        .map((s) => s.id)
        .join(', ');
      logger.warn(
        `fetchDataSourcesWithResults received WebSocket sources (caller should filter them out before calling): ${wsIds}. ` +
        `WebSocket sources are event listeners, not data providers — they should not be passed to fetch.`
      );
    }

    // ignoreAutoFetch 옵션이 true면 auto_fetch 필터 무시 (refetchDataSource에서 사용)
    // 그렇지 않으면 auto_fetch가 true인 데이터 소스만 처리 (websocket 타입 제외 — safety net)
    const targetSources = options?.ignoreAutoFetch
      ? sources.filter((source) => source.type !== 'websocket')
      : sources.filter(
          (source) => source.auto_fetch !== false && source.type !== 'websocket'
        );

    // 병렬 처리 (각 결과를 추적)
    const results = await Promise.all(
      targetSources.map(async (source): Promise<DataSourceResult> => {
        try {
          const data = await this.fetchDataSource(source, routeParams, queryParams, globalState, localState);
          this.dataCache.set(source.id, { data, timestamp: Date.now() });

          // errorCondition 체크: 200 응답이어도 조건 만족 시 에러로 처리
          const errorConditionCode = this.checkErrorCondition(source, data);
          if (errorConditionCode !== null) {
            const errorContext: ErrorContext = {
              status: errorConditionCode,
              message: `Error condition matched for ${source.id}`,
              data: data,
            };

            const resolver = getErrorHandlingResolver();
            const resolveResult = resolver.resolve(errorConditionCode, {
              errorHandling: source.errorHandling,
              onError: source.onError,
            });

            let errorHandled = false;
            if (resolveResult.handler) {
              // blocking 데이터소스의 경우:
              // 핸들러를 await하면 데드락 발생 (showErrorPage가 #main_content를 대기하지만,
              // blocking이 끝나야 #main_content가 마운트됨)
              // → 핸들러를 비동기로 실행하고 즉시 blocking 해제
              if (source.loading_strategy === 'blocking') {
                resolver.execute(resolveResult.handler, errorContext).catch((handlerError) => {
                  logger.error(`Error executing errorCondition handler for ${source.id}:`, handlerError);
                });
                errorHandled = true;
              } else {
                try {
                  await resolver.execute(resolveResult.handler, errorContext);
                  errorHandled = true;
                } catch (handlerError) {
                  logger.error(`Error executing errorCondition handler for ${source.id}:`, handlerError);
                }
              }
            }

            // fallback이 정의된 경우 렌더링용 데이터로 사용
            if (source.fallback !== undefined) {
              this.dataCache.set(source.id, { data: source.fallback, timestamp: Date.now() });
              logger.log(`Using fallback data for ${source.id} (errorCondition)`);
              return {
                id: source.id,
                state: 'success',
                data: source.fallback,
              };
            }

            return {
              id: source.id,
              state: 'error',
              error: new Error(`Error condition matched: ${errorConditionCode}`),
              errorCode: errorConditionCode,
              errorHandled,
            };
          }

          // onSuccess 핸들러 실행
          if (source.onSuccess) {
            try {
              const successContext: SuccessContext = {
                data,
                sourceId: source.id,
              };
              await this.executeOnSuccessHandler(source.onSuccess, successContext);
            } catch (handlerError) {
              logger.error(`Error executing onSuccess handler for ${source.id}:`, handlerError);
            }
          }

          return {
            id: source.id,
            state: 'success',
            data,
          };
        } catch (error) {
          logger.error(`Failed to fetch data source: ${source.id}`, error);

          // HTTP 에러 코드 추출
          const errorCode = (error as any)?.response?.status ||
                           (error as any)?.status ||
                           500;

          // 에러 컨텍스트 생성
          // API 응답의 message를 우선 사용하고, 없으면 Axios 에러 메시지 사용
          const responseData = (error as any)?.response?.data;
          const errorMessage = responseData?.message || (error as Error).message || 'Unknown error';
          const errorContext: ErrorContext = {
            status: errorCode,
            message: errorMessage,
            errors: responseData?.errors,
            data: responseData,
            statusText: (error as any)?.response?.statusText,
          };

          // ErrorHandlingResolver를 통해 핸들러 결정
          const resolver = getErrorHandlingResolver();
          const resolveResult = resolver.resolve(errorCode, {
            errorHandling: source.errorHandling,
            onError: source.onError,
          });

          let errorHandled = false;

          if (resolveResult.handler) {
            // blocking 데이터소스의 경우:
            // 핸들러를 await하면 데드락 발생 (showErrorPage가 #main_content를 대기하지만,
            // blocking이 끝나야 #main_content가 마운트됨)
            // → 핸들러를 비동기로 실행하고 즉시 blocking 해제
            if (source.loading_strategy === 'blocking') {
              resolver.execute(resolveResult.handler, errorContext).catch((handlerError) => {
                logger.error(`Error executing error handler for ${source.id}:`, handlerError);
              });
              errorHandled = true;
            } else {
              try {
                // 핸들러 실행
                await resolver.execute(resolveResult.handler, errorContext);
                errorHandled = true;
              } catch (handlerError) {
                logger.error(`Error executing error handler for ${source.id}:`, handlerError);
              }
            }
          }

          // fallback이 정의된 경우 렌더링용 데이터로 사용
          if (source.fallback !== undefined) {
            this.dataCache.set(source.id, { data: source.fallback, timestamp: Date.now() });
            logger.log(`Using fallback data for ${source.id}`);
            return {
              id: source.id,
              state: 'success',
              data: source.fallback,
            };
          }

          // 기존 onError 콜백도 호출 (호환성 유지)
          if (this.options.onError) {
            this.options.onError(error as Error, source);
          }

          return {
            id: source.id,
            state: 'error',
            error: error as Error,
            errorCode,
            errorHandled,
          };
        }
      }),
    );

    return results;
  }

  /**
   * 백그라운드에서 데이터 소스 fetch (비동기, 결과 반환 안 함)
   *
   * @param sources 데이터 소스 배열
   * @param routeParams 라우트 파라미터
   * @param queryParams 쿼리 파라미터
   * @param onUpdate 데이터 업데이트 콜백
   */
  fetchDataSourcesInBackground(
    sources: DataSource[],
    routeParams: Record<string, string> = {},
    queryParams: URLSearchParams = new URLSearchParams(),
    onUpdate?: (sourceId: string, data: any) => void,
  ): void {
    // auto_fetch가 true이고 loading_strategy가 background인 소스만 처리 (websocket 타입 제외)
    const backgroundSources = sources.filter(
      (source) => source.auto_fetch !== false && source.loading_strategy === 'background' && source.type !== 'websocket'
    );

    // 각 소스를 백그라운드에서 fetch (await 없이)
    backgroundSources.forEach((source) => {
      this.fetchDataSource(source, routeParams, queryParams)
        .then(async (data) => {
          this.dataCache.set(source.id, { data, timestamp: Date.now() });

          // errorCondition 체크: 200 응답이어도 조건 만족 시 에러로 처리
          const errorConditionCode = this.checkErrorCondition(source, data);
          if (errorConditionCode !== null) {
            const errorContext: ErrorContext = {
              status: errorConditionCode,
              message: `Error condition matched for ${source.id}`,
              data: data,
            };

            const resolver = getErrorHandlingResolver();
            const resolveResult = resolver.resolve(errorConditionCode, {
              errorHandling: source.errorHandling,
              onError: source.onError,
            });

            if (resolveResult.handler) {
              try {
                await resolver.execute(resolveResult.handler, errorContext);
              } catch (handlerError) {
                logger.error(`Error executing errorCondition handler for ${source.id}:`, handlerError);
              }
            }

            // fallback이 정의된 경우 렌더링용 데이터로 사용
            if (source.fallback !== undefined) {
              this.dataCache.set(source.id, { data: source.fallback, timestamp: Date.now() });
              logger.log(`Using fallback data for ${source.id} (errorCondition)`);
              if (onUpdate) {
                onUpdate(source.id, source.fallback);
              }
            }

            return; // onSuccess 스킵
          }

          // onSuccess 핸들러 실행
          if (source.onSuccess) {
            try {
              const successContext: SuccessContext = {
                data,
                sourceId: source.id,
              };
              await this.executeOnSuccessHandler(source.onSuccess, successContext);
            } catch (handlerError) {
              logger.error(`Error executing onSuccess handler for ${source.id}:`, handlerError);
            }
          }

          if (onUpdate) {
            onUpdate(source.id, data);
          }
        })
        .catch(async (error) => {
          logger.error(`Background fetch failed: ${source.id}`, error);

          // HTTP 에러 코드 추출
          const errorCode = (error as any)?.response?.status ||
                           (error as any)?.status ||
                           500;

          // 에러 컨텍스트 생성
          // API 응답의 message를 우선 사용하고, 없으면 Axios 에러 메시지 사용
          const responseData = (error as any)?.response?.data;
          const errorMessage = responseData?.message || (error as Error).message || 'Unknown error';
          const errorContext: ErrorContext = {
            status: errorCode,
            message: errorMessage,
            errors: responseData?.errors,
            data: responseData,
            statusText: (error as any)?.response?.statusText,
          };

          // ErrorHandlingResolver를 통해 핸들러 결정 및 실행
          const resolver = getErrorHandlingResolver();
          const resolveResult = resolver.resolve(errorCode, {
            errorHandling: source.errorHandling,
            onError: source.onError,
          });

          if (resolveResult.handler) {
            try {
              await resolver.execute(resolveResult.handler, errorContext);
            } catch (handlerError) {
              logger.error(`Error executing error handler for ${source.id}:`, handlerError);
            }
          }

          // fallback이 정의된 경우 렌더링용 데이터로 사용
          if (source.fallback !== undefined) {
            this.dataCache.set(source.id, { data: source.fallback, timestamp: Date.now() });
            logger.log(`Using fallback data for ${source.id}`);
            if (onUpdate) {
              onUpdate(source.id, source.fallback);
            }
            return;
          }

          // 기존 onError 콜백도 호출 (호환성 유지)
          if (this.options.onError) {
            this.options.onError(error as Error, source);
          }
        });
    });
  }

  /**
   * 단일 데이터 소스 fetch
   *
   * @param source 데이터 소스
   * @param routeParams 라우트 파라미터
   * @param queryParams 쿼리 파라미터
   * @param globalState _global 상태 (선택적)
   * @returns 데이터
   */
  private async fetchDataSource(
    source: DataSource,
    routeParams: Record<string, string>,
    queryParams: URLSearchParams,
    globalState?: Record<string, any>,
    localState?: Record<string, any>,
  ): Promise<any> {
    switch (source.type) {
      case 'api':
        return this.fetchApiDataSource(source, routeParams, queryParams, globalState, localState);
      case 'static':
        return source.data;
      case 'route_params':
        return routeParams;
      case 'query_params':
        return parseQueryParams(queryParams);
      default:
        throw new Error(`Unknown data source type: ${source.type}`);
    }
  }

  /**
   * API 데이터 소스 fetch
   *
   * @param source 데이터 소스
   * @param routeParams 라우트 파라미터
   * @param queryParams 쿼리 파라미터
   * @param globalState _global 상태 (선택적)
   * @param localState _local 상태 (선택적, params 표현식에서 _local 접근 지원)
   * @returns 데이터
   */
  private async fetchApiDataSource(
    source: DataSource,
    routeParams: Record<string, string>,
    queryParams: URLSearchParams,
    globalState?: Record<string, any>,
    localState?: Record<string, any>,
  ): Promise<any> {
    // 샘플 모드 분기 — 편집기 부팅 시에만 옵션이 주입됨.
    // 일반 렌더 모드는 옵션 미주입이므로 이 분기에 진입하지 않아 기존 fetch 경로 100% 보존.
    if (this.options.sampleProvider?.has(source.id)) {
      const sample = await Promise.resolve(this.options.sampleProvider.resolve(source));
      return sample;
    }

    if (!source.endpoint) {
      throw new Error(`API data source ${source.id} has no endpoint`);
    }

    // 표현식 평가용 컨텍스트 구성
    // _global이 전달된 경우 endpoint 표현식에서 _global.xxx 접근 가능
    // _local이 전달된 경우 params 표현식에서 _local.xxx 접근 가능
    // parseQueryParams를 사용하여 배열 쿼리 파라미터 지원 (예: sales_status[]=a&sales_status[]=b)
    const expressionContext: Record<string, any> = {
      route: routeParams,
      query: parseQueryParams(queryParams),
    };

    // globalState가 전달된 경우 _global로 추가
    if (globalState) {
      expressionContext._global = globalState;
    }

    // localState가 전달된 경우 _local로 추가
    // sequence 내 setState local 후 refetchDataSource에서 {{_local.xxx}} 접근 지원
    if (localState) {
      expressionContext._local = localState;
    }

    // endpoint의 {{...}} 표현식 치환 (RenderHelpers의 범용 함수 사용)
    // 복잡한 표현식({{route?.id}}, 삼항 연산자 등)도 지원
    // IMPORTANT: skipCache: true - 매 요청마다 새로운 컨텍스트로 평가해야 함 (페이지네이션 등)
    let endpoint = resolveExpressionString(source.endpoint, expressionContext, { skipCache: true });

    const method = source.method || 'GET';

    // contentType 해석 (multipart/form-data 지원)
    const resolvedContentType = source.contentType
      ? (typeof source.contentType === 'string' && source.contentType.includes('{{'))
        ? resolveExpressionString(source.contentType, expressionContext, { skipCache: true })
        : source.contentType
      : 'application/json';
    const isMultipart = resolvedContentType === 'multipart/form-data';

    // IMPORTANT: source.params를 deep clone하여 원본 보존 (레이아웃 캐시 변형 방지)
    // multipart/form-data인 경우 File/Blob 객체가 JSON 직렬화로 소실되므로 shallow clone 사용
    let params = isMultipart
      ? { ...(source.params || {}) }
      : JSON.parse(JSON.stringify(source.params || {}));

    // 파라미터 치환 (resolveParamValue 사용)
    // {{route.id}}, {{route?.id}}, {{query.page}}, 복잡한 표현식 모두 지원
    // IMPORTANT: 배열 등 원본 타입 유지를 위해 resolveParamValue 사용
    // (resolveExpressionString은 항상 문자열 반환하여 배열이 JSON 문자열로 변환됨)
    Object.entries(params).forEach(([key, value]) => {
      if (typeof value === 'string') {
        params[key] = resolveParamValue(value, expressionContext);
      }
    });

    // 빈 문자열인 파라미터 제거 (API로 전송하지 않음)
    Object.keys(params).forEach((key) => {
      if (params[key] === '') {
        delete params[key];
      }
    });

    // DevTools 요청 추적
    const devTools = getDevTools();
    let requestId: string | null = null;
    if (devTools?.isEnabled()) {
      // 데이터소스 정의 정보 등록
      devTools.trackDataSourceDefinition({
        id: source.id,
        type: source.type,
        endpoint: endpoint,
        method: method,
        autoFetch: source.auto_fetch,
        initLocal: source.initLocal,
        initGlobal: source.initGlobal,
      });

      // DevTools용 전체 URL 생성 (쿼리 파라미터 포함)
      let trackingUrl = endpoint;
      if (method === 'GET' && Object.keys(params).length > 0) {
        const searchParams = new URLSearchParams();
        Object.entries(params).forEach(([key, value]) => {
          if (Array.isArray(value)) {
            value.forEach(v => searchParams.append(key, String(v)));
          } else if (value != null) {
            searchParams.append(key, String(value));
          }
        });
        const queryString = searchParams.toString();
        trackingUrl = queryString ? `${endpoint}?${queryString}` : endpoint;
      }

      requestId = devTools.trackRequest(trackingUrl, method, {
        requestBody: method !== 'GET' ? params : undefined,
        dataSourceId: source.id,
      });
      devTools.trackDataSourceLoading(source.id);
    }

    try {
      let result: any;

      // auth_mode 결정 (auth_required: true는 'required'로 처리하여 하위 호환성 유지)
      const authMode = source.auth_mode ?? (source.auth_required === true ? 'required' : 'none');

      // 토큰 유무 확인
      const hasToken = AuthManager.getInstance().isAuthenticated();

      // auth_mode: 'required'이고 토큰이 없으면 요청 스킵 (fallback 사용)
      // 토큰 없이 ApiClient로 요청하면 401 → onUnauthorized → 로그인 리다이렉트 발생
      if (authMode === 'required' && !hasToken) {
        logger.log(`[DataSourceManager] Skipping ${source.id}: auth_mode is 'required' but no token available`);
        if (devTools?.isEnabled()) {
          devTools.trackDataSourceError(source.id, 'Skipped: auth_mode required but not authenticated');
          if (requestId) {
            devTools.failRequest(requestId, 'Skipped: not authenticated');
          }
        }
        const skipError = new Error('Auth required but not authenticated') as any;
        skipError.response = { status: 401, statusText: 'Unauthorized', data: null };
        skipError.status = 401;
        skipError._authSkipped = true;
        throw skipError;
      }

      // ApiClient 사용 여부 결정
      // - required: ApiClient 사용 (토큰 존재 확인됨)
      // - optional: 토큰이 있을 때만 ApiClient 사용
      // - none: 일반 fetch 사용
      const useApiClient = authMode === 'required' || (authMode === 'optional' && hasToken);

      if (useApiClient) {
        const apiClient = getApiClient();

        // ApiClient는 baseURL로 '/api'를 사용하므로, endpoint에서 '/api' 제거
        const normalizedEndpoint = endpoint.startsWith('/api/')
          ? endpoint.substring(4) // '/api/' 제거 -> '/'로 시작
          : endpoint.startsWith('/api')
          ? endpoint.substring(4) || '/' // '/api' 제거 -> '/' 또는 원래 경로
          : endpoint;

        if (this.options.onError) {
          logger.log(`[DataSourceManager] Normalized endpoint: ${endpoint} -> ${normalizedEndpoint}`);
        }

        // 전역 헤더 + 커스텀 헤더 병합 (source.headers가 globalHeaders보다 우선)
        // 1. globalHeaders에서 패턴 매칭된 헤더 추출
        const globalHeadersResolved = this.getMatchingGlobalHeaders(endpoint, expressionContext);

        // 2. source.headers 처리 (개별 데이터소스 헤더)
        const sourceHeaders: Record<string, string> = {};
        if (source.headers) {
          Object.entries(source.headers).forEach(([headerName, headerValue]) => {
            // 표현식 평가 ({{...}} 치환)
            const resolvedValue = resolveParamValue(headerValue, expressionContext);
            // null, undefined, 빈 문자열이 아닌 경우에만 헤더 추가
            if (resolvedValue != null && resolvedValue !== '') {
              sourceHeaders[headerName] = String(resolvedValue);
            }
          });
        }

        // 3. 병합 (source.headers가 globalHeaders를 덮어씀)
        const customHeaders: Record<string, string> = {
          ...globalHeadersResolved,
          ...sourceHeaders,
        };

        // 커스텀 헤더가 있는 경우 config에 포함
        const hasCustomHeaders = Object.keys(customHeaders).length > 0;

        // multipart/form-data인 경우 params를 FormData로 변환
        // Axios는 FormData를 받으면 Content-Type을 자동으로 multipart/form-data(+boundary)로 설정
        const requestData = isMultipart ? this.toFormData(params) : params;

        // HTTP 메서드에 따라 요청
        switch (method) {
          case 'GET':
            result = await apiClient.get(normalizedEndpoint, {
              params,
              ...(hasCustomHeaders && { headers: customHeaders }),
            });
            break;
          case 'POST':
            if (hasCustomHeaders) {
              result = await apiClient.post(normalizedEndpoint, requestData, { headers: customHeaders });
            } else {
              result = await apiClient.post(normalizedEndpoint, requestData);
            }
            break;
          case 'PUT':
            if (hasCustomHeaders) {
              result = await apiClient.put(normalizedEndpoint, requestData, { headers: customHeaders });
            } else {
              result = await apiClient.put(normalizedEndpoint, requestData);
            }
            break;
          case 'PATCH':
            if (hasCustomHeaders) {
              result = await apiClient.patch(normalizedEndpoint, requestData, { headers: customHeaders });
            } else {
              result = await apiClient.patch(normalizedEndpoint, requestData);
            }
            break;
          case 'DELETE':
            result = await apiClient.delete(normalizedEndpoint, {
              params,
              ...(hasCustomHeaders && { headers: customHeaders }),
            });
            break;
          default:
            throw new Error(`Unsupported HTTP method: ${method}`);
        }
      } else {
        // auth_mode가 'none'이거나 'optional'인데 토큰이 없는 경우 일반 fetch 사용
        let url = endpoint;

        // GET 요청인 경우 쿼리 스트링 생성
        if (method === 'GET') {
          const queryString = new URLSearchParams(params).toString();
          url = queryString ? `${endpoint}?${queryString}` : endpoint;
        }

        // 기본 헤더 (multipart/form-data는 Content-Type 생략 - 브라우저 자동 설정)
        const requestHeaders: Record<string, string> = {
          ...(isMultipart ? {} : { 'Content-Type': 'application/json' }),
          'Accept': 'application/json',
        };

        // g7_locale이 설정되어 있으면 Accept-Language 헤더로 전송
        if (typeof window !== 'undefined') {
          const locale = localStorage.getItem('g7_locale');
          if (locale) {
            requestHeaders['Accept-Language'] = locale;
          }
        }

        // 전역 헤더 적용 (globalHeaders)
        const globalHeadersResolved = this.getMatchingGlobalHeaders(endpoint, expressionContext);
        Object.assign(requestHeaders, globalHeadersResolved);

        // 커스텀 헤더 처리 (source.headers가 globalHeaders보다 우선)
        if (source.headers) {
          Object.entries(source.headers).forEach(([headerName, headerValue]) => {
            // 표현식 평가 ({{...}} 치환)
            const resolvedValue = resolveParamValue(headerValue, expressionContext);
            // null, undefined, 빈 문자열이 아닌 경우에만 헤더 추가
            if (resolvedValue != null && resolvedValue !== '') {
              requestHeaders[headerName] = String(resolvedValue);
            }
          });
        }

        // multipart/form-data인 경우 FormData로 변환
        let fetchBody: BodyInit | undefined;
        if (method !== 'GET') {
          if (isMultipart) {
            fetchBody = this.toFormData(params);
            // multipart 시 Content-Type 헤더 제거 (FormData 사용 시 브라우저가 boundary 포함하여 설정)
            delete requestHeaders['Content-Type'];
          } else {
            fetchBody = JSON.stringify(params);
          }
        }

        const response = await fetch(url, {
          method,
          headers: requestHeaders,
          body: fetchBody,
        });

        // 응답 본문 파싱 (성공/실패 모두)
        let responseBody: any;
        try {
          responseBody = await response.json();
        } catch {
          responseBody = null;
        }

        // DevTools 요청 완료 추적 (성공/실패 모두 상태 코드와 응답 기록)
        if (requestId && devTools?.isEnabled()) {
          devTools.completeRequest(requestId, response.status, responseBody);
          // 이미 기록했으므로 catch에서 중복 기록 방지
          requestId = null;
        }

        if (!response.ok) {
          // 에러 응답도 DevTools에 기록 후 throw
          if (devTools?.isEnabled()) {
            devTools.trackDataSourceError(source.id, `HTTP ${response.status}: ${response.statusText}`);
          }
          // response 정보를 Error에 포함하여 상위 catch에서 정확한 상태 코드 추출 가능
          const httpError = new Error(`HTTP error! status: ${response.status}`) as any;
          httpError.response = {
            status: response.status,
            statusText: response.statusText,
            data: responseBody,
          };
          httpError.status = response.status;
          throw httpError;
        }

        result = responseBody;

        // DevTools 데이터소스 로드 완료 추적
        if (devTools?.isEnabled()) {
          devTools.trackDataSourceLoaded(source.id, result);
        }
      }

      // DevTools 요청 완료 추적 (ApiClient 사용 시)
      if (useApiClient && requestId && devTools?.isEnabled()) {
        // ApiClient는 성공 응답만 반환하므로 200으로 기록
        // (에러 시 catch 블록으로 이동)
        devTools.completeRequest(requestId, 200, result);
        devTools.trackDataSourceLoaded(source.id, result);
      }

      return result;
    } catch (error) {
      // DevTools 요청 실패 추적 (ApiClient 에러 또는 네트워크 에러)
      if (requestId && devTools?.isEnabled()) {
        // axios 에러인 경우 상태 코드와 응답 데이터 추출
        const axiosError = error as any;
        if (axiosError?.response) {
          // HTTP 에러 응답이 있는 경우 상태 코드와 응답 본문 기록
          devTools.completeRequest(
            requestId,
            axiosError.response.status,
            axiosError.response.data
          );
        } else {
          // 네트워크 에러 등 응답이 없는 경우
          devTools.failRequest(requestId, error instanceof Error ? error.message : String(error));
        }
        devTools.trackDataSourceError(source.id, error instanceof Error ? error.message : String(error));
      }
      throw error;
    }
  }

  /**
   * 캐시된 데이터 조회
   *
   * TTL이 만료된 캐시는 자동으로 삭제됩니다.
   *
   * @param sourceId 데이터 소스 ID
   * @returns 캐시된 데이터 (없거나 만료되면 undefined)
   */
  getCachedData(sourceId: string): any | undefined {
    const entry = this.dataCache.get(sourceId);

    if (!entry) {
      return undefined;
    }

    // TTL 체크
    const now = Date.now();
    if (now - entry.timestamp > DataSourceManager.DATA_CACHE_TTL) {
      this.dataCache.delete(sourceId);
      return undefined;
    }

    return entry.data;
  }

  /**
   * 캐시 초기화
   */
  clearCache(): void {
    this.dataCache.clear();
  }

  /**
   * WebSocket 데이터 소스를 구독합니다.
   *
   * type이 'websocket'인 데이터 소스를 필터링하여
   * WebSocketManager를 통해 채널을 구독하고 이벤트를 리스닝합니다.
   *
   * @param sources 데이터 소스 배열
   * @param onUpdate 데이터 업데이트 시 호출될 콜백 (sourceId, data)
   * @returns 구독 키 배열 (구독 해제 시 사용)
   */
  subscribeWebSockets(
    sources: DataSource[],
    onUpdate: (sourceId: string, data: unknown) => void,
    bindingContext: Record<string, any> = {}
  ): string[] {
    const subscriptionKeys: string[] = [];
    const webSocketSources = sources.filter((s) => s.type === 'websocket');

    if (webSocketSources.length === 0) {
      return subscriptionKeys;
    }

    webSocketSources.forEach((source) => {
      if (!source.channel || !source.event) {
        logger.warn(`WebSocket source ${source.id} missing channel or event`);
        return;
      }

      // 채널/이벤트명에 포함된 표현식 평가 (예: core.user.notifications.{{current_user.data.uuid}})
      // 평가 실패 시 원본 문자열 유지 — 정적 채널은 영향 없음
      let resolvedChannel: string;
      let resolvedEvent: string;
      try {
        resolvedChannel = resolveExpressionString(source.channel, bindingContext, { skipCache: true });
        resolvedEvent = resolveExpressionString(source.event, bindingContext, { skipCache: true });
      } catch (error) {
        logger.error(`Failed to resolve WebSocket channel/event for source ${source.id}:`, error);
        return;
      }

      // 평가 결과 검증: 표현식 미평가, 빈 세그먼트(trailing/leading/연속 dot), 빈 문자열은 모두 잘못된 채널
      // 표현식이 undefined로 평가되어 빈 문자열로 치환된 경우 trailing dot 등이 발생함
      const isInvalidChannel = (channel: string): boolean => {
        if (!channel || channel.trim() === '') return true;
        if (channel.includes('{{')) return true;          // 미평가 표현식
        if (channel.endsWith('.') || channel.endsWith(':')) return true;  // trailing 빈 세그먼트
        if (channel.startsWith('.') || channel.startsWith(':')) return true; // leading 빈 세그먼트
        if (channel.includes('..') || channel.includes('::')) return true; // 중간 빈 세그먼트
        return false;
      };

      if (isInvalidChannel(resolvedChannel) || isInvalidChannel(resolvedEvent)) {
        logger.warn(
          `WebSocket source ${source.id} has invalid channel/event after expression resolution, skipping. ` +
          `Original channel="${source.channel}", resolved="${resolvedChannel}". ` +
          `Original event="${source.event}", resolved="${resolvedEvent}". ` +
          `Available context keys: [${Object.keys(bindingContext).join(', ')}]`
        );
        return;
      }

      const key = webSocketManager.subscribe(
        resolvedChannel,
        resolvedEvent,
        (data: unknown) => {
          // target_source가 지정된 경우 해당 소스의 캐시 업데이트
          const targetId = source.target_source || source.id;
          this.dataCache.set(targetId, { data, timestamp: Date.now() });
          onUpdate(targetId, data);
        },
        { channelType: source.channel_type || 'private' }
      );

      if (key) {
        subscriptionKeys.push(key);
      }
    });

    return subscriptionKeys;
  }

  /**
   * WebSocket 구독을 해제합니다.
   *
   * @param subscriptionKeys 구독 키 배열
   */
  unsubscribeWebSockets(subscriptionKeys: string[]): void {
    if (subscriptionKeys.length === 0) {
      return;
    }

    subscriptionKeys.forEach((key) => {
      webSocketManager.unsubscribe(key);
    });
  }

  /**
   * onSuccess 핸들러를 실행합니다.
   *
   * 데이터 소스 fetch 성공 후 정의된 onSuccess 핸들러를 실행합니다.
   * params에서 {{response.xxx}} 형태로 응답 데이터에 접근할 수 있습니다.
   *
   * @param onSuccess onSuccess 핸들러 설정
   * @param successContext 성공 컨텍스트 (data, sourceId)
   */
  private async executeOnSuccessHandler(
    onSuccess: OnSuccessHandler,
    successContext: SuccessContext
  ): Promise<void> {
    // 옵션 주입된 dispatcher 우선 (편집기 격리 모드), 없으면 전역 호스트 dispatcher
    // (편집기 캔버스의 onSuccess setState 가 호스트 globalState
    //  를 변경해 호스트 #app 이 unmount 되는 결함 방지)
    const actionDispatcher = this.options.actionDispatcher ?? getActionDispatcher();

    if (!actionDispatcher) {
      logger.warn('ActionDispatcher not available, skipping onSuccess handler');
      return;
    }

    // 핸들러 배열로 정규화
    const handlers = Array.isArray(onSuccess) ? onSuccess : [onSuccess];

    // 순차 실행
    for (const handler of handlers) {
      try {
        // response 컨텍스트로 핸들러 실행
        // {{response.data}}, {{response.sourceId}} 등으로 접근 가능
        await actionDispatcher.dispatchAction(
          {
            type: 'click', // 이벤트 타입은 의미 없음 (dispatchAction에서 사용하지 않음)
            ...handler,
          },
          {
            data: {
              response: successContext,
            },
          }
        );
      } catch (error) {
        logger.error(`Failed to execute onSuccess handler:`, error);
        throw error;
      }
    }
  }
}

/**
 * 전역 데이터 소스 매니저 인스턴스
 */
export const dataSourceManager = new DataSourceManager();
