import { randomUUID } from "crypto";
import amqp from "amqplib";
import { promisify } from "util";
import { AppDataSource } from "../database/data-source";
import { Command } from "../entities/Command";
import { chunkArray, commandTypePath } from "../interfaces/utils";
import CallAPI from "../interfaces/call-api";
import { getPosMasterNo, getOrgFullName } from "../utils/org-formatting";
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import { PosMaster } from "../entities/PosMaster";
import { Profile } from "../entities/Profile";
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
import { EmployeeTempPosMaster } from "../entities/EmployeeTempPosMaster";
import { ProfileEmployee } from "../entities/ProfileEmployee";
import { OrgRevision } from "../entities/OrgRevision";
import { EmployeePosition } from "../entities/EmployeePosition";
import { OrgChild1 } from "../entities/OrgChild1";
import { OrgChild2 } from "../entities/OrgChild2";
import { OrgChild3 } from "../entities/OrgChild3";
import { OrgChild4 } from "../entities/OrgChild4";
import { OrgRoot } from "../entities/OrgRoot";
import { PosMasterAssign, PosMasterAssignDTO } from "../entities/PosMasterAssign";
import { Position } from "../entities/Position";
import { In, Not, Repository } from "typeorm";
import { PosMasterAct } from "../entities/PosMasterAct";
import { PermissionOrg } from "../entities/PermissionOrg";
import { sendWebSocket } from "./webSocket";
import { PayloadSendNoti } from "../interfaces/utils";
import { PermissionProfile } from "../entities/PermissionProfile";
import { PosMasterHistory } from "../entities/PosMasterHistory";

const redis = require("redis");
const REDIS_HOST = process.env.REDIS_HOST;
const REDIS_PORT = process.env.REDIS_PORT;

let reconnectTimer: ReturnType<typeof setTimeout> | null = null;

function scheduleReconnect() {
  if (reconnectTimer) {
    return;
  }

  reconnectTimer = setTimeout(async () => {
    reconnectTimer = null;
    try {
      await init();
    } catch (error) {
      console.error("[AMQ] Reconnect failed:", error);
      scheduleReconnect();
    }
  }, 1000);
}

export let sendToQueue: (payload: any) => void;
export let sendToQueueOrg: (payload: any) => void;
export let sendToQueueOrgDraft: (payload: any) => void;
export let sendToQueueCommandNoti: (payload: any) => void;
export async function init() {
  //----> (1) Producer
  if (
    !process.env.AMQ_URL ||
    !process.env.AMQ_QUEUE ||
    !process.env.AMQ_QUEUE_ORG ||
    !process.env.AMQ_QUEUE_ORG_DRAFT ||
    !process.env.AMQ_QUEUE_COMMAND_NOTI
  )
    return;

  const {
    AMQ_URL: url,
    AMQ_QUEUE: queue,
    AMQ_QUEUE_ORG: queue_org,
    AMQ_QUEUE_ORG_DRAFT: queue_org_draft,
    AMQ_QUEUE_COMMAND_NOTI: queue_command_noti,
  } = process.env; //----> (1.2) get url and queue from .env

  const connection = await amqp.connect(url); //----> (1.3) set up url with amqp protocol

  console.log(connection ? "[AMQ] Connection success" : "[AMQ] Connection failed");

  connection.on("error", (error) => {
    console.error("[AMQ] Connection error:", error);
  });

  connection.on("close", () => {
    console.error("[AMQ] Connection closed. Scheduling reconnect...");
    scheduleReconnect();
  });

  const channel = await connection.createChannel(); //----> (1.4) create Channel

  console.log(channel ? "[AMQ] Create channel success" : "[AMQ] Create channel failed");

  channel.on("error", (error) => {
    console.error("[AMQ] Channel error:", error);
  });

  channel.on("close", () => {
    console.error("[AMQ] Channel closed. Scheduling reconnect...");
    scheduleReconnect();
  });

  channel.assertQueue(queue, { durable: true }), //----> (1.5) assert queue and set durable (if "true" save to disk on RabbitMQ)
    channel.assertQueue(queue_org, { durable: true }),
    channel.assertQueue(queue_org_draft, { durable: true }),
    channel.assertQueue(queue_command_noti, { durable: true }),
    channel.prefetch(1);

  sendToQueue = (payload: any, persistent = true) => {
    //----> (2) sendQueue To RabbitMQ and set persistent (if "true" redo the failed queue when server run again)
    channel.sendToQueue(queue, Buffer.from(JSON.stringify(payload)), {
      persistent,
    });
  };

  sendToQueueOrg = (payload: any, persistent = true) => {
    channel.sendToQueue(queue_org, Buffer.from(JSON.stringify(payload)), { persistent });
  };

  sendToQueueOrgDraft = (payload: any, persistent = true) => {
    channel.sendToQueue(queue_org_draft, Buffer.from(JSON.stringify(payload)), { persistent });
  };

  sendToQueueCommandNoti = (payload: any, persistent = true) => {
    channel.sendToQueue(queue_command_noti, Buffer.from(JSON.stringify(payload)), { persistent });
  };

  console.log("[AMQ] Listening for message...");
  createConsumer(queue, channel, handler), //----> (3) Process Consumer
    createConsumer(queue_org, channel, handler_org);
  createConsumer(queue_org_draft, channel, handler_org_draft);
  createConsumer(queue_command_noti, channel, handler_command_noti);
  // createConsumer(queue2, channel, handler2);
}

function createConsumer( //----> consumer
  queue: string,
  channel: amqp.Channel,
  handler: (msg: amqp.ConsumeMessage) => Promise<boolean> | boolean,
) {
  channel.consume(
    queue,
    async (msg) => {
      if (!msg) return;
      try {
        if (await handler(msg)) {
          console.log("[AMQ] Process Consumer success");
          return channel.ack(msg);
        }
        console.error(
          `[AMQ] Process Consumer failed on queue ${queue}, acknowledging without retry`,
        );
        return channel.ack(msg);
      } catch (error) {
        console.error(`[AMQ] Consumer processing error on queue ${queue}:`, error);
        try {
          console.error(`[AMQ] Acknowledging failed message on queue ${queue} without retry`);
          channel.ack(msg);
        } catch (channelError) {
          console.error(`[AMQ] Failed to ack/nack message on queue ${queue}:`, channelError);
        }
      }
    },
    { noAck: false },
  );
}

async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
  //----> condition before process consumer
  // const repo = AppDataSource.getRepository(Command);
  // const { data, token, user } = JSON.parse(msg.content.toString());
  // const { id, status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt } = data;
  // const command = await repo.findOne({
  //   where: { id: id },
  //   relations: ["commandType", "commandRecives"],
  // });
  // if (!command) return true;
  // let waiting_message = `ระบบทำการออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543}`;
  // let success_message = `ระบบออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543} เสร็จสิ้น`;
  // let error_message = `ระบบออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543} ผิดพลาด`
  // if(command.commandType?.code == "C-PM-47"){
  //   waiting_message = `ระบบทำการออกคำสั่งเลขที่ ${command.commandNo}`;
  //   success_message = `ระบบออกคำสั่งเลขที่ ${command.commandNo} เสร็จสิ้น`;
  //   error_message = `ระบบออกคำสั่งเลขที่ ${command.commandNo} ผิดพลาด`;
  // }
  // if (user) {
  //   sendWebSocket(
  //     "send-command-notification",
  //     {
  //       success: true,
  //       message: waiting_message,
  //       payload: command,
  //     },
  //     { userId: user?.sub },
  //   ).catch(console.error);
  // }
  // const path = commandTypePath(command.commandType.code);
  // if (path == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ");
  // return await new CallAPI()
  //  //chunk 50
  //   .PostData(
  //     {
  //       headers: { authorization: token },
  //     },
  //     path + "/excecute",
  //     {
  //       refIds: command.commandRecives //chunk
  //         .filter((x) => x.refId != null)
  //         .map((x) => ({
  //           refId: x.refId,
  //           commandNo: command.commandNo,
  //           commandYear: command.commandYear,
  //           commandId: command.id,
  //           remark: command.positionDetail,
  //           amount: x.amount,
  //           amountSpecial: x.amountSpecial,
  //           positionSalaryAmount: x.positionSalaryAmount,
  //           mouthSalaryAmount: x.mouthSalaryAmount,
  //           commandCode: command.commandType.commandCode,
  //           commandName: command.commandType.name,
  //           commandDateAffect: command.commandExcecuteDate,
  //           commandDateSign: command.commandAffectDate,
  //         })),
  //     },
  //     false,
  //   )
  //   .then(async (res) => {
  //     console.log("[AMQ] Excecute Command Success");
  //     Object.assign(command, { status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt });
  //     const result = await repo.save(command).catch((e) => console.log(e));
  //     if (user) {
  //       sendWebSocket(
  //         "send-command-notification",
  //         {
  //           success: true,
  //           message: success_message,
  //           payload: command,
  //         },
  //         { userId: user?.sub },
  //       ).catch(console.error);
  //     }
  //     return !!result;
  //   })
  //   .catch((e) => {
  //     console.error(e);
  //     if (user) {
  //       sendWebSocket(
  //         "send-command-notification",
  //         {
  //           success: false,
  //           message: error_message,
  //           payload: command,
  //         },
  //         { userId: user?.sub },
  //       ).catch(console.error);
  //     }
  //     return false;
  //   });
  const repo = AppDataSource.getRepository(Command);
  const { data, token, user } = JSON.parse(msg.content.toString());
  const { id, status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt } = data;

  const command = await repo.findOne({
    where: { id: id },
    relations: ["commandType", "commandRecives"],
  });
  if (!command) return true;

  let waiting_message = `ระบบทำการออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543}`;
  let success_message = `ระบบออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543} เสร็จสิ้น`;
  let error_message = `ระบบออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543} ผิดพลาด`;

  if (command.commandType?.code == "C-PM-47") {
    waiting_message = `ระบบทำการออกคำสั่งเลขที่ ${command.commandNo}`;
    success_message = `ระบบออกคำสั่งเลขที่ ${command.commandNo} เสร็จสิ้น`;
    error_message = `ระบบออกคำสั่งเลขที่ ${command.commandNo} ผิดพลาด`;
  }

  if (user) {
    sendWebSocket(
      "send-command-notification",
      { success: true, message: waiting_message, payload: command },
      { userId: user?.sub },
    ).catch(console.error);
  }

  const path = commandTypePath(command.commandType.code);
  if (path == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ");

  try {
    const allRefIds = new Set<string>();
    const chunks = chunkArray(
      command.commandRecives
        .filter((x) => x.refId != null)
        .map((x) => {
          const key = `${x.refId}-${command.id}`;
          if (allRefIds.has(key)) {
            return null;
          }
          allRefIds.add(key);

          return {
            refId: x.refId,
            commandNo: command.commandNo,
            commandYear: command.commandYear,
            commandId: command.id,
            remark: command.positionDetail,
            amount: x.amount,
            amountSpecial: x.amountSpecial,
            positionSalaryAmount: x.positionSalaryAmount,
            mouthSalaryAmount: x.mouthSalaryAmount,
            commandCode: command.commandType.commandCode,
            commandName: command.commandType.name,
            commandDateAffect: command.commandExcecuteDate,
            commandDateSign: command.commandAffectDate,
          };
        })
        .filter(Boolean),
      20,
    );

    for (const chunk of chunks) {
      await new CallAPI().PostData(
        { headers: { authorization: token } },
        path + "/excecute",
        { refIds: chunk },
        false,
      );
    }

    Object.assign(command, { status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt });
    const result = await repo.save(command);

    if (user) {
      sendWebSocket(
        "send-command-notification",
        { success: true, message: success_message, payload: command },
        { userId: user?.sub },
      ).catch(console.error);
    }

    console.log("[AMQ] Excecute Command Success");
    return !!result;
  } catch (e) {
    console.error(e);
    if (user) {
      sendWebSocket(
        "send-command-notification",
        { success: false, message: error_message, payload: command },
        { userId: user?.sub },
      ).catch(console.error);
    }
    return false;
  }
}

