/**
 * 다중 테이블 엑셀 업로드 범용 서비스
 *
 * 하나의 플랫 엑셀 데이터를 계층적 다중 테이블(2~N개)에
 * 트랜잭션으로 일괄 UPSERT하는 범용 엔진.
 *
 * 적용 사례:
 * - 거래처: customer_mng → customer_item_mapping → customer_item_prices
 * - 공급업체: supplier_mng → supplier_item_mapping → supplier_item_prices
 */

import { getPool } from "../database/db";
import { logger } from "../utils/logger";

// ================================
// 인터페이스 정의
// ================================

/** 테이블 계층 레벨 설정 */
export interface TableLevel {
  tableName: string;
  label: string;
  parentFkColumn?: string;
  parentRefColumn?: string;
  upsertMode: "upsert" | "insert";
  upsertKeyColumns?: string[];
  columns: ColumnDef[];
}

/** 컬럼 정의 */
export interface ColumnDef {
  dbColumn: string;
  excelHeader: string;
  required: boolean;
  defaultValue?: any;
}

/** 업로드 모드 정의 */
export interface UploadMode {
  id: string;
  label: string;
  description: string;
  activeLevels: number[];
}

/** 테이블 체인 설정 (범용) */
export interface TableChainConfig {
  id: string;
  name: string;
  description: string;
  levels: TableLevel[];
  uploadModes: UploadMode[];
}

/** 레벨별 업로드 결과 */
export interface LevelResult {
  tableName: string;
  inserted: number;
  updated: number;
}

/** 전체 업로드 결과 */
export interface MultiTableUploadResult {
  success: boolean;
  results: LevelResult[];
  totalRows: number;
  errors: string[];
}

// ================================
// 서비스 클래스
// ================================

