// ============================================================
// 개발관리 PART (M1 등록 / M2 조회) — wace_plm partMng.xml 1:1 이식.
//
// 매퍼 매핑 (원본: wace_plm/src/com/pms/mapper/partMng.xml):
//   partMngTempGridList  → listTemp()          (M1, status != 'release')
//   partMngGridList      → listRelease()       (M2, status = 'release')
//   partMngInfo          → getByObjid()
//   insertpartInfo       → create()            (38 컬럼 INSERT, wace 24 + 추가 15)
//   updatePartDetail     → update()            (21 컬럼 UPDATE)
//   partMngDeploy        → deploy()            (3-step 트랜잭션: isLastInit + history + deploy)
//   partMngIsLastInit    → (deploy 내부)
//   insertPartMngHistory → (deploy 내부)
//   partMngDelete        → removeMany()
//
// 채번 정책: part_mng.objid 는 bigint. 클라이언트가 part_objid 안 보내면
//   wace CommonUtils.createObjId() 1:1 구현(objidUtil.createObjId) 사용.
//
// EO_NO 채번: IS_LONGD='1' 이면 EOB{yy}-{seq} / 아니면 EO{yy}-{seq}. wace 그대로.
// ============================================================

import { PoolClient } from "pg";
import path from "path";
import fs from "fs";
import { getPool, transaction } from "../database/db";
import { logger } from "../utils/logger";
import { createObjId } from "../utils/objidUtil";
import { generateUUID } from "../utils/generateId";
import { PART_BASE_SIMPLE } from "./devPartSqlFragments";

// ─── 필터/바디 타입 ──────────────────────────────────────────

export interface PartTempListFilter {
  search_part_no?: string;
  search_part_name?: string;
  search_material?: string;
  search_spec?: string;
  search_part_type?: string;
  writer?: string;
  status?: string;
  status_arr?: string[];
  product_code?: string;
  upg_no?: string;
  page?: number;
  page_size?: number;
}

export interface PartListFilter extends PartTempListFilter {
  search_year?: string;
  search_hardness?: string;
  search_method?: string;
  search_surface?: string;
  customer_objid?: string;
  customer_cd?: string;
  project_name?: string;
  unit_code?: string;
  search_design_date_from?: string;
  search_design_date_to?: string;
  is_last?: string;
  eo?: string;
}

export interface PartCreateBody {
  part_objid?: string;
  part_no: string;
  part_name: string;
  unit?: string;
  qty?: string;
  spec?: string;
  material?: string;
  thickness?: string;
  width?: string;
  height?: string;
  out_diameter?: string;
  in_diameter?: string;
  length?: string;
  remark?: string;
  part_type: string;
  product_mgmt_objid?: string;
  supply_code?: string;
  maker?: string;
  contract_objid?: string;
  post_processing?: string;
  heat_treatment_hardness?: string;
  heat_treatment_method?: string;
  surface_treatment?: string;
  acctfg?: string;
  odrfg?: string;
  unit_dc?: string;
  unitmang_dc?: string;
  unitchng_nb?: string | number;
  lot_fg?: string;
  use_yn?: string;
  qc_fg?: string;
  setitem_fg?: string;
  req_fg?: string;
  unit_length?: string;
  unit_qty?: string;
}

export interface PartUpdateBody {
  part_name?: string;
  material?: string;
  heat_treatment_hardness?: string;
  heat_treatment_method?: string;
  surface_treatment?: string;
  maker?: string;
  part_type?: string;
  acctfg?: string;
  odrfg?: string;
  spec?: string;
  unit_dc?: string;
  unitmang_dc?: string;
  unitchng_nb?: string | number;
  lot_fg?: string;
  use_yn?: string;
  qc_fg?: string;
  setitem_fg?: string;
  req_fg?: string;
  unit_length?: string;
  unit_qty?: string;
  remark?: string;
}

// ─── 공용 검색 절 생성 ──────────────────────────────────────