// async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
//   const repo = AppDataSource.getRepository(Command);
//   const { data, token, user } = JSON.parse(msg.content.toString());
//   const { id, status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt } = data;

//   const command = await repo.findOne({
//     where: { id },
//     relations: ["commandType", "commandRecives"],
//   });

//   if (!command) return true;

//   if (user) {
//     sendWebSocket(
//       "send-command-notification",
//       {
//         success: true,
//         message: `ระบบทำการออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543}`,
//         payload: command,
//       },
//       { userId: user?.sub },
//     ).catch(console.error);
//   }

//   const path = commandTypePath(command.commandType.code);
//   if (path == null) throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ");

//   const recivers = command.commandRecives
//     .filter((x) => x.refId != null)
//     .map((x) => ({
//       refId: x.refId,
//       commandNo: command.commandNo,
//       commandYear: command.commandYear,
//       commandId: command.id,
//       remark: command.positionDetail,
//       amount: x.amount,
//       amountSpecial: x.amountSpecial,
//       positionSalaryAmount: x.positionSalaryAmount,
//       mouthSalaryAmount: x.mouthSalaryAmount,
//       commandCode: command.commandType.commandCode,
//       commandName: command.commandType.name,
//       commandDateAffect: command.commandExcecuteDate,
//       commandDateSign: command.commandAffectDate,
//     }));

//   const batchSize = 50;
//   const batches = Array.from({ length: Math.ceil(recivers.length / batchSize) }, (_, i) =>
//     recivers.slice(i * batchSize, i * batchSize + batchSize)
//   );

//   const api = new CallAPI();
//   try {
//     for (const batch of batches) {
//       await api.PostData(
//         { headers: { authorization: token } },
//         path + "/excecute",
//         { refIds: batch },
//         false
//       );
//     }

//     console.log("[AMQ] Excecute Command Success");
//     Object.assign(command, { status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt });
//     const result = await repo.save(command);

//     if (user) {
//       sendWebSocket(
//         "send-command-notification",
//         {
//           success: true,
//           message: `ระบบออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543} เสร็จสิ้น`,
//           payload: command,
//         },
//         { userId: user?.sub },
//       ).catch(console.error);
//     }

//     return !!result;
//   } catch (e) {
//     console.error(e);
//     if (user) {
//       sendWebSocket(
//         "send-command-notification",
//         {
//           success: false,
//           message: `ระบบออกคำสั่งเลขที่ ${command.commandNo}/${command.commandYear + 543} ผิดพลาด`,
//           payload: command,
//         },
//         { userId: user?.sub },
//       ).catch(console.error);
//     }
//     return false;
//   }
// }

async function handler_command_noti(msg: amqp.ConsumeMessage): Promise<boolean> {
  const { data, token, user } = JSON.parse(msg.content.toString());
  const { profiles, command } = data;

  try {
    let profilesNotiRequest: Promise<any> | undefined;
    if (!["C-PM-10"].includes(command.commandType.code)) {
      profilesNotiRequest = new CallAPI()
        .PostData(
          { headers: { authorization: token } },
          "/placement/noti/profiles",
          {
            subject: `${command.issue}`,
            body: `${command.issue}`,
            receiverUserIds: profiles,
            payload: "", // แนบไฟล์ (ถ้าจำเป็น)
            isSendMail: true,
            isSendInbox: true,
            isSendNotification: true,
          },
          false,
        )
        .catch((error) => {
          if (error.response) {
            // Server ตอบกลับ (มี status code 4xx หรือ 5xx)
            console.error("Error status:", error.response.status);
            console.error("Error data:", error.response.data);
            console.error("Error headers:", error.response.headers);
          } else if (error.request) {
            // ไม่มีการตอบกลับจาก server
            console.error("No response received:", error.request);
          } else {
            // เกิดข้อผิดพลาดอื่น เช่น โค้ด Axios ผิด
            console.error("Axios error:", error.message);
          }
          console.error("Full error object:", error);
        });
    }

    let profilesSend =
      command && command.commandSends.length > 0
        ? command.commandSends
          .filter((x: any) => x.profileId != null)
          .map((x: any) => ({
            receiverUserId: x.profileId,
            notiLink: "",
            isSendMail: x.commandSendCCs.map((x: any) => x.name == "EMAIL").length > 0,
            isSendInbox: x.commandSendCCs.map((x: any) => x.name == "INBOX").length > 0,
            isSendNotification: true,
          }))
        : [];
    const payloadStr = await PayloadSendNoti(command.id);
    const profilesSendRequest = new CallAPI()
      .PostData(
        { headers: { authorization: token } },
        "/placement/noti/profiles-send",
        {
          subject: `${command.issue}`,
          body: `${command.issue}`,
          receiverUserIds: profilesSend,
          payload: payloadStr, // แนบไฟล์ (ถ้าจำเป็น)
        },
        false,
      )
      .catch((error) => {
        if (error.response) {
          // Server ตอบกลับ (มี status code 4xx หรือ 5xx)
          console.error("Error status:", error.response.status);
          console.error("Error data:", error.response.data);
          console.error("Error headers:", error.response.headers);
        } else if (error.request) {
          // ไม่มีการตอบกลับจาก server
          console.error("No response received:", error.request);
        } else {
          // เกิดข้อผิดพลาดอื่น เช่น โค้ด Axios ผิด
          console.error("Axios error:", error.message);
        }
        console.error("Full error object:", error);
      });

    /*เฉพาะคำสั่ง C-PM-10 ให้ตัด profilesNotiRequest ที่ส่ง noti ครั้งแรกออก*/
    if (["C-PM-10"].includes(command.commandType.code)) {
      await Promise.all([profilesSendRequest]);
    } else {
      await Promise.all([profilesNotiRequest!, profilesSendRequest]);
    }

    console.log("[AMQ] Send Notification Success");
    return true;
  } catch (error) {
    console.error("[AMQ] Error:", error);
    return false;
  }
}

