import { query, queryOne, transaction } from "../database/db";
import { logger } from "../utils/logger";

// 공통코드 저장소를 wace `comm_code` 단일 테이블로 통일.
//   - 카테고리(top-level): parent_code_id IS NULL 또는 ''
//   - 코드(category 자식): parent_code_id = 부모.code_id  (재귀로 손자 단계까지 가능)
// 프론트가 기대하는 CodeCategory/CodeInfo 셰이프는 SELECT 프로젝션으로 맞춰 반환한다.
//
// 단일 테넌시(COMPANY_16): company_code/menu_objid 파라미터는 시그니처 유지를 위해 받지만 무시한다.

export interface CodeCategory {
  category_code: string;
  category_name: string;
  category_name_eng?: string | null;
  description?: string | null;
  sort_order: number;
  is_active: string;
  company_code: string;
  created_date?: Date | null;
  created_by?: string | null;
  updated_date?: Date | null;
  updated_by?: string | null;
}

export interface CodeInfo {
  code_category: string;
  code_value: string;
  code_name: string;
  code_name_eng?: string | null;
  description?: string | null;
  sort_order: number;
  is_active: string;
  company_code: string;
  menu_objid?: number | null;
  parent_code_value?: string | null;
  depth?: number;
  created_date?: Date | null;
  created_by?: string | null;
  updated_date?: Date | null;
  updated_by?: string | null;
}

export interface GetCategoriesParams {
  search?: string;
  isActive?: boolean;
  page?: number;
  size?: number;
}

export interface GetCodesParams {
  search?: string;
  isActive?: boolean;
  page?: number;
  size?: number;
}

export interface CreateCategoryData {
  categoryCode: string;
  categoryName: string;
  categoryNameEng?: string;
  description?: string;
  sortOrder?: number;
  isActive?: string;
}

export interface CreateCodeData {
  codeValue: string;
  codeName: string;
  codeNameEng?: string;
  description?: string;
  sortOrder?: number;
  isActive?: string;
  parentCodeValue?: string;
  depth?: number;
}

const CATEGORY_PROJECTION = `
  code_id        AS category_code,
  code_name      AS category_name,
  NULL::text     AS category_name_eng,
  NULL::text     AS description,
  sort_order     AS sort_order,
  CASE WHEN status = 'active' THEN 'Y' ELSE 'N' END AS is_active,
  '*'            AS company_code,
  regdate        AS created_date,
  writer         AS created_by,
  regdate        AS updated_date,
  writer         AS updated_by
`;

// 다음 objid 생성 (wace 시퀀스 없음 → MAX + 1)
async function nextObjid(): Promise<string> {
  const row = await queryOne<{ next_objid: string }>(
    `SELECT COALESCE(MAX(objid), 0) + 1 AS next_objid FROM comm_code`,
    []
  );
  return row?.next_objid ?? "1";
}

function mapStatus(isActive?: string | boolean): string | undefined {
  if (isActive === undefined) return undefined;
  if (typeof isActive === "boolean") return isActive ? "active" : "inActive";
  return isActive === "Y" ? "active" : isActive === "N" ? "inActive" : isActive;
}

export class CommonCodeService {
  /**
   * 카테고리(top-level) 목록 조회
   */
  async getCategories(
    params: GetCategoriesParams,
    _userCompanyCode?: string,
    _menuObjid?: number
  ) {
    try {
      const { search, isActive, page = 1, size = 20 } = params;

      const where: string[] = ["(parent_code_id IS NULL OR parent_code_id = '')"];
      const values: any[] = [];
      let p = 1;

      if (search) {
        where.push(`(code_name ILIKE $${p} OR code_id ILIKE $${p})`);
        values.push(`%${search}%`);
        p++;
      }

      if (isActive !== undefined) {
        where.push(`status = $${p++}`);
        values.push(isActive ? "active" : "inActive");
      }

      const whereSql = `WHERE ${where.join(" AND ")}`;
      const offset = (page - 1) * size;

      const categories = await query<CodeCategory>(
        `SELECT ${CATEGORY_PROJECTION}
         FROM comm_code
         ${whereSql}
         ORDER BY sort_order ASC, code_id ASC
         LIMIT $${p++} OFFSET $${p++}`,
        [...values, size, offset]
      );

      const countRow = await queryOne<{ count: string }>(
        `SELECT COUNT(*) AS count FROM comm_code ${whereSql}`,
        values
      );
      const total = parseInt(countRow?.count || "0");

      logger.info(
        `카테고리 조회 완료: comm_code top-level ${categories.length}건 / 총 ${total}건`
      );

      return { data: categories, total };
    } catch (error) {
      logger.error("카테고리 조회 중 오류:", error);
      throw error;
    }
  }