function buildCommonWhere(
  filter: PartTempListFilter & Partial<PartListFilter>,
  startIdx: number
): { sql: string; params: any[] } {
  const params: any[] = [];
  const conds: string[] = [];
  let idx = startIdx;

  if (filter.product_code) {
    conds.push(`T.PRODUCT_MGMT_OBJID = $${idx++}`);
    params.push(filter.product_code);
  }
  if (filter.upg_no) {
    conds.push(`T.UPG_NO = $${idx++}`);
    params.push(filter.upg_no);
  }
  if (filter.search_part_no) {
    conds.push(`UPPER(T.PART_NO) LIKE UPPER($${idx++})`);
    params.push(`%${filter.search_part_no}%`);
  }
  if (filter.search_part_name) {
    conds.push(`UPPER(T.PART_NAME) LIKE UPPER($${idx++})`);
    params.push(`%${filter.search_part_name}%`);
  }
  if (filter.search_material) {
    conds.push(`UPPER(T.MATERIAL) LIKE UPPER($${idx++})`);
    params.push(`%${filter.search_material}%`);
  }
  if (filter.search_spec) {
    conds.push(`UPPER(T.SPEC) LIKE UPPER($${idx++})`);
    params.push(`%${filter.search_spec}%`);
  }
  if (filter.search_part_type) {
    conds.push(`T.PART_TYPE = $${idx++}`);
    params.push(filter.search_part_type);
  }
  if (filter.writer) {
    conds.push(`T.WRITER = $${idx++}`);
    params.push(filter.writer);
  }
  if (filter.status) {
    conds.push(`T.STATUS = $${idx++}`);
    params.push(filter.status);
  }
  if (filter.status_arr && filter.status_arr.length > 0) {
    const placeholders = filter.status_arr.map(() => `$${idx++}`).join(",");
    conds.push(`T.STATUS IN (${placeholders})`);
    params.push(...filter.status_arr);
  }

  // M2 전용 추가 필터
  if (filter.search_design_date_from) {
    conds.push(`TO_DATE(T.DESIGN_DATE, 'YYYY-MM-DD') >= $${idx++}::timestamp`);
    params.push(filter.search_design_date_from);
  }
  if (filter.search_design_date_to) {
    conds.push(`TO_DATE(T.DESIGN_DATE, 'YYYY-MM-DD') <= $${idx++}::timestamp`);
    params.push(filter.search_design_date_to);
  }
  if (filter.is_last) {
    conds.push(`T.IS_LAST = $${idx++}`);
    params.push(filter.is_last);
  }
  if (filter.eo) {
    // wace 원본: AND T.EO_TEMP IS NULL OR EO_TEMP = '' — eo=1일 때만 적용
    conds.push(`(T.EO_TEMP IS NULL OR T.EO_TEMP = '')`);
  }

  return { sql: conds.length ? conds.join(" AND ") : "1=1", params };
}

function paginate(filter: { page?: number; page_size?: number }): { limit: number; offset: number; page: number; pageSize: number } {
  const page = Math.max(1, Number(filter.page) || 1);
  const pageSize = Math.min(500, Math.max(1, Number(filter.page_size) || 20));
  return { limit: pageSize, offset: (page - 1) * pageSize, page, pageSize };
}

// ─── M1 그리드 (status != 'release') ────────────────────────