async function handler_org(msg: amqp.ConsumeMessage): Promise<boolean> {
  //----> condition before process consume
  console.time("[AMQ] handler_org_total");
  const startTime = Date.now();
  console.log(`[AMQ] handler_org START at ${new Date(startTime).toISOString()}`);

  const repoPosmaster = AppDataSource.getRepository(PosMaster);
  const posMasterAssignRepository = AppDataSource.getRepository(PosMasterAssign);
  const posMasterActRepository = AppDataSource.getRepository(PosMasterAct);
  // const permissionProfilesRepository = AppDataSource.getRepository(PermissionProfile);
  // const repoEmployeePosmaster = AppDataSource.getRepository(EmployeePosMaster);
  // const repoEmployeeTempPosmaster = AppDataSource.getRepository(EmployeeTempPosMaster);
  const repoProfile = AppDataSource.getRepository(Profile);
  // const repoProfileEmployee = AppDataSource.getRepository(ProfileEmployee);
  // const employeePositionRepository = AppDataSource.getRepository(EmployeePosition);
  const repoOrgRevision = AppDataSource.getRepository(OrgRevision);
  // const orgRootRepository = AppDataSource.getRepository(OrgRoot);
  // const child1Repository = AppDataSource.getRepository(OrgChild1);
  // const child2Repository = AppDataSource.getRepository(OrgChild2);
  // const child3Repository = AppDataSource.getRepository(OrgChild3);
  // const child4Repository = AppDataSource.getRepository(OrgChild4);
  const { data, user } = JSON.parse(msg.content.toString());
  const { id, status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt } = data;
  console.log(`[AMQ] Received message - revisionId: ${id}, status: ${status}`);

  const targetOrgRevision = await repoOrgRevision.findOne({
    where: { id },
  });

  if (!targetOrgRevision) {
    console.error(`[AMQ] Skip publish: revision ${id} not found`);
    console.timeEnd("[AMQ] handler_org_total");
    return true;
  }

  if (targetOrgRevision.orgRevisionIsCurrent && !targetOrgRevision.orgRevisionIsDraft) {
    console.log(`[AMQ] Skip publish: revision ${id} is already current`);
    console.timeEnd("[AMQ] handler_org_total");
    return true;
  }

  if (!targetOrgRevision.orgRevisionIsDraft || targetOrgRevision.orgRevisionIsCurrent) {
    console.log(
      `[AMQ] Skip publish: revision ${id} is no longer publishable (isDraft=${targetOrgRevision.orgRevisionIsDraft}, isCurrent=${targetOrgRevision.orgRevisionIsCurrent})`,
    );
    console.timeEnd("[AMQ] handler_org_total");
    return true;
  }

  if (user) {
    sendWebSocket(
      "send-publish-org",
      {
        success: true,
        message: `ระบบกำลังทำการเผยแพร่โครงสร้างหน่วยงาน`,
      },
      { userId: user?.sub },
    ).catch(console.error);
  }

  console.time("[AMQ] query_revisions");
  const [orgRevisionPublish, orgRevisionDraft] = await Promise.all([
    repoOrgRevision
      .createQueryBuilder("orgRevision")
      .where("orgRevision.orgRevisionIsDraft = false")
      .andWhere("orgRevision.orgRevisionIsCurrent = true")
      .getOne(),
    repoOrgRevision
      .createQueryBuilder("orgRevision")
      .where("orgRevision.orgRevisionIsDraft = true")
      .andWhere("orgRevision.orgRevisionIsCurrent = false")
      .getOne(),
  ]);
  console.timeEnd("[AMQ] query_revisions");
  console.log(
    `[AMQ] orgRevisionPublish found: ${orgRevisionPublish ? orgRevisionPublish.id : "null"}`,
  );
  console.log(`[AMQ] orgRevisionDraft found: ${orgRevisionDraft ? orgRevisionDraft.id : "null"}`);

  // Validate: ต้องมี orgRevisionPublish เสมอสำหรับการเผยแพร่
  if (!orgRevisionPublish) {
    console.error(
      "[AMQ] Cannot publish: No current org revision found (isDraft=false, isCurrent=true)",
    );
    if (user) {
      sendWebSocket(
        "send-publish-org",
        {
          success: false,
          message: `ไม่พบข้อมูลโครงสร้างหน่วยงานปัจจุบัน ไม่สามารถเผยแพร่ได้`,
        },
        { userId: user?.sub },
      ).catch(console.error);
    }
    return false;
  }

  // Validate: ต้องมี orgRevisionDraft ที่จะเผยแพร่
  if (!orgRevisionDraft) {
    console.error(
      "[AMQ] Cannot publish: No draft org revision found (isDraft=true, isCurrent=false)",
    );
    if (user) {
      sendWebSocket(
        "send-publish-org",
        {
          success: false,
          message: `ไม่พบข้อมูลโครงสร้างหน่วยงานแบบร่าง ไม่สามารถเผยแพร่ได้`,
        },
        { userId: user?.sub },
      ).catch(console.error);
    }
    return false;
  }

  if (orgRevisionDraft.id !== targetOrgRevision.id) {
    console.log(
      `[AMQ] Skip publish: revision ${id} is stale because draft ${orgRevisionDraft.id} is now the active publish candidate`,
    );
    console.timeEnd("[AMQ] handler_org_total");
    return true;
  }

  // NOTE: ย้ายการอัปเดตสถานะไปไว้หลังจากทำงานเสร็จทั้งหมด
  // เพื่อป้องกันกรณี timeout/retry ทำให้สถานะเพี้ยน (ทุก row เป็น false,false)

  try {
    console.time("[AMQ] query_posMaster");
    const posMaster = await repoPosmaster.find({
      where: { orgRevisionId: id },
      relations: [
        "orgRoot",
        "orgChild4",
        "orgChild3",
        "orgChild2",
        "orgChild1",
        "positions",
        "positions.posLevel",
        "positions.posType",
        "positions.posExecutive",
      ],
    });
    console.timeEnd("[AMQ] query_posMaster");
    console.log(`[AMQ] posMaster count: ${posMaster.length}`);

    console.time("[AMQ] query_old_data");
    const oldPosMasters = await repoPosmaster.find({
      where: {
        orgRevisionId: orgRevisionPublish.id,
      },
      select: [
        "id",
        "current_holderId",
        "ancestorDNA",
        "posMasterNo",
        "posMasterNoPrefix",
        "posMasterNoSuffix",
        "orgRootId",
        "orgChild1Id",
        "orgChild2Id",
        "orgChild3Id",
        "orgChild4Id",
      ],
    });

    // Task #2160 ดึง posMasterAssign ของ revision เดิม
    const oldposMasterAssigns = await posMasterAssignRepository.find({
      relations: ["posMaster"],
      where: {
        posMaster: {
          orgRevisionId: orgRevisionPublish.id,
        },
      },
    });
    console.timeEnd("[AMQ] query_old_data");
    console.log(`[AMQ] oldPosMasters count: ${oldPosMasters.length}`);
    console.log(`[AMQ] oldposMasterAssigns count: ${oldposMasterAssigns.length}`);

    console.time("[AMQ] build_assignMap");
    // สร้าง assignMap เอาไว้เก็บ posMasterAssign.ancestorDNA ของ revision เดิม
    const assignMap = new Map<string, PosMasterAssignDTO[]>();
    for (const posmasterAssign of oldposMasterAssigns) {
      const dna = posmasterAssign.posMaster.ancestorDNA;
      if (!assignMap.has(dna)) {
        assignMap.set(dna, []);
      }
      assignMap.get(dna)!.push({
        id: posmasterAssign.id,
        posMasterId: posmasterAssign.posMasterId,
        assignId: posmasterAssign.assignId,
      });
    }
    console.timeEnd("[AMQ] build_assignMap");

    console.time("[AMQ] query_oldposMasterAct");
    // ดึง posMasterAct ของ revision เดิม xxx
    const oldposMasterAct = await posMasterActRepository.find({
      relations: ["posMaster", "posMasterChild"],
      where: {
        posMaster: {
          orgRevisionId: orgRevisionPublish.id,
        },
      },
    });
    console.timeEnd("[AMQ] query_oldposMasterAct");
    console.log(`[AMQ] oldposMasterAct count: ${oldposMasterAct.length}`);

    type ActKey = string; // `${parentDNA}|${childDNA}`

    console.time("[AMQ] build_maps");
    const posMasterActMap = new Map<ActKey, PosMasterAct[]>();
    for (const act of oldposMasterAct) {
      const parentDNA = act.posMaster?.ancestorDNA?.trim() ?? "";
      const childDNA = act.posMasterChild?.ancestorDNA?.trim() ?? "";
      const key = `${parentDNA}|${childDNA}`;

      if (!posMasterActMap.has(key)) {
        posMasterActMap.set(key, []);
      }
      posMasterActMap.get(key)!.push(act);
    }

    const posMasterIdMap = new Map<string, string>();
    for (const pm of posMaster) {
      posMasterIdMap.set(pm.ancestorDNA?.trim() ?? "", pm.id);
    }

    const oldPosMasterMap = new Map<string, PosMaster>();
    for (const oldPm of oldPosMasters) {
      const dna = oldPm.ancestorDNA?.trim();
      if (dna) {
        oldPosMasterMap.set(dna, oldPm);
      }
    }
    console.timeEnd("[AMQ] build_maps");

    const _null: any = null;

    // ===== BATCH PROCESSING: เตรียมข้อมูลก่อน loop =====
    console.time("[AMQ] prepare_batch_data");
    // 1. รวบรวม profileIds ทั้งหมดที่ต้องอัพเดท
    const profileIds = posMaster
      .filter((item) => item.next_holderId != null)
      .map((item) => item.next_holderId!)
      .filter((id) => id != null && id !== "");

    // 2. Batch load profiles ทั้งหมดในครั้งเดียว (แก้ปัญหา N+1 Query)
    const profilesMap = new Map<string, Profile>();
    if (profileIds.length > 0) {
      const profiles = await repoProfile.find({
        where: { id: In(profileIds) },
        relations: ["posType", "posLevel"],
      });
      profiles.forEach((p) => profilesMap.set(p.id, p));
    }
    console.log(`[AMQ] profiles to update: ${profilesMap.size}`);

    // 3. เตรียม arrays สำหรับ batch operations
    const profilesToSave: Profile[] = [];
    const posMasterAssignsToSave: PosMasterAssign[] = [];
    const historyRowsToSave: Partial<PosMasterHistory>[] = [];
    const posMasterUpdates: { id: string; current_holderId: string | null | undefined }[] = [];

    // ===== LOOP: เก็บข้อมูลทั้งหมด =====
    for (const item of posMaster) {
      const dna = item.ancestorDNA?.trim();
      const oldPm = dna ? oldPosMasterMap.get(dna) : null;

      // Task #2160 Clone posMasterAssign
      const assigns = assignMap.get(item.ancestorDNA);
      if (assigns && assigns.length > 0) {
        const newAssigns = assigns.map(({ id, ...fields }) =>
          posMasterAssignRepository.create({
            ...fields,
            posMasterId: item.id,
            createdAt: lastUpdatedAt,
            createdFullName: lastUpdateFullName,
            createdUserId: lastUpdateUserId,
            lastUpdatedAt: lastUpdatedAt,
            lastUpdateFullName: lastUpdateFullName,
            lastUpdateUserId: lastUpdateUserId,
          }),
        );
        posMasterAssignsToSave.push(...newAssigns);
      }

      // เตรียมข้อมูลสำหรับ update profile
      if (item.next_holderId != null && item.next_holderId !== "") {
        const profile = profilesMap.get(item.next_holderId);
        if (profile) {
          profile.posMasterNo = getPosMasterNo(item) ?? _null;
          profile.org = getOrgFullName(item) ?? _null;

          // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
          if (!item.isSit && item.positions.length > 0) {
            let position = item.positions.find((x) => x.positionIsSelected == true);
            if (position == null) {
              position = item.positions.find((x) => x.posLevelId == profile?.posLevelId);
              if (position == null) {
                const sorted = [...item.positions].sort((a, b) => a.orderNo - b.orderNo);
                position = sorted[0];
              }
            }

            profile.posLevelId = position?.posLevelId ?? _null;
            profile.posTypeId = position?.posTypeId ?? _null;
            profile.position = position?.positionName ?? _null;
            profile.positionField = position?.positionField ?? _null;
            profile.posExecutive = position?.posExecutive?.posExecutiveName ?? _null;
            profile.positionArea = position?.positionArea ?? _null;
            profile.positionExecutiveField = position?.positionExecutiveField ?? _null;
          }

          profilesToSave.push(profile);
        }
      }

      // เก็บข้อมูลสำหรับ update posMaster
      posMasterUpdates.push({
        id: item.id,
        current_holderId: item.next_holderId,
      });

      // เก็บ IDs ที่ต้องสร้าง history
      const oldHolderId = oldPm ? oldPm.current_holderId : null;
      const newHolderId = item?.next_holderId;
      const isHolderChanged = oldHolderId !== newHolderId;

      // เช็คว่า holder เดิม แต่ตำแหน่งเปลี่ยน
      const isSameHolder = oldHolderId === newHolderId && oldHolderId != null && newHolderId != null;
      const isPositionChanged =
        isSameHolder &&
        oldPm &&
        (oldPm.posMasterNo !== item.posMasterNo ||
          oldPm.posMasterNoPrefix !== item.posMasterNoPrefix ||
          oldPm.posMasterNoSuffix !== item.posMasterNoSuffix ||
          oldPm.orgRootId !== item.orgRoot?.id ||
          oldPm.orgChild1Id !== item.orgChild1?.id ||
          oldPm.orgChild2Id !== item.orgChild2?.id ||
          oldPm.orgChild3Id !== item.orgChild3?.id ||
          oldPm.orgChild4Id !== item.orgChild4?.id);

      if (isHolderChanged || isPositionChanged) {
        const nextHolderProfile =
          item.next_holderId != null && item.next_holderId !== ""
            ? profilesMap.get(item.next_holderId)
            : null;
        const selectedPosition =
          item.positions.length > 0
            ? item.positions.find((position) => position.positionIsSelected === true) ?? null
            : null;
        const shortName =
          [
            item.orgChild4?.orgChild4ShortName,
            item.orgChild3?.orgChild3ShortName,
            item.orgChild2?.orgChild2ShortName,
            item.orgChild1?.orgChild1ShortName,
            item.orgRoot?.orgRootShortName,
          ].find((name) => typeof name === "string" && name.trim().length > 0) ?? _null;

        // ถ้าเป็นตำแหน่งนั่งทับ (isSit = true) และมีคนครอง ใช้ตำแหน่งจาก profile แทน
        let positionName = selectedPosition?.positionName ?? _null;
        let posTypeName = selectedPosition?.posType?.posTypeName ?? _null;
        let posLevelName = selectedPosition?.posLevel?.posLevelName ?? _null;
        let posExecutiveName = selectedPosition?.posExecutive?.posExecutiveName ?? _null;
        if (item.isSit && nextHolderProfile) {
          positionName = nextHolderProfile.position ?? _null;
          posTypeName = nextHolderProfile.posType?.posTypeName ?? _null;
          posLevelName = nextHolderProfile.posLevel?.posLevelName ?? _null;
          posExecutiveName = nextHolderProfile.posExecutive ?? _null;
        }

        historyRowsToSave.push({
          ancestorDNA: item.ancestorDNA ? item.ancestorDNA : _null,
          prefix: nextHolderProfile?.prefix || _null,
          firstName: nextHolderProfile?.firstName || _null,
          lastName: nextHolderProfile?.lastName || _null,
          shortName,
          posMasterNoPrefix: item.posMasterNoPrefix ?? _null,
          posMasterNo: item.posMasterNo ?? _null,
          posMasterNoSuffix: item.posMasterNoSuffix ?? _null,
          position: positionName,
          posType: posTypeName,
          posLevel: posLevelName,
          posExecutive: posExecutiveName,
          profileId: nextHolderProfile?.id || _null,
          rootDnaId: item.orgRoot?.ancestorDNA || _null,
          child1DnaId: item.orgChild1?.ancestorDNA || _null,
          child2DnaId: item.orgChild2?.ancestorDNA || _null,
          child3DnaId: item.orgChild3?.ancestorDNA || _null,
          child4DnaId: item.orgChild4?.ancestorDNA || _null,
          createdUserId: "",
          createdFullName: "system",
          lastUpdateUserId: "",
          lastUpdateFullName: "system",
          createdAt: new Date(),
          lastUpdatedAt: new Date(),
        });
      }
    }
    console.timeEnd("[AMQ] prepare_batch_data");
    console.log(
      `[AMQ] Prepared - posMasterAssignsToSave: ${posMasterAssignsToSave.length}, profilesToSave: ${profilesToSave.length}, posMasterUpdates: ${posMasterUpdates.length}, historyCreateIds: ${historyRowsToSave.length}`,
    );

    // ===== BATCH EXECUTION: save ทีละ batch =====
    let shouldSkipPublishInTransaction = false;
    await AppDataSource.transaction(async (manager) => {
      const repoPosmaster = manager.getRepository(PosMaster);
      const posMasterAssignRepository = manager.getRepository(PosMasterAssign);
      const posMasterActRepository = manager.getRepository(PosMasterAct);
      const permissionProfilesRepository = manager.getRepository(PermissionProfile);
      const repoEmployeePosmaster = manager.getRepository(EmployeePosMaster);
      const repoEmployeeTempPosmaster = manager.getRepository(EmployeeTempPosMaster);
      const repoProfile = manager.getRepository(Profile);
      const repoProfileEmployee = manager.getRepository(ProfileEmployee);
      const employeePositionRepository = manager.getRepository(EmployeePosition);
      const repoOrgRevision = manager.getRepository(OrgRevision);
      const orgRootRepository = manager.getRepository(OrgRoot);
      const child1Repository = manager.getRepository(OrgChild1);
      const child2Repository = manager.getRepository(OrgChild2);
      const child3Repository = manager.getRepository(OrgChild3);
      const child4Repository = manager.getRepository(OrgChild4);
      const posMasterHistoryRepository = manager.getRepository(PosMasterHistory);

      const targetOrgRevision = await repoOrgRevision
        .createQueryBuilder("orgRevision")
        .setLock("pessimistic_write")
        .where("orgRevision.id = :id", { id })
        .getOne();

      if (!targetOrgRevision) {
        shouldSkipPublishInTransaction = true;
        return;
      }

      if (targetOrgRevision.orgRevisionIsCurrent && !targetOrgRevision.orgRevisionIsDraft) {
        shouldSkipPublishInTransaction = true;
        return;
      }

      if (!targetOrgRevision.orgRevisionIsDraft || targetOrgRevision.orgRevisionIsCurrent) {
        shouldSkipPublishInTransaction = true;
        return;
      }

      const orgRevisionPublish = await repoOrgRevision
        .createQueryBuilder("orgRevision")
        .setLock("pessimistic_write")
        .where("orgRevision.orgRevisionIsDraft = false")
        .andWhere("orgRevision.orgRevisionIsCurrent = true")
        .getOne();

      if (!orgRevisionPublish) {
        throw new Error("[AMQ] Cannot publish in transaction: no current org revision found");
      }

      const orgRevisionDraft = await repoOrgRevision
        .createQueryBuilder("orgRevision")
        .setLock("pessimistic_write")
        .where("orgRevision.id = :id", { id })
        .andWhere("orgRevision.orgRevisionIsDraft = true")
        .andWhere("orgRevision.orgRevisionIsCurrent = false")
        .getOne();

      if (!orgRevisionDraft) {
        shouldSkipPublishInTransaction = true;
        return;
      }

      // 4. Batch save posMasterAssign (chunk 500)
      console.time("[AMQ] batch_save_posMasterAssign");
      if (posMasterAssignsToSave.length > 0) {
        const chunks = chunkArray(posMasterAssignsToSave, 500);
        for (const chunk of chunks) {
          await posMasterAssignRepository.save(chunk);
        }
      }
      console.timeEnd("[AMQ] batch_save_posMasterAssign");

      // 5. Batch save profiles (chunk 200)
      console.time("[AMQ] batch_save_profiles");
      if (profilesToSave.length > 0) {
        const chunks = chunkArray(profilesToSave, 200);
        for (const chunk of chunks) {
          await repoProfile.save(chunk);
        }
      }
      console.timeEnd("[AMQ] batch_save_profiles");

      // 6. Batch update posMasters
      console.time("[AMQ] batch_update_posMasters");
      if (posMasterUpdates.length > 0) {
        const chunks = chunkArray(posMasterUpdates, 500);
        const posMasterTableName = repoPosmaster.metadata.tableName;
        for (const chunk of chunks as (typeof posMasterUpdates)[]) {
          const caseClauses = chunk.map(() => "WHEN ? THEN ?").join(" ");
          const wherePlaceholders = chunk.map(() => "?").join(", ");
          const params = chunk.flatMap((update: (typeof posMasterUpdates)[number]) => [
            update.id,
            update.current_holderId ?? null,
          ]);

          params.push(
            lastUpdateUserId,
            lastUpdateFullName,
            lastUpdatedAt,
            ...chunk.map((update: (typeof posMasterUpdates)[number]) => update.id),
          );

          await manager.query(
            `UPDATE \`${posMasterTableName}\`
             SET current_holderId = CASE id ${caseClauses} END,
                 next_holderId = NULL,
                 lastUpdateUserId = ?,
                 lastUpdateFullName = ?,
                 lastUpdatedAt = ?
             WHERE id IN (${wherePlaceholders})`,
            params,
          );
        }
      }
      console.timeEnd("[AMQ] batch_update_posMasters");

      // 7. Batch create history
      console.time("[AMQ] batch_create_history");
      if (historyRowsToSave.length > 0) {
        const chunks = chunkArray(historyRowsToSave, 500);
        for (const chunk of chunks) {
          await posMasterHistoryRepository.save(posMasterHistoryRepository.create(chunk));
        }
      }
      console.timeEnd("[AMQ] batch_create_history");

      // Clone oldposMasterAct
      console.time("[AMQ] clone_oldposMasterAct");
      const posMasterActRowsToInsert: Partial<PosMasterAct>[] = [];
      for (const act of oldposMasterAct) {
        const parentDNA = act.posMaster?.ancestorDNA?.trim()?.toLowerCase() ?? "";
        const childDNA = act.posMasterChild?.ancestorDNA?.trim()?.toLowerCase() ?? "";

        const newParentId = posMasterIdMap.get(parentDNA);
        const newChildId = posMasterIdMap.get(childDNA);

        if (!newParentId || !newChildId) continue;

        const { id, posMaster, posMasterChild, ...fields } = act;

        posMasterActRowsToInsert.push({
          ...fields,
          posMasterId: newParentId,
          posMasterChildId: newChildId,
          createdAt: new Date(),
          createdFullName: user ? user.name : "system",
          createdUserId: user ? user.sub : "system",
          lastUpdatedAt: new Date(),
          lastUpdateFullName: user ? user.name : "system",
          lastUpdateUserId: user ? user.sub : "system",
        });
      }
      if (posMasterActRowsToInsert.length > 0) {
        const chunks = chunkArray(posMasterActRowsToInsert, 500);
        for (const chunk of chunks) {
          await posMasterActRepository.insert(chunk);
        }
      }
      console.timeEnd("[AMQ] clone_oldposMasterAct");

      console.time("[AMQ] clone_org_structure");
      //new main revision
      const before = null;

      //ทุก orgRoot และ orgChild ข้างล่างนี้จะเป็นตัวเก่าที่ไม่ได้เป็น current revision
      //cone tree
      console.time("[AMQ] query_old_org_structure");
      //หา dna tree
      const [orgRoot, orgChild1, orgChild2, orgChild3, orgChild4] = await Promise.all([
        orgRootRepository.find({
          where: { orgRevisionId: orgRevisionPublish.id },
        }),
        child1Repository.find({
          where: { orgRevisionId: orgRevisionPublish.id },
        }),
        child2Repository.find({
          where: { orgRevisionId: orgRevisionPublish.id },
        }),
        child3Repository.find({
          where: { orgRevisionId: orgRevisionPublish.id },
        }),
        child4Repository.find({
          where: { orgRevisionId: orgRevisionPublish.id },
        }),
      ]);
      console.timeEnd("[AMQ] query_old_org_structure");
      console.log(
        `[AMQ] Old structure - orgRoot: ${orgRoot.length}, orgChild1: ${orgChild1.length}, orgChild2: ${orgChild2.length}, orgChild3: ${orgChild3.length}, orgChild4: ${orgChild4.length}`,
      );

      // Task #2172 ดึง orgRoot ของ revision ใหม่
      const newRoots = await orgRootRepository.find({
        where: { orgRevisionId: orgRevisionDraft.id },
      });
      // สร้าง newRootMap เอาไว้เก็บ orgRoot.ancestorDNA ของ revision ใหม่
      const newRootMap = new Map(newRoots.map((r) => [r.ancestorDNA, r.id]));
      const emptyAncestorDNA = "00000000-0000-0000-0000-000000000000";
      const hasEmptyAncestorDNA = (ancestorDNA?: string | null) =>
        ancestorDNA == null || ancestorDNA === emptyAncestorDNA;
      const hasSelfOrEmptyAncestorDNA = (node: { id: string; ancestorDNA: string | null }) =>
        node.ancestorDNA === node.id || hasEmptyAncestorDNA(node.ancestorDNA);
      const findMatchedNodeByAncestorDNA = <T extends { id: string; ancestorDNA: string | null }>(
        nodes: T[],
        node: T,
      ) =>
        nodes.find((item) => {
          if (hasSelfOrEmptyAncestorDNA(node)) {
            return hasEmptyAncestorDNA(item.ancestorDNA);
          }
          return item.ancestorDNA === node.ancestorDNA;
        });
      const [
        orgRootCurrent,
        orgChild1Current,
        orgChild2Current,
        orgChild3Current,
        orgChild4Current,
      ] = await Promise.all([
        orgRootRepository.find({ where: { orgRevisionId: orgRevisionDraft.id } }),
        child1Repository.find({ where: { orgRevisionId: orgRevisionDraft.id } }),
        child2Repository.find({ where: { orgRevisionId: orgRevisionDraft.id } }),
        child3Repository.find({ where: { orgRevisionId: orgRevisionDraft.id } }),
        child4Repository.find({ where: { orgRevisionId: orgRevisionDraft.id } }),
      ]);

      console.time("[AMQ] clone_permissionProfiles");
      // ดึง permissionProfiles ของ revision เดิม
      const oldPermissionProfiles = await permissionProfilesRepository.find({
        relations: ["orgRootTree"],
        where: {
          orgRootTree: {
            orgRevisionId: orgRevisionPublish.id,
          },
        },
      });
      const inserts: any[] = [];
      for (const permiss of oldPermissionProfiles) {
        // หา orgRootId ใหม่จาก newRootMap
        const newRootId = newRootMap.get(permiss.orgRootTree.ancestorDNA);
        if (!newRootId) continue;
        // ตัด id กับ orgRootTree ออกแล้วสร้าง object ใหม่
        const { id, orgRootTree, ...fields } = permiss;
        // เตรียมข้อมูลสำหรับ insert
        inserts.push({
          ...fields,
          orgRootId: newRootId,
          createdAt: lastUpdatedAt,
          createdFullName: lastUpdateFullName,
          createdUserId: lastUpdateUserId,
          lastUpdatedAt: lastUpdatedAt,
          lastUpdateFullName: lastUpdateFullName,
          lastUpdateUserId: lastUpdateUserId,
        });
      }
      // ทำการ insert ข้อมูลใหม่ครั้งเดียว
      if (inserts.length > 0) {
        await permissionProfilesRepository.insert(inserts);
      }
      console.timeEnd("[AMQ] clone_permissionProfiles");

      console.time("[AMQ] query_employee_org_structures");
      const [orgemployeePosMaster, orgemployeeTempPosMaster] = await Promise.all([
        repoEmployeePosmaster.find({
          where: { orgRevisionId: orgRevisionPublish.id },
          relations: ["positions"],
        }),
        repoEmployeeTempPosmaster.find({
          where: { orgRevisionId: orgRevisionPublish.id },
          relations: ["positions"],
        }),
      ]);
      console.timeEnd("[AMQ] query_employee_org_structures");
      console.log(`[AMQ] orgemployeePosMaster count: ${orgemployeePosMaster.length}`);
      console.log(`[AMQ] orgemployeeTempPosMaster count: ${orgemployeeTempPosMaster.length}`);

      const currentHolderIds = Array.from(
        new Set(
          orgemployeePosMaster
            .map((item) => item.current_holderId)
            .filter((holderId): holderId is string => !!holderId),
        ),
      );
      const validProfileIds = new Set<string>();
      if (currentHolderIds.length > 0) {
        const profiles = await repoProfileEmployee.find({
          select: ["id"],
          where: { id: In(currentHolderIds) },
        });
        profiles.forEach((profile) => validProfileIds.add(profile.id));
      }

      const _orgemployeePosMaster: EmployeePosMaster[] = orgemployeePosMaster.map((x) => ({
        ...x,
        current_holderId:
          x.current_holderId && validProfileIds.has(x.current_holderId) ? x.current_holderId : null,
        ancestorDNA:
          !x.ancestorDNA || x.ancestorDNA === "00000000-0000-0000-0000-000000000000"
            ? x.id
            : x.ancestorDNA,
      }));
      const _orgemployeeTempPosMaster: EmployeeTempPosMaster[] = orgemployeeTempPosMaster.map(
        (x) => ({
          ...x,
          ancestorDNA:
            x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
              ? x.id
              : x.ancestorDNA,
        }),
      );

      console.time("[AMQ] insert_employeePosMaster");
      await repoEmployeePosmaster
        .createQueryBuilder()
        .insert()
        .into(EmployeePosMaster)
        .values(_orgemployeePosMaster)
        .orUpdate({
          conflict_target: ["id"],
          overwrite: ["ancestorDNA"],
        })
        .execute();
      console.timeEnd("[AMQ] insert_employeePosMaster");

      const groupByParentId = <T extends { id: string }>(
        items: T[],
        getParentId: (item: T) => string | null | undefined,
      ) => {
        const grouped = new Map<string, T[]>();
        for (const item of items) {
          const parentId = getParentId(item);
          if (!parentId) {
            continue;
          }
          const current = grouped.get(parentId);
          if (current) {
            current.push(item);
          } else {
            grouped.set(parentId, [item]);
          }
        }
        return grouped;
      };

      const buildEmployeeNodeKey = (item: {
        orgRootId?: string | null;
        orgChild1Id?: string | null;
        orgChild2Id?: string | null;
        orgChild3Id?: string | null;
        orgChild4Id?: string | null;
      }) => {
        if (item.orgChild4Id) return `child4:${item.orgChild4Id}`;
        if (item.orgChild3Id && item.orgChild4Id == null) return `child3:${item.orgChild3Id}`;
        if (item.orgChild2Id && item.orgChild3Id == null) return `child2:${item.orgChild2Id}`;
        if (item.orgChild1Id && item.orgChild2Id == null) return `child1:${item.orgChild1Id}`;
        if (item.orgRootId && item.orgChild1Id == null) return `root:${item.orgRootId}`;
        return null;
      };

      const groupByEmployeeNode = <
        T extends {
          orgRootId?: string | null;
          orgChild1Id?: string | null;
          orgChild2Id?: string | null;
          orgChild3Id?: string | null;
          orgChild4Id?: string | null;
        },
      >(
        items: T[],
      ) => {
        const grouped = new Map<string, T[]>();
        for (const item of items) {
          const key = buildEmployeeNodeKey(item);
          if (!key) {
            continue;
          }
          const current = grouped.get(key);
          if (current) {
            current.push(item);
          } else {
            grouped.set(key, [item]);
          }
        }
        return grouped;
      };

      const employeePosMasterByNode = groupByEmployeeNode(_orgemployeePosMaster);
      const employeeTempPosMasterByNode = groupByEmployeeNode(_orgemployeeTempPosMaster);
      const getNodeKey = (level: "root" | "child1" | "child2" | "child3" | "child4", id: string) =>
        `${level}:${id}`;
      const orgChild1ByRoot = groupByParentId(orgChild1, (item) => item.orgRootId);
      const orgChild2ByChild1 = groupByParentId(orgChild2, (item) => item.orgChild1Id);
      const orgChild3ByChild2 = groupByParentId(orgChild3, (item) => item.orgChild2Id);
      const orgChild4ByChild3 = groupByParentId(orgChild4, (item) => item.orgChild3Id);
      type OrgDestinationIds = {
        orgRootId?: string | null;
        orgChild1Id?: string | null;
        orgChild2Id?: string | null;
        orgChild3Id?: string | null;
        orgChild4Id?: string | null;
      };
      type CloneEmployeeSource = {
        positions: EmployeePosition[];
      } & (EmployeePosMaster | EmployeeTempPosMaster);
      const buildAuditFields = (timestamp: Date) => ({
        createdUserId: "",
        createdFullName: "System Administrator",
        createdAt: timestamp,
        lastUpdateUserId: "",
        lastUpdateFullName: "System Administrator",
        lastUpdatedAt: timestamp,
      });
      const buildColumnData = <T extends object>(
        repository: Repository<T>,
        source: T,
      ): Partial<T> => {
        const row = {} as Partial<T>;
        const target = row as Record<string, unknown>;
        const sourceRecord = source as Record<string, unknown>;
        for (const column of repository.metadata.columns) {
          target[column.propertyName] = sourceRecord[column.propertyName];
        }
        return row;
      };
      const insertInChunks = async <T extends object>(
        repository: Repository<T>,
        rows: Partial<T>[],
        chunkSize: number,
      ) => {
        if (rows.length === 0) {
          return;
        }
        for (const chunk of chunkArray(rows, chunkSize) as Partial<T>[][]) {
          await repository.insert(chunk as Parameters<Repository<T>["insert"]>[0]);
        }
      };
      const buildEmployeeCloneBatch = <T extends CloneEmployeeSource>(
        items: T[],
        parentRepository: Repository<T>,
        positionParentKey: "posMasterId" | "posMasterTempId",
        targetIds: OrgDestinationIds,
      ) => {
        const parentRows: Partial<T>[] = [];
        const positionRows: Partial<EmployeePosition>[] = [];

        for (const item of items) {
          const parentId = randomUUID();
          const parentTimestamp = new Date();
          parentRows.push({
            ...buildColumnData(parentRepository, item),
            id: parentId,
            orgRevisionId: orgRevisionDraft.id,
            ...targetIds,
            ...buildAuditFields(parentTimestamp),
          });

          for (const position of item.positions ?? []) {
            const positionTimestamp = new Date();
            positionRows.push({
              ...buildColumnData(employeePositionRepository, position),
              id: randomUUID(),
              posMasterId: positionParentKey === "posMasterId" ? parentId : undefined,
              posMasterTempId: positionParentKey === "posMasterTempId" ? parentId : undefined,
              ...buildAuditFields(positionTimestamp),
            });
          }
        }

        return { parentRows, positionRows };
      };
      const cloneEmployeeNodeBatch = async <T extends CloneEmployeeSource>(
        items: T[],
        parentRepository: Repository<T>,
        positionParentKey: "posMasterId" | "posMasterTempId",
        targetIds: OrgDestinationIds,
      ) => {
        if (items.length === 0) {
          return;
        }
        const { parentRows, positionRows } = buildEmployeeCloneBatch(
          items,
          parentRepository,
          positionParentKey,
          targetIds,
        );
        await insertInChunks(parentRepository, parentRows, 200);
        await insertInChunks(employeePositionRepository, positionRows, 500);
      };

      await repoEmployeeTempPosmaster
        .createQueryBuilder()
        .insert()
        .into(EmployeeTempPosMaster)
        .values(_orgemployeeTempPosMaster)
        .orUpdate({
          conflict_target: ["id"],
          overwrite: ["ancestorDNA"],
        })
        .execute();

      //create org
      console.time("[AMQ] forEach_orgRoot");
      console.log(`[AMQ] Starting forEach orgRoot loop (${orgRoot.length} items)`);
      for (const x of orgRoot) {
        const dataId = x.id;
        const matchedOrgRoot = findMatchedNodeByAncestorDNA(orgRootCurrent, x);

        const filteredEmployeePosMaster =
          employeePosMasterByNode.get(getNodeKey("root", dataId)) ?? [];

        await cloneEmployeeNodeBatch(
          filteredEmployeePosMaster,
          repoEmployeePosmaster,
          "posMasterId",
          { orgRootId: matchedOrgRoot?.id ?? null },
        );

        await cloneEmployeeNodeBatch(
          employeeTempPosMasterByNode.get(getNodeKey("root", dataId)) ?? [],
          repoEmployeeTempPosmaster,
          "posMasterTempId",
          { orgRootId: matchedOrgRoot?.id ?? null },
        );

        for (const x of orgChild1ByRoot.get(dataId) ?? []) {
          const data1Id = x.id;
          const matchedOrgChild1 = findMatchedNodeByAncestorDNA(orgChild1Current, x);
          await cloneEmployeeNodeBatch(
            employeePosMasterByNode.get(getNodeKey("child1", data1Id)) ?? [],
            repoEmployeePosmaster,
            "posMasterId",
            {
              orgRootId: matchedOrgRoot?.id ?? null,
              orgChild1Id: matchedOrgChild1?.id ?? null,
            },
          );

          await cloneEmployeeNodeBatch(
            employeeTempPosMasterByNode.get(getNodeKey("child1", data1Id)) ?? [],
            repoEmployeeTempPosmaster,
            "posMasterTempId",
            {
              orgRootId: matchedOrgRoot?.id ?? null,
              orgChild1Id: matchedOrgChild1?.id ?? null,
            },
          );

          for (const x of orgChild2ByChild1.get(data1Id) ?? []) {
            const data2Id = x.id;
            const matchedOrgChild2 = findMatchedNodeByAncestorDNA(orgChild2Current, x);
            await cloneEmployeeNodeBatch(
              employeePosMasterByNode.get(getNodeKey("child2", data2Id)) ?? [],
              repoEmployeePosmaster,
              "posMasterId",
              {
                orgRootId: matchedOrgRoot?.id ?? null,
                orgChild1Id: matchedOrgChild1?.id ?? null,
                orgChild2Id: matchedOrgChild2?.id ?? null,
              },
            );

            await cloneEmployeeNodeBatch(
              employeeTempPosMasterByNode.get(getNodeKey("child2", data2Id)) ?? [],
              repoEmployeeTempPosmaster,
              "posMasterTempId",
              {
                orgRootId: dataId,
                orgChild1Id: data1Id,
                orgChild2Id: data2Id,
              },
            );

            for (const x of orgChild3ByChild2.get(data2Id) ?? []) {
              const data3Id = x.id;
              const matchedOrgChild3 = findMatchedNodeByAncestorDNA(orgChild3Current, x);
              await cloneEmployeeNodeBatch(
                employeePosMasterByNode.get(getNodeKey("child3", data3Id)) ?? [],
                repoEmployeePosmaster,
                "posMasterId",
                {
                  orgRootId: matchedOrgRoot?.id ?? null,
                  orgChild1Id: matchedOrgChild1?.id ?? null,
                  orgChild2Id: matchedOrgChild2?.id ?? null,
                  orgChild3Id: matchedOrgChild3?.id ?? null,
                },
              );

              await cloneEmployeeNodeBatch(
                employeeTempPosMasterByNode.get(getNodeKey("child3", data3Id)) ?? [],
                repoEmployeeTempPosmaster,
                "posMasterTempId",
                {
                  orgRootId: matchedOrgRoot?.id ?? null,
                  orgChild1Id: matchedOrgChild1?.id ?? null,
                  orgChild2Id: matchedOrgChild2?.id ?? null,
                  orgChild3Id: matchedOrgChild3?.id ?? null,
                },
              );

              for (const x of orgChild4ByChild3.get(data3Id) ?? []) {
                const data4Id = x.id;
                const matchedOrgChild4 = findMatchedNodeByAncestorDNA(orgChild4Current, x);
                await cloneEmployeeNodeBatch(
                  employeePosMasterByNode.get(getNodeKey("child4", data4Id)) ?? [],
                  repoEmployeePosmaster,
                  "posMasterId",
                  {
                    orgRootId: matchedOrgRoot?.id ?? null,
                    orgChild1Id: matchedOrgChild1?.id ?? null,
                    orgChild2Id: matchedOrgChild2?.id ?? null,
                    orgChild3Id: matchedOrgChild3?.id ?? null,
                    orgChild4Id: matchedOrgChild4?.id ?? null,
                  },
                );

                await cloneEmployeeNodeBatch(
                  employeeTempPosMasterByNode.get(getNodeKey("child4", data4Id)) ?? [],
                  repoEmployeeTempPosmaster,
                  "posMasterTempId",
                  {
                    orgRootId: matchedOrgRoot?.id ?? null,
                    orgChild1Id: matchedOrgChild1?.id ?? null,
                    orgChild2Id: matchedOrgChild2?.id ?? null,
                    orgChild3Id: matchedOrgChild3?.id ?? null,
                    orgChild4Id: matchedOrgChild4?.id ?? null,
                  },
                );
              }
            }
          }
        }
      }

      const [employeePosMaster, employeeTempPosMaster] = await Promise.all([
        repoEmployeePosmaster.find({
          where: { orgRevisionId: orgRevisionDraft.id },
          relations: ["positions", "positions.posLevel", "positions.posType"],
        }),
        repoEmployeeTempPosmaster.find({
          where: { orgRevisionId: orgRevisionDraft.id },
          relations: ["positions", "positions.posLevel", "positions.posType"],
        }),
      ]);
      const profileEmployeeIds = Array.from(
        new Set(
          [...employeePosMaster, ...employeeTempPosMaster]
            .map((item) => item.next_holderId)
            .filter((profileId): profileId is string => !!profileId),
        ),
      );
      const profileEmployeeMap = new Map<string, ProfileEmployee>();
      if (profileEmployeeIds.length > 0) {
        const profiles = await repoProfileEmployee.findBy({
          id: In(profileEmployeeIds),
        });
        profiles.forEach((profile) => profileEmployeeMap.set(profile.id, profile));
      }
      const updatedProfileEmployeeIds = new Set<string>();
      const employeePosMasterIdsToTouch: string[] = [];
      const employeeTempPosMasterIdsToTouch: string[] = [];

      for (const item of employeePosMaster) {
        if (item.next_holderId != null) {
          const profile = profileEmployeeMap.get(item.next_holderId);
          const position = item.positions.find((x) => x.positionIsSelected == true);
          const _null: any = null;
          if (profile != null) {
            profile.posLevelId = position?.posLevelId ?? _null;
            profile.posTypeId = position?.posTypeId ?? _null;
            profile.position = position?.positionName ?? _null;
            updatedProfileEmployeeIds.add(profile.id);
          }
        }
        employeePosMasterIdsToTouch.push(item.id);
      }
      for (const item of employeeTempPosMaster) {
        if (item.next_holderId != null) {
          const profile = profileEmployeeMap.get(item.next_holderId);
          const position = item.positions.find((x) => x.positionIsSelected == true);
          const _null: any = null;
          if (profile != null) {
            profile.posLevelId = position?.posLevelId ?? _null;
            profile.posTypeId = position?.posTypeId ?? _null;
            profile.position = position?.positionName ?? _null;
            updatedProfileEmployeeIds.add(profile.id);
          }
        }
        employeeTempPosMasterIdsToTouch.push(item.id);
      }
      if (employeePosMasterIdsToTouch.length > 0) {
        const chunks = chunkArray(employeePosMasterIdsToTouch, 500);
        const employeePosMasterTableName = repoEmployeePosmaster.metadata.tableName;
        for (const chunk of chunks) {
          const wherePlaceholders = chunk.map(() => "?").join(", ");
          await manager.query(
            `UPDATE \`${employeePosMasterTableName}\`
             SET lastUpdateUserId = ?,
                 lastUpdateFullName = ?,
                 lastUpdatedAt = ?
             WHERE id IN (${wherePlaceholders})`,
            [lastUpdateUserId, lastUpdateFullName, lastUpdatedAt, ...chunk],
          );
        }
      }
      if (employeeTempPosMasterIdsToTouch.length > 0) {
        const chunks = chunkArray(employeeTempPosMasterIdsToTouch, 500);
        const employeeTempPosMasterTableName = repoEmployeeTempPosmaster.metadata.tableName;
        for (const chunk of chunks) {
          const wherePlaceholders = chunk.map(() => "?").join(", ");
          await manager.query(
            `UPDATE \`${employeeTempPosMasterTableName}\`
             SET lastUpdateUserId = ?,
                 lastUpdateFullName = ?,
                 lastUpdatedAt = ?
             WHERE id IN (${wherePlaceholders})`,
            [lastUpdateUserId, lastUpdateFullName, lastUpdatedAt, ...chunk],
          );
        }
      }
      if (updatedProfileEmployeeIds.size > 0) {
        const profilesToSave = Array.from(updatedProfileEmployeeIds)
          .map((profileId) => profileEmployeeMap.get(profileId))
          .filter((profile): profile is ProfileEmployee => !!profile);
        const chunks = chunkArray(profilesToSave, 200);
        for (const chunk of chunks) {
          await repoProfileEmployee.save(chunk);
        }
      }
      console.timeEnd("[AMQ] clone_org_structure");

      console.time("[AMQ] save_revision_status");
      orgRevisionPublish.orgRevisionIsDraft = false;
      orgRevisionPublish.orgRevisionIsCurrent = false;
      await repoOrgRevision.save(orgRevisionPublish);

      orgRevisionDraft.orgRevisionIsCurrent = true;
      orgRevisionDraft.orgRevisionIsDraft = false;
      await repoOrgRevision.save(orgRevisionDraft);
      console.timeEnd("[AMQ] save_revision_status");
    });

    if (shouldSkipPublishInTransaction) {
      console.log(
        `[AMQ] Skip publish in transaction: revision ${id} state changed before write phase`,
      );
      console.timeEnd("[AMQ] handler_org_total");
      return true;
    }

    console.log("[AMQ] Excecute Organization Success");
    if (user) {
      sendWebSocket(
        "send-publish-org",
        {
          success: true,
          message: `ระบบทำการเผยแพร่โครงสร้างหน่วยงานเรียบร้อยแล้ว`,
        },
        { userId: user?.sub },
      ).catch(console.error);
    }

    console.log(`[AMQ] handler_org SUCCESS - Total time: ${Date.now() - startTime}ms`);
    console.timeEnd("[AMQ] handler_org_total");

    await clearMenuAndRoleCache();
    return true;
  } catch (error) {
    const totalTime = Date.now() - startTime;
    console.error(`[AMQ] handler_org ERROR after ${totalTime}ms:`, error);
    if (user) {
      sendWebSocket(
        "send-publish-org",
        {
          success: false,
          message: `ระบบทำการเผยแพร่โครงสร้างหน่วยงานไม่สำเร็จ`,
        },
        { userId: user?.sub },
      ).catch(console.error);
    }
    console.timeEnd("[AMQ] handler_org_total");
    return false;
  }
}