  /**
   * 카테고리에 속한 코드 전체 조회 (재귀 — 손자 이하 포함, depth/parent_code_value 부여)
   */
  async getCodes(
    categoryCode: string,
    params: GetCodesParams,
    _userCompanyCode?: string,
    _menuObjid?: number
  ) {
    try {
      const { search, isActive, page = 1, size = 20 } = params;

      const filters: string[] = [];
      const values: any[] = [categoryCode];
      let p = 2;

      if (search) {
        filters.push(`(code_name ILIKE $${p} OR code_value ILIKE $${p})`);
        values.push(`%${search}%`);
        p++;
      }
      if (isActive !== undefined) {
        filters.push(`is_active = $${p++}`);
        values.push(isActive ? "Y" : "N");
      }

      const innerWhere = filters.length ? `WHERE ${filters.join(" AND ")}` : "";
      const offset = (page - 1) * size;

      const sql = `
        WITH RECURSIVE descendants AS (
          SELECT
            objid, code_id, parent_code_id, code_name, sort_order, status, regdate, writer,
            1 AS depth,
            NULL::varchar AS rel_parent
          FROM comm_code
          WHERE parent_code_id = $1
          UNION ALL
          SELECT
            c.objid, c.code_id, c.parent_code_id, c.code_name, c.sort_order, c.status, c.regdate, c.writer,
            d.depth + 1,
            d.code_id AS rel_parent
          FROM comm_code c
          INNER JOIN descendants d ON c.parent_code_id = d.code_id
        ),
        projected AS (
          SELECT
            $1::text       AS code_category,
            code_id        AS code_value,
            code_name      AS code_name,
            NULL::text     AS code_name_eng,
            NULL::text     AS description,
            sort_order     AS sort_order,
            CASE WHEN status = 'active' THEN 'Y' ELSE 'N' END AS is_active,
            '*'::text      AS company_code,
            NULL::int      AS menu_objid,
            rel_parent     AS parent_code_value,
            depth          AS depth,
            regdate        AS created_date,
            writer         AS created_by,
            regdate        AS updated_date,
            writer         AS updated_by
          FROM descendants
        )
        SELECT * FROM projected
        ${innerWhere}
        ORDER BY depth ASC, sort_order ASC, code_value ASC
        LIMIT $${p++} OFFSET $${p++}
      `;

      const codes = await query<CodeInfo>(sql, [...values, size, offset]);

      const countSql = `
        WITH RECURSIVE descendants AS (
          SELECT code_id, parent_code_id, code_name, sort_order, status,
                 1 AS depth, NULL::varchar AS rel_parent
          FROM comm_code WHERE parent_code_id = $1
          UNION ALL
          SELECT c.code_id, c.parent_code_id, c.code_name, c.sort_order, c.status,
                 d.depth + 1, d.code_id
          FROM comm_code c INNER JOIN descendants d ON c.parent_code_id = d.code_id
        ),
        projected AS (
          SELECT code_id AS code_value, code_name,
                 CASE WHEN status='active' THEN 'Y' ELSE 'N' END AS is_active
          FROM descendants
        )
        SELECT COUNT(*) AS count FROM projected ${innerWhere}
      `;
      const countRow = await queryOne<{ count: string }>(countSql, values);
      const total = parseInt(countRow?.count || "0");

      logger.info(
        `코드 조회 완료: 카테고리=${categoryCode}, 결과 ${codes.length}건 / 총 ${total}건`
      );

      return { data: codes, total };
    } catch (error) {
      logger.error(`코드 조회 중 오류 (${categoryCode}):`, error);
      throw error;
    }
  }