export async function listTemp(filter: PartTempListFilter) {
  const { limit, offset, page, pageSize } = paginate(filter);
  const where = buildCommonWhere(filter, 1);

  // M1 추가 컬럼: PARTNER_TITLE, BOM_REPORT_OBJID/CHILD_OBJID/QTY/QTY_TEMP, Q_QTY, PARENT_PART_INFO, SORT
  const baseSql = `
    SELECT T.*,
           CASE WHEN T.REVISION IS NULL THEN '0' ELSE T.REVISION END AS SORT,
           O.PARTNER_TITLE,
           (SELECT PART_NO FROM PART_MNG SP WHERE SP.OBJID::varchar = Q.PARENT_PART_NO) AS PARENT_PART_INFO,
           Q.BOM_REPORT_OBJID,
           Q.OBJID AS OBJID_QTY,
           Q.CHILD_OBJID,
           Q.QTY     AS Q_QTY_RAW,
           Q.QTY_TEMP,
           (CASE
              WHEN Q.STATUS = 'deploy' THEN Q.QTY
              WHEN (Q.QTY_TEMP IS NULL OR Q.QTY_TEMP = '') THEN Q.QTY
              ELSE Q.QTY_TEMP
            END) AS Q_QTY
      FROM (${PART_BASE_SIMPLE}) T
      LEFT JOIN (
        SELECT PART_OBJID,
               ARRAY_TO_STRING(ARRAY_AGG(PARTNER_TITLE ORDER BY SEQ), ',') AS PARTNER_TITLE
          FROM (
            SELECT OSM.PART_OBJID, OSM.SEQ,
                   OSM.SEQ || '. ' || (SELECT SUPPLY_NAME FROM ADMIN_SUPPLY_MNG
                                        WHERE OBJID::varchar = OSM.PARTNER_OBJID::varchar) AS PARTNER_TITLE
              FROM ORDER_SPEC_MNG OSM
          ) OSMO
         GROUP BY PART_OBJID
      ) O ON T.OBJID::varchar = O.PART_OBJID::varchar
      LEFT JOIN BOM_PART_QTY Q ON (
        T.OBJID::varchar IN (
          SELECT DISTINCT PM1.OBJID::varchar
            FROM PART_MNG PM1, PART_MNG PM2
           WHERE PM1.STATUS = 'changing'
             AND PM2.OBJID::varchar = Q.PART_NO
             AND PM1.PART_NO = PM2.PART_NO
        )
        AND Q.STATUS = 'beforeEdit'
      )
     WHERE ${where.sql}
       AND COALESCE(T.STATUS, '') <> 'release'
     ORDER BY PARENT_PART_INFO NULLS LAST, T.PART_NO
  `;

  const pool = getPool();
  const dataSql = `${baseSql} LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}`;
  const countSql = `SELECT COUNT(*)::int AS total FROM (${baseSql}) X`;

  const [dataRes, countRes] = await Promise.all([
    pool.query(dataSql, [...where.params, limit, offset]),
    pool.query(countSql, where.params),
  ]);

  return {
    rows: dataRes.rows,
    total: countRes.rows[0]?.total ?? 0,
    page,
    pageSize,
  };
}

// ─── M2 그리드 (status = 'release') ─────────────────────────

export async function listRelease(filter: PartListFilter) {
  const { limit, offset, page, pageSize } = paginate(filter);
  const where = buildCommonWhere(filter, 1);

  const baseSql = `
    SELECT
       ROW_NUMBER() OVER (
         ORDER BY T.PART_NO,
                  CASE WHEN T.REVISION LIKE 'RE%' THEN 0 ELSE 1 END,
                  T.REVISION DESC
       ) AS NUM,
       T.*,
       /* wace 1:1 — PART_TYPE='0000063'(SET) 면 '1', 그 외엔 BOM_QTY 합 */
       CASE WHEN T.PART_TYPE = '0000063' THEN '1'
            ELSE (SELECT SUM(CASE WHEN COALESCE(Q.QTY,'') = '' THEN 0 ELSE Q.QTY::numeric END)::varchar
                    FROM BOM_PART_QTY Q
                   WHERE Q.LAST_PART_OBJID = T.OBJID::varchar)
       END AS BOM_QTY
      FROM (${PART_BASE_SIMPLE}) T
     WHERE ${where.sql}
       AND T.STATUS = 'release'
     ORDER BY T.PART_NO,
              CASE WHEN T.REVISION LIKE 'RE%' THEN 0 ELSE 1 END,
              T.REVISION DESC
  `;

  const pool = getPool();
  const dataSql = `${baseSql} LIMIT $${where.params.length + 1} OFFSET $${where.params.length + 2}`;
  const countSql = `SELECT COUNT(*)::int AS total FROM (${baseSql}) X`;

  const [dataRes, countRes] = await Promise.all([
    pool.query(dataSql, [...where.params, limit, offset]),
    pool.query(countSql, where.params),
  ]);

  return {
    rows: dataRes.rows,
    total: countRes.rows[0]?.total ?? 0,
    page,
    pageSize,
  };
}

