import { Router, Request, Response } from "express";
import { getPool } from "../database/db";
import logger from "../utils/logger";
import { authenticateToken } from "../middleware/authMiddleware";
import { numberingRuleService } from "../services/numberingRuleService";

const router = Router();

// SQL 인젝션 방지: 테이블명/컬럼명 패턴 검증
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;

function isSafeIdentifier(name: string): boolean {
  return SAFE_IDENTIFIER.test(name);
}

interface AutoGenMappingInfo {
  numberingRuleId: string;
  targetColumn: string;
  showResultModal?: boolean;
  shareAcrossItems?: boolean;
}

interface HiddenMappingInfo {
  valueSource: "json_extract" | "db_column" | "static";
  targetColumn: string;
  staticValue?: string;
  sourceJsonColumn?: string;
  sourceJsonKey?: string;
  sourceDbColumn?: string;
}

interface MappingInfo {
  targetTable: string;
  columnMapping: Record<string, string>;
  autoGenMappings?: AutoGenMappingInfo[];
  hiddenMappings?: HiddenMappingInfo[];
}

interface StatusConditionRule {
  whenColumn: string;
  operator: string;
  whenValue: string;
  thenValue: string;
}

interface ConditionalValueRule {
  conditions: StatusConditionRule[];
  defaultValue?: string;
}

interface StatusChangeRuleBody {
  targetTable: string;
  targetColumn: string;
  lookupMode?: "auto" | "manual";
  manualItemField?: string;
  manualPkColumn?: string;
  valueType: "fixed" | "conditional";
  fixedValue?: string;
  conditionalValue?: ConditionalValueRule;
  // 하위호환: 기존 형식
  value?: string;
  condition?: string;
}

interface ExecuteActionBody {
  action?: string;
  tasks?: TaskBody[];
  data: {
    items?: Record<string, unknown>[];
    fieldValues?: Record<string, unknown>;
  };
  mappings?: {
    cardList?: MappingInfo | null;
    field?: MappingInfo | null;
  };
  statusChanges?: StatusChangeRuleBody[];
  cartChanges?: {
    toCreate?: Record<string, unknown>[];
    toUpdate?: Record<string, unknown>[];
    toDelete?: (string | number)[];
  };
}

interface TaskBody {
  id: string;
  type: string;
  targetTable?: string;
  targetColumn?: string;
  operationType?: "assign" | "add" | "subtract" | "multiply" | "divide" | "conditional" | "db-conditional";
  valueSource?: "fixed" | "linked" | "reference";
  fixedValue?: string;
  sourceField?: string;
  referenceTable?: string;
  referenceColumn?: string;
  referenceJoinKey?: string;
  conditionalValue?: ConditionalValueRule;
  // db-conditional 전용 (DB 컬럼 간 비교 후 값 판정)
  compareColumn?: string;
  compareOperator?: "=" | "!=" | ">" | "<" | ">=" | "<=";
  compareWith?: string;
  dbThenValue?: string;
  dbElseValue?: string;
  lookupMode?: "auto" | "manual";
  manualItemField?: string;
  manualPkColumn?: string;
  cartScreenId?: string;
  preCondition?: {
    column: string;
    expectedValue: string;
    failMessage?: string;
  };
}

function resolveStatusValue(
  valueType: string,
  fixedValue: string,
  conditionalValue: ConditionalValueRule | undefined,
  item: Record<string, unknown>
): string {
  if (valueType !== "conditional" || !conditionalValue) return fixedValue;

  for (const cond of conditionalValue.conditions) {
    const actual = String(item[cond.whenColumn] ?? "");
    const expected = cond.whenValue;
    let match = false;

    switch (cond.operator) {
      case "=": match = actual === expected; break;
      case "!=": match = actual !== expected; break;
      case ">": match = parseFloat(actual) > parseFloat(expected); break;
      case "<": match = parseFloat(actual) < parseFloat(expected); break;
      case ">=": match = parseFloat(actual) >= parseFloat(expected); break;
      case "<=": match = parseFloat(actual) <= parseFloat(expected); break;
      default: match = actual === expected;
    }

    if (match) return cond.thenValue;
  }

  return conditionalValue.defaultValue ?? fixedValue;
}