  /**
   * 카테고리 생성 (parent_code_id = '')
   */
  async createCategory(
    data: CreateCategoryData,
    createdBy: string,
    _companyCode: string,
    _menuObjid?: number
  ) {
    try {
      const objid = await nextObjid();
      const status = mapStatus(data.isActive) || "active";

      const category = await queryOne<CodeCategory>(
        `INSERT INTO comm_code (objid, code_id, parent_code_id, code_name, status, sort_order, regdate, writer)
         VALUES ($1, $2, '', $3, $4, $5, NOW(), $6)
         RETURNING ${CATEGORY_PROJECTION}`,
        [
          objid,
          data.categoryCode,
          data.categoryName,
          status,
          data.sortOrder ?? 0,
          createdBy,
        ]
      );

      logger.info(`카테고리 생성: ${data.categoryCode} (objid=${objid})`);
      return category;
    } catch (error) {
      logger.error("카테고리 생성 중 오류:", error);
      throw error;
    }
  }

  /**
   * 카테고리 수정
   */
  async updateCategory(
    categoryCode: string,
    data: Partial<CreateCategoryData>,
    updatedBy: string,
    _companyCode?: string
  ) {
    try {
      const sets: string[] = [];
      const values: any[] = [];
      let p = 1;

      if (data.categoryName !== undefined) {
        sets.push(`code_name = $${p++}`);
        values.push(data.categoryName);
      }
      if (data.sortOrder !== undefined) {
        sets.push(`sort_order = $${p++}`);
        values.push(data.sortOrder);
      }
      if (data.isActive !== undefined) {
        sets.push(`status = $${p++}`);
        values.push(mapStatus(data.isActive));
      }
      // writer/regdate를 갱신 흔적으로 사용 (wace 스키마에 updated_* 컬럼이 없음)
      sets.push(`writer = $${p++}`);
      values.push(updatedBy);
      sets.push(`regdate = NOW()`);

      if (sets.filter((s) => !s.startsWith("writer") && s !== "regdate = NOW()").length === 0) {
        // 변경 필드 없음 — 현재 값을 그대로 반환
        const current = await queryOne<CodeCategory>(
          `SELECT ${CATEGORY_PROJECTION} FROM comm_code
           WHERE code_id = $1 AND (parent_code_id IS NULL OR parent_code_id = '')`,
          [categoryCode]
        );
        return current;
      }

      const sql = `UPDATE comm_code
         SET ${sets.join(", ")}
         WHERE code_id = $${p} AND (parent_code_id IS NULL OR parent_code_id = '')
         RETURNING ${CATEGORY_PROJECTION}`;
      values.push(categoryCode);

      const category = await queryOne<CodeCategory>(sql, values);

      logger.info(`카테고리 수정: ${categoryCode}`);
      return category;
    } catch (error) {
      logger.error(`카테고리 수정 중 오류 (${categoryCode}):`, error);
      throw error;
    }
  }

  /**
   * 카테고리 삭제 (자식 존재 시 거부)
   */
  async deleteCategory(categoryCode: string, _companyCode?: string) {
    try {
      const child = await queryOne<{ cnt: string }>(
        `SELECT COUNT(*) AS cnt FROM comm_code WHERE parent_code_id = $1`,
        [categoryCode]
      );
      if (parseInt(child?.cnt || "0") > 0) {
        throw new Error("자식 코드가 존재하여 카테고리를 삭제할 수 없습니다.");
      }

      await query(
        `DELETE FROM comm_code
         WHERE code_id = $1 AND (parent_code_id IS NULL OR parent_code_id = '')`,
        [categoryCode]
      );

      logger.info(`카테고리 삭제: ${categoryCode}`);
    } catch (error) {
      logger.error(`카테고리 삭제 중 오류 (${categoryCode}):`, error);
      throw error;
    }
  }