// ─── 단건 상세 ──────────────────────────────────────────────

export async function getByObjid(objid: string) {
  const sql = `SELECT T.* FROM (${PART_BASE_SIMPLE}) T WHERE T.OBJID = $1`;
  const r = await getPool().query(sql, [objid]);
  return r.rows[0] ?? null;
}

// ─── 신규 등록 (insertpartInfo + 15 추가컬럼) ───────────────

export async function create(userId: string, body: PartCreateBody): Promise<string> {
  if (!body.part_no || !body.part_name) {
    throw new Error("필수값 누락: part_no, part_name");
  }
  const partObjid = body.part_objid && String(body.part_objid).trim() !== ""
    ? String(body.part_objid)
    : createObjId();

  const sql = `
    INSERT INTO PART_MNG (
       OBJID, PART_NO, PART_NAME, UNIT, QTY, SPEC, MATERIAL,
       THICKNESS, WIDTH, HEIGHT, OUT_DIAMETER, IN_DIAMETER, LENGTH,
       REMARK, STATUS, REG_DATE, WRITER, IS_LAST,
       PART_TYPE, PRODUCT_MGMT_OBJID, SUPPLY_CODE, MAKER, CONTRACT_OBJID, POST_PROCESSING,
       HEAT_TREATMENT_HARDNESS, HEAT_TREATMENT_METHOD, SURFACE_TREATMENT,
       ACCTFG, ODRFG, UNIT_DC, UNITMANG_DC, UNITCHNG_NB,
       LOT_FG, USE_YN, QC_FG, SETITEM_FG, REQ_FG, UNIT_LENGTH, UNIT_QTY
    ) VALUES (
       $1::numeric, $2, $3, $4, $5, $6, $7,
       $8, $9, $10, $11, $12, $13,
       $14, 'create', NOW(), $15, '1',
       $16, $17, $18, $19, $20, $21,
       $22, $23, $24,
       $25, $26, $27, $28, $29,
       $30, $31, $32, $33, $34, $35, $36
    )
  `;

  const params = [
    partObjid,
    body.part_no, body.part_name, body.unit ?? null, body.qty ?? null, body.spec ?? null, body.material ?? null,
    body.thickness ?? null, body.width ?? null, body.height ?? null, body.out_diameter ?? null, body.in_diameter ?? null, body.length ?? null,
    body.remark ?? null, userId,
    body.part_type, body.product_mgmt_objid ?? null, body.supply_code ?? null, body.maker ?? null, body.contract_objid ?? null, body.post_processing ?? null,
    body.heat_treatment_hardness ?? null, body.heat_treatment_method ?? null, body.surface_treatment ?? null,
    body.acctfg ?? null, body.odrfg ?? null, body.unit_dc ?? null, body.unitmang_dc ?? null, body.unitchng_nb ?? null,
    body.lot_fg ?? null, body.use_yn ?? null, body.qc_fg ?? null, body.setitem_fg ?? null, body.req_fg ?? null, body.unit_length ?? null, body.unit_qty ?? null,
  ];

  await getPool().query(sql, params);
  return partObjid;
}

// ─── 상세 수정 (updatePartDetail, 21 컬럼) ──────────────────