router.post("/execute-action", authenticateToken, async (req: Request, res: Response) => {
  const pool = getPool();
  const client = await pool.connect();

  try {
    const companyCode = (req as any).user?.companyCode;
    const userId = (req as any).user?.userId;
    if (!companyCode) {
      return res.status(401).json({ success: false, message: "인증 정보가 없습니다." });
    }

    const { action, tasks, data, mappings, statusChanges, cartChanges } = req.body as ExecuteActionBody;
    const items = data?.items ?? [];
    const fieldValues = data?.fieldValues ?? {};

    logger.info("[pop/execute-action] 요청", {
      action: action ?? "task-list",
      companyCode,
      userId,
      itemCount: items.length,
      hasFieldValues: Object.keys(fieldValues).length > 0,
      hasMappings: !!mappings,
      statusChangeCount: statusChanges?.length ?? 0,
      taskCount: tasks?.length ?? 0,
      hasCartChanges: !!cartChanges,
    });

    await client.query("BEGIN");

    let processedCount = 0;
    let insertedCount = 0;
    let deletedCount = 0;
    const generatedCodes: Array<{ targetColumn: string; code: string; showResultModal?: boolean }> = [];

    // ======== v2: tasks 배열 기반 처리 ========
    if (tasks && tasks.length > 0) {
      for (const task of tasks) {
        switch (task.type) {
          case "data-save": {
            // 매핑 기반 INSERT (기존 inbound-confirm INSERT 로직 재사용)
            const cardMapping = mappings?.cardList;
            const fieldMapping = mappings?.field;

            if (cardMapping?.targetTable && Object.keys(cardMapping.columnMapping).length > 0) {
              if (!isSafeIdentifier(cardMapping.targetTable)) {
                throw new Error(`유효하지 않은 테이블명: ${cardMapping.targetTable}`);
              }

              const allAutoGen = [
                ...(fieldMapping?.autoGenMappings ?? []),
                ...(cardMapping?.autoGenMappings ?? []),
              ];

              // 일괄 채번: shareAcrossItems=true인 매핑은 루프 전 1회만 채번
              const sharedCodes: Record<string, string> = {};
              for (const ag of allAutoGen) {
                if (!ag.shareAcrossItems) continue;
                if (!ag.numberingRuleId || !ag.targetColumn) continue;
                if (!isSafeIdentifier(ag.targetColumn)) continue;
                try {
                  const code = await numberingRuleService.allocateCode(
                    ag.numberingRuleId, companyCode, { ...fieldValues, ...(items[0] ?? {}) },
                  );
                  sharedCodes[ag.targetColumn] = code;
                  generatedCodes.push({ targetColumn: ag.targetColumn, code, showResultModal: ag.showResultModal ?? false });
                  logger.info("[pop/execute-action] 일괄 채번 완료", {
                    ruleId: ag.numberingRuleId, targetColumn: ag.targetColumn, code,
                  });
                } catch (err: any) {
                  logger.error("[pop/execute-action] 일괄 채번 실패", { ruleId: ag.numberingRuleId, error: err.message });
                }
              }

              for (const item of items) {
                const columns: string[] = ["company_code"];
                const values: unknown[] = [companyCode];

                for (const [sourceField, targetColumn] of Object.entries(cardMapping.columnMapping)) {
                  if (!isSafeIdentifier(targetColumn)) continue;
                  columns.push(`"${targetColumn}"`);
                  values.push(item[sourceField] ?? null);
                }

                if (fieldMapping?.targetTable === cardMapping.targetTable) {
                  for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
                    if (!isSafeIdentifier(targetColumn)) continue;
                    if (columns.includes(`"${targetColumn}"`)) continue;
                    columns.push(`"${targetColumn}"`);
                    values.push(fieldValues[sourceField] ?? null);
                  }
                }

                const allHidden = [
                  ...(fieldMapping?.hiddenMappings ?? []),
                  ...(cardMapping?.hiddenMappings ?? []),
                ];
                for (const hm of allHidden) {
                  if (!hm.targetColumn || !isSafeIdentifier(hm.targetColumn)) continue;
                  if (columns.includes(`"${hm.targetColumn}"`)) continue;
                  let value: unknown = null;
                  if (hm.valueSource === "static") {
                    value = hm.staticValue ?? null;
                  } else if (hm.valueSource === "json_extract" && hm.sourceJsonColumn && hm.sourceJsonKey) {
                    const jsonCol = item[hm.sourceJsonColumn];
                    if (typeof jsonCol === "object" && jsonCol !== null) {
                      value = (jsonCol as Record<string, unknown>)[hm.sourceJsonKey] ?? null;
                    } else if (typeof jsonCol === "string") {
                      try { value = JSON.parse(jsonCol)[hm.sourceJsonKey] ?? null; } catch { /* skip */ }
                    }
                  } else if (hm.valueSource === "db_column" && hm.sourceDbColumn) {
                    value = item[hm.sourceDbColumn] ?? fieldValues[hm.sourceDbColumn] ?? null;
                  }
                  columns.push(`"${hm.targetColumn}"`);
                  values.push(value);
                }

                for (const ag of allAutoGen) {
                  if (!ag.numberingRuleId || !ag.targetColumn) continue;
                  if (!isSafeIdentifier(ag.targetColumn)) continue;
                  if (columns.includes(`"${ag.targetColumn}"`)) continue;

                  if (ag.shareAcrossItems && sharedCodes[ag.targetColumn]) {
                    columns.push(`"${ag.targetColumn}"`);
                    values.push(sharedCodes[ag.targetColumn]);
                  } else if (!ag.shareAcrossItems) {
                    try {
                      const generatedCode = await numberingRuleService.allocateCode(
                        ag.numberingRuleId, companyCode, { ...fieldValues, ...item },
                      );
                      columns.push(`"${ag.targetColumn}"`);
                      values.push(generatedCode);
                      generatedCodes.push({ targetColumn: ag.targetColumn, code: generatedCode, showResultModal: ag.showResultModal ?? false });
                    } catch (err: any) {
                      logger.error("[pop/execute-action] 채번 실패", { ruleId: ag.numberingRuleId, error: err.message });
                    }
                  }
                }

                if (!columns.includes('"created_date"')) {
                  columns.push('"created_date"');
                  values.push(new Date().toISOString());
                }
                if (!columns.includes('"updated_date"')) {
                  columns.push('"updated_date"');
                  values.push(new Date().toISOString());
                }
                if (!columns.includes('"writer"') && userId) {
                  columns.push('"writer"');
                  values.push(userId);
                }

                if (columns.length > 1) {
                  const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
                  await client.query(
                    `INSERT INTO "${cardMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`,
                    values,
                  );
                  insertedCount++;
                }
              }
            }
            break;
          }

          case "data-update": {
            if (!task.targetTable || !task.targetColumn) break;
            if (!isSafeIdentifier(task.targetTable) || !isSafeIdentifier(task.targetColumn)) break;

            const opType = task.operationType ?? "assign";
            const valSource = task.valueSource ?? "fixed";
            const lookupMode = task.lookupMode ?? "auto";

            let itemField: string;
            let pkColumn: string;

            if (lookupMode === "manual" && task.manualItemField && task.manualPkColumn) {
              if (!isSafeIdentifier(task.manualPkColumn)) break;
              itemField = task.manualItemField;
              pkColumn = task.manualPkColumn;
            } else if (task.targetTable === "cart_items") {
              itemField = "__cart_id";
              pkColumn = "id";
            } else {
              itemField = "__cart_row_key";
              const pkResult = await client.query(
                `SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = $1::regclass AND i.indisprimary`,
                [task.targetTable],
              );
              pkColumn = pkResult.rows[0]?.attname || "id";
            }

            const lookupValues = items.map((item) => item[itemField] ?? item[itemField.replace(/^__cart_/, "")]).filter(Boolean);
            if (lookupValues.length === 0) break;

            if (opType === "conditional" && task.conditionalValue) {
              for (let i = 0; i < lookupValues.length; i++) {
                const item = items[i] ?? {};
                const resolved = resolveStatusValue("conditional", task.fixedValue ?? "", task.conditionalValue, item);
                const autoUpdated = task.targetColumn !== "updated_date" ? `, "updated_date" = NOW()` : "";
                let condWhere = `WHERE company_code = $2 AND "${pkColumn}" = $3`;
                const condParams: unknown[] = [resolved, companyCode, lookupValues[i]];
                if (task.preCondition?.column && task.preCondition?.expectedValue) {
                  if (!isSafeIdentifier(task.preCondition.column)) throw new Error(`유효하지 않은 preCondition 컬럼명: ${task.preCondition.column}`);
                  condWhere += ` AND "${task.preCondition.column}" = $4`;
                  condParams.push(task.preCondition.expectedValue);
                }
                const condResult = await client.query(
                  `UPDATE "${task.targetTable}" SET "${task.targetColumn}" = $1${autoUpdated} ${condWhere}`,
                  condParams,
                );
                if (task.preCondition && condResult.rowCount === 0) {
                  const err = new Error(task.preCondition.failMessage || "조건이 일치하지 않아 처리할 수 없습니다.");
                  (err as any).isPreConditionFail = true;
                  throw err;
                }
                processedCount++;
              }
            } else if (opType === "db-conditional") {
              if (task.preCondition) {
                logger.warn("[pop/execute-action] db-conditional에는 preCondition 미지원, 무시됨", {
                  taskId: task.id, preCondition: task.preCondition,
                });
              }
              if (!task.compareColumn || !task.compareOperator || !task.compareWith) break;
              if (!isSafeIdentifier(task.compareColumn) || !isSafeIdentifier(task.compareWith)) break;

              const thenVal = task.dbThenValue ?? "";
              const elseVal = task.dbElseValue ?? "";
              const op = task.compareOperator;
              const validOps = ["=", "!=", ">", "<", ">=", "<="];
              if (!validOps.includes(op)) break;

              const caseSql = `CASE WHEN COALESCE("${task.compareColumn}"::numeric, 0) ${op} COALESCE("${task.compareWith}"::numeric, 0) THEN $1 ELSE $2 END`;

              const autoUpdatedDb = task.targetColumn !== "updated_date" ? `, "updated_date" = NOW()` : "";
              const placeholders = lookupValues.map((_, i) => `$${i + 4}`).join(", ");
              await client.query(
                `UPDATE "${task.targetTable}" SET "${task.targetColumn}" = ${caseSql}${autoUpdatedDb} WHERE company_code = $3 AND "${pkColumn}" IN (${placeholders})`,
                [thenVal, elseVal, companyCode, ...lookupValues],
              );
              processedCount += lookupValues.length;
            } else {
              for (let i = 0; i < lookupValues.length; i++) {
                const item = items[i] ?? {};
                let value: unknown;

                if (valSource === "linked") {
                  value = item[task.sourceField ?? ""] ?? null;
                } else {
                  const raw = task.fixedValue ?? "";
                  if (raw === "__CURRENT_USER__") {
                    value = userId;
                  } else if (raw === "__CURRENT_TIME__") {
                    value = new Date().toISOString();
                  } else {
                    value = raw;
                  }
                }

                let setSql: string;
                if (opType === "add") {
                  setSql = `"${task.targetColumn}" = COALESCE("${task.targetColumn}"::numeric, 0) + $1::numeric`;
                } else if (opType === "subtract") {
                  setSql = `"${task.targetColumn}" = COALESCE("${task.targetColumn}"::numeric, 0) - $1::numeric`;
                } else if (opType === "multiply") {
                  setSql = `"${task.targetColumn}" = COALESCE("${task.targetColumn}"::numeric, 0) * $1::numeric`;
                } else if (opType === "divide") {
                  setSql = `"${task.targetColumn}" = CASE WHEN $1::numeric = 0 THEN COALESCE("${task.targetColumn}"::numeric, 0) ELSE COALESCE("${task.targetColumn}"::numeric, 0) / $1::numeric END`;
                } else {
                  setSql = `"${task.targetColumn}" = $1`;
                }

                const autoUpdatedDate = task.targetColumn !== "updated_date" ? `, "updated_date" = NOW()` : "";
                let whereSql = `WHERE company_code = $2 AND "${pkColumn}" = $3`;
                const queryParams: unknown[] = [value, companyCode, lookupValues[i]];
                if (task.preCondition?.column && task.preCondition?.expectedValue) {
                  if (!isSafeIdentifier(task.preCondition.column)) {
                    throw new Error(`유효하지 않은 preCondition 컬럼명: ${task.preCondition.column}`);
                  }
                  whereSql += ` AND "${task.preCondition.column}" = $4`;
                  queryParams.push(task.preCondition.expectedValue);
                }
                const updateResult = await client.query(
                  `UPDATE "${task.targetTable}" SET ${setSql}${autoUpdatedDate} ${whereSql}`,
                  queryParams,
                );
                if (task.preCondition && updateResult.rowCount === 0) {
                  const err = new Error(task.preCondition.failMessage || "조건이 일치하지 않아 처리할 수 없습니다.");
                  (err as any).isPreConditionFail = true;
                  throw err;
                }
                processedCount++;
              }
            }

            logger.info("[pop/execute-action] data-update 실행", {
              table: task.targetTable,
              column: task.targetColumn,
              opType,
              count: lookupValues.length,
            });
            break;
          }

          case "data-delete": {
            if (!task.targetTable) break;
            if (!isSafeIdentifier(task.targetTable)) break;

            const pkResult = await client.query(
              `SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = $1::regclass AND i.indisprimary`,
              [task.targetTable],
            );
            const pkCol = pkResult.rows[0]?.attname || "id";
            const deleteKeys = items.map((item) => item[pkCol] ?? item["id"]).filter(Boolean);

            if (deleteKeys.length > 0) {
              const placeholders = deleteKeys.map((_, i) => `$${i + 2}`).join(", ");
              await client.query(
                `DELETE FROM "${task.targetTable}" WHERE company_code = $1 AND "${pkCol}" IN (${placeholders})`,
                [companyCode, ...deleteKeys],
              );
              deletedCount += deleteKeys.length;
            }
            break;
          }

          case "cart-save": {
            // cartChanges 처리 (M-9에서 확장)
            if (!cartChanges) break;
            const { toCreate, toUpdate, toDelete } = cartChanges;

            if (toCreate && toCreate.length > 0) {
              for (const item of toCreate) {
                const cols = Object.keys(item).filter(isSafeIdentifier);
                if (cols.length === 0) continue;
                const allCols = ["company_code", ...cols.map((c) => `"${c}"`)];
                const allVals = [companyCode, ...cols.map((c) => item[c])];
                const placeholders = allVals.map((_, i) => `$${i + 1}`).join(", ");
                await client.query(
                  `INSERT INTO "cart_items" (${allCols.join(", ")}) VALUES (${placeholders})`,
                  allVals,
                );
                insertedCount++;
              }
            }

            if (toUpdate && toUpdate.length > 0) {
              for (const item of toUpdate) {
                const id = item.id;
                if (!id) continue;
                const cols = Object.keys(item).filter((c) => c !== "id" && isSafeIdentifier(c));
                if (cols.length === 0) continue;
                const setClauses = cols.map((c, i) => `"${c}" = $${i + 3}`).join(", ");
                await client.query(
                  `UPDATE "cart_items" SET ${setClauses} WHERE id = $1 AND company_code = $2`,
                  [id, companyCode, ...cols.map((c) => item[c])],
                );
                processedCount++;
              }
            }

            if (toDelete && toDelete.length > 0) {
              const placeholders = toDelete.map((_, i) => `$${i + 2}`).join(", ");
              await client.query(
                `DELETE FROM "cart_items" WHERE company_code = $1 AND id IN (${placeholders})`,
                [companyCode, ...toDelete],
              );
              deletedCount += toDelete.length;
            }

            logger.info("[pop/execute-action] cart-save 실행", {
              created: toCreate?.length ?? 0,
              updated: toUpdate?.length ?? 0,
              deleted: toDelete?.length ?? 0,
            });
            break;
          }

          default:
            logger.warn("[pop/execute-action] 프론트 전용 작업 타입, 백엔드 무시", { type: task.type });
        }
      }
    }
    // ======== v1 레거시: action 기반 처리 ========
    else if (action === "inbound-confirm") {
      // 1. 매핑 기반 INSERT (장바구니 데이터 -> 대상 테이블)
      const cardMapping = mappings?.cardList;
      const fieldMapping = mappings?.field;

      if (cardMapping?.targetTable && Object.keys(cardMapping.columnMapping).length > 0) {
        if (!isSafeIdentifier(cardMapping.targetTable)) {
          throw new Error(`유효하지 않은 테이블명: ${cardMapping.targetTable}`);
        }

        const allAutoGen = [
          ...(fieldMapping?.autoGenMappings ?? []),
          ...(cardMapping?.autoGenMappings ?? []),
        ];

        // 일괄 채번: shareAcrossItems=true인 매핑은 루프 전 1회만 채번
        const sharedCodes: Record<string, string> = {};
        for (const ag of allAutoGen) {
          if (!ag.shareAcrossItems) continue;
          if (!ag.numberingRuleId || !ag.targetColumn) continue;
          if (!isSafeIdentifier(ag.targetColumn)) continue;
          try {
            const code = await numberingRuleService.allocateCode(
              ag.numberingRuleId, companyCode, { ...fieldValues, ...(items[0] ?? {}) },
            );
            sharedCodes[ag.targetColumn] = code;
            generatedCodes.push({ targetColumn: ag.targetColumn, code, showResultModal: ag.showResultModal ?? false });
            logger.info("[pop/execute-action] 일괄 채번 완료", {
              ruleId: ag.numberingRuleId, targetColumn: ag.targetColumn, code,
            });
          } catch (err: any) {
            logger.error("[pop/execute-action] 일괄 채번 실패", { ruleId: ag.numberingRuleId, error: err.message });
          }
        }

        for (const item of items) {
          const columns: string[] = ["company_code"];
          const values: unknown[] = [companyCode];

          for (const [sourceField, targetColumn] of Object.entries(cardMapping.columnMapping)) {
            if (!isSafeIdentifier(targetColumn)) continue;
            columns.push(`"${targetColumn}"`);
            values.push(item[sourceField] ?? null);
          }

          if (fieldMapping?.targetTable === cardMapping.targetTable) {
            for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
              if (!isSafeIdentifier(targetColumn)) continue;
              if (columns.includes(`"${targetColumn}"`)) continue;
              columns.push(`"${targetColumn}"`);
              values.push(fieldValues[sourceField] ?? null);
            }
          }

          const allHidden = [
            ...(fieldMapping?.hiddenMappings ?? []),
            ...(cardMapping?.hiddenMappings ?? []),
          ];
          for (const hm of allHidden) {
            if (!hm.targetColumn || !isSafeIdentifier(hm.targetColumn)) continue;
            if (columns.includes(`"${hm.targetColumn}"`)) continue;

            let value: unknown = null;
            if (hm.valueSource === "static") {
              value = hm.staticValue ?? null;
            } else if (hm.valueSource === "json_extract" && hm.sourceJsonColumn && hm.sourceJsonKey) {
              const jsonCol = item[hm.sourceJsonColumn];
              if (typeof jsonCol === "object" && jsonCol !== null) {
                value = (jsonCol as Record<string, unknown>)[hm.sourceJsonKey] ?? null;
              } else if (typeof jsonCol === "string") {
                try { value = JSON.parse(jsonCol)[hm.sourceJsonKey] ?? null; } catch { /* skip */ }
              }
            } else if (hm.valueSource === "db_column" && hm.sourceDbColumn) {
              value = item[hm.sourceDbColumn] ?? fieldValues[hm.sourceDbColumn] ?? null;
            }

            columns.push(`"${hm.targetColumn}"`);
            values.push(value);
          }

          for (const ag of allAutoGen) {
            if (!ag.numberingRuleId || !ag.targetColumn) continue;
            if (!isSafeIdentifier(ag.targetColumn)) continue;
            if (columns.includes(`"${ag.targetColumn}"`)) continue;

            if (ag.shareAcrossItems && sharedCodes[ag.targetColumn]) {
              columns.push(`"${ag.targetColumn}"`);
              values.push(sharedCodes[ag.targetColumn]);
            } else if (!ag.shareAcrossItems) {
              try {
                const generatedCode = await numberingRuleService.allocateCode(
                  ag.numberingRuleId, companyCode, { ...fieldValues, ...item },
                );
                columns.push(`"${ag.targetColumn}"`);
                values.push(generatedCode);
                generatedCodes.push({ targetColumn: ag.targetColumn, code: generatedCode, showResultModal: ag.showResultModal ?? false });
                logger.info("[pop/execute-action] 채번 완료", {
                  ruleId: ag.numberingRuleId, targetColumn: ag.targetColumn, generatedCode,
                });
              } catch (err: any) {
                logger.error("[pop/execute-action] 채번 실패", { ruleId: ag.numberingRuleId, error: err.message });
              }
            }
          }

          if (!columns.includes('"created_date"')) {
            columns.push('"created_date"');
            values.push(new Date().toISOString());
          }
          if (!columns.includes('"updated_date"')) {
            columns.push('"updated_date"');
            values.push(new Date().toISOString());
          }
          if (!columns.includes('"writer"') && userId) {
            columns.push('"writer"');
            values.push(userId);
          }

          if (columns.length > 1) {
            const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
            const sql = `INSERT INTO "${cardMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`;

            logger.info("[pop/execute-action] INSERT 실행", {
              table: cardMapping.targetTable,
              columnCount: columns.length,
            });

            await client.query(sql, values);
            insertedCount++;
          }
        }
      }

      if (
        fieldMapping?.targetTable &&
        Object.keys(fieldMapping.columnMapping).length > 0 &&
        fieldMapping.targetTable !== cardMapping?.targetTable
      ) {
        if (!isSafeIdentifier(fieldMapping.targetTable)) {
          throw new Error(`유효하지 않은 테이블명: ${fieldMapping.targetTable}`);
        }

        const columns: string[] = ["company_code"];
        const values: unknown[] = [companyCode];

        for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
          if (!isSafeIdentifier(targetColumn)) continue;
          columns.push(`"${targetColumn}"`);
          values.push(fieldValues[sourceField] ?? null);
        }

        if (!columns.includes('"created_date"')) {
          columns.push('"created_date"');
          values.push(new Date().toISOString());
        }
        if (!columns.includes('"updated_date"')) {
          columns.push('"updated_date"');
          values.push(new Date().toISOString());
        }
        if (!columns.includes('"writer"') && userId) {
          columns.push('"writer"');
          values.push(userId);
        }

        if (columns.length > 1) {
          const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
          const sql = `INSERT INTO "${fieldMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`;
          await client.query(sql, values);
        }
      }

      // 2. 상태 변경 규칙 실행 (설정 기반)
      if (statusChanges && statusChanges.length > 0) {
        for (const rule of statusChanges) {
          if (!rule.targetTable || !rule.targetColumn) continue;
          if (!isSafeIdentifier(rule.targetTable) || !isSafeIdentifier(rule.targetColumn)) {
            logger.warn("[pop/execute-action] 유효하지 않은 식별자, 건너뜀", { table: rule.targetTable, column: rule.targetColumn });
            continue;
          }

          const valueType = rule.valueType ?? "fixed";
          const fixedValue = rule.fixedValue ?? rule.value ?? "";
          const lookupMode = rule.lookupMode ?? "auto";

          // 조회 키 결정: 아이템 필드(itemField) -> 대상 테이블 PK 컬럼(pkColumn)
          let itemField: string;
          let pkColumn: string;

          if (lookupMode === "manual" && rule.manualItemField && rule.manualPkColumn) {
            if (!isSafeIdentifier(rule.manualPkColumn)) {
              logger.warn("[pop/execute-action] 수동 PK 컬럼 유효하지 않음", { manualPkColumn: rule.manualPkColumn });
              continue;
            }
            itemField = rule.manualItemField;
            pkColumn = rule.manualPkColumn;
            logger.info("[pop/execute-action] 수동 조회 키", { itemField, pkColumn, table: rule.targetTable });
          } else if (rule.targetTable === "cart_items") {
            itemField = "__cart_id";
            pkColumn = "id";
          } else {
            itemField = "__cart_row_key";
            const pkResult = await client.query(
              `SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = $1::regclass AND i.indisprimary`,
              [rule.targetTable]
            );
            pkColumn = pkResult.rows[0]?.attname || "id";
          }

          const lookupValues = items.map((item) => item[itemField] ?? item[itemField.replace(/^__cart_/, "")]).filter(Boolean);
          if (lookupValues.length === 0) {
            logger.warn("[pop/execute-action] 조회 키 값 없음, 건너뜀", { table: rule.targetTable, itemField });
            continue;
          }

          if (valueType === "fixed") {
            const autoUpd = rule.targetColumn !== "updated_date" ? `, "updated_date" = NOW()` : "";
            const placeholders = lookupValues.map((_, i) => `$${i + 3}`).join(", ");
            const sql = `UPDATE "${rule.targetTable}" SET "${rule.targetColumn}" = $1${autoUpd} WHERE company_code = $2 AND "${pkColumn}" IN (${placeholders})`;
            await client.query(sql, [fixedValue, companyCode, ...lookupValues]);
            processedCount += lookupValues.length;
          } else {
            for (let i = 0; i < lookupValues.length; i++) {
              const item = items[i] ?? {};
              const resolvedValue = resolveStatusValue(valueType, fixedValue, rule.conditionalValue, item);
              const autoUpd2 = rule.targetColumn !== "updated_date" ? `, "updated_date" = NOW()` : "";
              await client.query(
                `UPDATE "${rule.targetTable}" SET "${rule.targetColumn}" = $1${autoUpd2} WHERE company_code = $2 AND "${pkColumn}" = $3`,
                [resolvedValue, companyCode, lookupValues[i]]
              );
              processedCount++;
            }
          }

          logger.info("[pop/execute-action] 상태 변경 실행", {
            table: rule.targetTable, column: rule.targetColumn, lookupMode, itemField, pkColumn, count: lookupValues.length,
          });
        }
      }
    }

    await client.query("COMMIT");

    logger.info("[pop/execute-action] 완료", {
      action: action ?? "task-list",
      companyCode,
      processedCount,
      insertedCount,
      deletedCount,
    });

    return res.json({
      success: true,
      message: `${processedCount}건 처리${insertedCount > 0 ? `, ${insertedCount}건 생성` : ""}${deletedCount > 0 ? `, ${deletedCount}건 삭제` : ""}`,
      data: { processedCount, insertedCount, deletedCount, generatedCodes },
    });
  } catch (error: any) {
    await client.query("ROLLBACK");

    if (error.isPreConditionFail) {
      logger.warn("[pop/execute-action] preCondition 실패", { message: error.message });
      return res.status(409).json({
        success: false,
        message: error.message,
        errorCode: "PRE_CONDITION_FAIL",
      });
    }

    logger.error("[pop/execute-action] 오류:", error);
    return res.status(500).json({
      success: false,
      message: error.message || "처리 중 오류가 발생했습니다.",
    });
  } finally {
    client.release();
  }
});

export default router;