  /**
   * 코드 생성 — parent_code_id는 parentCodeValue가 있으면 그 값, 없으면 categoryCode
   */
  async createCode(
    categoryCode: string,
    data: CreateCodeData,
    createdBy: string,
    _companyCode: string,
    _menuObjid: number
  ) {
    try {
      const objid = await nextObjid();
      const parentCodeId = data.parentCodeValue || categoryCode;
      const status = mapStatus(data.isActive) || "active";

      // INSERT 후 동일한 셰이프(CodeInfo)로 다시 SELECT
      await query(
        `INSERT INTO comm_code (objid, code_id, parent_code_id, code_name, status, sort_order, regdate, writer)
         VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7)`,
        [
          objid,
          data.codeValue,
          parentCodeId,
          data.codeName,
          status,
          data.sortOrder ?? 0,
          createdBy,
        ]
      );

      const inserted = await queryOne<CodeInfo>(
        `SELECT
           $1::text AS code_category,
           code_id  AS code_value,
           code_name,
           NULL::text AS code_name_eng,
           NULL::text AS description,
           sort_order,
           CASE WHEN status = 'active' THEN 'Y' ELSE 'N' END AS is_active,
           '*'::text AS company_code,
           NULL::int AS menu_objid,
           CASE WHEN parent_code_id = $1 THEN NULL ELSE parent_code_id END AS parent_code_value,
           CASE WHEN parent_code_id = $1 THEN 1 ELSE 2 END AS depth,
           regdate AS created_date,
           writer  AS created_by,
           regdate AS updated_date,
           writer  AS updated_by
         FROM comm_code WHERE code_id = $2`,
        [categoryCode, data.codeValue]
      );

      logger.info(
        `코드 생성: ${categoryCode}.${data.codeValue} (parent=${parentCodeId})`
      );
      return inserted;
    } catch (error) {
      logger.error(
        `코드 생성 중 오류 (${categoryCode}.${data.codeValue}):`,
        error
      );
      throw error;
    }
  }

  /**
   * 코드 수정
   */
  async updateCode(
    categoryCode: string,
    codeValue: string,
    data: Partial<CreateCodeData>,
    updatedBy: string,
    _companyCode?: string
  ) {
    try {
      const sets: string[] = [];
      const values: any[] = [];
      let p = 1;

      if (data.codeName !== undefined) {
        sets.push(`code_name = $${p++}`);
        values.push(data.codeName);
      }
      if (data.sortOrder !== undefined) {
        sets.push(`sort_order = $${p++}`);
        values.push(data.sortOrder);
      }
      if (data.isActive !== undefined) {
        sets.push(`status = $${p++}`);
        values.push(mapStatus(data.isActive));
      }
      if (data.parentCodeValue !== undefined) {
        // 부모 변경 — categoryCode 또는 다른 코드의 code_id로
        const newParent = data.parentCodeValue || categoryCode;
        sets.push(`parent_code_id = $${p++}`);
        values.push(newParent);
      }

      sets.push(`writer = $${p++}`);
      values.push(updatedBy);
      sets.push(`regdate = NOW()`);

      if (sets.length === 2) {
        // writer + regdate 만 남음 (변경 없음)
        const current = await queryOne<CodeInfo>(
          `SELECT
             $1::text AS code_category,
             code_id  AS code_value,
             code_name,
             NULL::text AS code_name_eng,
             NULL::text AS description,
             sort_order,
             CASE WHEN status='active' THEN 'Y' ELSE 'N' END AS is_active,
             '*'::text AS company_code,
             NULL::int AS menu_objid,
             CASE WHEN parent_code_id = $1 THEN NULL ELSE parent_code_id END AS parent_code_value,
             CASE WHEN parent_code_id = $1 THEN 1 ELSE 2 END AS depth,
             regdate AS created_date, writer AS created_by, regdate AS updated_date, writer AS updated_by
           FROM comm_code WHERE code_id = $2`,
          [categoryCode, codeValue]
        );
        return current;
      }

      await query(
        `UPDATE comm_code SET ${sets.join(", ")} WHERE code_id = $${p}`,
        [...values, codeValue]
      );

      const updated = await queryOne<CodeInfo>(
        `SELECT
           $1::text AS code_category,
           code_id  AS code_value,
           code_name,
           NULL::text AS code_name_eng,
           NULL::text AS description,
           sort_order,
           CASE WHEN status='active' THEN 'Y' ELSE 'N' END AS is_active,
           '*'::text AS company_code,
           NULL::int AS menu_objid,
           CASE WHEN parent_code_id = $1 THEN NULL ELSE parent_code_id END AS parent_code_value,
           CASE WHEN parent_code_id = $1 THEN 1 ELSE 2 END AS depth,
           regdate AS created_date, writer AS created_by, regdate AS updated_date, writer AS updated_by
         FROM comm_code WHERE code_id = $2`,
        [categoryCode, codeValue]
      );

      logger.info(`코드 수정: ${categoryCode}.${codeValue}`);
      return updated;
    } catch (error) {
      logger.error(`코드 수정 중 오류 (${categoryCode}.${codeValue}):`, error);
      throw error;
    }
  }