export async function update(objid: string, body: PartUpdateBody): Promise<number> {
  const sql = `
    UPDATE PART_MNG SET
       PART_NAME               = $1,
       MATERIAL                = $2,
       HEAT_TREATMENT_HARDNESS = $3,
       HEAT_TREATMENT_METHOD   = $4,
       SURFACE_TREATMENT       = $5,
       MAKER                   = $6,
       PART_TYPE               = $7,
       ACCTFG                  = $8,
       ODRFG                   = $9,
       SPEC                    = $10,
       UNIT_DC                 = $11,
       UNITMANG_DC             = $12,
       UNITCHNG_NB             = $13,
       LOT_FG                  = $14,
       USE_YN                  = $15,
       QC_FG                   = $16,
       SETITEM_FG              = $17,
       REQ_FG                  = $18,
       UNIT_LENGTH             = $19,
       UNIT_QTY                = $20,
       REMARK                  = $21,
       EDIT_DATE               = NOW()
     WHERE OBJID = $22
  `;
  const r = await getPool().query(sql, [
    body.part_name ?? null, body.material ?? null,
    body.heat_treatment_hardness ?? null, body.heat_treatment_method ?? null, body.surface_treatment ?? null,
    body.maker ?? null, body.part_type ?? null,
    body.acctfg ?? null, body.odrfg ?? null, body.spec ?? null,
    body.unit_dc ?? null, body.unitmang_dc ?? null,
    body.unitchng_nb ?? null,
    body.lot_fg ?? null, body.use_yn ?? null, body.qc_fg ?? null,
    body.setitem_fg ?? null, body.req_fg ?? null,
    body.unit_length ?? null, body.unit_qty ?? null,
    body.remark ?? null,
    objid,
  ]);
  return r.rowCount ?? 0;
}

// ─── 확정 (M1→M2): 다중 objids 순차 트랜잭션 ────────────────