async function clearMenuAndRoleCache(): Promise<void> {
  const redisClient = redis.createClient({
    host: REDIS_HOST,
    port: REDIS_PORT,
  });

  const keysAsync = promisify(redisClient.keys).bind(redisClient);
  const delAsync = promisify(redisClient.del).bind(redisClient);

  try {
    const menuKeys = await keysAsync("menu_*");
    if (menuKeys.length > 0) {
      await delAsync(...menuKeys);
      console.log(`[AMQ] Cleared ${menuKeys.length} menu cache keys`);
    }

    const roleKeys = await keysAsync("role_*");
    if (roleKeys.length > 0) {
      await delAsync(...roleKeys);
      console.log(`[AMQ] Cleared ${roleKeys.length} role cache keys`);
    }

    const posMasterKeys = await keysAsync("posMaster_*");
    if (posMasterKeys.length > 0) {
      await delAsync(...posMasterKeys);
      console.log(`[AMQ] Cleared ${posMasterKeys.length} posMaster cache keys`);
    }

    const userKeys = await keysAsync("user_*");
    if (userKeys.length > 0) {
      await delAsync(...userKeys);
      console.log(`[AMQ] Cleared ${userKeys.length} user cache keys`);
    }

    const orgKeys = await keysAsync("org_*");
    if (orgKeys.length > 0) {
      await delAsync(...orgKeys);
      console.log(`[AMQ] Cleared ${orgKeys.length} org cache keys`);
    }
  } finally {
    redisClient.quit();
  }
}