  /**
   * 코드 삭제 (자식 존재 시 거부)
   */
  async deleteCode(
    categoryCode: string,
    codeValue: string,
    _companyCode?: string
  ) {
    try {
      const child = await queryOne<{ cnt: string }>(
        `SELECT COUNT(*) AS cnt FROM comm_code WHERE parent_code_id = $1`,
        [codeValue]
      );
      if (parseInt(child?.cnt || "0") > 0) {
        throw new Error("자식 코드가 존재하여 코드를 삭제할 수 없습니다.");
      }

      await query(`DELETE FROM comm_code WHERE code_id = $1`, [codeValue]);
      logger.info(`코드 삭제: ${categoryCode}.${codeValue}`);
    } catch (error) {
      logger.error(`코드 삭제 중 오류 (${categoryCode}.${codeValue}):`, error);
      throw error;
    }
  }

  /**
   * 카테고리별 활성 코드 옵션 (드롭다운/화면관리용)
   * — 직속 자식만 (depth=1), 활성만
   */
  async getCodeOptions(categoryCode: string, _userCompanyCode?: string) {
    try {
      const codes = await query<{
        code_value: string;
        code_name: string;
        code_name_eng: string | null;
        sort_order: number;
      }>(
        `SELECT code_id AS code_value, code_name, NULL::text AS code_name_eng, sort_order
         FROM comm_code
         WHERE parent_code_id = $1 AND status = 'active'
         ORDER BY sort_order ASC, code_id ASC`,
        [categoryCode]
      );

      return codes.map((c) => ({
        value: c.code_value,
        label: c.code_name,
        labelEng: c.code_name_eng,
      }));
    } catch (error) {
      logger.error(`코드 옵션 조회 중 오류 (${categoryCode}):`, error);
      throw error;
    }
  }

  /**
   * 코드 정렬순서 일괄 변경 (드래그 앤 드롭)
   */
  async reorderCodes(
    categoryCode: string,
    codes: Array<{ codeValue: string; sortOrder: number }>,
    updatedBy: string
  ) {
    try {
      const valid: Array<{ codeValue: string; sortOrder: number }> = [];
      if (codes.length > 0) {
        const placeholders = codes.map((_, i) => `$${i + 1}`).join(", ");
        const existing = await query<{ code_id: string }>(
          `SELECT code_id FROM comm_code WHERE code_id IN (${placeholders})`,
          codes.map((c) => c.codeValue)
        );
        const existSet = new Set(existing.map((e) => e.code_id));
        for (const c of codes) {
          if (existSet.has(c.codeValue)) valid.push(c);
        }
      }

      if (valid.length === 0) {
        throw new Error(
          `카테고리 ${categoryCode}에 순서를 변경할 유효한 코드가 없습니다.`
        );
      }

      await transaction(async (client) => {
        for (const { codeValue, sortOrder } of valid) {
          await client.query(
            `UPDATE comm_code SET sort_order = $1, writer = $2, regdate = NOW()
             WHERE code_id = $3`,
            [sortOrder, updatedBy, codeValue]
          );
        }
      });

      logger.info(
        `코드 정렬순서 변경: ${categoryCode} ${valid.length}/${codes.length}건`
      );
    } catch (error) {
      logger.error(`코드 정렬순서 변경 중 오류 (${categoryCode}):`, error);
      throw error;
    }
  }