export async function deploy(userId: string, objids: string[]): Promise<{ deployed: number; eo_nos: Record<string, string> }> {
  if (!objids || objids.length === 0) return { deployed: 0, eo_nos: {} };

  const eoNos: Record<string, string> = {};
  let deployed = 0;

  await transaction(async (client: PoolClient) => {
    for (const objid of objids) {
      // 1) 동일 PART_NO 모두 IS_LAST='0' (wace partMngIsLastInit 1:1)
      await client.query(
        `UPDATE PART_MNG SET IS_LAST = '0', EDIT_DATE = NOW()
          WHERE PART_NO = (SELECT PART_NO FROM PART_MNG WHERE OBJID = $1)`,
        [objid]
      );

      // 2) PART_MNG_HISTORY 이력 INSERT (wace insertPartMngHistory 1:1, BOM_PART_QTY 미연계)
      //    BOM 컨텍스트(CHILD_OBJID/CHANGE_OPTION 등)는 deploy 단계에선 NULL.
      await client.query(
        `INSERT INTO PART_MNG_HISTORY (
            objid, product_mgmt_objid, upg_no, part_no, part_name, unit,
            qty, spec, material, weight, part_type, remark,
            es_spec, ms_spec, change_option, design_apply_point, management_flag,
            revision, status, reg_date, edit_date, writer, is_last,
            eo_no, eo_temp, excel_upload_seq, sourcing_code, sub_material,
            parent_part_no, design_date, eo_date, deploy_date,
            thickness, width, height, out_diameter, in_diameter, length,
            supply_code, change_type, contract_objid, maker,
            his_reg_date, his_writer, his_status,
            heat_treatment_hardness, heat_treatment_method, surface_treatment
          )
          SELECT
            P.OBJID::numeric, P.PRODUCT_MGMT_OBJID, P.UPG_NO, P.PART_NO, P.PART_NAME, P.UNIT,
            P.QTY, P.SPEC, P.MATERIAL, P.WEIGHT, P.PART_TYPE, P.REMARK,
            P.ES_SPEC, P.MS_SPEC, P.CHANGE_OPTION, P.DESIGN_APPLY_POINT, P.MANAGEMENT_FLAG,
            P.REVISION, P.STATUS, P.REG_DATE, NOW(), $2, P.IS_LAST,
            P.EO_NO, P.EO_TEMP, P.EXCEL_UPLOAD_SEQ::varchar, P.SOURCING_CODE, P.SUB_MATERIAL,
            P.PARENT_PART_NO, P.DESIGN_DATE, P.EO_DATE, P.DEPLOY_DATE,
            P.THICKNESS, P.WIDTH, P.HEIGHT, P.OUT_DIAMETER, P.IN_DIAMETER, P.LENGTH,
            P.SUPPLY_CODE, P.CHANGE_TYPE, P.CONTRACT_OBJID, P.MAKER,
            NOW(), $2, 'DEPLOY',
            P.HEAT_TREATMENT_HARDNESS, P.HEAT_TREATMENT_METHOD, P.SURFACE_TREATMENT
          FROM PART_MNG P
          WHERE P.OBJID = $1`,
        [objid, userId]
      );

      // 3) 본 행 deploy: IS_LAST='1', STATUS='release', DEPLOY_DATE=NOW(),
      //    REVISION=COALESCE→'RE', EO_DATE=오늘, EO_NO=신규 채번 (IS_LONGD에 따라 EOB/EO 분기).
      const deployRes = await client.query<{ eo_no: string }>(
        `UPDATE PART_MNG P SET
            IS_LAST     = '1',
            EDIT_DATE   = NOW(),
            DEPLOY_DATE = NOW(),
            STATUS      = 'release',
            REVISION    = (CASE WHEN COALESCE(P.REVISION,'') = '' THEN 'RE' ELSE P.REVISION END),
            EO_DATE     = TO_CHAR(NOW(),'YYYY-MM-DD'),
            EO_NO       = (
              CASE WHEN P.IS_LONGD = '1' THEN
                'EOB' || TO_CHAR(NOW(),'yy') || '-' || LPAD(
                  (SELECT COALESCE(SUBSTR(MAX(SP.EO_NO), 7, 8)::integer + 1, 1)::varchar
                     FROM PART_MNG SP
                    WHERE SP.EO_NO LIKE 'EOB' || TO_CHAR(NOW(),'yy') || '-%'
                      AND SP.PART_NO <> P.PART_NO
                      AND COALESCE(SP.REVISION, '') <> COALESCE(P.REVISION, '')
                  ), 4, '0')
              ELSE
                'EO'  || TO_CHAR(NOW(),'yy') || '-' || LPAD(
                  (SELECT COALESCE(SUBSTR(MAX(SP.EO_NO), 6, 8)::integer + 1, 1)::varchar
                     FROM PART_MNG SP
                    WHERE SP.EO_NO IS NOT NULL
                      AND SP.EO_NO NOT LIKE 'EOB%'
                      AND SP.PART_NO <> P.PART_NO
                      AND COALESCE(SP.REVISION, '') <> COALESCE(P.REVISION, '')
                  ), 4, '0')
              END
            )
          WHERE OBJID = $1
          RETURNING EO_NO AS eo_no`,
        [objid]
      );

      if (deployRes.rowCount && deployRes.rowCount > 0) {
        deployed += deployRes.rowCount;
        eoNos[objid] = deployRes.rows[0]?.eo_no ?? "";
      }
    }
  });

  logger.info("PART deploy 완료", { count: deployed, userId });
  return { deployed, eo_nos: eoNos };
}

// ─── 다중 삭제 (wace partMngDelete — POSITION 트릭 → ANY 배열) ─

export async function removeMany(objids: string[]): Promise<number> {
  if (!objids || objids.length === 0) return 0;
  // bigint 컬럼이므로 numeric[] 캐스팅
  const r = await getPool().query(
    `DELETE FROM PART_MNG WHERE OBJID = ANY($1::numeric[])`,
    [objids]
  );
  return r.rowCount ?? 0;
}

// ─── 도면 다중 업로드 ─────────────────────────────────────────
// wace partMngTempList.jsp btnDrawingUpload + PartMngController.uploadDrawingFilesForPartList 1:1.
//
// 흐름:
//   1) IS_LAST='1' part_mng 전체 조회 → PART_NO → OBJID 맵
//   2) 각 파일별:
//      a) 확장자(STP/STEP/DWG/DXF/PDF) → doc_type 결정
//         (STP/STEP=3D_CAD, DWG/DXF=2D_DRAWING_CAD, PDF=2D_PDF_CAD)
//      b) 파일명에서 알려진 확장자 반복 제거 (.idw .dwg .dxf .stp .step .pdf .chg)
//      c) PART_NO 매칭: 정확 일치 우선 → 안 되면 startsWith (가장 긴 prefix)
//      d) 매칭 성공 → attach_file_info INSERT (target_objid = part_mng.objid)
//      e) 매칭 실패 → notFoundCount++ 파일 삭제

