/**
 * 채번 규칙 관리 서비스
 */

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

interface NumberingRulePart {
  id?: number;
  order: number;
  partType: string;
  generationMethod: string;
  autoConfig?: any;
  manualConfig?: any;
  generatedValue?: string;
  separatorAfter?: string;
}

/**
 * 파트 배열에서 autoConfig.separatorAfter를 파트 레벨로 추출
 */
function extractSeparatorAfterFromParts(parts: any[]): any[] {
  return parts.map((part) => {
    if (part.autoConfig?.separatorAfter !== undefined) {
      part.separatorAfter = part.autoConfig.separatorAfter;
    }
    return part;
  });
}

/**
 * 파트별 개별 구분자를 사용하여 코드 결합
 * 마지막 파트의 separatorAfter는 무시됨
 */
function joinPartsWithSeparators(partValues: string[], sortedParts: any[], globalSeparator: string): string {
  let result = "";
  partValues.forEach((val, idx) => {
    result += val;
    if (idx < partValues.length - 1) {
      const sep = sortedParts[idx].separatorAfter ?? sortedParts[idx].autoConfig?.separatorAfter ?? globalSeparator;
      result += sep;
    }
  });
  return result;
}

interface NumberingRuleConfig {
  ruleId: string;
  ruleName: string;
  description?: string;
  parts: NumberingRulePart[];
  separator?: string;
  resetPeriod?: string;
  currentSequence?: number;
  tableName?: string;
  columnName?: string;
  companyCode?: string;
  menuObjid?: number;
  scopeType?: string;
  // 카테고리 조건
  categoryColumn?: string;
  categoryValueId?: number;
  categoryValueLabel?: string; // 조회 시 조인해서 가져옴
  createdAt?: string;
  updatedAt?: string;
  createdBy?: string;
}

class NumberingRuleService {
  /**
   * 순번(sequence) 파트를 제외한 나머지 파트 값들을 조합해 prefix_key 생성
   * 이 키가 같으면 같은 순번 계열, 다르면 001부터 재시작
   */
  private async buildPrefixKey(
    rule: NumberingRuleConfig,
    formData?: Record<string, any>
  ): Promise<string> {
    const sortedParts = [...rule.parts].sort((a: any, b: any) => a.order - b.order);
    const prefixParts: string[] = [];

    for (const part of sortedParts) {
      if (part.partType === "sequence") continue;

      if (part.generationMethod === "manual") {
        // 수동 입력 파트는 prefix에서 제외 (값이 매번 달라질 수 있으므로)
        continue;
      }

      const autoConfig = (part as any).autoConfig || {};

      switch (part.partType) {
        case "date": {
          const dateFormat = autoConfig.dateFormat || "YYYYMMDD";
          if (autoConfig.useColumnValue && autoConfig.sourceColumnName && formData) {
            const columnValue = formData[autoConfig.sourceColumnName];
            if (columnValue) {
              const dateValue = columnValue instanceof Date ? columnValue : new Date(columnValue);
              if (!isNaN(dateValue.getTime())) {
                prefixParts.push(this.formatDate(dateValue, dateFormat));
                break;
              }
            }
          }
          prefixParts.push(this.formatDate(new Date(), dateFormat));
          break;
        }

        case "text": {
          prefixParts.push(autoConfig.textValue || "TEXT");
          break;
        }

        case "number": {
          const length = autoConfig.numberLength || 3;
          const value = autoConfig.numberValue || 1;
          prefixParts.push(String(value).padStart(length, "0"));
          break;
        }

        case "category": {
          const categoryKey = autoConfig.categoryKey;
          const categoryMappings = autoConfig.categoryMappings || [];

          if (!categoryKey || !formData) {
            prefixParts.push("");
            break;
          }

          const columnName = categoryKey.includes(".")
            ? categoryKey.split(".")[1]
            : categoryKey;
          const selectedValue = formData[columnName];

          if (!selectedValue) {
            prefixParts.push("");
            break;
          }

          const selectedValueStr = String(selectedValue);
          let mapping = categoryMappings.find((m: any) => {
            if (m.categoryValueId?.toString() === selectedValueStr) return true;
            if (m.categoryValueCode && m.categoryValueCode === selectedValueStr) return true;
            if (m.categoryValueLabel === selectedValueStr) return true;
            return false;
          });

          if (!mapping) {
            try {
              const pool = getPool();
              const [catTableName, catColumnName] = categoryKey.includes(".")
                ? categoryKey.split(".")
                : [categoryKey, categoryKey];
              const cvResult = await pool.query(
                `SELECT value_id, value_label FROM category_values 
                 WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`,
                [catTableName, catColumnName, selectedValueStr]
              );
              if (cvResult.rows.length > 0) {
                const resolvedId = cvResult.rows[0].value_id;
                const resolvedLabel = cvResult.rows[0].value_label;
                mapping = categoryMappings.find((m: any) => {
                  if (m.categoryValueId?.toString() === String(resolvedId)) return true;
                  if (m.categoryValueLabel === resolvedLabel) return true;
                  return false;
                });
              }
            } catch { /* ignore */ }
          }

          prefixParts.push(mapping?.format || selectedValueStr);
          break;
        }

        default:
          break;
      }
    }

    return prefixParts.join("|");
  }

  /**
   * prefix_key 기반으로 현재 순번 조회 (새 테이블 사용)
   */
  private async getSequenceForPrefix(
    client: any,
    ruleId: string,
    companyCode: string,
    prefixKey: string
  ): Promise<number> {
    const result = await client.query(
      `SELECT current_sequence FROM numbering_rule_sequences 
       WHERE rule_id = $1 AND company_code = $2 AND prefix_key = $3`,
      [ruleId, companyCode, prefixKey]
    );
    return result.rows.length > 0 ? result.rows[0].current_sequence : 0;
  }

  /**
   * prefix_key 기반으로 순번 증가 (UPSERT)
   */
  private async incrementSequenceForPrefix(
    client: any,
    ruleId: string,
    companyCode: string,
    prefixKey: string
  ): Promise<number> {
    const result = await client.query(
      `INSERT INTO numbering_rule_sequences (rule_id, company_code, prefix_key, current_sequence, last_allocated_at)
       VALUES ($1, $2, $3, 1, NOW())
       ON CONFLICT (rule_id, company_code, prefix_key)
       DO UPDATE SET current_sequence = numbering_rule_sequences.current_sequence + 1,
                     last_allocated_at = NOW()
       RETURNING current_sequence`,
      [ruleId, companyCode, prefixKey]
    );
    return result.rows[0].current_sequence;
  }
  /**
   * 규칙 목록 조회 (전체)
   */
  async getRuleList(companyCode: string): Promise<NumberingRuleConfig[]> {
    try {
      logger.info("채번 규칙 목록 조회 시작", { companyCode });

      const pool = getPool();

      // 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
      let query: string;
      let params: any[];

      if (companyCode === "*") {
        // 최고 관리자: 모든 회사 데이터 조회 가능
        query = `
          SELECT 
            rule_id AS "ruleId",
            rule_name AS "ruleName",
            description,
            separator,
            reset_period AS "resetPeriod",
            current_sequence AS "currentSequence",
            table_name AS "tableName",
            column_name AS "columnName",
            company_code AS "companyCode",
            category_column AS "categoryColumn",
            category_value_id AS "categoryValueId",
            created_at AS "createdAt",
            updated_at AS "updatedAt",
            created_by AS "createdBy"
          FROM numbering_rules
          ORDER BY created_at DESC
        `;
        params = [];
        logger.info("최고 관리자 전체 채번 규칙 조회");
      } else {
        // 일반 회사: 자신의 회사 데이터만 조회 (company_code="*" 제외)
        query = `
          SELECT 
            rule_id AS "ruleId",
            rule_name AS "ruleName",
            description,
            separator,
            reset_period AS "resetPeriod",
            current_sequence AS "currentSequence",
            table_name AS "tableName",
            column_name AS "columnName",
            company_code AS "companyCode",
            category_column AS "categoryColumn",
            category_value_id AS "categoryValueId",
            created_at AS "createdAt",
            updated_at AS "updatedAt",
            created_by AS "createdBy"
          FROM numbering_rules
          WHERE company_code = $1
          ORDER BY created_at DESC
        `;
        params = [companyCode];
        logger.info("회사별 채번 규칙 조회", { companyCode });
      }

      const result = await pool.query(query, params);

      // 각 규칙의 파트 정보 조회
      for (const rule of result.rows) {
        let partsQuery: string;
        let partsParams: any[];

        if (companyCode === "*") {
          // 최고 관리자: 모든 파트 조회
          partsQuery = `
            SELECT 
              id,
              part_order AS "order",
              part_type AS "partType",
              generation_method AS "generationMethod",
              auto_config AS "autoConfig",
              manual_config AS "manualConfig"
            FROM numbering_rule_parts
            WHERE rule_id = $1
            ORDER BY part_order
          `;
          partsParams = [rule.ruleId];
        } else {
          // 일반 회사: 자신의 파트만 조회
          partsQuery = `
            SELECT 
              id,
              part_order AS "order",
              part_type AS "partType",
              generation_method AS "generationMethod",
              auto_config AS "autoConfig",
              manual_config AS "manualConfig"
            FROM numbering_rule_parts
            WHERE rule_id = $1 AND company_code = $2
            ORDER BY part_order
          `;
          partsParams = [rule.ruleId, companyCode];
        }

        const partsResult = await pool.query(partsQuery, partsParams);
        rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
      }

      logger.info(`채번 규칙 목록 조회 완료: ${result.rows.length}개`, {
        companyCode,
      });
      return result.rows;
    } catch (error: any) {
      logger.error(`채번 규칙 목록 조회 중 에러: ${error.message}`);
      throw error;
    }
  }

