import { AppDataSource } from "../database/data-source";
import { Profile } from "../entities/Profile";
import { ProfileEmployee } from "../entities/ProfileEmployee";
// import { PosMaster } from "../entities/PosMaster";
// import { EmployeePosMaster } from "../entities/EmployeePosMaster";
// import { OrgRoot } from "../entities/OrgRoot";
import {
  createUserHaveProfile,
  getUser,
  getUserByUsername,
  updateUserAttributes,
  deleteUser,
  getRoles,
  addUserRoles,
  getAllUsersPaginated,
  withRetry,
  RateLimiter,
} from "../keycloak";
import { OrgRevision } from "../entities/OrgRevision";
import { SyncProgressManager, SyncProgressState } from "../utils/sync-progress";

export interface UserProfileAttributes {
  profileId: string | null;
  orgRootDnaId: string | null;
  orgChild1DnaId: string | null;
  orgChild2DnaId: string | null;
  orgChild3DnaId: string | null;
  orgChild4DnaId: string | null;
  empType: string | null;
  prefix?: string | null;
}

/**
 * Keycloak Attribute Service
 * Service for syncing profileId and orgRootDnaId to Keycloak user attributes
 */
export class KeycloakAttributeService {
  private profileRepo = AppDataSource.getRepository(Profile);
  private profileEmployeeRepo = AppDataSource.getRepository(ProfileEmployee);
  // private posMasterRepo = AppDataSource.getRepository(PosMaster);
  // private employeePosMasterRepo = AppDataSource.getRepository(EmployeePosMaster);
  // private orgRootRepo = AppDataSource.getRepository(OrgRoot);
  private orgRevisionRepo = AppDataSource.getRepository(OrgRevision);