const DRAWING_KNOWN_EXTS = [".idw", ".dwg", ".dxf", ".stp", ".step", ".pdf", ".chg"];

function removeDrawingExtensions(fileName: string): string {
  let result = fileName;
  let removed = true;
  while (removed) {
    removed = false;
    const lastDot = result.lastIndexOf(".");
    if (lastDot > 0) {
      const ext = result.substring(lastDot).toLowerCase();
      if (DRAWING_KNOWN_EXTS.includes(ext)) {
        result = result.substring(0, lastDot);
        removed = true;
      }
    }
  }
  return result;
}

function findMatchingPartNo(
  fileNameWithoutExt: string,
  partNoSet: Set<string>
): string | null {
  if (!fileNameWithoutExt || partNoSet.size === 0) return null;
  if (partNoSet.has(fileNameWithoutExt)) return fileNameWithoutExt;
  let best: string | null = null;
  for (const pn of partNoSet) {
    if (fileNameWithoutExt.startsWith(pn)) {
      if (!best || pn.length > best.length) best = pn;
    }
  }
  return best;
}

export interface DrawingMultiUploadDetail {
  fileName: string;
  partNo?: string;
  docType?: string;
  status: "success" | "fail" | "notFound" | "unsupported";
  reason?: string;
}

export interface DrawingMultiUploadResult {
  successCount: number;
  failCount: number;
  notFoundCount: number;
  details: DrawingMultiUploadDetail[];
}