  /**
   * 현재 메뉴에서 사용 가능한 규칙 목록 조회 (메뉴 스코프)
   *
   * 메뉴 스코프 규칙:
   * - menuObjid가 제공되면 형제 메뉴의 채번 규칙 포함
   * - 우선순위: menu (형제 메뉴) > table > global
   */
  async getAvailableRulesForMenu(
    companyCode: string,
    menuObjid?: number
  ): Promise<NumberingRuleConfig[]> {
    let menuAndChildObjids: number[] = []; // catch 블록에서 접근 가능하도록 함수 최상단에 선언

    try {
      logger.info("메뉴별 사용 가능한 채번 규칙 조회 시작 (메뉴 스코프)", {
        companyCode,
        menuObjid,
      });

      const pool = getPool();

      // 1. 선택한 메뉴와 하위 메뉴 OBJID 조회 (형제 메뉴 제외)
      if (menuObjid) {
        menuAndChildObjids = await getMenuAndChildObjids(menuObjid);
        logger.info("선택한 메뉴 및 하위 메뉴 OBJID 목록", {
          menuObjid,
          menuAndChildObjids,
        });
      }

      // menuObjid가 없으면 global 규칙만 반환
      if (!menuObjid || menuAndChildObjids.length === 0) {
        let query: string;
        let params: any[];

        if (companyCode === "*") {
          // 최고 관리자: 모든 global 규칙 조회
          query = `
            SELECT 
              rule_id AS "ruleId",
              rule_name AS "ruleName",
              description,
              separator,
              reset_period AS "resetPeriod",
              current_sequence AS "currentSequence",
              table_name AS "tableName",
              column_name AS "columnName",
              company_code AS "companyCode",
              category_column AS "categoryColumn",
              category_value_id AS "categoryValueId",
              created_at AS "createdAt",
              updated_at AS "updatedAt",
              created_by AS "createdBy"
            FROM numbering_rules
            WHERE 1=1
            ORDER BY created_at DESC
          `;
          params = [];
        } else {
          // 일반 회사: 자신의 global 규칙만 조회 (company_code="*" 제외)
          query = `
            SELECT 
              rule_id AS "ruleId",
              rule_name AS "ruleName",
              description,
              separator,
              reset_period AS "resetPeriod",
              current_sequence AS "currentSequence",
              table_name AS "tableName",
              column_name AS "columnName",
              company_code AS "companyCode",
              category_column AS "categoryColumn",
              category_value_id AS "categoryValueId",
              created_at AS "createdAt",
              updated_at AS "updatedAt",
              created_by AS "createdBy"
            FROM numbering_rules
            WHERE company_code = $1             ORDER BY created_at DESC
          `;
          params = [companyCode];
        }

        const result = await pool.query(query, params);

        // 파트 정보 추가
        for (const rule of result.rows) {
          let partsQuery: string;
          let partsParams: any[];

          if (companyCode === "*") {
            partsQuery = `
              SELECT 
                id,
                part_order AS "order",
                part_type AS "partType",
                generation_method AS "generationMethod",
                auto_config AS "autoConfig",
                manual_config AS "manualConfig"
              FROM numbering_rule_parts
              WHERE rule_id = $1
              ORDER BY part_order
            `;
            partsParams = [rule.ruleId];
          } else {
            partsQuery = `
              SELECT 
                id,
                part_order AS "order",
                part_type AS "partType",
                generation_method AS "generationMethod",
                auto_config AS "autoConfig",
                manual_config AS "manualConfig"
              FROM numbering_rule_parts
              WHERE rule_id = $1 AND company_code = $2
              ORDER BY part_order
            `;
            partsParams = [rule.ruleId, companyCode];
          }

          const partsResult = await pool.query(partsQuery, partsParams);
          rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
        }

        return result.rows;
      }

      // 2. 메뉴 스코프: 형제 메뉴의 채번 규칙 조회
      // 우선순위: menu (형제 메뉴) > table > global
      let query: string;
      let params: any[];

      if (companyCode === "*") {
        // 최고 관리자: 모든 규칙 조회
        query = `
          SELECT 
            rule_id AS "ruleId",
            rule_name AS "ruleName",
            description,
            separator,
            reset_period AS "resetPeriod",
            current_sequence AS "currentSequence",
            table_name AS "tableName",
            column_name AS "columnName",
            company_code AS "companyCode",
            category_column AS "categoryColumn",
            category_value_id AS "categoryValueId",
            created_at AS "createdAt",
            updated_at AS "updatedAt",
            created_by AS "createdBy"
          FROM numbering_rules
          ORDER BY created_at DESC
        `;
        params = [];
        logger.info("최고 관리자: 전체 채번 규칙 조회");
      } else {
        // 일반 회사: 자신의 규칙만 조회
        query = `
          SELECT 
            rule_id AS "ruleId",
            rule_name AS "ruleName",
            description,
            separator,
            reset_period AS "resetPeriod",
            current_sequence AS "currentSequence",
            table_name AS "tableName",
            column_name AS "columnName",
            company_code AS "companyCode",
            category_column AS "categoryColumn",
            category_value_id AS "categoryValueId",
            created_at AS "createdAt",
            updated_at AS "updatedAt",
            created_by AS "createdBy"
          FROM numbering_rules
          WHERE company_code = $1
          ORDER BY created_at DESC
        `;
        params = [companyCode];
        logger.info("회사별 채번 규칙 조회", { companyCode });
      }

      logger.info("🔍 채번 규칙 쿼리 실행", {
        queryPreview: query.substring(0, 200),
        paramsTypes: params.map((p) => typeof p),
        paramsValues: params,
      });

      const result = await pool.query(query, params);

      logger.debug("채번 규칙 쿼리 성공", { ruleCount: result.rows.length });

      // 파트 정보 추가
      for (const rule of result.rows) {
        try {
          let partsQuery: string;
          let partsParams: any[];

          if (companyCode === "*") {
            partsQuery = `
              SELECT 
                id,
                part_order AS "order",
                part_type AS "partType",
                generation_method AS "generationMethod",
                auto_config AS "autoConfig",
                manual_config AS "manualConfig"
              FROM numbering_rule_parts
              WHERE rule_id = $1
              ORDER BY part_order
            `;
            partsParams = [rule.ruleId];
          } else {
            partsQuery = `
              SELECT 
                id,
                part_order AS "order",
                part_type AS "partType",
                generation_method AS "generationMethod",
                auto_config AS "autoConfig",
                manual_config AS "manualConfig"
              FROM numbering_rule_parts
              WHERE rule_id = $1 AND company_code = $2
              ORDER BY part_order
            `;
            partsParams = [rule.ruleId, companyCode];
          }

          const partsResult = await pool.query(partsQuery, partsParams);
          rule.parts = extractSeparatorAfterFromParts(partsResult.rows);

          logger.info("✅ 규칙 파트 조회 성공", {
            ruleId: rule.ruleId,
            ruleName: rule.ruleName,
            partsCount: partsResult.rows.length,
          });
        } catch (partError: any) {
          logger.error("❌ 규칙 파트 조회 실패", {
            ruleId: rule.ruleId,
            ruleName: rule.ruleName,
            error: partError.message,
            errorCode: partError.code,
            errorStack: partError.stack,
          });
          throw partError;
        }
      }

      logger.info("메뉴별 사용 가능한 채번 규칙 조회 완료", {
        companyCode,
        menuObjid,
        menuAndChildCount: menuAndChildObjids.length,
        count: result.rowCount,
      });

      return result.rows;
    } catch (error: any) {
      logger.error("메뉴별 채번 규칙 조회 실패", {
        error: error.message,
        errorCode: error.code,
        errorStack: error.stack,
        companyCode,
        menuObjid,
        menuAndChildObjids: menuAndChildObjids || [],
      });
      throw error;
    }
  }