  /**
   * 카테고리 중복 검사
   * — categoryCode: comm_code 전체에서 code_id 중복 확인
   * — categoryName: top-level 내에서 code_name 중복 확인
   * — categoryNameEng: 영문명 미지원이므로 중복 없음
   */
  async checkCategoryDuplicate(
    field: "categoryCode" | "categoryName" | "categoryNameEng",
    value: string,
    excludeCategoryCode?: string,
    _userCompanyCode?: string
  ): Promise<{ isDuplicate: boolean; message: string }> {
    try {
      const v = (value || "").trim();
      if (!v) return { isDuplicate: false, message: "값을 입력해주세요." };

      if (field === "categoryNameEng") {
        return {
          isDuplicate: false,
          message: "영문명은 사용하지 않습니다.",
        };
      }

      let sql = "";
      const values: any[] = [];
      let p = 1;

      if (field === "categoryCode") {
        sql = `SELECT code_id FROM comm_code WHERE code_id = $${p++}`;
        values.push(v);
      } else {
        sql = `SELECT code_id FROM comm_code
               WHERE code_name = $${p++}
                 AND (parent_code_id IS NULL OR parent_code_id = '')`;
        values.push(v);
      }

      if (excludeCategoryCode) {
        sql += ` AND code_id != $${p++}`;
        values.push(excludeCategoryCode);
      }
      sql += ` LIMIT 1`;

      const found = await queryOne<{ code_id: string }>(sql, values);
      const isDuplicate = !!found;
      const labels = {
        categoryCode: "카테고리 코드",
        categoryName: "카테고리명",
        categoryNameEng: "카테고리 영문명",
      };
      return {
        isDuplicate,
        message: isDuplicate
          ? `이미 사용 중인 ${labels[field]}입니다.`
          : `사용 가능한 ${labels[field]}입니다.`,
      };
    } catch (error) {
      logger.error(`카테고리 중복 검사 중 오류 (${field}: ${value}):`, error);
      throw error;
    }
  }

  /**
   * 코드 중복 검사
   * — codeValue: comm_code 전체에서 code_id 중복
   * — codeName: 같은 카테고리 트리 안에서 code_name 중복
   */
  async checkCodeDuplicate(
    categoryCode: string,
    field: "codeValue" | "codeName" | "codeNameEng",
    value: string,
    excludeCodeValue?: string,
    _userCompanyCode?: string
  ): Promise<{ isDuplicate: boolean; message: string }> {
    try {
      const v = (value || "").trim();
      if (!v) return { isDuplicate: false, message: "값을 입력해주세요." };

      if (field === "codeNameEng") {
        return {
          isDuplicate: false,
          message: "영문명은 사용하지 않습니다.",
        };
      }

      if (field === "codeValue") {
        let sql = `SELECT code_id FROM comm_code WHERE code_id = $1`;
        const values: any[] = [v];
        if (excludeCodeValue) {
          sql += ` AND code_id != $2`;
          values.push(excludeCodeValue);
        }
        sql += ` LIMIT 1`;
        const found = await queryOne<{ code_id: string }>(sql, values);
        return {
          isDuplicate: !!found,
          message: found
            ? "이미 사용 중인 코드값입니다."
            : "사용 가능한 코드값입니다.",
        };
      }

      // codeName: 카테고리 트리(재귀) 내에서 같은 이름 검색
      let sql = `
        WITH RECURSIVE descendants AS (
          SELECT code_id, code_name FROM comm_code WHERE parent_code_id = $1
          UNION ALL
          SELECT c.code_id, c.code_name FROM comm_code c
          INNER JOIN descendants d ON c.parent_code_id = d.code_id
        )
        SELECT code_id FROM descendants WHERE code_name = $2`;
      const values: any[] = [categoryCode, v];
      if (excludeCodeValue) {
        sql += ` AND code_id != $3`;
        values.push(excludeCodeValue);
      }
      sql += ` LIMIT 1`;
      const found = await queryOne<{ code_id: string }>(sql, values);
      return {
        isDuplicate: !!found,
        message: found
          ? "이미 사용 중인 코드명입니다."
          : "사용 가능한 코드명입니다.",
      };
    } catch (error) {
      logger.error(
        `코드 중복 검사 중 오류 (${categoryCode}, ${field}: ${value}):`,
        error
      );
      throw error;
    }
  }