class MultiTableExcelService {
  /**
   * 다중 테이블 엑셀 업로드 실행
   *
   * @param config 테이블 체인 설정
   * @param modeId 업로드 모드 ID
   * @param rows 엑셀에서 파싱된 플랫 JSON 배열 (excelHeader 기준)
   * @param companyCode 회사 코드
   * @param userId 사용자 ID
   */
  async uploadMultiTable(
    config: TableChainConfig,
    modeId: string,
    rows: Record<string, any>[],
    companyCode: string,
    userId: string
  ): Promise<MultiTableUploadResult> {
    const result: MultiTableUploadResult = {
      success: false,
      results: [],
      totalRows: rows.length,
      errors: [],
    };

    const mode = config.uploadModes.find((m) => m.id === modeId);
    if (!mode) {
      result.errors.push(`업로드 모드를 찾을 수 없습니다: ${modeId}`);
      return result;
    }

    const activeLevels = mode.activeLevels
      .map((i) => config.levels[i])
      .filter(Boolean);

    if (activeLevels.length === 0) {
      result.errors.push("활성화된 테이블 레벨이 없습니다.");
      return result;
    }

    // 레벨별 결과 초기화
    for (const level of activeLevels) {
      result.results.push({
        tableName: level.tableName,
        inserted: 0,
        updated: 0,
      });
    }

    const pool = getPool();
    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      // 각 레벨의 실제 DB 컬럼 존재 여부 캐시
      const existingColsCache = new Map<string, Set<string>>();
      for (const level of activeLevels) {
        const colsResult = await client.query(
          `SELECT column_name FROM information_schema.columns
           WHERE table_schema = 'public' AND table_name = $1`,
          [level.tableName]
        );
        existingColsCache.set(
          level.tableName,
          new Set(colsResult.rows.map((r: any) => r.column_name))
        );
      }

      // 엑셀 헤더 → DB 컬럼 매핑 테이블 구축 (활성 레벨만)
      const headerToColumn = new Map<string, { levelIndex: number; dbColumn: string }>();
      for (let i = 0; i < activeLevels.length; i++) {
        for (const col of activeLevels[i].columns) {
          headerToColumn.set(col.excelHeader, {
            levelIndex: i,
            dbColumn: col.dbColumn,
          });
        }
      }

      // 행 단위로 처리 (트랜잭션 내)
      // 부모 ID 캐시: 각 레벨에서 upsertKey → 반환된 PK 매핑
      const pkCaches: Map<string, string | number>[] = activeLevels.map(
        () => new Map()
      );

      for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
        const row = rows[rowIdx];

        try {
          let parentId: string | number | null = null;
          let parentLevelData: Record<string, any> = {};

          for (let lvlIdx = 0; lvlIdx < activeLevels.length; lvlIdx++) {
            const level = activeLevels[lvlIdx];
            const levelResult = result.results[lvlIdx];
            const existingCols = existingColsCache.get(level.tableName)!;

            const levelData: Record<string, any> = {};
            for (const colDef of level.columns) {
              const excelValue = row[colDef.excelHeader];
              if (excelValue !== undefined && excelValue !== null && excelValue !== "") {
                levelData[colDef.dbColumn] = excelValue;
              } else if (colDef.defaultValue !== undefined) {
                levelData[colDef.dbColumn] = colDef.defaultValue;
              }
            }

            const hasAnyData = Object.keys(levelData).length > 0;
            if (!hasAnyData && lvlIdx > 0) {
              break;
            }

            const missingRequired = level.columns
              .filter((c) => c.required && !levelData[c.dbColumn])
              .map((c) => c.excelHeader);

            if (missingRequired.length > 0) {
              result.errors.push(
                `[행 ${rowIdx + 1}] ${level.label} 필수 컬럼 누락: ${missingRequired.join(", ")}`
              );
              break;
            }

            // 부모 FK 주입: parentRefColumn이 'id'가 아닌 경우 부모 데이터에서 해당 컬럼 값 사용
            if (lvlIdx > 0 && level.parentFkColumn && parentId !== null) {
              if (
                level.parentRefColumn &&
                level.parentRefColumn !== "id" &&
                parentLevelData[level.parentRefColumn] !== undefined &&
                parentLevelData[level.parentRefColumn] !== null
              ) {
                levelData[level.parentFkColumn] = String(
                  parentLevelData[level.parentRefColumn]
                );
              } else {
                levelData[level.parentFkColumn] = String(parentId);
              }
            }

            if (existingCols.has("company_code")) {
              levelData.company_code = companyCode;
            }
            if (existingCols.has("writer")) {
              levelData.writer = userId;
            }

            const upsertKey = level.upsertKeyColumns
              ? level.upsertKeyColumns.map((k) => String(levelData[k] ?? "")).join("|||")
              : null;

            let returnedId: string | number;

            if (level.upsertMode === "upsert" && upsertKey) {
              const cachedId = pkCaches[lvlIdx].get(upsertKey);
              if (cachedId !== undefined) {
                returnedId = cachedId;
              } else {
                returnedId = await this.upsertRow(
                  client,
                  level,
                  levelData,
                  existingCols,
                  companyCode,
                  levelResult
                );
                pkCaches[lvlIdx].set(upsertKey, returnedId);
              }
            } else {
              returnedId = await this.insertRow(
                client,
                level.tableName,
                levelData,
                existingCols
              );
              levelResult.inserted++;
            }

            parentId = returnedId;
            parentLevelData = { ...levelData };
          }
        } catch (error: any) {
          result.errors.push(`[행 ${rowIdx + 1}] 처리 실패: ${error.message}`);
          logger.error(`[행 ${rowIdx + 1}] 처리 실패:`, error);
        }
      }

      await client.query("COMMIT");
      result.success =
        result.errors.length === 0 ||
        result.results.some((r) => r.inserted + r.updated > 0);