  /**
   * 화면용 채번 규칙 조회 (테이블 기반 필터링 - 간소화)
   * @param companyCode 회사 코드
   * @param tableName 화면의 테이블명
   * @returns 해당 테이블의 채번 규칙 목록
   */
  async getAvailableRulesForScreen(
    companyCode: string,
    tableName: string
  ): Promise<NumberingRuleConfig[]> {
    try {
      logger.info("화면용 채번 규칙 조회", {
        companyCode,
        tableName,
      });

      const pool = getPool();

      // 멀티테넌시: 최고 관리자 vs 일반 회사
      let query: string;
      let params: any[];

      if (companyCode === "*") {
        // 최고 관리자: 모든 회사의 규칙 조회 가능 (최고 관리자 전용 규칙 제외)
        query = `
          SELECT 
            rule_id AS "ruleId",
            rule_name AS "ruleName",
            description,
            separator,
            reset_period AS "resetPeriod",
            current_sequence AS "currentSequence",
            table_name AS "tableName",
            column_name AS "columnName",
            company_code AS "companyCode",
            category_column AS "categoryColumn",
            category_value_id AS "categoryValueId",
            created_at AS "createdAt",
            updated_at AS "updatedAt",
            created_by AS "createdBy"
          FROM numbering_rules
          WHERE company_code != '*'
            AND table_name = $1
          ORDER BY created_at DESC
        `;
        params = [tableName];
        logger.info("최고 관리자: 일반 회사 채번 규칙 조회");
      } else {
        // 일반 회사: 자신의 규칙만 조회
        query = `
          SELECT 
            rule_id AS "ruleId",
            rule_name AS "ruleName",
            description,
            separator,
            reset_period AS "resetPeriod",
            current_sequence AS "currentSequence",
            table_name AS "tableName",
            column_name AS "columnName",
            company_code AS "companyCode",
            category_column AS "categoryColumn",
            category_value_id AS "categoryValueId",
            created_at AS "createdAt",
            updated_at AS "updatedAt",
            created_by AS "createdBy"
          FROM numbering_rules
          WHERE company_code = $1
            AND table_name = $2
          ORDER BY created_at DESC
        `;
        params = [companyCode, tableName];
      }

      const result = await pool.query(query, params);

      // 각 규칙의 파트 정보 로드
      for (const rule of result.rows) {
        const partsQuery = `
          SELECT 
            id,
            part_order AS "order",
            part_type AS "partType",
            generation_method AS "generationMethod",
            auto_config AS "autoConfig",
            manual_config AS "manualConfig"
          FROM numbering_rule_parts
          WHERE rule_id = $1
            AND company_code = $2
          ORDER BY part_order
        `;

        const partsResult = await pool.query(partsQuery, [
          rule.ruleId,
          companyCode === "*" ? rule.companyCode : companyCode,
        ]);

        rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
      }

      logger.info(`화면용 채번 규칙 조회 완료: ${result.rows.length}개`, {
        companyCode,
        tableName,
      });

      return result.rows;
    } catch (error: any) {
      logger.error("화면용 채번 규칙 조회 실패", error);
      throw error;
    }
  }

  /**
   * 특정 규칙 조회
   */
  async getRuleById(
    ruleId: string,
    companyCode: string
  ): Promise<NumberingRuleConfig | null> {
    const pool = getPool();

    // 멀티테넌시: 최고 관리자만 company_code="*" 데이터를 볼 수 있음
    let query: string;
    let params: any[];

    if (companyCode === "*") {
      // 최고 관리자: 모든 규칙 조회 가능
      query = `
        SELECT 
          rule_id AS "ruleId",
          rule_name AS "ruleName",
          description,
          separator,
          reset_period AS "resetPeriod",
          current_sequence AS "currentSequence",
          table_name AS "tableName",
          column_name AS "columnName",
          company_code AS "companyCode",
          category_column AS "categoryColumn",
          category_value_id AS "categoryValueId",
          created_at AS "createdAt",
          updated_at AS "updatedAt",
          created_by AS "createdBy"
        FROM numbering_rules
        WHERE rule_id = $1
      `;
      params = [ruleId];
    } else {
      // 일반 회사: 자신의 규칙만 조회
      query = `
        SELECT 
          rule_id AS "ruleId",
          rule_name AS "ruleName",
          description,
          separator,
          reset_period AS "resetPeriod",
          current_sequence AS "currentSequence",
          table_name AS "tableName",
          column_name AS "columnName",
          company_code AS "companyCode",
          category_column AS "categoryColumn",
          category_value_id AS "categoryValueId",
          created_at AS "createdAt",
          updated_at AS "updatedAt",
          created_by AS "createdBy"
        FROM numbering_rules
        WHERE rule_id = $1 AND company_code = $2
      `;
      params = [ruleId, companyCode];
    }

    const result = await pool.query(query, params);
    if (result.rowCount === 0) {
      return null;
    }

    const rule = result.rows[0];

    // 파트 정보 조회
    let partsQuery: string;
    let partsParams: any[];

    if (companyCode === "*") {
      partsQuery = `
        SELECT 
          id,
          part_order AS "order",
          part_type AS "partType",
          generation_method AS "generationMethod",
          auto_config AS "autoConfig",
          manual_config AS "manualConfig"
        FROM numbering_rule_parts
        WHERE rule_id = $1
        ORDER BY part_order
      `;
      partsParams = [ruleId];
    } else {
      partsQuery = `
        SELECT 
          id,
          part_order AS "order",
          part_type AS "partType",
          generation_method AS "generationMethod",
          auto_config AS "autoConfig",
          manual_config AS "manualConfig"
        FROM numbering_rule_parts
        WHERE rule_id = $1 AND company_code = $2
        ORDER BY part_order
      `;
      partsParams = [ruleId, companyCode];
    }

    const partsResult = await pool.query(partsQuery, partsParams);
    rule.parts = extractSeparatorAfterFromParts(partsResult.rows);

    return rule;
  }