  /**
   * 계층구조 코드 조회 — parentCodeValue/depth 필터 지원
   */
  async getHierarchicalCodes(
    categoryCode: string,
    parentCodeValue?: string | null,
    depth?: number,
    _userCompanyCode?: string,
    _menuObjid?: number
  ) {
    try {
      const filters: string[] = [`is_active = 'Y'`];
      const values: any[] = [categoryCode];
      let p = 2;

      if (parentCodeValue === null || parentCodeValue === undefined) {
        filters.push(`(parent_code_value IS NULL OR parent_code_value = '')`);
      } else if (parentCodeValue !== "") {
        filters.push(`parent_code_value = $${p++}`);
        values.push(parentCodeValue);
      }

      if (depth !== undefined) {
        filters.push(`depth = $${p++}`);
        values.push(depth);
      }

      const whereSql = filters.length ? `WHERE ${filters.join(" AND ")}` : "";

      const sql = `
        WITH RECURSIVE descendants AS (
          SELECT objid, code_id, parent_code_id, code_name, sort_order, status, regdate, writer,
                 1 AS depth, NULL::varchar AS rel_parent
          FROM comm_code WHERE parent_code_id = $1
          UNION ALL
          SELECT c.objid, c.code_id, c.parent_code_id, c.code_name, c.sort_order, c.status, c.regdate, c.writer,
                 d.depth + 1, d.code_id
          FROM comm_code c INNER JOIN descendants d ON c.parent_code_id = d.code_id
        ),
        projected AS (
          SELECT
            $1::text   AS code_category,
            code_id    AS code_value,
            code_name,
            NULL::text AS code_name_eng,
            NULL::text AS description,
            sort_order,
            CASE WHEN status='active' THEN 'Y' ELSE 'N' END AS is_active,
            '*'::text  AS company_code,
            NULL::int  AS menu_objid,
            rel_parent AS parent_code_value,
            depth,
            regdate    AS created_date,
            writer     AS created_by,
            regdate    AS updated_date,
            writer     AS updated_by
          FROM descendants
        )
        SELECT * FROM projected
        ${whereSql}
        ORDER BY sort_order ASC, code_value ASC
      `;
      return await query<CodeInfo>(sql, values);
    } catch (error) {
      logger.error(`계층구조 코드 조회 중 오류 (${categoryCode}):`, error);
      throw error;
    }
  }

  /**
   * 카테고리 전체 트리 조회 (flat + tree)
   */
  async getCodeTree(
    categoryCode: string,
    _userCompanyCode?: string,
    _menuObjid?: number
  ) {
    try {
      const sql = `
        WITH RECURSIVE descendants AS (
          SELECT objid, code_id, parent_code_id, code_name, sort_order, status, regdate, writer,
                 1 AS depth, NULL::varchar AS rel_parent
          FROM comm_code WHERE parent_code_id = $1 AND status = 'active'
          UNION ALL
          SELECT c.objid, c.code_id, c.parent_code_id, c.code_name, c.sort_order, c.status, c.regdate, c.writer,
                 d.depth + 1, d.code_id
          FROM comm_code c INNER JOIN descendants d ON c.parent_code_id = d.code_id
          WHERE c.status = 'active'
        )
        SELECT
          $1::text   AS code_category,
          code_id    AS code_value,
          code_name,
          NULL::text AS code_name_eng,
          NULL::text AS description,
          sort_order,
          'Y'::text  AS is_active,
          '*'::text  AS company_code,
          NULL::int  AS menu_objid,
          rel_parent AS parent_code_value,
          depth,
          regdate    AS created_date,
          writer     AS created_by,
          regdate    AS updated_date,
          writer     AS updated_by
        FROM descendants
        ORDER BY depth ASC, sort_order ASC, code_value ASC
      `;
      const flat = await query<CodeInfo>(sql, [categoryCode]);

      const buildTree = (
        nodes: CodeInfo[],
        parentValue: string | null = null
      ): any[] =>
        nodes
          .filter((n) => (n.parent_code_value || null) === parentValue)
          .map((n) => ({ ...n, children: buildTree(nodes, n.code_value) }));

      return { flat, tree: buildTree(flat) };
    } catch (error) {
      logger.error(`코드 트리 조회 중 오류 (${categoryCode}):`, error);
      throw error;
    }
  }

  /**
   * 자식 코드 존재 여부
   */
  async hasChildren(
    _categoryCode: string,
    codeValue: string,
    _companyCode?: string
  ): Promise<boolean> {
    try {
      const row = await queryOne<{ count: string }>(
        `SELECT COUNT(*) AS count FROM comm_code WHERE parent_code_id = $1`,
        [codeValue]
      );
      return parseInt(row?.count || "0") > 0;
    } catch (error) {
      logger.error(`자식 코드 확인 중 오류 (${codeValue}):`, error);
      throw error;
    }
  }
}