export async function drawingMultiUpload(
  userId: string,
  companyCode: string,
  files: Express.Multer.File[],
  partNoList: string[] | null | undefined
): Promise<DrawingMultiUploadResult> {
  const pool = getPool();

  // 1) 매칭 후보 PART 조회
  //    partNoList 지정 → 그 목록 IN 절로 제한 (M1 등록 화면, 현재 그리드 기반)
  //    partNoList 없음 → IS_LAST='1' 전체 (M2 조회 화면 — 페이지 밖 파트도 매칭 허용)
  //    (wace partMng.xml partMngListByPartNos: `<if PART_NO_LIST != null>` 1:1)
  const hasList = Array.isArray(partNoList) && partNoList.length > 0;
  const partRes = hasList
    ? await pool.query<{ objid: string; part_no: string }>(
        `SELECT objid::text AS objid, part_no
           FROM part_mng
          WHERE is_last = '1'
            AND part_no = ANY($1::text[])`,
        [partNoList]
      )
    : await pool.query<{ objid: string; part_no: string }>(
        `SELECT objid::text AS objid, part_no
           FROM part_mng
          WHERE is_last = '1' AND part_no IS NOT NULL`
      );
  const partNoMap = new Map<string, string>();
  for (const r of partRes.rows) {
    if (r.part_no) partNoMap.set(r.part_no, r.objid);
  }
  const partNoSet = new Set(partNoMap.keys());
  logger.info("도면 다중 업로드 시작", {
    files: files.length,
    scope: hasList ? "visible" : "all",
    requestedPartNos: hasList ? partNoList!.length : null,
    partCandidates: partNoMap.size,
  });

  // 2) 회사/날짜 폴더 준비 (fileController.ts 와 동일 경로 규약)
  const baseUploadDir = path.join(process.cwd(), "uploads");
  const today = new Date();
  const year = today.getFullYear();
  const month = String(today.getMonth() + 1).padStart(2, "0");
  const day = String(today.getDate()).padStart(2, "0");
  const dateFolder = `${year}/${month}/${day}`;
  const actualCompanyCode = companyCode === "*" ? "company_*" : companyCode;
  const finalUploadDir = path.join(baseUploadDir, actualCompanyCode, dateFolder);
  if (!fs.existsSync(finalUploadDir)) {
    fs.mkdirSync(finalUploadDir, { recursive: true });
  }

  const result: DrawingMultiUploadResult = {
    successCount: 0,
    failCount: 0,
    notFoundCount: 0,
    details: [],
  };

  for (const file of files) {
    // 파일명 UTF-8 디코딩
    let originalName: string;
    try {
      originalName = Buffer.from(file.originalname, "latin1").toString("utf8");
    } catch {
      originalName = file.originalname;
    }

    // 확장자 → doc_type
    const ext = path
      .extname(originalName)
      .toLowerCase()
      .replace(".", "")
      .toUpperCase();
    let docType = "";
    let docTypeName = "";
    if (ext === "STP" || ext === "STEP") {
      docType = "3D_CAD";
      docTypeName = "3D CAD 첨부파일";
    } else if (ext === "DWG" || ext === "DXF") {
      docType = "2D_DRAWING_CAD";
      docTypeName = "2D(Drawing) CAD 첨부파일";
    } else if (ext === "PDF") {
      docType = "2D_PDF_CAD";
      docTypeName = "2D(PDF) CAD 첨부파일";
    } else {
      result.failCount++;
      result.details.push({
        fileName: originalName,
        status: "unsupported",
        reason: `지원하지 않는 확장자: ${ext}`,
      });
      try { fs.unlinkSync(file.path); } catch {}
      continue;
    }

    // 파일명 ↔ part_no 매칭
    const nameWithoutExt = removeDrawingExtensions(originalName);
    const matchedPartNo = findMatchingPartNo(nameWithoutExt, partNoSet);
    if (!matchedPartNo) {
      result.notFoundCount++;
      result.details.push({
        fileName: originalName,
        status: "notFound",
        reason: `품번 매칭 실패 (${nameWithoutExt})`,
      });
      try { fs.unlinkSync(file.path); } catch {}
      continue;
    }
    const targetObjid = partNoMap.get(matchedPartNo)!;

    // 임시 → 최종 위치 이동
    const sanitizedName = originalName
      .replace(/[\/\\:*?"<>|]/g, "_")
      .replace(/\s+/g, "_")
      .replace(/_{2,}/g, "_");
    const savedFileName = `${Date.now()}_${sanitizedName}`;
    const finalFilePath = path.join(finalUploadDir, savedFileName);
    try {
      fs.renameSync(file.path, finalFilePath);
    } catch (e: any) {
      logger.error("도면 파일 저장 실패", { error: e.message, file: originalName });
      result.failCount++;
      result.details.push({
        fileName: originalName,
        partNo: matchedPartNo,
        docType,
        status: "fail",
        reason: "파일 저장 실패",
      });
      continue;
    }

    const relativePath = `/${actualCompanyCode}/${dateFolder}/${savedFileName}`;
    const fullFilePath = `/uploads${relativePath}`;

    // attach_file_info INSERT
    const objidValue = parseInt(
      generateUUID().replace(/-/g, "").substring(0, 15),
      16
    );
    try {
      await pool.query(
        `INSERT INTO attach_file_info (
          objid, target_objid, saved_file_name, real_file_name, doc_type, doc_type_name,
          file_size, file_ext, file_path, company_code, writer, regdate, status
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW(), 'ACTIVE')`,
        [
          objidValue,
          targetObjid,
          savedFileName,
          originalName,
          docType,
          docTypeName,
          file.size,
          ext.toLowerCase(),
          fullFilePath,
          companyCode,
          userId,
        ]
      );
      result.successCount++;
      result.details.push({
        fileName: originalName,
        partNo: matchedPartNo,
        docType,
        status: "success",
      });
    } catch (e: any) {
      logger.error("attach_file_info INSERT 실패", {
        error: e.message,
        file: originalName,
      });
      result.failCount++;
      result.details.push({
        fileName: originalName,
        partNo: matchedPartNo,
        docType,
        status: "fail",
        reason: e.message,
      });
      try { fs.unlinkSync(finalFilePath); } catch {}
    }
  }

  logger.info("도면 다중 업로드 완료", result);
  return result;
}