async function handler_org_draft(msg: amqp.ConsumeMessage): Promise<boolean> {
  const { data, token, user } = JSON.parse(msg.content.toString());
  const { requestBody, request, revision } = data;
  const posMasterRepository = AppDataSource.getRepository(PosMaster);
  const positionRepository = AppDataSource.getRepository(Position);
  const employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster);
  const employeeTempPosMasterRepository = AppDataSource.getRepository(EmployeeTempPosMaster);
  const posMasterAssignRepository = AppDataSource.getRepository(PosMasterAssign);
  const posMasterActRepository = AppDataSource.getRepository(PosMasterAct);
  const permissionOrgRepository = AppDataSource.getRepository(PermissionOrg);
  const employeePositionRepository = AppDataSource.getRepository(EmployeePosition);
  const orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
  const orgRootRepository = AppDataSource.getRepository(OrgRoot);
  const child1Repository = AppDataSource.getRepository(OrgChild1);
  const child2Repository = AppDataSource.getRepository(OrgChild2);
  const child3Repository = AppDataSource.getRepository(OrgChild3);
  const child4Repository = AppDataSource.getRepository(OrgChild4);
  if (user) {
    sendWebSocket(
      "send-create-draft-org",
      {
        success: true,
        message: `ระบบกำลังทำการสร้างแบบร่างโครงสร้างหน่วยงาน`,
      },
      { userId: user?.sub },
    ).catch(console.error);
  }
  try {
    //cone tree
    if (
      requestBody.typeDraft.toUpperCase() == "ORG" ||
      requestBody.typeDraft.toUpperCase() == "ORG_POSITION" ||
      requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" ||
      requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" ||
      requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE"
    ) {
      //cone by revisionId
      if (requestBody.orgRevisionId == null)
        throw new HttpError(HttpStatusCode.NOT_FOUND, "not found.");
      const _revision = await orgRevisionRepository.findOne({
        where: { id: requestBody.orgRevisionId },
      });
      if (!_revision) throw new HttpError(HttpStatusCode.NOT_FOUND, "not found.");

      //หา dna tree ถ้าไม่มีให้เอาตัวเองเป็น dna
      const orgRoot = await orgRootRepository.find({
        where: { orgRevisionId: requestBody.orgRevisionId },
        order: { orgRootOrder: "ASC" },
      });
      let _orgRoot: any = orgRoot.map((x) => ({
        ...x,
        ancestorDNA:
          x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
            ? x.id
            : x.ancestorDNA,
      }));
      await orgRootRepository.save(_orgRoot);

      const orgChild1 = await child1Repository.find({
        where: { orgRevisionId: requestBody.orgRevisionId },
        order: { orgChild1Order: "ASC" },
      });
      let _orgChild1: any = orgChild1.map((x) => ({
        ...x,
        ancestorDNA:
          x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
            ? x.id
            : x.ancestorDNA,
      }));
      await child1Repository.save(_orgChild1);

      const orgChild2 = await child2Repository.find({
        where: { orgRevisionId: requestBody.orgRevisionId },
        order: { orgChild2Order: "ASC" },
      });
      let _orgChild2: any = orgChild2.map((x) => ({
        ...x,
        ancestorDNA:
          x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
            ? x.id
            : x.ancestorDNA,
      }));
      await child2Repository.save(_orgChild2);

      const orgChild3 = await child3Repository.find({
        where: { orgRevisionId: requestBody.orgRevisionId },
        order: { orgChild3Order: "ASC" },
      });
      let _orgChild3: any = orgChild3.map((x) => ({
        ...x,
        ancestorDNA:
          x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
            ? x.id
            : x.ancestorDNA,
      }));
      await child3Repository.save(_orgChild3);

      const orgChild4 = await child4Repository.find({
        where: { orgRevisionId: requestBody.orgRevisionId },
        order: { orgChild4Order: "ASC" },
      });
      let _orgChild4: any = orgChild4.map((x) => ({
        ...x,
        ancestorDNA:
          x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
            ? x.id
            : x.ancestorDNA,
      }));
      await child4Repository.save(_orgChild4);

      //หา dna posmaster ถ้าไม่มีให้เอาตัวเองเป็น dna
      const orgPosMaster = await posMasterRepository.find({
        where: { orgRevisionId: requestBody.orgRevisionId },
        relations: ["positions"],
      });

      let _orgPosMaster: PosMaster[] = [];
      if (
        requestBody.typeDraft.toUpperCase() == "ORG_POSITION" ||
        requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON" ||
        requestBody.typeDraft.toUpperCase() == "ORG_POSITION_ROLE" ||
        requestBody.typeDraft.toUpperCase() == "ORG_POSITION_PERSON_ROLE"
      ) {
        _orgPosMaster = orgPosMaster
          .filter(
            (x) => x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000",
          )
          .map((x) => ({
            ...x,
            ancestorDNA:
              x.ancestorDNA == null || x.ancestorDNA == "00000000-0000-0000-0000-000000000000"
                ? x.id
                : x.ancestorDNA,
          }));
        await posMasterRepository.save(_orgPosMaster);
      }
      // Create org
      for await (const x0 of _orgRoot) {
        var dataId = x0.id;
        var _null: any = null;
        // console.log(`ch0 ${x0.orgRootOrder}`);
        // console.log(dataId);
        delete x0.id;
        const data = Object.assign(new OrgRoot(), x0);
        data.orgRevisionId = revision.id;
        data.createdUserId = request.sub;
        data.createdFullName = request.name;
        data.createdAt = new Date();
        data.lastUpdateUserId = request.sub;
        data.lastUpdateFullName = request.name;
        data.lastUpdatedAt = new Date();
        await orgRootRepository.save(data);

        const orgTypes = new Set([
          "ORG_POSITION",
          "ORG_POSITION_PERSON",
          "ORG_POSITION_ROLE",
          "ORG_POSITION_PERSON_ROLE",
        ]);

        if (orgTypes.has(requestBody.typeDraft.toUpperCase())) {
          // Create posMaster
          for await (const item of orgPosMaster.filter(
            (x: PosMaster) => x.orgRootId == dataId && x.orgChild1Id == null,
          ) as any) {
            let posMasterAssign = await posMasterAssignRepository.find({
              where: { posMasterId: item.id },
            });
            delete item.id;
            const posMaster = Object.assign(new PosMaster(), item);
            posMaster.positions = [];

            if (
              ["ORG_POSITION_PERSON", "ORG_POSITION_PERSON_ROLE"].includes(
                requestBody.typeDraft.toUpperCase(),
              )
            ) {
              posMaster.next_holderId = item.current_holderId;
              // if (posMaster.next_holderId) {
              //   posMaster.conditionReason = _null;
              //   posMaster.isCondition = false;
              // }
            } else {
              posMaster.next_holderId = null;
              posMaster.isSit = false;
            }

            if (
              ["ORG_POSITION_ROLE", "ORG_POSITION_PERSON_ROLE"].includes(
                requestBody.typeDraft.toUpperCase(),
              )
            ) {
              posMaster.authRoleId = item.authRoleId;
            } else {
              posMaster.authRoleId = null;
            }

            posMaster.current_holderId = null;
            posMaster.orgRevisionId = revision.id;
            posMaster.orgRootId = data.id;
            posMaster.createdUserId = request.sub;
            posMaster.createdFullName = request.name;
            posMaster.createdAt = new Date();
            posMaster.lastUpdateUserId = request.sub;
            posMaster.lastUpdateFullName = request.name;
            posMaster.lastUpdatedAt = new Date();
            await posMasterRepository.save(posMaster);

            // // Copy assignments
            // await posMasterAssignRepository.save(
            //   posMasterAssign.map(({ id, ...rest }: PosMasterAssign) => ({
            //     ...rest,
            //     posMasterId: posMaster.id,
            //   })),
            // );

            // Create positions
            for await (const pos of item.positions) {
              delete pos.id;
              const position = Object.assign(new Position(), pos);
              position.posMasterId = posMaster.id;

              if (
                ["ORG_POSITION", "ORG_POSITION_ROLE"].includes(requestBody.typeDraft.toUpperCase())
              ) {
                position.positionIsSelected = false;
              }

              position.createdUserId = request.sub;
              position.createdFullName = request.name;
              position.createdAt = new Date();
              position.lastUpdateUserId = request.sub;
              position.lastUpdateFullName = request.name;
              position.lastUpdatedAt = new Date();
              await positionRepository.save(position);
            }
          }
        }

        // Create orgChild1
        for await (const x1 of _orgChild1.filter((x: OrgChild1) => x.orgRootId == dataId)) {
          var data1Id = x1.id;
          // console.log(`ch1 ${x1.orgChild1Order}`);
          // console.log(data1Id);
          delete x1.id;
          const data1 = Object.assign(new OrgChild1(), x1);
          data1.orgRootId = data.id;
          data1.orgRevisionId = revision.id;
          data1.createdUserId = request.sub;
          data1.createdFullName = request.name;
          data1.createdAt = new Date();
          data1.lastUpdateUserId = request.sub;
          data1.lastUpdateFullName = request.name;
          data1.lastUpdatedAt = new Date();
          await child1Repository.save(data1);

          if (orgTypes.has(requestBody.typeDraft.toUpperCase())) {
            // Create posMaster
            for await (const item of orgPosMaster.filter(
              (x: PosMaster) => x.orgChild1Id == data1Id && x.orgChild2Id == null,
            ) as any) {
              let posMasterAssign = await posMasterAssignRepository.find({
                where: { posMasterId: item.id },
              });
              delete item.id;
              const posMaster = Object.assign(new PosMaster(), item);
              posMaster.positions = [];

              if (
                ["ORG_POSITION_PERSON", "ORG_POSITION_PERSON_ROLE"].includes(
                  requestBody.typeDraft.toUpperCase(),
                )
              ) {
                posMaster.next_holderId = item.current_holderId;
                // if (posMaster.next_holderId) {
                //   posMaster.conditionReason = _null;
                //   posMaster.isCondition = false;
                // }
              } else {
                posMaster.next_holderId = null;
                posMaster.isSit = false;
              }

              if (
                ["ORG_POSITION_ROLE", "ORG_POSITION_PERSON_ROLE"].includes(
                  requestBody.typeDraft.toUpperCase(),
                )
              ) {
                posMaster.authRoleId = item.authRoleId;
              } else {
                posMaster.authRoleId = null;
              }

              posMaster.current_holderId = null;
              posMaster.orgRevisionId = revision.id;
              posMaster.orgRootId = data.id;
              posMaster.orgChild1Id = data1.id;
              posMaster.createdUserId = request.sub;
              posMaster.createdFullName = request.name;
              posMaster.createdAt = new Date();
              posMaster.lastUpdateUserId = request.sub;
              posMaster.lastUpdateFullName = request.name;
              posMaster.lastUpdatedAt = new Date();
              await posMasterRepository.save(posMaster);

              // // Copy assignments
              // await posMasterAssignRepository.save(
              //   posMasterAssign.map(({ id, ...rest }: PosMasterAssign) => ({
              //     ...rest,
              //     posMasterId: posMaster.id,
              //   })),
              // );

              // Create positions
              for await (const pos of item.positions) {
                delete pos.id;
                const position = Object.assign(new Position(), pos);
                position.posMasterId = posMaster.id;

                if (
                  ["ORG_POSITION", "ORG_POSITION_ROLE"].includes(
                    requestBody.typeDraft.toUpperCase(),
                  )
                ) {
                  position.positionIsSelected = false;
                }

                position.createdUserId = request.sub;
                position.createdFullName = request.name;
                position.createdAt = new Date();
                position.lastUpdateUserId = request.sub;
                position.lastUpdateFullName = request.name;
                position.lastUpdatedAt = new Date();
                await positionRepository.save(position);
              }
            }
          }
          for await (const x2 of _orgChild2.filter((x: OrgChild2) => x.orgChild1Id == data1Id)) {
            var data2Id = x2.id;
            // console.log(`ch2 ${x2.orgChild2Order}`);
            // console.log(data2Id);
            delete x2.id;
            const data2 = Object.assign(new OrgChild2(), x2);
            data2.orgChild1Id = data1.id;
            data2.orgRootId = data.id;
            data2.orgRevisionId = revision.id;
            data2.createdUserId = request.sub;
            data2.createdFullName = request.name;
            data2.createdAt = new Date();
            data2.lastUpdateUserId = request.sub;
            data2.lastUpdateFullName = request.name;
            data2.lastUpdatedAt = new Date();
            await child2Repository.save(data2);

            if (orgTypes.has(requestBody.typeDraft.toUpperCase())) {
              // Create posMaster
              for await (const item of orgPosMaster.filter(
                (x: PosMaster) => x.orgChild2Id == data2Id && x.orgChild3Id == null,
              ) as any) {
                let posMasterAssign = await posMasterAssignRepository.find({
                  where: { posMasterId: item.id },
                });
                delete item.id;
                const posMaster = Object.assign(new PosMaster(), item);
                posMaster.positions = [];

                if (
                  ["ORG_POSITION_PERSON", "ORG_POSITION_PERSON_ROLE"].includes(
                    requestBody.typeDraft.toUpperCase(),
                  )
                ) {
                  posMaster.next_holderId = item.current_holderId;
                  // if (posMaster.next_holderId) {
                  //   posMaster.conditionReason = _null;
                  //   posMaster.isCondition = false;
                  // }
                } else {
                  posMaster.next_holderId = null;
                  posMaster.isSit = false;
                }

                if (
                  ["ORG_POSITION_ROLE", "ORG_POSITION_PERSON_ROLE"].includes(
                    requestBody.typeDraft.toUpperCase(),
                  )
                ) {
                  posMaster.authRoleId = item.authRoleId;
                } else {
                  posMaster.authRoleId = null;
                }

                posMaster.current_holderId = null;
                posMaster.orgRevisionId = revision.id;
                posMaster.orgRootId = data.id;
                posMaster.orgChild1Id = data1.id;
                posMaster.orgChild2Id = data2.id;
                posMaster.createdUserId = request.sub;
                posMaster.createdFullName = request.name;
                posMaster.createdAt = new Date();
                posMaster.lastUpdateUserId = request.sub;
                posMaster.lastUpdateFullName = request.name;
                posMaster.lastUpdatedAt = new Date();
                await posMasterRepository.save(posMaster);

                // // Copy assignments
                // await posMasterAssignRepository.save(
                //   posMasterAssign.map(({ id, ...rest }: PosMasterAssign) => ({
                //     ...rest,
                //     posMasterId: posMaster.id,
                //   })),
                // );

                // Create positions
                for await (const pos of item.positions) {
                  delete pos.id;
                  const position = Object.assign(new Position(), pos);
                  position.posMasterId = posMaster.id;
                  if (
                    ["ORG_POSITION", "ORG_POSITION_ROLE"].includes(
                      requestBody.typeDraft.toUpperCase(),
                    )
                  ) {
                    position.positionIsSelected = false;
                  }
                  position.createdUserId = request.sub;
                  position.createdFullName = request.name;
                  position.createdAt = new Date();
                  position.lastUpdateUserId = request.sub;
                  position.lastUpdateFullName = request.name;
                  position.lastUpdatedAt = new Date();
                  await positionRepository.save(position);
                }
              }
            }

            // Create org
            for await (const x3 of _orgChild3.filter((x: OrgChild3) => x.orgChild2Id == data2Id)) {
              var data3Id = x3.id;
              // console.log(`ch3 ${x3.orgChild3Order}`);
              // console.log(data3Id);
              delete x3.id;
              const data3 = Object.assign(new OrgChild3(), x3);
              data3.orgChild2Id = data2.id;
              data3.orgChild1Id = data1.id;
              data3.orgRootId = data.id;
              data3.orgRevisionId = revision.id;
              data3.createdUserId = request.sub;
              data3.createdFullName = request.name;
              data3.createdAt = new Date();
              data3.lastUpdateUserId = request.sub;
              data3.lastUpdateFullName = request.name;
              data3.lastUpdatedAt = new Date();
              await child3Repository.save(data3);

              if (orgTypes.has(requestBody.typeDraft.toUpperCase())) {
                // Create posMaster
                for await (const item of orgPosMaster.filter(
                  (x: PosMaster) => x.orgChild3Id == data3Id && x.orgChild4Id == null,
                ) as any) {
                  let posMasterAssign = await posMasterAssignRepository.find({
                    where: { posMasterId: item.id },
                  });
                  delete item.id;
                  const posMaster = Object.assign(new PosMaster(), item);
                  posMaster.positions = [];

                  if (
                    ["ORG_POSITION_PERSON", "ORG_POSITION_PERSON_ROLE"].includes(
                      requestBody.typeDraft.toUpperCase(),
                    )
                  ) {
                    posMaster.next_holderId = item.current_holderId;
                    // if (posMaster.next_holderId) {
                    //   posMaster.conditionReason = _null;
                    //   posMaster.isCondition = false;
                    // }
                  } else {
                    posMaster.next_holderId = null;
                    posMaster.isSit = false;
                  }

                  if (
                    ["ORG_POSITION_ROLE", "ORG_POSITION_PERSON_ROLE"].includes(
                      requestBody.typeDraft.toUpperCase(),
                    )
                  ) {
                    posMaster.authRoleId = item.authRoleId;
                  } else {
                    posMaster.authRoleId = null;
                  }

                  posMaster.current_holderId = null;
                  posMaster.orgRevisionId = revision.id;
                  posMaster.orgRootId = data.id;
                  posMaster.orgChild1Id = data1.id;
                  posMaster.orgChild2Id = data2.id;
                  posMaster.orgChild3Id = data3.id;
                  posMaster.createdUserId = request.sub;
                  posMaster.createdFullName = request.name;
                  posMaster.createdAt = new Date();
                  posMaster.lastUpdateUserId = request.sub;
                  posMaster.lastUpdateFullName = request.name;
                  posMaster.lastUpdatedAt = new Date();
                  await posMasterRepository.save(posMaster);

                  // // Copy assignments
                  // await posMasterAssignRepository.save(
                  //   posMasterAssign.map(({ id, ...rest }: PosMasterAssign) => ({
                  //     ...rest,
                  //     posMasterId: posMaster.id,
                  //   })),
                  // );

                  // Create positions
                  for await (const pos of item.positions) {
                    delete pos.id;
                    const position = Object.assign(new Position(), pos);
                    position.posMasterId = posMaster.id;
                    if (
                      ["ORG_POSITION", "ORG_POSITION_ROLE"].includes(
                        requestBody.typeDraft.toUpperCase(),
                      )
                    ) {
                      position.positionIsSelected = false;
                    }
                    position.createdUserId = request.sub;
                    position.createdFullName = request.name;
                    position.createdAt = new Date();
                    position.lastUpdateUserId = request.sub;
                    position.lastUpdateFullName = request.name;
                    position.lastUpdatedAt = new Date();
                    await positionRepository.save(position);
                  }
                }
              }
              // Create org
              for await (const x4 of _orgChild4.filter(
                (x: OrgChild4) => x.orgChild3Id == data3Id,
              )) {
                var data4Id = x4.id;
                // console.log(`ch4 ${x4.orgChild4Order}`);
                // console.log(data4Id);
                delete x4.id;
                const data4 = Object.assign(new OrgChild4(), x4);
                data4.orgChild3Id = data3.id;
                data4.orgChild2Id = data2.id;
                data4.orgChild1Id = data1.id;
                data4.orgRootId = data.id;
                data4.orgRevisionId = revision.id;
                data4.createdUserId = request.sub;
                data4.createdFullName = request.name;
                data4.createdAt = new Date();
                data4.lastUpdateUserId = request.sub;
                data4.lastUpdateFullName = request.name;
                data4.lastUpdatedAt = new Date();
                await child4Repository.save(data4);

                if (orgTypes.has(requestBody.typeDraft.toUpperCase())) {
                  // Create posMaster
                  for await (const item of orgPosMaster.filter(
                    (x: PosMaster) => x.orgChild4Id == data4Id,
                  ) as any) {
                    let posMasterAssign = await posMasterAssignRepository.find({
                      where: { posMasterId: item.id },
                    });
                    delete item.id;
                    const posMaster = Object.assign(new PosMaster(), item);
                    posMaster.positions = [];

                    if (
                      ["ORG_POSITION_PERSON", "ORG_POSITION_PERSON_ROLE"].includes(
                        requestBody.typeDraft.toUpperCase(),
                      )
                    ) {
                      posMaster.next_holderId = item.current_holderId;
                      // if (posMaster.next_holderId) {
                      //   posMaster.conditionReason = _null;
                      //   posMaster.isCondition = false;
                      // }
                    } else {
                      posMaster.next_holderId = null;
                      posMaster.isSit = false;
                    }

                    if (
                      ["ORG_POSITION_ROLE", "ORG_POSITION_PERSON_ROLE"].includes(
                        requestBody.typeDraft.toUpperCase(),
                      )
                    ) {
                      posMaster.authRoleId = item.authRoleId;
                    } else {
                      posMaster.authRoleId = null;
                    }

                    posMaster.current_holderId = null;
                    posMaster.orgRevisionId = revision.id;
                    posMaster.orgRootId = data.id;
                    posMaster.orgChild1Id = data1.id;
                    posMaster.orgChild2Id = data2.id;
                    posMaster.orgChild3Id = data3.id;
                    posMaster.orgChild4Id = data4.id;
                    posMaster.createdUserId = request.sub;
                    posMaster.createdFullName = request.name;
                    posMaster.createdAt = new Date();
                    posMaster.lastUpdateUserId = request.sub;
                    posMaster.lastUpdateFullName = request.name;
                    posMaster.lastUpdatedAt = new Date();
                    await posMasterRepository.save(posMaster);

                    // // Copy assignments
                    // await posMasterAssignRepository.save(
                    //   posMasterAssign.map(({ id, ...rest }: PosMasterAssign) => ({
                    //     ...rest,
                    //     posMasterId: posMaster.id,
                    //   })),
                    // );

                    // Create positions
                    for await (const pos of item.positions) {
                      delete pos.id;
                      const position = Object.assign(new Position(), pos);
                      position.posMasterId = posMaster.id;
                      if (
                        ["ORG_POSITION", "ORG_POSITION_ROLE"].includes(
                          requestBody.typeDraft.toUpperCase(),
                        )
                      ) {
                        position.positionIsSelected = false;
                      }
                      position.createdUserId = request.sub;
                      position.createdFullName = request.name;
                      position.createdAt = new Date();
                      position.lastUpdateUserId = request.sub;
                      position.lastUpdateFullName = request.name;
                      position.lastUpdatedAt = new Date();
                      await positionRepository.save(position);
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
    //ลบ daft เก่าที่ค้างแล้วยังไม่ได้เผยแพร่
    const _orgRevisions = await orgRevisionRepository.find({
      where: [{ orgRevisionIsDraft: true, id: Not(revision.id) }],
    });
    const _roots = await orgRootRepository.find({
      where: [{ orgRevisionId: In(_orgRevisions.map((x) => x.id)) }],
    });
    const _posMasters = await posMasterRepository.find({
      where: [{ orgRevisionId: In(_orgRevisions.map((x) => x.id)) }],
    });
    const _employeePosMasters = await employeePosMasterRepository.find({
      where: [{ orgRevisionId: In(_orgRevisions.map((x) => x.id)) }],
    });
    const _employeeTempPosMasters = await employeeTempPosMasterRepository.find({
      where: [{ orgRevisionId: In(_orgRevisions.map((x) => x.id)) }],
    });
    await positionRepository.delete({ posMasterId: In(_posMasters.map((x) => x.id)) });
    await employeePositionRepository.delete({
      posMasterId: In(_employeePosMasters.map((x) => x.id)),
    });
    await employeePositionRepository.delete({
      posMasterTempId: In(_employeeTempPosMasters.map((x) => x.id)),
    });
    await posMasterAssignRepository.delete({ posMasterId: In(_posMasters.map((x) => x.id)) });
    await posMasterActRepository.delete({ posMasterId: In(_posMasters.map((x) => x.id)) }); //ใช้ posMasterId ของ revision: draft *แต่ยังไม่เจอช็อดไหนที่ใช้โครงสร้างแบบร่างในรักษาการแทน
    await posMasterActRepository.delete({
      //ใช้ posMasterId ของ revision: draft *แต่ยังไม่เจอช็อดไหนที่ใช้โครงสร้างแบบร่างในรักษาการแทน
      posMasterChildId: In(_posMasters.map((x) => x.id)),
    });
    // await posMasterRepository.remove(_posMasters);
    const batchSize = 1000;
    const removeInBatches = async (repository: any, data: any, label: any) => {
      for (let i = 0; i < data.length; i += batchSize) {
        const batch = data.slice(i, i + batchSize);
        try {
          await repository.remove(batch);
          console.log(`Removed ${label} batch ${i / batchSize + 1}`);
        } catch (error) {
          console.log(`Failed to remove ${label} batch ${i / batchSize + 1}`);
        }
      }
    };
    await removeInBatches(posMasterRepository, _posMasters, "PosMaster");
    await removeInBatches(employeePosMasterRepository, _employeePosMasters, "EmployeePosMaster");
    await removeInBatches(
      employeeTempPosMasterRepository,
      _employeeTempPosMasters,
      "EmployeeTempPosMaster",
    );
    await child4Repository.delete({ orgRevisionId: In(_orgRevisions.map((x) => x.id)) });
    await child3Repository.delete({ orgRevisionId: In(_orgRevisions.map((x) => x.id)) });
    await child2Repository.delete({ orgRevisionId: In(_orgRevisions.map((x) => x.id)) });
    await child1Repository.delete({ orgRevisionId: In(_orgRevisions.map((x) => x.id)) });
    // Task #2160 อัพเดทหน้าที่จัดการโครงสร้างแบบร่าง
    if (
      [
        "ORG",
        "ORG_POSITION",
        "ORG_POSITION_PERSON",
        "ORG_POSITION_ROLE",
        "ORG_POSITION_PERSON_ROLE",
      ].includes(requestBody.typeDraft?.toUpperCase())
    ) {
      const _newRoots = await orgRootRepository.find({
        where: { orgRevisionId: revision.id },
      });
      const newRootMap = new Map(_newRoots.map((r) => [r.ancestorDNA, r.id]));
      for (const oldRoot of _roots) {
        const newRootId = newRootMap.get(oldRoot.ancestorDNA);
        if (!newRootId) continue;
        // อัพเดท orgRootId ที่อยู่ภายใต้ orgRevision แบบร่างเดิมเป็นของ orgRevision แบบร่างใหม่
        await permissionOrgRepository.update({ orgRootId: oldRoot.id }, { orgRootId: newRootId });
      }
    } else {
      await permissionOrgRepository.delete({
        orgRootId: In(_roots.map((x) => x.id)),
      });
    }
    await orgRootRepository.delete({ orgRevisionId: In(_orgRevisions.map((x) => x.id)) });
    await orgRevisionRepository.remove(_orgRevisions);

    console.log("[AMQ] Create Draft Success");
    if (user) {
      await sendWebSocket(
        "send-create-draft-org",
        {
          success: true,
          message: `ระบบทำการสร้างแบบร่างโครงสร้างหน่วยงานเรียบร้อยแล้ว`,
        },
        { userId: user?.sub },
      ).catch(console.error);
    }
    return true;
  } catch (error) {
    console.error(error);
    if (user) {
      await sendWebSocket(
        "send-create-draft-org",
        {
          success: false,
          message: `ระบบทำการสร้างแบบร่างโครงสร้างหน่วยงานไม่สำเร็จ`,
        },
        { userId: user?.sub },
      ).catch(console.error);
    }
    return false;
  }
}