  /**
   * Get profile attributes (profileId and orgRootDnaId) from database
   * Searches in Profile table first (ข้าราชการ), then ProfileEmployee (ลูกจ้าง)
   *
   * @param keycloakUserId - Keycloak user ID
   * @returns UserProfileAttributes with profileId and orgRootDnaId
   */
  async getUserProfileAttributes(keycloakUserId: string): Promise<UserProfileAttributes> {
    // First, try to find in Profile (ข้าราชการ)
    const revisionCurrent = await this.orgRevisionRepo.findOne({
      where: { orgRevisionIsCurrent: true },
    });
    const revisionId = revisionCurrent ? revisionCurrent.id : null;
    const profileResult = await this.profileRepo
      .createQueryBuilder("p")
      .leftJoinAndSelect("p.current_holders", "pm")
      .leftJoinAndSelect("pm.orgRoot", "orgRoot")
      .leftJoinAndSelect("pm.orgChild1", "orgChild1")
      .leftJoinAndSelect("pm.orgChild2", "orgChild2")
      .leftJoinAndSelect("pm.orgChild3", "orgChild3")
      .leftJoinAndSelect("pm.orgChild4", "orgChild4")
      .where("p.keycloak = :keycloakUserId", { keycloakUserId })
      .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId })
      .getOne();

    if (
      profileResult &&
      profileResult.current_holders &&
      profileResult.current_holders.length > 0
    ) {
      const currentPos = profileResult.current_holders[0];
      const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
      const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
      const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
      const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
      const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
      return {
        profileId: profileResult.id,
        orgRootDnaId,
        orgChild1DnaId,
        orgChild2DnaId,
        orgChild3DnaId,
        orgChild4DnaId,
        empType: "OFFICER",
        prefix: profileResult.prefix,
      };
    }

    // If not found in Profile, try ProfileEmployee (ลูกจ้าง)
    // First, get the profileEmployee to check employeeClass
    const profileEmployeeBasic = await this.profileEmployeeRepo
      .createQueryBuilder("pe")
      .where("pe.keycloak = :keycloakUserId", { keycloakUserId })
      .getOne();

    if (!profileEmployeeBasic) {
      // Return null values if no profile found
      return {
        profileId: null,
        orgRootDnaId: null,
        orgChild1DnaId: null,
        orgChild2DnaId: null,
        orgChild3DnaId: null,
        orgChild4DnaId: null,
        empType: null,
        prefix: null,
      };
    }

    // Check employeeClass to determine which table to query
    const isPermEmployee = profileEmployeeBasic.employeeClass === "PERM";

    if (isPermEmployee) {
      // ลูกจ้างประจำ (PERM) - ใช้ EmployeePosMaster
      const profileEmployeeResult = await this.profileEmployeeRepo
        .createQueryBuilder("pe")
        .leftJoinAndSelect("pe.current_holders", "epm")
        .leftJoinAndSelect("epm.orgRoot", "orgRoot")
        .leftJoinAndSelect("epm.orgChild1", "orgChild1")
        .leftJoinAndSelect("epm.orgChild2", "orgChild2")
        .leftJoinAndSelect("epm.orgChild3", "orgChild3")
        .leftJoinAndSelect("epm.orgChild4", "orgChild4")
        .where("pe.keycloak = :keycloakUserId", { keycloakUserId })
        .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId })
        .getOne();

      if (
        profileEmployeeResult &&
        profileEmployeeResult.current_holders &&
        profileEmployeeResult.current_holders.length > 0
      ) {
        const currentPos = profileEmployeeResult.current_holders[0];
        const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
        const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
        const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
        const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
        const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
        return {
          profileId: profileEmployeeResult.id,
          orgRootDnaId,
          orgChild1DnaId,
          orgChild2DnaId,
          orgChild3DnaId,
          orgChild4DnaId,
          empType: profileEmployeeResult.employeeClass,
          prefix: profileEmployeeResult.prefix,
        };
      }
    } else {
      // ลูกจ้างชั่วคราว (TEMP) - ใช้ EmployeeTempPosMaster
      const profileEmployeeResult = await this.profileEmployeeRepo
        .createQueryBuilder("pe")
        .leftJoinAndSelect("pe.current_holderTemps", "etpm")
        .leftJoinAndSelect("etpm.orgRoot", "orgRoot")
        .leftJoinAndSelect("etpm.orgChild1", "orgChild1")
        .leftJoinAndSelect("etpm.orgChild2", "orgChild2")
        .leftJoinAndSelect("etpm.orgChild3", "orgChild3")
        .leftJoinAndSelect("etpm.orgChild4", "orgChild4")
        .where("pe.keycloak = :keycloakUserId", { keycloakUserId })
        .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId })
        .getOne();

      if (
        profileEmployeeResult &&
        profileEmployeeResult.current_holderTemps &&
        profileEmployeeResult.current_holderTemps.length > 0
      ) {
        const currentPos = profileEmployeeResult.current_holderTemps[0];
        const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
        const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
        const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
        const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
        const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
        return {
          profileId: profileEmployeeResult.id,
          orgRootDnaId,
          orgChild1DnaId,
          orgChild2DnaId,
          orgChild3DnaId,
          orgChild4DnaId,
          empType: profileEmployeeResult.employeeClass,
          prefix: profileEmployeeResult.prefix,
        };
      }
    }

    // Return null values if no profile found
    return {
      profileId: null,
      orgRootDnaId: null,
      orgChild1DnaId: null,
      orgChild2DnaId: null,
      orgChild3DnaId: null,
      orgChild4DnaId: null,
      empType: null,
      prefix: null,
    };
  }

  /**
   * Get profile attributes by profile ID directly
   * Used for syncing specific profiles
   *
   * @param profileId - Profile ID
   * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง
   * @returns UserProfileAttributes with profileId and orgRootDnaId
   */
  async getAttributesByProfileId(
    profileId: string,
    profileType: "PROFILE" | "PROFILE_EMPLOYEE",
  ): Promise<UserProfileAttributes> {
    const revisionCurrent = await this.orgRevisionRepo.findOne({
      where: { orgRevisionIsCurrent: true },
    });
    const revisionId = revisionCurrent ? revisionCurrent.id : null;
    if (profileType === "PROFILE") {
      const profileResult = await this.profileRepo
        .createQueryBuilder("p")
        .leftJoinAndSelect("p.current_holders", "pm")
        .leftJoinAndSelect("pm.orgRoot", "orgRoot")
        .leftJoinAndSelect("pm.orgChild1", "orgChild1")
        .leftJoinAndSelect("pm.orgChild2", "orgChild2")
        .leftJoinAndSelect("pm.orgChild3", "orgChild3")
        .leftJoinAndSelect("pm.orgChild4", "orgChild4")
        .where("p.id = :profileId", { profileId })
        .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId })
        .getOne();

      if (
        profileResult &&
        profileResult.current_holders &&
        profileResult.current_holders.length > 0
      ) {
        const currentPos = profileResult.current_holders[0];
        const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
        const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
        const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
        const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
        const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";
        return {
          profileId: profileResult.id,
          orgRootDnaId,
          orgChild1DnaId,
          orgChild2DnaId,
          orgChild3DnaId,
          orgChild4DnaId,
          empType: "OFFICER",
          prefix: profileResult.prefix,
        };
      }
    } else {
      // First, get the profileEmployee to check employeeClass
      const profileEmployeeBasic = await this.profileEmployeeRepo
        .createQueryBuilder("pe")
        .where("pe.id = :profileId", { profileId })
        .getOne();

      if (!profileEmployeeBasic) {
        return {
          profileId: null,
          orgRootDnaId: null,
          orgChild1DnaId: null,
          orgChild2DnaId: null,
          orgChild3DnaId: null,
          orgChild4DnaId: null,
          empType: null,
          prefix: null,
        };
      }

      // Check employeeClass to determine which table to query
      const isPermEmployee = profileEmployeeBasic.employeeClass === "PERM";

      if (isPermEmployee) {
        // ลูกจ้างประจำ (PERM) - ใช้ EmployeePosMaster
        const profileEmployeeResult = await this.profileEmployeeRepo
          .createQueryBuilder("pe")
          .leftJoinAndSelect("pe.current_holders", "epm")
          .leftJoinAndSelect("epm.orgRoot", "orgRoot")
          .leftJoinAndSelect("epm.orgChild1", "orgChild1")
          .leftJoinAndSelect("epm.orgChild2", "orgChild2")
          .leftJoinAndSelect("epm.orgChild3", "orgChild3")
          .leftJoinAndSelect("epm.orgChild4", "orgChild4")
          .where("pe.id = :profileId", { profileId })
          .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId })
          .getOne();

        if (
          profileEmployeeResult &&
          profileEmployeeResult.current_holders &&
          profileEmployeeResult.current_holders.length > 0
        ) {
          const currentPos = profileEmployeeResult.current_holders[0];
          const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
          const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
          const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
          const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
          const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";

          return {
            profileId: profileEmployeeResult.id,
            orgRootDnaId,
            orgChild1DnaId,
            orgChild2DnaId,
            orgChild3DnaId,
            orgChild4DnaId,
            empType: profileEmployeeResult.employeeClass,
            prefix: profileEmployeeResult.prefix,
          };
        }
      } else {
        // ลูกจ้างชั่วคราว (TEMP) - ใช้ EmployeeTempPosMaster
        const profileEmployeeResult = await this.profileEmployeeRepo
          .createQueryBuilder("pe")
          .leftJoinAndSelect("pe.current_holderTemps", "etpm")
          .leftJoinAndSelect("etpm.orgRoot", "orgRoot")
          .leftJoinAndSelect("etpm.orgChild1", "orgChild1")
          .leftJoinAndSelect("etpm.orgChild2", "orgChild2")
          .leftJoinAndSelect("etpm.orgChild3", "orgChild3")
          .leftJoinAndSelect("etpm.orgChild4", "orgChild4")
          .where("pe.id = :profileId", { profileId })
          .andWhere("orgRoot.orgRevisionId = :revisionId", { revisionId })
          .getOne();

        if (
          profileEmployeeResult &&
          profileEmployeeResult.current_holderTemps &&
          profileEmployeeResult.current_holderTemps.length > 0
        ) {
          const currentPos = profileEmployeeResult.current_holderTemps[0];
          const orgRootDnaId = currentPos.orgRoot?.ancestorDNA || "";
          const orgChild1DnaId = currentPos.orgChild1?.ancestorDNA || "";
          const orgChild2DnaId = currentPos.orgChild2?.ancestorDNA || "";
          const orgChild3DnaId = currentPos.orgChild3?.ancestorDNA || "";
          const orgChild4DnaId = currentPos.orgChild4?.ancestorDNA || "";

          return {
            profileId: profileEmployeeResult.id,
            orgRootDnaId,
            orgChild1DnaId,
            orgChild2DnaId,
            orgChild3DnaId,
            orgChild4DnaId,
            empType: profileEmployeeResult.employeeClass,
            prefix: profileEmployeeResult.prefix,
          };
        }
      }
    }

    return {
      profileId: null,
      orgRootDnaId: null,
      orgChild1DnaId: null,
      orgChild2DnaId: null,
      orgChild3DnaId: null,
      orgChild4DnaId: null,
      empType: null,
      prefix: null,
    };
  }

  /**
   * Sync user attributes to Keycloak
   *
   * @param keycloakUserId - Keycloak user ID
   * @returns true if sync successful, false otherwise
   */
  async syncUserAttributes(keycloakUserId: string): Promise<boolean> {
    try {
      const attributes = await this.getUserProfileAttributes(keycloakUserId);

      if (!attributes.profileId) {
        console.log(`No profile found for Keycloak user ${keycloakUserId}`);
        return false;
      }

      // Prepare attributes for Keycloak (must be arrays)
      const keycloakAttributes: Record<string, string[]> = {
        profileId: [attributes.profileId],
        orgRootDnaId: [attributes.orgRootDnaId || ""],
        orgChild1DnaId: [attributes.orgChild1DnaId || ""],
        orgChild2DnaId: [attributes.orgChild2DnaId || ""],
        orgChild3DnaId: [attributes.orgChild3DnaId || ""],
        orgChild4DnaId: [attributes.orgChild4DnaId || ""],
        empType: [attributes.empType || ""],
        prefix: [attributes.prefix || ""],
      };

      const success = await updateUserAttributes(keycloakUserId, keycloakAttributes);

      if (success) {
        console.log(`Synced attributes for Keycloak user ${keycloakUserId}:`, attributes);
      }

      return success;
    } catch (error) {
      console.error(`Error syncing attributes for Keycloak user ${keycloakUserId}:`, error);
      return false;
    }
  }

  /**
   * Sync attributes when organization changes
   * This is called when a user moves to a different organization
   *
   * @param profileId - Profile ID
   * @param profileType - 'PROFILE' for ข้าราชการ or 'PROFILE_EMPLOYEE' for ลูกจ้าง
   * @returns true if sync successful, false otherwise
   */
  async syncOnOrganizationChange(
    profileId: string,
    profileType: "PROFILE" | "PROFILE_EMPLOYEE",
  ): Promise<boolean> {
    try {
      // Get the keycloak userId from the profile
      let keycloakUserId: string | null = null;

      if (profileType === "PROFILE") {
        const profile = await this.profileRepo.findOne({ where: { id: profileId } });
        keycloakUserId = profile?.keycloak || "";
      } else {
        const profileEmployee = await this.profileEmployeeRepo.findOne({
          where: { id: profileId },
        });
        keycloakUserId = profileEmployee?.keycloak || "";
      }

      if (!keycloakUserId) {
        console.log(`No Keycloak user ID found for profile ${profileId}`);
        return false;
      }

      return await this.syncUserAttributes(keycloakUserId);
    } catch (error) {
      console.error(`Error syncing organization change for profile ${profileId}:`, error);
      return false;
    }
  }

  /**
   * Check if Keycloak user has empty/null empType attribute
   * @param keycloakUserId - Keycloak user ID
   * @returns Object with isEmpty flag and currentEmpType value
   */
  async checkEmpTypeEmpty(keycloakUserId: string): Promise<{
    isEmpty: boolean;
    currentEmpType?: string;
  }> {
    try {
      const user = await getUser(keycloakUserId);

      if (!user || !user.attributes) {
        return { isEmpty: true };
      }

      const empType = user.attributes.empType?.[0];

      return {
        isEmpty: !empType || empType.trim() === "",
        currentEmpType: empType || "",
      };
    } catch (error) {
      console.error(`[checkEmpTypeEmpty] Error for user ${keycloakUserId}:`, error);
      return { isEmpty: true }; // Assume empty on error
    }
  }

  /**
   * Sync profiles with missing empType for a specific month
   * @param options - Sync configuration
   * @returns Sync results summary
   */
  async syncMissingEmpTypeByMonth(options: {
    month: string; // "YYYY-MM" format
    profileType?: "PROFILE" | "PROFILE_EMPLOYEE";
    dryRun?: boolean;
    concurrency?: number;
    rateLimit?: number;
  }): Promise<{
    month: string;
    profileType: string;
    totalProfiles: number;
    profilesChecked: number;
    missingEmpType: number;
    syncSuccess: number;
    syncFailed: number;
    skipped: number;
    executionTime: string;
    dryRun: boolean;
  }> {
    const startTime = Date.now();
    const {
      month,
      profileType = "PROFILE",
      dryRun = false,
      concurrency = 5,
      rateLimit = 10,
    } = options;

    const result = {
      month,
      profileType,
      totalProfiles: 0,
      profilesChecked: 0,
      missingEmpType: 0,
      syncSuccess: 0,
      syncFailed: 0,
      skipped: 0,
      executionTime: "",
      dryRun,
    };

    let rateLimiter: RateLimiter | null = null;

    try {
      // Parse month (YYYY-MM) to date range
      const [year, monthNum] = month.split("-").map(Number);
      const startDate = new Date(Date.UTC(year, monthNum - 1, 1, 0, 0, 0));
      const endDate = new Date(Date.UTC(year, monthNum, 0, 23, 59, 59, 999));

      console.log(
        `[syncMissingEmpTypeByMonth] Processing ${profileType} for ${month} (${startDate.toISOString()} to ${endDate.toISOString()})`,
      );

      // Initialize rate limiter if rate limiting is enabled
      if (rateLimit && rateLimit > 0) {
        rateLimiter = new RateLimiter(rateLimit);
        console.log(
          `[syncMissingEmpTypeByMonth] Rate limiting enabled: ${rateLimit} requests/second`,
        );
      }

      // Select repository based on profile type
      const repo = profileType === "PROFILE" ? this.profileRepo : this.profileEmployeeRepo;

      // Query profiles updated within the month
      const profiles = await repo
        .createQueryBuilder("p")
        .where("p.keycloak IS NOT NULL")
        .andWhere("p.keycloak != :empty", { empty: "" })
        .andWhere("p.isDelete = :isDelete", { isDelete: false })
        .andWhere("p.lastUpdatedAt BETWEEN :start AND :end", {
          start: startDate,
          end: endDate,
        })
        .orderBy("p.lastUpdatedAt", "ASC")
        .getMany();

      result.totalProfiles = profiles.length;
      console.log(`[syncMissingEmpTypeByMonth] Found ${profiles.length} profiles to check`);

      if (profiles.length === 0) {
        result.executionTime = `${((Date.now() - startTime) / 1000).toFixed(2)}s`;
        return result;
      }

      // Process profiles in parallel with concurrency limit
      for (let i = 0; i < profiles.length; i += concurrency) {
        const batch = profiles.slice(i, i + concurrency);

        await Promise.all(
          batch.map(async (profile) => {
            // Apply rate limiting if enabled
            if (rateLimiter) {
              await rateLimiter.throttle();
            }

            const keycloakUserId = profile.keycloak;
            if (!keycloakUserId) {
              return {
                profileId: profile.id,
                status: "skipped" as const,
                reason: "No keycloak ID",
              };
            }

            try {
              // Check if empType is empty in Keycloak
              const { isEmpty, currentEmpType } = await this.checkEmpTypeEmpty(keycloakUserId);

              result.profilesChecked++;

              if (!isEmpty) {
                result.skipped++;
                return {
                  profileId: profile.id,
                  status: "skipped" as const,
                  reason: "empType already exists",
                  empType: currentEmpType,
                };
              }

              result.missingEmpType++;

              if (dryRun) {
                return {
                  profileId: profile.id,
                  status: "skipped" as const,
                  reason: "dry run",
                  wouldSync: true,
                };
              }

              // Sync the profile
              const success = await withRetry(
                async () => this.syncOnOrganizationChange(profile.id, profileType),
                3, // maxRetries
                1000, // baseDelay
              );

              if (success) {
                result.syncSuccess++;
                return {
                  profileId: profile.id,
                  status: "synced" as const,
                };
              } else {
                result.syncFailed++;
                return {
                  profileId: profile.id,
                  status: "failed" as const,
                  reason: "Sync returned false",
                };
              }
            } catch (error: any) {
              result.syncFailed++;
              return {
                profileId: profile.id,
                status: "failed" as const,
                reason: error.message || "Unknown error",
              };
            }
          }),
        );

        // Log progress every 50 profiles
        const completed = Math.min(i + concurrency, profiles.length);
        if (completed % 50 === 0 || completed === profiles.length) {
          console.log(
            `[syncMissingEmpTypeByMonth] Progress: ${completed}/${profiles.length} profiles processed`,
          );
        }
      }

      result.executionTime = `${((Date.now() - startTime) / 1000).toFixed(2)}s`;
      console.log(
        `[syncMissingEmpTypeByMonth] Completed: total=${result.totalProfiles}, checked=${result.profilesChecked}, missing=${result.missingEmpType}, synced=${result.syncSuccess}, failed=${result.syncFailed}, skipped=${result.skipped}, elapsed=${result.executionTime}`,
      );
    } catch (error) {
      console.error("[syncMissingEmpTypeByMonth] Error:", error);
      throw error;
    }

    return result;
  }

  /**
   * Clear org DNA attributes in Keycloak for given profiles
   * Sets all org DNA fields to empty strings
   *
   * @param profileIds - Array of profile IDs to clear
   * @param profileType - 'PROFILE' for officers or 'PROFILE_EMPLOYEE' for employees
   * @returns Object with success/failed counts and details
   */
  async clearOrgDnaAttributes(
    profileIds: string[],
    profileType: "PROFILE" | "PROFILE_EMPLOYEE",
  ): Promise<{
    total: number;
    success: number;
    failed: number;
    details: Array<{ profileId: string; status: "success" | "failed"; error?: string }>;
  }> {
    const result = {
      total: profileIds.length,
      success: 0,
      failed: 0,
      details: [] as Array<{ profileId: string; status: "success" | "failed"; error?: string }>,
    };

    for (const profileId of profileIds) {
      try {
        // Get the keycloak userId from the profile
        let keycloakUserId: string | null = null;

        if (profileType === "PROFILE") {
          const profile = await this.profileRepo.findOne({ where: { id: profileId } });
          keycloakUserId = profile?.keycloak || "";
        } else {
          const profileEmployee = await this.profileEmployeeRepo.findOne({
            where: { id: profileId },
          });
          keycloakUserId = profileEmployee?.keycloak || "";
        }

        if (!keycloakUserId) {
          result.failed++;
          result.details.push({
            profileId,
            status: "failed",
            error: "No Keycloak user ID found",
          });
          continue;
        }

        // Clear org DNA attributes by setting them to empty strings
        const clearedAttributes: Record<string, string[]> = {
          orgRootDnaId: [""],
          orgChild1DnaId: [""],
          orgChild2DnaId: [""],
          orgChild3DnaId: [""],
          orgChild4DnaId: [""],
        };

        const success = await updateUserAttributes(keycloakUserId, clearedAttributes);

        if (success) {
          result.success++;
          result.details.push({
            profileId,
            status: "success",
          });
          console.log(`Cleared org DNA attributes for profile ${profileId} (${profileType})`);
        } else {
          result.failed++;
          result.details.push({
            profileId,
            status: "failed",
            error: "Failed to update Keycloak attributes",
          });
        }
      } catch (error: any) {
        result.failed++;
        result.details.push({
          profileId,
          status: "failed",
          error: error.message || "Unknown error",
        });
        console.error(`Error clearing org DNA attributes for profile ${profileId}:`, error);
      }
    }

    return result;
  }

  /**
   * Batch sync multiple users with unlimited count and parallel processing
   * Useful for initial sync or periodic updates
   *
   * Features:
   * - Resume from checkpoint after failures
   * - Automatic retry with exponential backoff
   * - Rate limiting to avoid overwhelming Keycloak
   * - Progress tracking and persistence
   *
   * @param options - Optional configuration
   * @returns Object with success count and details
   */
  async batchSyncUsers(options?: {
    limit?: number;
    concurrency?: number;
    resume?: boolean; // Resume from last checkpoint
    maxRetries?: number; // Retry attempts for failed operations
    rateLimit?: number; // Requests per second
    clearProgress?: boolean; // Start fresh, ignore existing progress
  }): Promise<{
    total: number;
    success: number;
    failed: number;
    details: any[];
    resumed?: boolean;
  }> {
    const limit = options?.limit;
    const concurrency = options?.concurrency ?? 5;
    const resume = options?.resume ?? false;
    const maxRetries = options?.maxRetries ?? 3;
    const rateLimit = options?.rateLimit ?? 10;
    const clearProgress = options?.clearProgress ?? false;

    const result = {
      total: 0,
      success: 0,
      failed: 0,
      details: [] as any[],
      resumed: false,
    };

    let progressState: SyncProgressState | null = null;
    let rateLimiter: RateLimiter | null = null;

    try {
      // Handle progress file based on options
      if (clearProgress) {
        SyncProgressManager.clear();
        console.log("[batchSyncUsers] Cleared existing progress, starting fresh");
      }

      // Load existing progress if resume is requested
      if (resume && !clearProgress) {
        progressState = SyncProgressManager.load();
        if (progressState) {
          result.resumed = true;
          console.log(
            `[batchSyncUsers] Resuming from checkpoint: ${progressState.lastSyncedIndex}/${progressState.totalProfiles}`,
          );
          SyncProgressManager.logProgress(progressState);
        } else {
          console.log("[batchSyncUsers] No existing progress found, starting fresh");
        }
      }

      // Build query for profiles with keycloak IDs (ข้าราชการ)
      const profileQuery = this.profileRepo
        .createQueryBuilder("p")
        .where("p.keycloak IS NOT NULL")
        .andWhere("p.keycloak != :empty", { empty: "" });

      // Build query for profileEmployees with keycloak IDs (ลูกจ้าง)
      const profileEmployeeQuery = this.profileEmployeeRepo
        .createQueryBuilder("pe")
        .where("pe.keycloak IS NOT NULL")
        .andWhere("pe.keycloak != :empty", { empty: "" });

      // Apply limit if specified (for testing purposes)
      if (limit !== undefined) {
        profileQuery.take(limit);
        profileEmployeeQuery.take(limit);
      }

      // Get profiles from both tables
      const [profiles, profileEmployees] = await Promise.all([
        profileQuery.getMany(),
        profileEmployeeQuery.getMany(),
      ]);

      const allProfiles = [
        ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })),
        ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })),
      ];

      result.total = allProfiles.length;

      // Initialize or resume progress state
      if (!progressState) {
        const profileIds = allProfiles.map((p) => p.profile.id);
        progressState = SyncProgressManager.initialize(profileIds);
        SyncProgressManager.save(progressState);
        console.log(`[batchSyncUsers] Starting sync of ${profileIds.length} profiles`);
      }

      // Initialize rate limiter if rate limiting is enabled
      if (rateLimit && rateLimit > 0) {
        rateLimiter = new RateLimiter(rateLimit);
        console.log(`[batchSyncUsers] Rate limiting enabled: ${rateLimit} requests/second`);
      }

      // Determine starting index based on progress
      let startIndex = progressState.lastSyncedIndex;

      // Process in parallel with concurrency limit
      const processedResults = await this.processInParallelWithProgress(
        allProfiles,
        concurrency,
        startIndex,
        async ({ profile, type }, index) => {
          // Apply rate limiting if enabled
          if (rateLimiter) {
            await rateLimiter.throttle();
          }

          const keycloakUserId = profile.keycloak;

          try {
            // Wrap sync operation with retry logic
            const success = await withRetry(
              async () => this.syncOnOrganizationChange(profile.id, type),
              maxRetries,
              1000, // Base delay: 1 second
            );

            if (success) {
              result.success++;
              return {
                profileId: profile.id,
                keycloakUserId,
                status: "success",
              };
            } else {
              result.failed++;
              // Add to failed profiles in progress state
              SyncProgressManager.addFailedProfile(
                progressState!,
                index,
                profile.id,
                "Sync returned false",
              );
              return {
                profileId: profile.id,
                keycloakUserId,
                status: "failed",
                error: "Sync returned false",
              };
            }
          } catch (error: any) {
            result.failed++;
            // Add to failed profiles in progress state
            SyncProgressManager.addFailedProfile(
              progressState!,
              index,
              profile.id,
              error.message || String(error),
            );
            return {
              profileId: profile.id,
              keycloakUserId,
              status: "error",
              error: error.message || String(error),
            };
          }
        },
        progressState,
        (updatedState) => {
          // Save progress after each batch
          SyncProgressManager.save(updatedState);
          // Log progress every 50 items
          if (
            updatedState.lastSyncedIndex % 50 === 0 ||
            updatedState.lastSyncedIndex === updatedState.totalProfiles
          ) {
            SyncProgressManager.logProgress(updatedState);
          }
        },
      );

      // Separate results from errors
      for (const resultItem of processedResults) {
        if ("error" in resultItem) {
          result.failed++;
          result.details.push({
            profileId: "unknown",
            keycloakUserId: "unknown",
            status: "error",
            error: JSON.stringify(resultItem.error),
          });
        } else {
          result.details.push(resultItem);
        }
      }

      // Clear progress on successful completion
      SyncProgressManager.clear();

      const elapsed = SyncProgressManager.formatElapsedTime(progressState.startTime);
      console.log(
        `[batchSyncUsers] Completed: total=${result.total}, success=${result.success}, failed=${result.failed}, elapsed=${elapsed}`,
      );

      // Log failed profiles summary
      if (progressState.failedProfiles.length > 0) {
        console.log(
          `[batchSyncUsers] Failed profiles (${progressState.failedProfiles.length}):`,
          progressState.failedProfiles.map((f) => `${f.profileId}(${f.error})`).join(", "),
        );
      }
    } catch (error) {
      console.error("[batchSyncUsers] Error in batch sync:", error);
      // Save progress before throwing
      if (progressState) {
        SyncProgressManager.save(progressState);
        console.log("[batchSyncUsers] Progress saved. Use resume=true to continue.");
      }
      throw error;
    }

    return result;
  }

  /**
   * Process items in parallel with concurrency limit and progress tracking
   * Extends processInParallel with progress state management
   */
  private async processInParallelWithProgress<T, R>(
    items: T[],
    concurrencyLimit: number,
    startIndex: number,
    processor: (item: T, index: number) => Promise<R>,
    progressState: SyncProgressState,
    onProgress?: (state: SyncProgressState) => void,
  ): Promise<Array<R | { error: any }>> {
    const results: Array<R | { error: any }> = [];

    // Start from the saved checkpoint index
    for (let i = startIndex; i < items.length; i += concurrencyLimit) {
      const batch = items.slice(i, i + concurrencyLimit);

      // Process batch in parallel with error handling
      const batchResults = await Promise.all(
        batch.map(async (item, batchIndex) => {
          const actualIndex = i + batchIndex;
          try {
            return await processor(item, actualIndex);
          } catch (error) {
            return { error };
          }
        }),
      );

      results.push(...batchResults);

      // Update progress state
      progressState.lastSyncedIndex = Math.min(i + concurrencyLimit, items.length);

      // Call progress callback
      if (onProgress) {
        onProgress(progressState);
      }
    }

    return results;
  }

  /**
   * Get current Keycloak attributes for a user
   *
   * @param keycloakUserId - Keycloak user ID
   * @returns Current attributes from Keycloak
   */
  async getCurrentKeycloakAttributes(
    keycloakUserId: string,
  ): Promise<UserProfileAttributes | null> {
    try {
      const user = await getUser(keycloakUserId);

      if (!user || !user.attributes) {
        return null;
      }

      return {
        profileId: user.attributes.profileId?.[0] || "",
        orgRootDnaId: user.attributes.orgRootDnaId?.[0] || "",
        orgChild1DnaId: user.attributes.orgChild1DnaId?.[0] || "",
        orgChild2DnaId: user.attributes.orgChild2DnaId?.[0] || "",
        orgChild3DnaId: user.attributes.orgChild3DnaId?.[0] || "",
        orgChild4DnaId: user.attributes.orgChild4DnaId?.[0] || "",
        empType: user.attributes.empType?.[0] || "",
        prefix: user.attributes.prefix?.[0] || "",
      };
    } catch (error) {
      console.error(`Error getting Keycloak attributes for user ${keycloakUserId}:`, error);
      return null;
    }
  }

  /**
   * Ensure Keycloak user exists for a profile
   * Creates user if keycloak field is empty OR if stored keycloak ID doesn't exist in Keycloak
   *
   * @param profileId - Profile ID
   * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE'
   * @returns Object with status and details
   */
  async ensureKeycloakUser(
    profileId: string,
    profileType: "PROFILE" | "PROFILE_EMPLOYEE",
  ): Promise<{
    success: boolean;
    action: "created" | "verified" | "skipped" | "error";
    keycloakUserId?: string;
    error?: string;
  }> {
    try {
      // Get profile from database
      let profile: Profile | ProfileEmployee | null = null;

      if (profileType === "PROFILE") {
        profile = await this.profileRepo.findOne({ where: { id: profileId } });
      } else {
        profile = await this.profileEmployeeRepo.findOne({ where: { id: profileId } });
      }

      if (!profile) {
        return {
          success: false,
          action: "error",
          error: `Profile ${profileId} not found in database`,
        };
      }

      // Check if citizenId exists
      if (!profile.citizenId) {
        return {
          success: false,
          action: "skipped",
          error: "No citizenId found",
        };
      }

      // Case 1: keycloak field is empty -> create new user
      if (!profile.keycloak || profile.keycloak.trim() === "") {
        const result = await this.createKeycloakUserFromProfile(profile, profileType);
        return result;
      }

      // Case 2: keycloak field is not empty -> verify user exists in Keycloak
      const existingUser = await getUser(profile.keycloak);

      if (!existingUser) {
        // User doesn't exist in Keycloak, create new one
        console.log(
          `Keycloak user ${profile.keycloak} not found in Keycloak, creating new user for profile ${profileId}`,
        );
        const result = await this.createKeycloakUserFromProfile(profile, profileType);
        return result;
      }

      // User exists in Keycloak, verified
      return {
        success: true,
        action: "verified",
        keycloakUserId: profile.keycloak,
      };
    } catch (error: any) {
      console.error(`Error ensuring Keycloak user for profile ${profileId}:`, error);
      return {
        success: false,
        action: "error",
        error: error.message || "Unknown error",
      };
    }
  }

  /**
   * Create Keycloak user from profile data
   *
   * @param profile - Profile or ProfileEmployee entity
   * @param profileType - 'PROFILE' or 'PROFILE_EMPLOYEE'
   * @returns Object with status and details
   */
  private async createKeycloakUserFromProfile(
    profile: Profile | ProfileEmployee,
    profileType: "PROFILE" | "PROFILE_EMPLOYEE",
  ): Promise<{
    success: boolean;
    action: "created" | "verified" | "skipped" | "error";
    keycloakUserId?: string;
    error?: string;
  }> {
    try {
      // Check if user already exists by username (citizenId)
      const existingUserByUsername = await getUserByUsername(profile.citizenId);
      if (Array.isArray(existingUserByUsername) && existingUserByUsername.length > 0) {
        // User already exists with this username, update the keycloak field
        const existingUserId = existingUserByUsername[0].id;
        console.log(
          `User with citizenId ${profile.citizenId} already exists in Keycloak with ID ${existingUserId}`,
        );

        // Update the keycloak field in database
        if (profileType === "PROFILE") {
          await this.profileRepo.update(profile.id, { keycloak: existingUserId });
        } else {
          await this.profileEmployeeRepo.update(profile.id, { keycloak: existingUserId });
        }

        // Assign default USER role to existing user
        const userRole = await getRoles("USER");
        if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) {
          const roleAssigned = await addUserRoles(existingUserId, [
            { id: String(userRole.id), name: String(userRole.name) },
          ]);
          if (roleAssigned) {
            console.log(`Assigned USER role to existing user ${existingUserId}`);
          } else {
            console.warn(`Failed to assign USER role to existing user ${existingUserId}`);
          }
        } else {
          console.warn(`USER role not found in Keycloak`);
        }

        return {
          success: true,
          action: "verified",
          keycloakUserId: existingUserId,
        };
      }

      // Create new user in Keycloak
      const createResult = await createUserHaveProfile(
        profile.citizenId,
        "P@ssw0rd",
        profile.id,
        profile.prefix,
        {
          firstName: profile.firstName || "",
          lastName: profile.lastName || "",
          email: profile.email || undefined,
          enabled: true,
        },
      );

      if (!createResult || typeof createResult !== "string") {
        return {
          success: false,
          action: "error",
          error: "Failed to create user in Keycloak",
        };
      }

      const keycloakUserId = createResult;

      // Update the keycloak field in database
      if (profileType === "PROFILE") {
        await this.profileRepo.update(profile.id, { keycloak: keycloakUserId });
      } else {
        await this.profileEmployeeRepo.update(profile.id, { keycloak: keycloakUserId });
      }

      // Assign default USER role
      const userRole = await getRoles("USER");
      if (userRole && typeof userRole === "object" && "id" in userRole && "name" in userRole) {
        const roleAssigned = await addUserRoles(keycloakUserId, [
          { id: String(userRole.id), name: String(userRole.name) },
        ]);
        if (roleAssigned) {
          console.log(`Assigned USER role to user ${keycloakUserId}`);
        } else {
          console.warn(`Failed to assign USER role to user ${keycloakUserId}`);
        }
      } else {
        console.warn(`USER role not found in Keycloak`);
      }

      console.log(
        `Created Keycloak user for profile ${profile.id} (citizenId: ${profile.citizenId}) with ID ${keycloakUserId}`,
      );

      return {
        success: true,
        action: "created",
        keycloakUserId,
      };
    } catch (error: any) {
      console.error(`Error creating Keycloak user for profile ${profile.id}:`, error);
      return {
        success: false,
        action: "error",
        error: error.message || "Unknown error",
      };
    }
  }

  /**
   * Process items in parallel with concurrency limit
   */
  private async processInParallel<T, R>(
    items: T[],
    concurrencyLimit: number,
    processor: (item: T, index: number) => Promise<R>,
  ): Promise<Array<R | { error: any }>> {
    const results: Array<R | { error: any }> = [];

    // Process items in batches
    for (let i = 0; i < items.length; i += concurrencyLimit) {
      const batch = items.slice(i, i + concurrencyLimit);

      // Process batch in parallel with error handling
      const batchResults = await Promise.all(
        batch.map(async (item, batchIndex) => {
          try {
            return await processor(item, i + batchIndex);
          } catch (error) {
            return { error };
          }
        }),
      );

      results.push(...batchResults);

      // Log progress after each batch
      const completed = Math.min(i + concurrencyLimit, items.length);
      console.log(`Progress: ${completed}/${items.length}`);
    }

    return results;
  }

  /**
   * Batch ensure Keycloak users for all profiles
   * Processes all rows in Profile and ProfileEmployee tables
   *
   * @returns Object with total, success, failed counts and details
   */
  async batchEnsureKeycloakUsers(): Promise<{
    total: number;
    created: number;
    verified: number;
    skipped: number;
    failed: number;
    details: Array<{
      profileId: string;
      profileType: string;
      action: string;
      keycloakUserId?: string;
      error?: string;
    }>;
  }> {
    const result = {
      total: 0,
      created: 0,
      verified: 0,
      skipped: 0,
      failed: 0,
      details: [] as Array<{
        profileId: string;
        profileType: string;
        action: string;
        keycloakUserId?: string;
        error?: string;
      }>,
    };

    try {
      // Get all profiles from Profile table (ข้าราชการ)
      const profiles = await this.profileRepo.find({ where: { isLeave: false } }); // Only active profiles

      // Get all profiles from ProfileEmployee table (ลูกจ้าง)
      const profileEmployees = await this.profileEmployeeRepo.find({ where: { isLeave: false } }); // Only active profiles

      const allProfiles = [
        ...profiles.map((p) => ({ profile: p, type: "PROFILE" as const })),
        ...profileEmployees.map((p) => ({ profile: p, type: "PROFILE_EMPLOYEE" as const })),
      ];

      result.total = allProfiles.length;

      // Process in parallel with concurrency limit
      const CONCURRENCY_LIMIT = 5; // Adjust based on environment

      const processedResults = await this.processInParallel(
        allProfiles,
        CONCURRENCY_LIMIT,
        async ({ profile, type }) => {
          const ensureResult = await this.ensureKeycloakUser(profile.id, type);

          // Update counters
          switch (ensureResult.action) {
            case "created":
              result.created++;
              break;
            case "verified":
              result.verified++;
              break;
            case "skipped":
              result.skipped++;
              break;
            case "error":
              result.failed++;
              break;
          }

          return {
            profileId: profile.id,
            profileType: type,
            action: ensureResult.action,
            keycloakUserId: ensureResult.keycloakUserId,
            error: ensureResult.error,
          };
        },
      );

      // Separate results from errors
      for (const resultItem of processedResults) {
        if ("error" in resultItem) {
          result.failed++;
          result.details.push({
            profileId: "unknown",
            profileType: "unknown",
            action: "error",
            error: JSON.stringify(resultItem.error),
          });
        } else {
          result.details.push(resultItem);
        }
      }

      console.log(
        `Batch ensure Keycloak users completed: total=${result.total}, created=${result.created}, verified=${result.verified}, skipped=${result.skipped}, failed=${result.failed}`,
      );
    } catch (error) {
      console.error("Error in batch ensure Keycloak users:", error);
    }

    return result;
  }

  /**
   * Clear orphaned Keycloak users
   * Deletes users in Keycloak that are not referenced in Profile or ProfileEmployee tables
   *
   * @param skipUsernames - Array of usernames to skip (e.g., ['super_admin'])
   * @returns Object with counts and details
   */
  async clearOrphanedKeycloakUsers(skipUsernames: string[] = []): Promise<{
    totalInKeycloak: number;
    totalInDatabase: number;
    orphanedCount: number;
    deleted: number;
    skipped: number;
    failed: number;
    details: Array<{
      keycloakUserId: string;
      username: string;
      action: "deleted" | "skipped" | "error";
      error?: string;
    }>;
  }> {
    const result = {
      totalInKeycloak: 0,
      totalInDatabase: 0,
      orphanedCount: 0,
      deleted: 0,
      skipped: 0,
      failed: 0,
      details: [] as Array<{
        keycloakUserId: string;
        username: string;
        action: "deleted" | "skipped" | "error";
        error?: string;
      }>,
    };

    try {
      // Get all keycloak IDs from database (Profile + ProfileEmployee)
      const profiles = await this.profileRepo
        .createQueryBuilder("p")
        .where("p.keycloak IS NOT NULL")
        .andWhere("p.keycloak != :empty", { empty: "" })
        .getMany();

      const profileEmployees = await this.profileEmployeeRepo
        .createQueryBuilder("pe")
        .where("pe.keycloak IS NOT NULL")
        .andWhere("pe.keycloak != :empty", { empty: "" })
        .getMany();

      // Create a Set of all keycloak IDs in database for O(1) lookup
      const databaseKeycloakIds = new Set<string>();
      for (const p of profiles) {
        if (p.keycloak) databaseKeycloakIds.add(p.keycloak);
      }
      for (const pe of profileEmployees) {
        if (pe.keycloak) databaseKeycloakIds.add(pe.keycloak);
      }

      result.totalInDatabase = databaseKeycloakIds.size;

      // Get all users from Keycloak with pagination to avoid response size limits
      const keycloakUsers = await getAllUsersPaginated();
      if (!keycloakUsers || typeof keycloakUsers !== "object") {
        throw new Error("Failed to get users from Keycloak");
      }

      result.totalInKeycloak = keycloakUsers.length;

      // Find orphaned users (in Keycloak but not in database)
      const orphanedUsers = keycloakUsers.filter((user: any) => !databaseKeycloakIds.has(user.id));
      result.orphanedCount = orphanedUsers.length;

      // Delete orphaned users (skip protected ones)
      for (const user of orphanedUsers) {
        const username = user.username;
        const userId = user.id;

        // Check if user should be skipped
        if (skipUsernames.includes(username)) {
          result.skipped++;
          result.details.push({
            keycloakUserId: userId,
            username,
            action: "skipped",
          });
          continue;
        }

        // Delete user from Keycloak
        try {
          const deleteSuccess = await deleteUser(userId);
          if (deleteSuccess) {
            result.deleted++;
            result.details.push({
              keycloakUserId: userId,
              username,
              action: "deleted",
            });
          } else {
            result.failed++;
            result.details.push({
              keycloakUserId: userId,
              username,
              action: "error",
              error: "Failed to delete user from Keycloak",
            });
          }
        } catch (error: any) {
          result.failed++;
          result.details.push({
            keycloakUserId: userId,
            username,
            action: "error",
            error: error.message || "Unknown error",
          });
        }
      }

      console.log(
        `Clear orphaned Keycloak users completed: totalInKeycloak=${result.totalInKeycloak}, totalInDatabase=${result.totalInDatabase}, orphaned=${result.orphanedCount}, deleted=${result.deleted}, skipped=${result.skipped}, failed=${result.failed}`,
      );
    } catch (error) {
      console.error("Error in clear orphaned Keycloak users:", error);
      throw error;
    }

    return result;
  }
}