      logger.info("다중 테이블 엑셀 업로드 완료:", {
        results: result.results,
        errors: result.errors.length,
      });
    } catch (error: any) {
      await client.query("ROLLBACK");
      result.errors.push(`트랜잭션 실패: ${error.message}`);
      logger.error("다중 테이블 엑셀 업로드 트랜잭션 실패:", error);
    } finally {
      client.release();
    }

    return result;
  }

  /**
   * UPSERT 행 처리 (존재하면 UPDATE, 없으면 INSERT)
   * @returns 해당 행의 PK (id)
   */
  private async upsertRow(
    client: any,
    level: TableLevel,
    data: Record<string, any>,
    existingCols: Set<string>,
    companyCode: string,
    levelResult: LevelResult
  ): Promise<string | number> {
    const { tableName, upsertKeyColumns } = level;

    if (!upsertKeyColumns || upsertKeyColumns.length === 0) {
      const id = await this.insertRow(client, tableName, data, existingCols);
      levelResult.inserted++;
      return id;
    }

    // UPSERT 키로 기존 행 조회
    const whereClause = upsertKeyColumns
      .map((col, i) => `"${col}" = $${i + 1}`)
      .join(" AND ");

    const companyIdx = upsertKeyColumns.length + 1;
    const companyWhere = existingCols.has("company_code")
      ? ` AND company_code = $${companyIdx}`
      : "";

    const params = upsertKeyColumns.map((col) => data[col]);
    if (existingCols.has("company_code")) {
      params.push(companyCode);
    }

    const existing = await client.query(
      `SELECT id FROM "${tableName}" WHERE ${whereClause}${companyWhere} LIMIT 1`,
      params
    );

    if (existing.rows.length > 0) {
      // UPDATE
      const existingId = existing.rows[0].id;
      const skipCols = new Set([
        "id",
        "company_code",
        "created_date",
        ...upsertKeyColumns,
      ]);
      const updateKeys = Object.keys(data).filter(
        (k) => !skipCols.has(k) && existingCols.has(k)
      );

      if (updateKeys.length > 0) {
        const setClauses = updateKeys.map((k, i) => `"${k}" = $${i + 1}`);
        const setValues = updateKeys.map((k) => data[k]);
        const updatedDateClause = existingCols.has("updated_date")
          ? `, updated_date = NOW()`
          : "";

        await client.query(
          `UPDATE "${tableName}" SET ${setClauses.join(", ")}${updatedDateClause}
           WHERE id = $${setValues.length + 1}`,
          [...setValues, existingId]
        );
      }
      levelResult.updated++;
      return existingId;
    } else {
      // INSERT
      const id = await this.insertRow(client, tableName, data, existingCols);
      levelResult.inserted++;
      return id;
    }
  }

  /**
   * 단순 INSERT 후 PK(id) 반환
   */
  private async insertRow(
    client: any,
    tableName: string,
    data: Record<string, any>,
    existingCols: Set<string>
  ): Promise<string | number> {
    // DB에 실제 존재하는 컬럼만 필터
    const cols = Object.keys(data).filter(
      (k) => existingCols.has(k) && k !== "id" && data[k] !== undefined
    );

    const hasCreatedDate = existingCols.has("created_date");
    const colList = hasCreatedDate ? [...cols, "created_date"] : cols;
    const placeholders = cols.map((_, i) => `$${i + 1}`);
    const valList = hasCreatedDate ? [...placeholders, "NOW()"] : placeholders;
    const values = cols.map((k) => data[k]);

    const result = await client.query(
      `INSERT INTO "${tableName}" (${colList.map((c) => `"${c}"`).join(", ")})
       VALUES (${valList.join(", ")})
       RETURNING id`,
      values
    );

    return result.rows[0].id;
  }

  /**
   * 모드에 맞는 템플릿 데이터 생성 (엑셀 다운로드용)
   */
  generateTemplateData(
    config: TableChainConfig,
    modeId: string
  ): { headers: string[]; requiredHeaders: string[]; sampleRow: Record<string, string> } {
    const mode = config.uploadModes.find((m) => m.id === modeId);
    if (!mode) {
      throw new Error(`업로드 모드를 찾을 수 없습니다: ${modeId}`);
    }

    const headers: string[] = [];
    const requiredHeaders: string[] = [];
    const sampleRow: Record<string, string> = {};

    for (const levelIdx of mode.activeLevels) {
      const level = config.levels[levelIdx];
      if (!level) continue;

      for (const col of level.columns) {
        headers.push(col.excelHeader);
        if (col.required) {
          requiredHeaders.push(col.excelHeader);
        }
        sampleRow[col.excelHeader] = col.required ? `(필수)` : "";
      }
    }

    return { headers, requiredHeaders, sampleRow };
  }

  // ================================
  // 자동 감지
  // ================================

  // 엑셀 업로드에서 제외할 시스템 컬럼
  private static SYSTEM_COLUMNS = new Set([
    "id",
    "company_code",
    "writer",
    "created_date",
    "updated_date",
    "created_at",
    "updated_at",
  ]);

  /**
   * 루트 테이블에서 자식/손자 테이블을 자동 탐색하여
   * TableChainConfig를 실시간으로 생성한다.
   *
   * @param screenId 화면 ID (선택). 제공 시 관련 화면의 테이블을 참고하여
   *                 다수 자식 중 올바른 체인을 선택한다.
   */
  async autoDetectTableChain(
    rootTableName: string,
    companyCode: string,
    screenId?: number
  ): Promise<TableChainConfig> {
    const pool = getPool();

    // 1) 루트 테이블이 존재하는지 확인
    const tableCheck = await pool.query(
      `SELECT 1 FROM information_schema.tables
       WHERE table_schema = 'public' AND table_name = $1`,
      [rootTableName]
    );
    if (tableCheck.rows.length === 0) {
      throw new Error(`테이블이 존재하지 않습니다: ${rootTableName}`);
    }

    // 2) 화면 컨텍스트에서 관련 테이블 세트 + 보이는 컬럼 추출
    let contextTables = new Set<string>();
    let visibleColumnsMap = new Map<string, Set<string>>();

    if (screenId) {
      const ctx = await this.getScreenContext(pool, screenId, companyCode);
      contextTables = ctx.contextTables;
      visibleColumnsMap = ctx.visibleColumns;
    }

    logger.info("자동 감지 시작", {
      rootTableName,
      screenId,
      contextTables: [...contextTables],
      visibleColumnsPerTable: Object.fromEntries(
        [...visibleColumnsMap].map(([k, v]) => [k, [...v]])
      ),
    });

    // 3) 루트부터 재귀적으로 자식 탐색 (최대 3레벨)
    const levels: TableLevel[] = [];
    await this.buildLevelChain(pool, rootTableName, companyCode, levels, 0, 3, contextTables);

    // 3.5) 화면 레이아웃에서 보이는 컬럼으로 필터링
    if (visibleColumnsMap.size > 0) {
      for (const level of levels) {
        const visibleCols = visibleColumnsMap.get(level.tableName);
        if (visibleCols && visibleCols.size > 0) {
          const before = level.columns.length;
          level.columns = level.columns.filter((col) => visibleCols.has(col.dbColumn));
          logger.info(`컬럼 필터링: ${level.tableName}`, {
            before,
            after: level.columns.length,
            visibleCols: [...visibleCols],
          });
        }
      }
    }

    // 4) 업로드 모드 자동 생성
    const uploadModes: UploadMode[] = [];
    for (let depth = 0; depth < levels.length; depth++) {
      const activeLevels = Array.from({ length: depth + 1 }, (_, i) => i);
      const labels = activeLevels.map((i) => levels[i].label);
      uploadModes.push({
        id: `auto_mode_${depth}`,
        label: labels.join(" + "),
        description: `${labels.join(", ")} 일괄 등록`,
        activeLevels,
      });
    }

    // 5) 라벨 생성
    const rootLabel = await this.getTableLabel(pool, rootTableName, companyCode);

    return {
      id: rootTableName,
      name: rootLabel,
      description: `${rootLabel} 다중 테이블 엑셀 업로드 (자동 감지)`,
      levels,
      uploadModes,
    };
  }

  /**
   * 재귀적으로 테이블 계층 빌드
   * contextTables: 화면의 관련 테이블 세트 (자식 선택 시 우선순위에 사용)
   */
  private async buildLevelChain(
    pool: any,
    tableName: string,
    companyCode: string,
    levels: TableLevel[],
    depth: number,
    maxDepth: number,
    contextTables: Set<string>
  ): Promise<void> {
    if (depth >= maxDepth) return;

    const columns = await this.getTableColumns(pool, tableName, companyCode);

    const { upsertMode, upsertKeyColumns } = await this.detectUpsertKeys(
      pool,
      tableName
    );

    const label = await this.getTableLabel(pool, tableName, companyCode);

    let parentFkColumn: string | undefined;
    let parentRefColumn: string | undefined;
    if (depth > 0 && levels.length > 0) {
      const parentTable = levels[depth - 1].tableName;
      const fkInfo = await this.findFkColumn(pool, tableName, parentTable, companyCode);
      if (fkInfo) {
        parentFkColumn = fkInfo.fkColumn;
        parentRefColumn = fkInfo.refColumn;
      }
    }

    const level: TableLevel = {
      tableName,
      label,
      upsertMode,
      upsertKeyColumns: upsertMode === "upsert" ? upsertKeyColumns : undefined,
      columns,
      ...(parentFkColumn ? { parentFkColumn, parentRefColumn } : {}),
    };

    levels.push(level);

    const childTables = await this.findChildTables(pool, tableName, companyCode);
    if (childTables.length > 0) {
      const bestChild = this.pickBestChild(tableName, childTables, contextTables);
      await this.buildLevelChain(pool, bestChild, companyCode, levels, depth + 1, maxDepth, contextTables);
    }
  }

  /**
   * 여러 자식 테이블 중 가장 관련성 높은 하나를 선택
   *
   * 우선순위:
   * 1. 화면 컨텍스트 테이블에 포함된 자식 (같은 화면 관련 모달이 사용하는 테이블)
   * 2. 부모 테이블명의 접두사를 공유하는 자식
   * 3. 첫 번째 자식
   */
  private pickBestChild(
    parentTable: string,
    children: string[],
    contextTables: Set<string>
  ): string {
    if (children.length === 1) return children[0];

    // 1순위: 화면 컨텍스트 테이블에 있는 자식
    if (contextTables.size > 0) {
      const contextMatch = children.find((c) => contextTables.has(c));
      if (contextMatch) {
        logger.info(`pickBestChild: 화면 컨텍스트 매칭 - ${contextMatch}`, {
          parentTable,
          candidates: children,
        });
        return contextMatch;
      }
    }

    // 2순위: 부모 테이블의 접두사 매칭 (예: customer_mng → customer_item_mapping)
    const parentPrefix = parentTable.split("_")[0];
    const prefixMatch = children.find((c) => c.startsWith(parentPrefix + "_"));
    if (prefixMatch) return prefixMatch;

    return children[0];
  }

  /**
   * 화면 ID로부터 관련 화면 정보를 통합 추출:
   * 1) contextTables: 자식 테이블 선택에 사용할 관련 테이블 세트
   * 2) visibleColumns: 각 테이블별 화면에서 실제 사용 중인 컬럼 세트
   *
   * 추출 전략:
   * A) 화면 레이아웃에서 모달 참조(targetScreenId, modalScreenId)를 재귀 추적
   * B) 화면명 키워드로 관련 화면 검색
   * C) 같은 테이블의 등록/수정 폼 검색
   */
  private async getScreenContext(
    pool: any,
    screenId: number,
    companyCode: string
  ): Promise<{
    contextTables: Set<string>;
    visibleColumns: Map<string, Set<string>>;
  }> {
    const emptyResult = {
      contextTables: new Set<string>(),
      visibleColumns: new Map<string, Set<string>>(),
    };

    const screenResult = await pool.query(
      `SELECT screen_name, table_name FROM screen_definitions WHERE screen_id = $1`,
      [screenId]
    );

    if (screenResult.rows.length === 0) return emptyResult;

    const screenName: string = screenResult.rows[0].screen_name;
    const rootTable: string = screenResult.rows[0].table_name;
    const companyPrefix = screenName.split(/\s+/)[0] || "";

    const contextTables = new Set<string>();
    const collectedScreenIds = new Set<number>();

    // ─── A) 화면명 키워드로 관련 화면 검색 (자식 테이블 선택용) ───
    const suffixWords = new Set(["화면", "모달", "폼", "페이지"]);
    const nameTokens = screenName.split(/\s+/);
    const middleTokens = nameTokens.filter((t, i) => {
      if (i === 0) return false;
      if (suffixWords.has(t)) return false;
      return t.length >= 2;
    });

    for (const token of middleTokens) {
      const relatedResult = await pool.query(
        `SELECT screen_id, table_name
         FROM screen_definitions
         WHERE screen_name LIKE $1
           AND table_name IS NOT NULL`,
        [`%${token}%`]
      );
      for (const row of relatedResult.rows) {
        if (row.table_name && row.table_name !== rootTable) {
          contextTables.add(row.table_name);
        }
        collectedScreenIds.add(row.screen_id);
      }
    }

    // ─── B) 모달 참조 체인 재귀 추적 (컬럼 추출용) ───
    await this.collectModalChain(pool, screenId, collectedScreenIds, 0, 4);

    // ─── C) 수집된 테이블의 등록/수정/입력 폼도 포함 (컬럼 추출용) ───
    const allTablesInChain = new Set<string>([rootTable]);
    if (collectedScreenIds.size > 0) {
      const idArr = [...collectedScreenIds];
      const ph = idArr.map((_, i) => `$${i + 1}`).join(",");
      const tableResult = await pool.query(
        `SELECT DISTINCT table_name FROM screen_definitions WHERE screen_id IN (${ph}) AND table_name IS NOT NULL`,
        idArr
      );
      for (const row of tableResult.rows) {
        allTablesInChain.add(row.table_name);
      }
    }

    for (const tbl of allTablesInChain) {
      const formResult = await pool.query(
        `SELECT sd.screen_id
         FROM screen_definitions sd
         JOIN screen_layouts_v3 lv ON sd.screen_id = lv.screen_id
         WHERE sd.table_name = $1
           AND sd.screen_name LIKE $2
           AND (sd.screen_name LIKE '%등록%' OR sd.screen_name LIKE '%수정%' OR sd.screen_name LIKE '%입력%')`,
        [tbl, `${companyPrefix}%`]
      );
      for (const row of formResult.rows) {
        collectedScreenIds.add(row.screen_id);
      }
    }

    // ─── 수집된 화면들의 layout_data에서 테이블별 컬럼 추출 ───
    const visibleColumns = new Map<string, Set<string>>();

    if (collectedScreenIds.size > 0) {
      const screenIdArray = [...collectedScreenIds];
      const placeholders = screenIdArray.map((_, i) => `$${i + 1}`).join(",");

      const layoutResult = await pool.query(
        `SELECT sd.screen_id, sd.table_name, lv.layout_data
         FROM screen_definitions sd
         JOIN screen_layouts_v3 lv ON sd.screen_id = lv.screen_id
         WHERE sd.screen_id IN (${placeholders})
           AND lv.layout_data IS NOT NULL`,
        screenIdArray
      );

      for (const row of layoutResult.rows) {
        const tableName = row.table_name;
        if (!tableName) continue;

        if (tableName !== rootTable) {
          contextTables.add(tableName);
        }

        if (!visibleColumns.has(tableName)) {
          visibleColumns.set(tableName, new Set());
        }
        const colSet = visibleColumns.get(tableName)!;
        this.extractColumnsFromLayout(row.layout_data, colSet);
      }
    }

    logger.info("화면 컨텍스트 추출", {
      screenId,
      screenName,
      collectedScreenCount: collectedScreenIds.size,
      contextTables: [...contextTables],
      visibleColumnsPerTable: Object.fromEntries(
        [...visibleColumns].map(([k, v]) => [k, [...v]])
      ),
    });

    return { contextTables, visibleColumns };
  }

  /**
   * 화면 레이아웃에서 모달 참조(targetScreenId, modalScreenId)를 재귀적으로 추적
   * 최대 depth까지 모달 → 서브모달 → ... 체인을 따라감
   */
  private async collectModalChain(
    pool: any,
    screenId: number,
    collected: Set<number>,
    depth: number,
    maxDepth: number
  ): Promise<void> {
    if (depth >= maxDepth || collected.has(screenId)) return;
    collected.add(screenId);

    const layoutResult = await pool.query(
      `SELECT lv.layout_data
       FROM screen_layouts_v3 lv
       WHERE lv.screen_id = $1 AND lv.layout_data IS NOT NULL`,
      [screenId]
    );

    if (layoutResult.rows.length === 0) return;

    const layoutData = layoutResult.rows[0].layout_data;
    const referencedIds = new Set<number>();
    this.extractModalReferences(layoutData, referencedIds);

    for (const refId of referencedIds) {
      await this.collectModalChain(pool, refId, collected, depth + 1, maxDepth);
    }
  }

  /**
   * layout_data에서 targetScreenId, modalScreenId 값을 추출
   */
  private extractModalReferences(obj: any, refs: Set<number>): void {
    if (!obj || typeof obj !== "object") return;

    if (Array.isArray(obj)) {
      for (const item of obj) {
        if (item && typeof item === "object") {
          this.extractModalReferences(item, refs);
        }
      }
      return;
    }

    for (const key of ["targetScreenId", "modalScreenId"]) {
      const val = obj[key];
      if (val !== undefined && val !== null) {
        const num = Number(val);
        if (!isNaN(num) && num > 0) {
          refs.add(num);
        }
      }
    }

    for (const key of Object.keys(obj)) {
      const val = obj[key];
      if (val && typeof val === "object") {
        this.extractModalReferences(val, refs);
      }
    }
  }

  /**
   * layout_data JSON에서 사용 중인 컬럼명을 재귀적으로 추출
   * - columnName 속성 (v2-input, v2-select 등)
   * - columns[].name 속성 (테이블 리스트, 분할 패널)
   * - dot notation (supplier_mng.supplier_name) 은 JOIN이므로 제외
   */
  private extractColumnsFromLayout(
    layoutData: any,
    colSet: Set<string>
  ): void {
    if (!layoutData || typeof layoutData !== "object") return;

    if (Array.isArray(layoutData)) {
      for (const item of layoutData) {
        if (item && typeof item === "object") {
          this.extractColumnsFromLayout(item, colSet);
        }
      }
      return;
    }

    // columnName 속성 (폼 필드)
    const cn = layoutData.columnName;
    if (cn && typeof cn === "string" && cn.trim() && !cn.includes(".")) {
      colSet.add(cn);
    }

    // columns 배열 (리스트/테이블)
    const cols = layoutData.columns;
    if (Array.isArray(cols)) {
      for (const col of cols) {
        if (col && typeof col === "object") {
          const name = col.name;
          if (name && typeof name === "string" && !name.includes(".")) {
            colSet.add(name);
          }
        }
      }
    }

    // 재귀 탐색
    for (const key of Object.keys(layoutData)) {
      const val = layoutData[key];
      if (val && typeof val === "object") {
        this.extractColumnsFromLayout(val, colSet);
      }
    }
  }

  /**
   * 특정 테이블을 참조하는 자식 테이블 목록 찾기
   * 1차: table_type_columns의 reference_table
   * 2차: pg_constraint FK
   */
  private async findChildTables(
    pool: any,
    parentTable: string,
    companyCode: string
  ): Promise<string[]> {
    // 1) table_type_columns에서 이 테이블을 reference_table로 가진 다른 테이블 검색
    const ttcResult = await pool.query(
      `SELECT DISTINCT table_name
       FROM table_type_columns
       WHERE reference_table = $1
         AND table_name != $1
         AND company_code IN ($2, '*')
       ORDER BY table_name`,
      [parentTable, companyCode]
    );

    if (ttcResult.rows.length > 0) {
      return ttcResult.rows.map((r: any) => r.table_name);
    }

    // 2) fallback: pg_constraint FK
    const fkResult = await pool.query(
      `SELECT DISTINCT c2.relname AS child_table
       FROM pg_constraint con
       JOIN pg_class c1 ON con.confrelid = c1.oid
       JOIN pg_class c2 ON con.conrelid = c2.oid
       JOIN pg_namespace ns ON c2.relnamespace = ns.oid
       WHERE ns.nspname = 'public'
         AND c1.relname = $1
         AND con.contype = 'f'
         AND c2.relname != $1
       ORDER BY c2.relname`,
      [parentTable]
    );

    return fkResult.rows.map((r: any) => r.child_table);
  }

  /**
   * 자식 테이블에서 부모 테이블을 참조하는 FK 컬럼 찾기
   */
  private async findFkColumn(
    pool: any,
    childTable: string,
    parentTable: string,
    companyCode: string
  ): Promise<{ fkColumn: string; refColumn: string } | null> {
    // 1) table_type_columns
    const ttcResult = await pool.query(
      `SELECT column_name, reference_column
       FROM table_type_columns
       WHERE table_name = $1
         AND reference_table = $2
         AND company_code IN ($3, '*')
       ORDER BY CASE WHEN company_code = '*' THEN 1 ELSE 0 END
       LIMIT 1`,
      [childTable, parentTable, companyCode]
    );

    if (ttcResult.rows.length > 0) {
      return {
        fkColumn: ttcResult.rows[0].column_name,
        refColumn: ttcResult.rows[0].reference_column || "id",
      };
    }

    // 2) pg_constraint FK
    const fkResult = await pool.query(
      `SELECT a_child.attname AS fk_column,
              a_parent.attname AS ref_column
       FROM pg_constraint con
       JOIN pg_class c_child ON con.conrelid = c_child.oid
       JOIN pg_class c_parent ON con.confrelid = c_parent.oid
       JOIN pg_namespace ns ON c_child.relnamespace = ns.oid
       JOIN pg_attribute a_child ON a_child.attrelid = c_child.oid AND a_child.attnum = ANY(con.conkey)
       JOIN pg_attribute a_parent ON a_parent.attrelid = c_parent.oid AND a_parent.attnum = ANY(con.confkey)
       WHERE ns.nspname = 'public'
         AND c_child.relname = $1
         AND c_parent.relname = $2
         AND con.contype = 'f'
       LIMIT 1`,
      [childTable, parentTable]
    );

    if (fkResult.rows.length > 0) {
      return {
        fkColumn: fkResult.rows[0].fk_column,
        refColumn: fkResult.rows[0].ref_column,
      };
    }

    return null;
  }

  /**
   * 테이블의 컬럼 목록을 가져와 ColumnDef 배열로 변환
   * 시스템 컬럼과 FK 컬럼은 제외
   */
  private async getTableColumns(
    pool: any,
    tableName: string,
    companyCode: string
  ): Promise<ColumnDef[]> {
    const result = await pool.query(
      `SELECT
         c.column_name,
         c.is_nullable AS db_is_nullable,
         c.column_default,
         COALESCE(NULLIF(ttc.column_label, c.column_name), cl.column_label) AS column_label,
         COALESCE(ttc.reference_table, cl.reference_table) AS reference_table,
         COALESCE(ttc.is_nullable, cl.is_nullable) AS ttc_is_nullable
       FROM information_schema.columns c
       LEFT JOIN table_type_columns cl
         ON c.table_name = cl.table_name AND c.column_name = cl.column_name AND cl.company_code = '*'
       LEFT JOIN table_type_columns ttc
         ON c.table_name = ttc.table_name AND c.column_name = ttc.column_name AND ttc.company_code = $2
       WHERE c.table_schema = 'public' AND c.table_name = $1
       ORDER BY c.ordinal_position`,
      [tableName, companyCode]
    );

    const columns: ColumnDef[] = [];
    for (const row of result.rows) {
      const colName: string = row.column_name;

      // 시스템 컬럼 제외
      if (MultiTableExcelService.SYSTEM_COLUMNS.has(colName)) continue;

      // FK 컬럼 제외
      if (row.reference_table && colName.endsWith("_id")) continue;

      const hasDefault = row.column_default !== null;
      const dbNullable = row.db_is_nullable === "YES";
      const ttcNotNull = row.ttc_is_nullable === "N";
      const isNullable = ttcNotNull ? false : dbNullable;
      const isRequired = !isNullable && !hasDefault;

      columns.push({
        dbColumn: colName,
        excelHeader: row.column_label || colName,
        required: isRequired,
      });
    }

    return columns;
  }

  /**
   * UPSERT 키 감지: UNIQUE 제약조건 → 없으면 insert 모드
   * company_code가 포함된 UNIQUE 제약조건에서 company_code를 제외한 컬럼 사용
   */
  private async detectUpsertKeys(
    pool: any,
    tableName: string
  ): Promise<{ upsertMode: "upsert" | "insert"; upsertKeyColumns: string[] }> {
    const result = await pool.query(
      `SELECT con.conname,
              array_agg(a.attname ORDER BY x.n) AS columns
       FROM pg_constraint con
       JOIN pg_class c ON con.conrelid = c.oid
       JOIN pg_namespace ns ON c.relnamespace = ns.oid
       CROSS JOIN LATERAL unnest(con.conkey) WITH ORDINALITY AS x(attnum, n)
       JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = x.attnum
       WHERE ns.nspname = 'public'
         AND c.relname = $1
         AND con.contype = 'u'
       GROUP BY con.conname
       ORDER BY con.conname
       LIMIT 1`,
      [tableName]
    );

    if (result.rows.length > 0) {
      let rawCols = result.rows[0].columns;
      // pg 드라이버가 array_agg를 문자열로 반환할 수 있음
      if (typeof rawCols === "string") {
        rawCols = rawCols.replace(/[{}]/g, "").split(",").map((s: string) => s.trim());
      }
      const cols: string[] = (rawCols as string[]).filter(
        (c: string) => c !== "company_code"
      );
      if (cols.length > 0) {
        return { upsertMode: "upsert", upsertKeyColumns: cols };
      }
    }

    return { upsertMode: "insert", upsertKeyColumns: [] };
  }

  /**
   * 테이블의 한글 라벨 가져오기
   */
  private async getTableLabel(
    pool: any,
    tableName: string,
    companyCode: string
  ): Promise<string> {
    const result = await pool.query(
      `SELECT COALESCE(
         (SELECT table_label FROM table_labels WHERE table_name = $1 LIMIT 1),
         $1
       ) AS label`,
      [tableName]
    );
    return result.rows[0]?.label || tableName;
  }
}

export const multiTableExcelService = new MultiTableExcelService();