  /**
   * 규칙 생성
   */
  async createRule(
    config: NumberingRuleConfig,
    companyCode: string,
    userId: string
  ): Promise<NumberingRuleConfig> {
    const pool = getPool();
    const client = await pool.connect();

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

      // 마스터 삽입
      const insertRuleQuery = `
        INSERT INTO numbering_rules (
          rule_id, rule_name, description, separator, reset_period,
          current_sequence, table_name, column_name, company_code, 
          category_column, category_value_id, created_by
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
        RETURNING 
          rule_id AS "ruleId",
          rule_name AS "ruleName",
          description,
          separator,
          reset_period AS "resetPeriod",
          current_sequence AS "currentSequence",
          table_name AS "tableName",
          column_name AS "columnName",
          company_code AS "companyCode",
          category_column AS "categoryColumn",
          category_value_id AS "categoryValueId",
          created_at AS "createdAt",
          updated_at AS "updatedAt",
          created_by AS "createdBy"
      `;

      const ruleResult = await client.query(insertRuleQuery, [
        config.ruleId,
        config.ruleName,
        config.description || null,
        config.separator || "-",
        config.resetPeriod || "none",
        config.currentSequence || 1,
        config.tableName || null,
        config.columnName || null,
        companyCode,
        config.categoryColumn || null,
        config.categoryValueId || null,
        userId,
      ]);

      // 파트 삽입
      const parts: NumberingRulePart[] = [];
      for (const part of config.parts) {
        const insertPartQuery = `
          INSERT INTO numbering_rule_parts (
            rule_id, part_order, part_type, generation_method,
            auto_config, manual_config, company_code
          ) VALUES ($1, $2, $3, $4, $5, $6, $7)
          RETURNING 
            id,
            part_order AS "order",
            part_type AS "partType",
            generation_method AS "generationMethod",
            auto_config AS "autoConfig",
            manual_config AS "manualConfig"
        `;

        // auto_config에 separatorAfter 포함
        const autoConfigWithSep = { ...(part.autoConfig || {}), separatorAfter: part.separatorAfter ?? "-" };
        
        const partResult = await client.query(insertPartQuery, [
          config.ruleId,
          part.order,
          part.partType,
          part.generationMethod,
          JSON.stringify(autoConfigWithSep),
          JSON.stringify(part.manualConfig || {}),
          companyCode,
        ]);

        const savedPart = partResult.rows[0];
        // autoConfig에서 separatorAfter를 추출하여 파트 레벨로 이동
        if (savedPart.autoConfig?.separatorAfter !== undefined) {
          savedPart.separatorAfter = savedPart.autoConfig.separatorAfter;
        }
        parts.push(savedPart);
      }

      await client.query("COMMIT");
      logger.info("채번 규칙 생성 완료", {
        ruleId: config.ruleId,
        companyCode,
      });
      return { ...ruleResult.rows[0], parts };
    } catch (error: any) {
      await client.query("ROLLBACK");
      logger.error("채번 규칙 생성 실패", { error: error.message });
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * 규칙 수정
   */
  async updateRule(
    ruleId: string,
    updates: Partial<NumberingRuleConfig>,
    companyCode: string
  ): Promise<NumberingRuleConfig> {
    const pool = getPool();
    const client = await pool.connect();

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

      const updateRuleQuery = `
        UPDATE numbering_rules
        SET 
          rule_name = COALESCE($1, rule_name),
          description = COALESCE($2, description),
          separator = COALESCE($3, separator),
          reset_period = COALESCE($4, reset_period),
          table_name = COALESCE($5, table_name),
          column_name = COALESCE($6, column_name),
          category_column = COALESCE($7, category_column),
          category_value_id = COALESCE($8, category_value_id),
          updated_at = NOW()
        WHERE rule_id = $9 AND company_code = $10
        RETURNING 
          rule_id AS "ruleId",
          rule_name AS "ruleName",
          description,
          separator,
          reset_period AS "resetPeriod",
          current_sequence AS "currentSequence",
          table_name AS "tableName",
          column_name AS "columnName",
          company_code AS "companyCode",
          category_column AS "categoryColumn",
          category_value_id AS "categoryValueId",
          created_at AS "createdAt",
          updated_at AS "updatedAt",
          created_by AS "createdBy"
      `;

      const ruleResult = await client.query(updateRuleQuery, [
        updates.ruleName,
        updates.description,
        updates.separator,
        updates.resetPeriod,
        updates.tableName,
        updates.columnName,
        updates.categoryColumn,
        updates.categoryValueId,
        ruleId,
        companyCode,
      ]);

      if (ruleResult.rowCount === 0) {
        throw new Error("규칙을 찾을 수 없거나 권한이 없습니다");
      }

      // 파트 업데이트
      let parts: NumberingRulePart[] = [];
      if (updates.parts) {
        await client.query(
          "DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2",
          [ruleId, companyCode]
        );

        for (const part of updates.parts) {
          const insertPartQuery = `
            INSERT INTO numbering_rule_parts (
              rule_id, part_order, part_type, generation_method,
              auto_config, manual_config, company_code
            ) VALUES ($1, $2, $3, $4, $5, $6, $7)
            RETURNING 
              id,
              part_order AS "order",
              part_type AS "partType",
              generation_method AS "generationMethod",
              auto_config AS "autoConfig",
              manual_config AS "manualConfig"
          `;

          const autoConfigWithSep = { ...(part.autoConfig || {}), separatorAfter: part.separatorAfter ?? "-" };
          
          const partResult = await client.query(insertPartQuery, [
            ruleId,
            part.order,
            part.partType,
            part.generationMethod,
            JSON.stringify(autoConfigWithSep),
            JSON.stringify(part.manualConfig || {}),
            companyCode,
          ]);

          const savedPart = partResult.rows[0];
          if (savedPart.autoConfig?.separatorAfter !== undefined) {
            savedPart.separatorAfter = savedPart.autoConfig.separatorAfter;
          }
          parts.push(savedPart);
        }
      }

      await client.query("COMMIT");
      logger.info("채번 규칙 수정 완료", { ruleId, companyCode });
      return { ...ruleResult.rows[0], parts };
    } catch (error: any) {
      await client.query("ROLLBACK");
      logger.error("채번 규칙 수정 실패", {
        ruleId,
        companyCode,
        error: error.message,
        stack: error.stack,
        updates,
      });
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * 규칙 삭제
   */
  async deleteRule(ruleId: string, companyCode: string): Promise<void> {
    const pool = getPool();
    const query = `
      DELETE FROM numbering_rules
      WHERE rule_id = $1 AND company_code = $2
    `;

    const result = await pool.query(query, [ruleId, companyCode]);

    if (result.rowCount === 0) {
      throw new Error("규칙을 찾을 수 없거나 권한이 없습니다");
    }

    logger.info("채번 규칙 삭제 완료", { ruleId, companyCode });
  }

  /**
   * 코드 미리보기 (순번 증가 없음)
   * @param ruleId 채번 규칙 ID
   * @param companyCode 회사 코드
   * @param formData 폼 데이터 (카테고리 기반 채번 시 사용)
   */
  async previewCode(
    ruleId: string,
    companyCode: string,
    formData?: Record<string, any>
  ): Promise<string> {
    const rule = await this.getRuleById(ruleId, companyCode);
    if (!rule) throw new Error("규칙을 찾을 수 없습니다");

    // prefix_key 기반 순번 조회
    const prefixKey = await this.buildPrefixKey(rule, formData);
    const pool = getPool();
    const currentSeq = await this.getSequenceForPrefix(pool, ruleId, companyCode, prefixKey);

    logger.info("미리보기: prefix_key 기반 순번 조회", {
      ruleId, prefixKey, currentSeq,
    });

    const parts = await Promise.all(rule.parts
      .sort((a: any, b: any) => a.order - b.order)
      .map(async (part: any) => {
        if (part.generationMethod === "manual") {
          return "____";
        }

        const autoConfig = part.autoConfig || {};

        switch (part.partType) {
          case "sequence": {
            const length = autoConfig.sequenceLength || 3;
            const nextSequence = currentSeq + 1;
            return String(nextSequence).padStart(length, "0");
          }

          case "number": {
            // 숫자 (고정 자릿수)
            const length = autoConfig.numberLength || 3;
            const value = autoConfig.numberValue || 1;
            return String(value).padStart(length, "0");
          }

          case "date": {
            // 날짜 (다양한 날짜 형식)
            const dateFormat = autoConfig.dateFormat || "YYYYMMDD";

            // 컬럼 기준 생성인 경우 폼 데이터에서 날짜 추출
            if (
              autoConfig.useColumnValue &&
              autoConfig.sourceColumnName &&
              formData
            ) {
              const columnValue = formData[autoConfig.sourceColumnName];
              if (columnValue) {
                const dateValue =
                  columnValue instanceof Date
                    ? columnValue
                    : new Date(columnValue);

                if (!isNaN(dateValue.getTime())) {
                  return this.formatDate(dateValue, dateFormat);
                }
              }
            }

            return this.formatDate(new Date(), dateFormat);
          }

          case "text": {
            // 텍스트 (고정 문자열)
            return autoConfig.textValue || "TEXT";
          }

          case "category": {
            // 카테고리 기반 코드 생성
            const categoryKey = autoConfig.categoryKey; // 예: "item_info.material"
            const categoryMappings = autoConfig.categoryMappings || [];

            if (!categoryKey || !formData) {
              logger.warn("카테고리 키 또는 폼 데이터 없음", {
                categoryKey,
                hasFormData: !!formData,
              });
              return "";
            }

            // categoryKey에서 컬럼명 추출 (예: "item_info.material" -> "material")
            const columnName = categoryKey.includes(".")
              ? categoryKey.split(".")[1]
              : categoryKey;

            // 폼 데이터에서 해당 컬럼의 값 가져오기
            const selectedValue = formData[columnName];

            logger.info("카테고리 파트 처리", {
              categoryKey,
              columnName,
              selectedValue,
              formDataKeys: Object.keys(formData),
              mappingsCount: categoryMappings.length,
            });

            if (!selectedValue) {
              logger.warn("카테고리 값이 선택되지 않음", {
                columnName,
                formDataKeys: Object.keys(formData),
              });
              return "";
            }

            // 카테고리 매핑에서 해당 값에 대한 형식 찾기
            // selectedValue는 valueCode일 수 있음 (V2Select에서 valueCode를 value로 사용)
            const selectedValueStr = String(selectedValue);
            let mapping = categoryMappings.find((m: any) => {
              // ID로 매칭 (기존 방식: V2Select가 valueId를 사용하던 경우)
              if (m.categoryValueId?.toString() === selectedValueStr)
                return true;
              // valueCode로 매칭 (매핑에 categoryValueCode가 있는 경우)
              if (m.categoryValueCode && m.categoryValueCode === selectedValueStr)
                return true;
              // 라벨로 매칭 (폴백)
              if (m.categoryValueLabel === selectedValueStr) return true;
              return false;
            });

            // 매핑을 못 찾았으면 category_values 테이블에서 valueCode → valueId 역변환 시도
            if (!mapping) {
              try {
                const pool = getPool();
                const [catTableName, catColumnName] = categoryKey.includes(".")
                  ? categoryKey.split(".")
                  : [categoryKey, categoryKey];
                const cvResult = await pool.query(
                  `SELECT value_id, value_code, value_label FROM category_values 
                   WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`,
                  [catTableName, catColumnName, selectedValueStr]
                );
                if (cvResult.rows.length > 0) {
                  const resolvedId = cvResult.rows[0].value_id;
                  const resolvedLabel = cvResult.rows[0].value_label;
                  mapping = categoryMappings.find((m: any) => {
                    if (m.categoryValueId?.toString() === String(resolvedId)) return true;
                    if (m.categoryValueLabel === resolvedLabel) return true;
                    return false;
                  });
                  if (mapping) {
                    logger.info("카테고리 매핑 역변환 성공 (valueCode→valueId)", {
                      valueCode: selectedValueStr,
                      resolvedId,
                      resolvedLabel,
                      format: mapping.format,
                    });
                  }
                }
              } catch (lookupError: any) {
                logger.warn("카테고리 값 역변환 조회 실패", { error: lookupError.message });
              }
            }

            if (mapping) {
              logger.info("카테고리 매핑 적용", {
                selectedValue,
                format: mapping.format,
                categoryValueLabel: mapping.categoryValueLabel,
              });
              return mapping.format || "";
            }

            logger.warn("카테고리 매핑을 찾을 수 없음", {
              selectedValue,
              availableMappings: categoryMappings.map((m: any) => ({
                id: m.categoryValueId,
                label: m.categoryValueLabel,
              })),
            });
            return "";
          }

          default:
            logger.warn("알 수 없는 파트 타입", { partType: part.partType });
            return "";
        }
      }));

    const sortedRuleParts = rule.parts.sort((a: any, b: any) => a.order - b.order);
    const previewCode = joinPartsWithSeparators(parts, sortedRuleParts, rule.separator || "");
    logger.info("코드 미리보기 생성", {
      ruleId,
      previewCode,
      companyCode,
      hasFormData: !!formData,
    });
    return previewCode;
  }

  /**
   * 코드 할당 (저장 시점에 실제 순번 증가)
   * @param ruleId 채번 규칙 ID
   * @param companyCode 회사 코드
   * @param formData 폼 데이터 (날짜 컬럼 기준 생성 시 사용)
   * @param userInputCode 사용자가 편집한 최종 코드 (수동 입력 부분 추출용)
   */
  async allocateCode(
    ruleId: string,
    companyCode: string,
    formData?: Record<string, any>,
    userInputCode?: string
  ): Promise<string> {
    const pool = getPool();
    const client = await pool.connect();

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

      const rule = await this.getRuleById(ruleId, companyCode);
      if (!rule) throw new Error("규칙을 찾을 수 없습니다");

      // prefix_key 기반 순번: 순번 이외 파트 조합으로 prefix 생성
      const prefixKey = await this.buildPrefixKey(rule, formData);
      const hasSequence = rule.parts.some((p: any) => p.partType === "sequence");

      // 순번이 있으면 prefix_key 기반으로 UPSERT하여 다음 순번 획득
      let allocatedSequence = 0;
      if (hasSequence) {
        allocatedSequence = await this.incrementSequenceForPrefix(
          client, ruleId, companyCode, prefixKey
        );
        // 호환성을 위해 기존 current_sequence도 업데이트
        await client.query(
          "UPDATE numbering_rules SET current_sequence = current_sequence + 1 WHERE rule_id = $1 AND company_code = $2",
          [ruleId, companyCode]
        );
      }

      logger.info("allocateCode: prefix_key 기반 순번 할당", {
        ruleId, prefixKey, allocatedSequence,
      });

      // 수동 입력 파트가 있고, 사용자가 입력한 코드가 있으면 수동 입력 부분 추출
      const manualParts = rule.parts.filter(
        (p: any) => p.generationMethod === "manual"
      );
      let extractedManualValues: string[] = [];

      if (manualParts.length > 0 && userInputCode) {
        const previewParts = await Promise.all(rule.parts
          .sort((a: any, b: any) => a.order - b.order)
          .map(async (part: any) => {
            if (part.generationMethod === "manual") {
              return "____";
            }
            const autoConfig = part.autoConfig || {};
            switch (part.partType) {
              case "sequence": {
                const length = autoConfig.sequenceLength || 3;
                return "X".repeat(length);
              }
              case "text":
                return autoConfig.textValue || "";
              case "date":
                return "DATEPART";
              case "category": {
                const catKey2 = autoConfig.categoryKey;
                const catMappings2 = autoConfig.categoryMappings || [];

                if (!catKey2 || !formData) {
                  return "CATEGORY";
                }

                const colName2 = catKey2.includes(".")
                  ? catKey2.split(".")[1]
                  : catKey2;
                const selVal2 = formData[colName2];

                if (!selVal2) {
                  return "CATEGORY";
                }

                const selValStr2 = String(selVal2);
                let catMapping2 = catMappings2.find((m: any) => {
                  if (m.categoryValueId?.toString() === selValStr2) return true;
                  if (m.categoryValueCode && m.categoryValueCode === selValStr2) return true;
                  if (m.categoryValueLabel === selValStr2) return true;
                  return false;
                });

                if (!catMapping2) {
                  try {
                    const pool2 = getPool();
                    const [ct2, cc2] = catKey2.includes(".") ? catKey2.split(".") : [catKey2, catKey2];
                    const cvr2 = await pool2.query(
                      `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`,
                      [ct2, cc2, selValStr2]
                    );
                    if (cvr2.rows.length > 0) {
                      const rid2 = cvr2.rows[0].value_id;
                      const rlabel2 = cvr2.rows[0].value_label;
                      catMapping2 = catMappings2.find((m: any) => {
                        if (m.categoryValueId?.toString() === String(rid2)) return true;
                        if (m.categoryValueLabel === rlabel2) return true;
                        return false;
                      });
                    }
                  } catch { /* ignore */ }
                }

                return catMapping2?.format || "CATEGORY";
              }
              default:
                return "";
            }
          }));

        const sortedPartsForTemplate = rule.parts.sort((a: any, b: any) => a.order - b.order);
        const previewTemplate = joinPartsWithSeparators(previewParts, sortedPartsForTemplate, rule.separator || "");

        const templateParts = previewTemplate.split("____");
        if (templateParts.length > 1) {
          let remainingCode = userInputCode;
          for (let i = 0; i < templateParts.length - 1; i++) {
            const prefix = templateParts[i];
            const suffix = templateParts[i + 1];

            if (prefix && remainingCode.startsWith(prefix)) {
              remainingCode = remainingCode.slice(prefix.length);
            }

            if (suffix) {
              const suffixStart = suffix.replace(/X+|DATEPART/g, "");
              const manualEndIndex = suffixStart
                ? remainingCode.indexOf(suffixStart)
                : remainingCode.length;
              if (manualEndIndex > 0) {
                extractedManualValues.push(
                  remainingCode.slice(0, manualEndIndex)
                );
                remainingCode = remainingCode.slice(manualEndIndex);
              }
            } else {
              extractedManualValues.push(remainingCode);
            }
          }
        }

        logger.info(
          `수동 입력 값 추출: userInputCode=${userInputCode}, previewTemplate=${previewTemplate}, extractedManualValues=${JSON.stringify(extractedManualValues)}`
        );
      }

      let manualPartIndex = 0;
      const parts = await Promise.all(rule.parts
        .sort((a: any, b: any) => a.order - b.order)
        .map(async (part: any) => {
          if (part.generationMethod === "manual") {
            const manualValue =
              extractedManualValues[manualPartIndex] ||
              part.manualConfig?.value ||
              "";
            manualPartIndex++;
            return manualValue;
          }

          const autoConfig = part.autoConfig || {};

          switch (part.partType) {
            case "sequence": {
              const length = autoConfig.sequenceLength || 3;
              return String(allocatedSequence).padStart(length, "0");
            }

            case "number": {
              const length = autoConfig.numberLength || 3;
              const value = autoConfig.numberValue || 1;
              return String(value).padStart(length, "0");
            }

            case "date": {
              const dateFormat = autoConfig.dateFormat || "YYYYMMDD";

              if (
                autoConfig.useColumnValue &&
                autoConfig.sourceColumnName &&
                formData
              ) {
                const columnValue = formData[autoConfig.sourceColumnName];
                if (columnValue) {
                  const dateValue =
                    columnValue instanceof Date
                      ? columnValue
                      : new Date(columnValue);

                  if (!isNaN(dateValue.getTime())) {
                    return this.formatDate(dateValue, dateFormat);
                  }
                }
              }

              return this.formatDate(new Date(), dateFormat);
            }

            case "text": {
              return autoConfig.textValue || "TEXT";
            }

            case "category": {
              const categoryKey = autoConfig.categoryKey;
              const categoryMappings = autoConfig.categoryMappings || [];

              if (!categoryKey || !formData) {
                return "";
              }

              const columnName = categoryKey.includes(".")
                ? categoryKey.split(".")[1]
                : categoryKey;

              const selectedValue = formData[columnName];

              if (!selectedValue) {
                return "";
              }

              const selectedValueStr = String(selectedValue);
              let allocMapping = categoryMappings.find((m: any) => {
                if (m.categoryValueId?.toString() === selectedValueStr) return true;
                if (m.categoryValueCode && m.categoryValueCode === selectedValueStr) return true;
                if (m.categoryValueLabel === selectedValueStr) return true;
                return false;
              });

              if (!allocMapping) {
                try {
                  const pool3 = getPool();
                  const [ct3, cc3] = categoryKey.includes(".") ? categoryKey.split(".") : [categoryKey, categoryKey];
                  const cvr3 = await pool3.query(
                    `SELECT value_id, value_label FROM category_values WHERE table_name = $1 AND column_name = $2 AND value_code = $3 LIMIT 1`,
                    [ct3, cc3, selectedValueStr]
                  );
                  if (cvr3.rows.length > 0) {
                    const rid3 = cvr3.rows[0].value_id;
                    const rlabel3 = cvr3.rows[0].value_label;
                    allocMapping = categoryMappings.find((m: any) => {
                      if (m.categoryValueId?.toString() === String(rid3)) return true;
                      if (m.categoryValueLabel === rlabel3) return true;
                      return false;
                    });
                  }
                } catch { /* ignore */ }
              }

              if (allocMapping) {
                return allocMapping.format || "";
              }

              return "";
            }

            default:
              return "";
          }
        }));

      const sortedPartsForAlloc = rule.parts.sort((a: any, b: any) => a.order - b.order);
      const allocatedCode = joinPartsWithSeparators(parts, sortedPartsForAlloc, rule.separator || "");

      await client.query("COMMIT");
      logger.info("코드 할당 완료", { ruleId, allocatedCode, companyCode });
      return allocatedCode;
    } catch (error: any) {
      await client.query("ROLLBACK");
      logger.error("코드 할당 실패", {
        ruleId,
        companyCode,
        error: error.message,
        stack: error.stack,
      });
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * @deprecated 기존 generateCode는 allocateCode를 사용하세요
   */
  async generateCode(ruleId: string, companyCode: string): Promise<string> {
    logger.warn(
      "generateCode는 deprecated 되었습니다. previewCode 또는 allocateCode를 사용하세요"
    );
    return this.allocateCode(ruleId, companyCode);
  }

  private formatDate(date: Date, format: string): string {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");

    switch (format) {
      case "YYYY":
        return String(year);
      case "YY":
        return String(year).slice(-2);
      case "YYYYMM":
        return `${year}${month}`;
      case "YYMM":
        return `${String(year).slice(-2)}${month}`;
      case "YYYYMMDD":
        return `${year}${month}${day}`;
      case "YYMMDD":
        return `${String(year).slice(-2)}${month}${day}`;
      default:
        return `${year}${month}${day}`;
    }
  }

  async resetSequence(ruleId: string, companyCode: string): Promise<void> {
    const pool = getPool();
    // 새 테이블의 모든 prefix 순번 초기화
    await pool.query(
      "DELETE FROM numbering_rule_sequences WHERE rule_id = $1 AND company_code = $2",
      [ruleId, companyCode]
    );
    // 기존 테이블도 초기화 (호환성)
    await pool.query(
      "UPDATE numbering_rules SET current_sequence = 0, updated_at = NOW() WHERE rule_id = $1 AND company_code = $2",
      [ruleId, companyCode]
    );
    logger.info("시퀀스 초기화 완료 (prefix별 순번 포함)", { ruleId, companyCode });
  }

  /**
   * [테스트] 테스트 테이블에서 채번 규칙 목록 조회
   * numbering_rules 테이블 사용
   */
  async getRulesFromTest(
    companyCode: string,
    menuObjid?: number
  ): Promise<NumberingRuleConfig[]> {
    try {
      logger.info("[테스트] 채번 규칙 목록 조회 시작", {
        companyCode,
        menuObjid,
      });

      const pool = getPool();

      // 멀티테넌시: 최고 관리자 vs 일반 회사
      let query: string;
      let params: any[];

      if (companyCode === "*") {
        // 최고 관리자: 모든 규칙 조회
        query = `
          SELECT 
            rule_id AS "ruleId",
            rule_name AS "ruleName",
            description,
            separator,
            reset_period AS "resetPeriod",
            current_sequence AS "currentSequence",
            table_name AS "tableName",
            column_name AS "columnName",
            company_code AS "companyCode",
            category_column AS "categoryColumn",
            category_value_id AS "categoryValueId",
            created_at AS "createdAt",
            updated_at AS "updatedAt",
            created_by AS "createdBy"
          FROM numbering_rules
          ORDER BY created_at DESC
        `;
        params = [];
      } else {
        // 일반 회사: 자신의 규칙만 조회
        query = `
          SELECT 
            rule_id AS "ruleId",
            rule_name AS "ruleName",
            description,
            separator,
            reset_period AS "resetPeriod",
            current_sequence AS "currentSequence",
            table_name AS "tableName",
            column_name AS "columnName",
            company_code AS "companyCode",
            category_column AS "categoryColumn",
            category_value_id AS "categoryValueId",
            created_at AS "createdAt",
            updated_at AS "updatedAt",
            created_by AS "createdBy"
          FROM numbering_rules
          WHERE company_code = $1
          ORDER BY created_at DESC
        `;
        params = [companyCode];
      }

      const result = await pool.query(query, params);

      // 각 규칙의 파트 정보 조회
      for (const rule of result.rows) {
        const partsQuery = `
          SELECT 
            id,
            part_order AS "order",
            part_type AS "partType",
            generation_method AS "generationMethod",
            auto_config AS "autoConfig",
            manual_config AS "manualConfig"
          FROM numbering_rule_parts
          WHERE rule_id = $1 AND company_code = $2
          ORDER BY part_order
        `;
        const partsResult = await pool.query(partsQuery, [
          rule.ruleId,
          companyCode === "*" ? rule.companyCode : companyCode,
        ]);
        rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
      }

      logger.info("[테스트] 채번 규칙 목록 조회 완료", {
        companyCode,
        menuObjid,
        count: result.rows.length,
      });

      return result.rows;
    } catch (error: any) {
      logger.error("[테스트] 채번 규칙 목록 조회 실패", {
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }

  /**
   * 테이블명 + 컬럼명 기반으로 채번규칙 조회
   * numbering_rules 테이블 사용
   */
  async getNumberingRuleByColumn(
    companyCode: string,
    tableName: string,
    columnName: string
  ): Promise<NumberingRuleConfig | null> {
    try {
      logger.info("테이블+컬럼 기반 채번 규칙 조회 시작 (테스트)", {
        companyCode,
        tableName,
        columnName,
      });

      const pool = getPool();
      const query = `
        SELECT
          r.rule_id AS "ruleId",
          r.rule_name AS "ruleName",
          r.description,
          r.separator,
          r.reset_period AS "resetPeriod",
          r.current_sequence AS "currentSequence",
          r.table_name AS "tableName",
          r.column_name AS "columnName",
          r.company_code AS "companyCode",
          r.category_column AS "categoryColumn",
          r.category_value_id AS "categoryValueId",
          cv.value_label AS "categoryValueLabel",
          r.created_at AS "createdAt",
          r.updated_at AS "updatedAt",
          r.created_by AS "createdBy"
        FROM numbering_rules r
        LEFT JOIN category_values cv ON r.category_value_id = cv.value_id
        WHERE r.company_code = $1
          AND r.table_name = $2
          AND r.column_name = $3
          AND r.category_value_id IS NULL
        LIMIT 1
      `;
      const params = [companyCode, tableName, columnName];

      const result = await pool.query(query, params);

      if (result.rows.length === 0) {
        logger.info("테이블+컬럼 기반 채번 규칙을 찾을 수 없음", {
          companyCode,
          tableName,
          columnName,
        });
        return null;
      }

      const rule = result.rows[0];

      // 파트 정보 조회 (테스트 테이블)
      const partsQuery = `
        SELECT
          id,
          part_order AS "order",
          part_type AS "partType",
          generation_method AS "generationMethod",
          auto_config AS "autoConfig",
          manual_config AS "manualConfig"
        FROM numbering_rule_parts
        WHERE rule_id = $1 AND company_code = $2
        ORDER BY part_order
      `;
      const partsResult = await pool.query(partsQuery, [
        rule.ruleId,
        companyCode,
      ]);
      rule.parts = extractSeparatorAfterFromParts(partsResult.rows);

      logger.info("테이블+컬럼 기반 채번 규칙 조회 성공 (테스트)", {
        ruleId: rule.ruleId,
        ruleName: rule.ruleName,
      });
      return rule;
    } catch (error: any) {
      logger.error("테이블+컬럼 기반 채번 규칙 조회 실패 (테스트)", {
        error: error.message,
        stack: error.stack,
        companyCode,
        tableName,
        columnName,
      });
      throw error;
    }
  }

  /**
   * [테스트] 테스트 테이블에 채번규칙 저장
   * numbering_rules 테이블 사용
   */
  async saveRuleToTest(
    config: NumberingRuleConfig,
    companyCode: string,
    createdBy: string
  ): Promise<NumberingRuleConfig> {
    const pool = getPool();
    const client = await pool.connect();

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

      logger.info("테스트 테이블에 채번 규칙 저장 시작", {
        ruleId: config.ruleId,
        ruleName: config.ruleName,
        tableName: config.tableName,
        columnName: config.columnName,
        companyCode,
      });

      // 기존 규칙 확인
      const existingQuery = `
        SELECT rule_id FROM numbering_rules 
        WHERE rule_id = $1 AND company_code = $2
      `;
      const existingResult = await client.query(existingQuery, [
        config.ruleId,
        companyCode,
      ]);

      if (existingResult.rows.length > 0) {
        // 업데이트
        const updateQuery = `
          UPDATE numbering_rules SET
            rule_name = $1,
            description = $2,
            separator = $3,
            reset_period = $4,
            table_name = $5,
            column_name = $6,
            category_column = $7,
            category_value_id = $8,
            updated_at = NOW()
          WHERE rule_id = $9 AND company_code = $10
        `;
        await client.query(updateQuery, [
          config.ruleName,
          config.description || "",
          config.separator || "-",
          config.resetPeriod || "none",
          config.tableName || "",
          config.columnName || "",
          config.categoryColumn || null,
          config.categoryValueId || null,
          config.ruleId,
          companyCode,
        ]);

        // 기존 파트 삭제
        await client.query(
          "DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2",
          [config.ruleId, companyCode]
        );
      } else {
        // 신규 등록
        const insertQuery = `
          INSERT INTO numbering_rules (
            rule_id, rule_name, description, separator, reset_period,
            current_sequence, table_name, column_name, company_code,
            category_column, category_value_id,
            created_at, updated_at, created_by
          ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), NOW(), $12)
        `;
        await client.query(insertQuery, [
          config.ruleId,
          config.ruleName,
          config.description || "",
          config.separator || "-",
          config.resetPeriod || "none",
          config.currentSequence || 1,
          config.tableName || "",
          config.columnName || "",
          companyCode,
          config.categoryColumn || null,
          config.categoryValueId || null,
          createdBy,
        ]);
      }

      // 파트 저장
      if (config.parts && config.parts.length > 0) {
        for (const part of config.parts) {
          const partInsertQuery = `
            INSERT INTO numbering_rule_parts (
              rule_id, part_order, part_type, generation_method,
              auto_config, manual_config, company_code, created_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
          `;
          const autoConfigWithSep = { ...(part.autoConfig || {}), separatorAfter: part.separatorAfter ?? "-" };
          
          await client.query(partInsertQuery, [
            config.ruleId,
            part.order,
            part.partType,
            part.generationMethod,
            JSON.stringify(autoConfigWithSep),
            JSON.stringify(part.manualConfig || {}),
            companyCode,
          ]);
        }
      }

      await client.query("COMMIT");

      logger.info("테스트 테이블에 채번 규칙 저장 완료", {
        ruleId: config.ruleId,
        companyCode,
      });

      return config;
    } catch (error: any) {
      await client.query("ROLLBACK");
      logger.error("테스트 테이블에 채번 규칙 저장 실패", {
        error: error.message,
        stack: error.stack,
        ruleId: config.ruleId,
        companyCode,
      });
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * [테스트] 테스트 테이블에서 채번규칙 삭제
   * numbering_rules 테이블 사용
   */
  async deleteRuleFromTest(ruleId: string, companyCode: string): Promise<void> {
    const pool = getPool();
    const client = await pool.connect();

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

      logger.info("테스트 테이블에서 채번 규칙 삭제 시작", {
        ruleId,
        companyCode,
      });

      // 파트 먼저 삭제
      await client.query(
        "DELETE FROM numbering_rule_parts WHERE rule_id = $1 AND company_code = $2",
        [ruleId, companyCode]
      );

      // 규칙 삭제
      const result = await client.query(
        "DELETE FROM numbering_rules WHERE rule_id = $1 AND company_code = $2",
        [ruleId, companyCode]
      );

      await client.query("COMMIT");

      logger.info("테스트 테이블에서 채번 규칙 삭제 완료", {
        ruleId,
        companyCode,
        deletedCount: result.rowCount,
      });
    } catch (error: any) {
      await client.query("ROLLBACK");
      logger.error("테스트 테이블에서 채번 규칙 삭제 실패", {
        error: error.message,
        ruleId,
        companyCode,
      });
      throw error;
    } finally {
      client.release();
    }
  }

  /**
   * [테스트] 카테고리 값에 따라 적절한 채번규칙 조회
   * 1. 해당 카테고리 값에 매칭되는 규칙 찾기
   * 2. 없으면 기본 규칙(category_value_id가 NULL인) 찾기
   */
  async getNumberingRuleByColumnWithCategory(
    companyCode: string,
    tableName: string,
    columnName: string,
    categoryColumn?: string,
    categoryValueId?: number
  ): Promise<NumberingRuleConfig | null> {
    try {
      logger.info("카테고리 조건 포함 채번 규칙 조회 시작", {
        companyCode,
        tableName,
        columnName,
        categoryColumn,
        categoryValueId,
      });

      const pool = getPool();

      // 1. 카테고리 값에 매칭되는 규칙 찾기
      if (categoryColumn && categoryValueId) {
        const categoryQuery = `
          SELECT
            r.rule_id AS "ruleId",
            r.rule_name AS "ruleName",
            r.description,
            r.separator,
            r.reset_period AS "resetPeriod",
            r.current_sequence AS "currentSequence",
            r.table_name AS "tableName",
            r.column_name AS "columnName",
            r.company_code AS "companyCode",
            r.category_column AS "categoryColumn",
            r.category_value_id AS "categoryValueId",
            cv.value_label AS "categoryValueLabel",
            r.created_at AS "createdAt",
            r.updated_at AS "updatedAt",
            r.created_by AS "createdBy"
          FROM numbering_rules r
          LEFT JOIN category_values cv ON r.category_value_id = cv.value_id
          WHERE r.company_code = $1
            AND r.table_name = $2
            AND r.column_name = $3
            AND r.category_column = $4
            AND r.category_value_id = $5
          LIMIT 1
        `;
        const categoryResult = await pool.query(categoryQuery, [
          companyCode,
          tableName,
          columnName,
          categoryColumn,
          categoryValueId,
        ]);

        if (categoryResult.rows.length > 0) {
          const rule = categoryResult.rows[0];
          // 파트 정보 조회
          const partsQuery = `
            SELECT
              id,
              part_order AS "order",
              part_type AS "partType",
              generation_method AS "generationMethod",
              auto_config AS "autoConfig",
              manual_config AS "manualConfig"
            FROM numbering_rule_parts
            WHERE rule_id = $1 AND company_code = $2
            ORDER BY part_order
          `;
          const partsResult = await pool.query(partsQuery, [
            rule.ruleId,
            companyCode,
          ]);
          rule.parts = extractSeparatorAfterFromParts(partsResult.rows);

          logger.info("카테고리 조건 매칭 채번 규칙 찾음", {
            ruleId: rule.ruleId,
            categoryValueLabel: rule.categoryValueLabel,
          });
          return rule;
        }
      }

      // 2. 기본 규칙 찾기 (category_value_id가 NULL인)
      const defaultQuery = `
        SELECT
          r.rule_id AS "ruleId",
          r.rule_name AS "ruleName",
          r.description,
          r.separator,
          r.reset_period AS "resetPeriod",
          r.current_sequence AS "currentSequence",
          r.table_name AS "tableName",
          r.column_name AS "columnName",
          r.company_code AS "companyCode",
          r.category_column AS "categoryColumn",
          r.category_value_id AS "categoryValueId",
          r.created_at AS "createdAt",
          r.updated_at AS "updatedAt",
          r.created_by AS "createdBy"
        FROM numbering_rules r
        WHERE r.company_code = $1
          AND r.table_name = $2
          AND r.column_name = $3
          AND r.category_value_id IS NULL
        LIMIT 1
      `;
      const defaultResult = await pool.query(defaultQuery, [
        companyCode,
        tableName,
        columnName,
      ]);

      if (defaultResult.rows.length > 0) {
        const rule = defaultResult.rows[0];
        // 파트 정보 조회
        const partsQuery = `
          SELECT
            id,
            part_order AS "order",
            part_type AS "partType",
            generation_method AS "generationMethod",
            auto_config AS "autoConfig",
            manual_config AS "manualConfig"
          FROM numbering_rule_parts
          WHERE rule_id = $1 AND company_code = $2
          ORDER BY part_order
        `;
        const partsResult = await pool.query(partsQuery, [
          rule.ruleId,
          companyCode,
        ]);
        rule.parts = extractSeparatorAfterFromParts(partsResult.rows);

        logger.info("기본 채번 규칙 찾음 (카테고리 조건 없음)", {
          ruleId: rule.ruleId,
        });
        return rule;
      }

      logger.info("채번 규칙을 찾을 수 없음", {
        companyCode,
        tableName,
        columnName,
        categoryColumn,
        categoryValueId,
      });
      return null;
    } catch (error: any) {
      logger.error("카테고리 조건 포함 채번 규칙 조회 실패", {
        error: error.message,
        stack: error.stack,
      });
      throw error;
    }
  }

  /**
   * [테스트] 특정 테이블.컬럼의 모든 채번 규칙 조회 (카테고리 조건별)
   */
  async getRulesByTableColumn(
    companyCode: string,
    tableName: string,
    columnName: string
  ): Promise<NumberingRuleConfig[]> {
    try {
      const pool = getPool();
      const query = `
        SELECT
          r.rule_id AS "ruleId",
          r.rule_name AS "ruleName",
          r.description,
          r.separator,
          r.reset_period AS "resetPeriod",
          r.current_sequence AS "currentSequence",
          r.table_name AS "tableName",
          r.column_name AS "columnName",
          r.company_code AS "companyCode",
          r.category_column AS "categoryColumn",
          r.category_value_id AS "categoryValueId",
          cv.value_label AS "categoryValueLabel",
          r.created_at AS "createdAt",
          r.updated_at AS "updatedAt",
          r.created_by AS "createdBy"
        FROM numbering_rules r
        LEFT JOIN category_values cv ON r.category_value_id = cv.value_id
        WHERE r.company_code = $1
          AND r.table_name = $2
          AND r.column_name = $3
        ORDER BY r.category_value_id NULLS FIRST, r.created_at
      `;
      const result = await pool.query(query, [
        companyCode,
        tableName,
        columnName,
      ]);

      // 각 규칙의 파트 정보 조회
      for (const rule of result.rows) {
        const partsQuery = `
          SELECT
            id,
            part_order AS "order",
            part_type AS "partType",
            generation_method AS "generationMethod",
            auto_config AS "autoConfig",
            manual_config AS "manualConfig"
          FROM numbering_rule_parts
          WHERE rule_id = $1 AND company_code = $2
          ORDER BY part_order
        `;
        const partsResult = await pool.query(partsQuery, [
          rule.ruleId,
          companyCode,
        ]);
        rule.parts = extractSeparatorAfterFromParts(partsResult.rows);
      }

      return result.rows;
    } catch (error: any) {
      logger.error("테이블.컬럼별 채번 규칙 목록 조회 실패", {
        error: error.message,
      });
      throw error;
    }
  }

  /**
   * 회사별 채번규칙 복제 (테이블 기반)
   * numbering_rules, numbering_rule_parts 테이블 사용
   * 복제 후 화면 레이아웃의 numberingRuleId 참조도 업데이트
   */
  async copyRulesForCompany(
    sourceCompanyCode: string,
    targetCompanyCode: string
  ): Promise<{
    copiedCount: number;
    skippedCount: number;
    details: string[];
    ruleIdMap: Record<string, string>;
  }> {
    const pool = getPool();
    const client = await pool.connect();

    const result = {
      copiedCount: 0,
      skippedCount: 0,
      details: [] as string[],
      ruleIdMap: {} as Record<string, string>,
    };

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

      // 0. 대상 회사의 기존 채번규칙 삭제 (깨끗하게 복제하기 위해)
      // 먼저 파트 삭제
      await client.query(
        `DELETE FROM numbering_rule_parts 
         WHERE rule_id IN (SELECT rule_id FROM numbering_rules WHERE company_code = $1)`,
        [targetCompanyCode]
      );
      // 규칙 삭제
      const deleteResult = await client.query(
        `DELETE FROM numbering_rules WHERE company_code = $1 RETURNING rule_id`,
        [targetCompanyCode]
      );
      if (deleteResult.rowCount && deleteResult.rowCount > 0) {
        logger.info("기존 채번규칙 삭제", {
          targetCompanyCode,
          deletedCount: deleteResult.rowCount,
        });
      }

      // 1. 원본 회사의 채번규칙 조회 - numbering_rules 사용
      const sourceRulesResult = await client.query(
        `SELECT * FROM numbering_rules WHERE company_code = $1`,
        [sourceCompanyCode]
      );

      logger.info("원본 채번규칙 조회", {
        sourceCompanyCode,
        count: sourceRulesResult.rowCount,
      });

      // 2. 각 채번규칙 복제
      for (const rule of sourceRulesResult.rows) {
        // 새 rule_id 생성
        const newRuleId = `rule-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

        // 이미 존재하는지 확인 (이름 기반) - numbering_rules 사용
        const existsCheck = await client.query(
          `SELECT rule_id FROM numbering_rules 
           WHERE company_code = $1 AND rule_name = $2`,
          [targetCompanyCode, rule.rule_name]
        );

        if (existsCheck.rows.length > 0) {
          // 이미 존재하면 매핑만 추가
          result.ruleIdMap[rule.rule_id] = existsCheck.rows[0].rule_id;
          result.skippedCount++;
          result.details.push(`건너뜀 (이미 존재): ${rule.rule_name}`);
          continue;
        }

        // 채번규칙 복제 - numbering_rules 사용
        await client.query(
          `INSERT INTO numbering_rules (
            rule_id, rule_name, description, separator, reset_period,
            current_sequence, table_name, column_name, company_code,
            created_at, updated_at, created_by, category_column, category_value_id
          ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW(), $10, $11, $12)`,
          [
            newRuleId,
            rule.rule_name,
            rule.description,
            rule.separator,
            rule.reset_period,
            0, // 시퀀스 초기화
            rule.table_name,
            rule.column_name,
            targetCompanyCode,
            rule.created_by,
            rule.category_column,
            rule.category_value_id,
          ]
        );

        // 채번규칙 파트 복제 - numbering_rule_parts 사용
        const partsResult = await client.query(
          `SELECT * FROM numbering_rule_parts WHERE rule_id = $1 ORDER BY part_order`,
          [rule.rule_id]
        );

        for (const part of partsResult.rows) {
          await client.query(
            `INSERT INTO numbering_rule_parts (
              rule_id, part_order, part_type, generation_method,
              auto_config, manual_config, company_code, created_at
            ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())`,
            [
              newRuleId,
              part.part_order,
              part.part_type,
              part.generation_method,
              part.auto_config ? JSON.stringify(part.auto_config) : null,
              part.manual_config ? JSON.stringify(part.manual_config) : null,
              targetCompanyCode,
            ]
          );
        }

        // 매핑 추가
        result.ruleIdMap[rule.rule_id] = newRuleId;
        result.copiedCount++;
        result.details.push(`복제 완료: ${rule.rule_name}`);
        logger.info("채번규칙 복제 완료", {
          ruleName: rule.rule_name,
          oldRuleId: rule.rule_id,
          newRuleId,
        });
      }

      // 3. 화면 레이아웃의 numberingRuleId 참조 업데이트
      if (Object.keys(result.ruleIdMap).length > 0) {
        logger.info("화면 레이아웃 numberingRuleId 참조 업데이트 시작", {
          targetCompanyCode,
          mappingCount: Object.keys(result.ruleIdMap).length,
        });

        // 대상 회사의 모든 화면 레이아웃 조회
        const layoutsResult = await client.query(
          `SELECT sl.layout_id, sl.properties
           FROM screen_layouts sl
           JOIN screen_definitions sd ON sl.screen_id = sd.screen_id
           WHERE sd.company_code = $1
           AND sl.properties::text LIKE '%numberingRuleId%'`,
          [targetCompanyCode]
        );

        let updatedLayouts = 0;

        for (const layout of layoutsResult.rows) {
          let propsStr = JSON.stringify(layout.properties);
          let updated = false;

          // 각 매핑에 대해 치환
          for (const [oldRuleId, newRuleId] of Object.entries(
            result.ruleIdMap
          )) {
            if (propsStr.includes(`"${oldRuleId}"`)) {
              propsStr = propsStr
                .split(`"${oldRuleId}"`)
                .join(`"${newRuleId}"`);
              updated = true;
            }
          }

          if (updated) {
            await client.query(
              `UPDATE screen_layouts SET properties = $1::jsonb WHERE layout_id = $2`,
              [propsStr, layout.layout_id]
            );
            updatedLayouts++;
          }
        }

        logger.info("화면 레이아웃 numberingRuleId 참조 업데이트 완료", {
          targetCompanyCode,
          updatedLayouts,
        });
        result.details.push(
          `화면 레이아웃 ${updatedLayouts}개의 채번규칙 참조 업데이트`
        );
      }

      await client.query("COMMIT");

      logger.info("회사별 채번규칙 복제 완료", {
        sourceCompanyCode,
        targetCompanyCode,
        copiedCount: result.copiedCount,
        skippedCount: result.skippedCount,
        ruleIdMapCount: Object.keys(result.ruleIdMap).length,
      });

      return result;
    } catch (error) {
      await client.query("ROLLBACK");
      logger.error("회사별 채번규칙 복제 실패", {
        error,
        sourceCompanyCode,
        targetCompanyCode,
      });
      throw error;
    } finally {
      client.release();
    }
  }
}

export const numberingRuleService = new NumberingRuleService();
