import {
  repoStatusEnum,
  type RepositoryVisibility,
  type RepoStatus,
} from "@/types/Repository";
import { membershipRoleEnum } from "@/types/organizations";
import { Octokit } from "@octokit/rest";
import type { Config } from "@/types/config";
import type { Organization, Repository } from "./db/schema";
import { httpPost, httpGet, httpDelete, httpPut, httpPatch } from "./http-client";
import { createMirrorJob } from "./helpers";
import { db, organizations, repositories } from "./db";
import { eq, and, ne } from "drizzle-orm";
import { decryptConfigTokens } from "./utils/config-encryption";
import { formatDateShort } from "./utils";
import { buildGithubSourceAuthPayload } from "./utils/mirror-source-auth";
import {
  parseRepositoryMetadataState,
  serializeRepositoryMetadataState,
} from "./metadata-state";

/**
 * Helper function to get organization configuration including destination override
 */
export const getOrganizationConfig = async ({
  orgName,
  userId,
}: {
  orgName: string;
  userId: string;
}): Promise<Organization | null> => {
  try {
    const result = await db
      .select()
      .from(organizations)
      .where(and(eq(organizations.name, orgName), eq(organizations.userId, userId)))
      .limit(1);

    if (!result[0]) {
      return null;
    }

    // Validate and cast the membershipRole to ensure type safety
    const rawOrg = result[0];
    const membershipRole = membershipRoleEnum.parse(rawOrg.membershipRole);
    const status = repoStatusEnum.parse(rawOrg.status);

    return {
      ...rawOrg,
      membershipRole,
      status,
    } as Organization;
  } catch (error) {
    console.error(`Error fetching organization config for ${orgName}:`, error);
    return null;
  }
};

/**
 * Enhanced async version of getGiteaRepoOwner that supports organization overrides
 */
export const getGiteaRepoOwnerAsync = async ({
  config,
  repository,
}: {
  config: Partial<Config>;
  repository: Repository;
}): Promise<string> => {
  if (!config.githubConfig || !config.giteaConfig) {
    throw new Error("GitHub or Gitea config is required.");
  }

  if (!config.giteaConfig.defaultOwner) {
    throw new Error("Gitea username is required.");
  }

  if (!config.userId) {
    throw new Error("User ID is required for organization overrides.");
  }

  // Check if repository is starred
  if (repository.isStarred) {
    const starredReposMode = config.githubConfig.starredReposMode || "dedicated-org";
    if (starredReposMode === "preserve-owner") {
      return repository.organization || repository.owner;
    }
    return config.githubConfig.starredReposOrg || "starred";
  }

  // Check for repository-specific override (second highest priority)
  if (repository.destinationOrg) {
    console.log(`Using repository override: ${repository.fullName} -> ${repository.destinationOrg}`);
    return repository.destinationOrg;
  }

  // Check for organization-specific override
  if (repository.organization) {
    const orgConfig = await getOrganizationConfig({
      orgName: repository.organization,
      userId: config.userId,
    });

    if (orgConfig?.destinationOrg) {
      console.log(`Using organization override: ${repository.organization} -> ${orgConfig.destinationOrg}`);
      return orgConfig.destinationOrg;
    }
  }

  // For personal repos (not organization repos), fall back to the default strategy

  // Fall back to existing strategy logic
  return getGiteaRepoOwner({ config, repository });
};

export const getGiteaRepoOwner = ({
  config,
  repository,
}: {
  config: Partial<Config>;
  repository: Repository;
}): string => {
  if (!config.githubConfig || !config.giteaConfig) {
    throw new Error("GitHub or Gitea config is required.");
  }

  if (!config.giteaConfig.defaultOwner) {
    throw new Error("Gitea username is required.");
  }

  // Check if repository is starred
  if (repository.isStarred) {
    const starredReposMode = config.githubConfig.starredReposMode || "dedicated-org";
    if (starredReposMode === "preserve-owner") {
      return repository.organization || repository.owner;
    }
    return config.githubConfig.starredReposOrg || "starred";
  }

  // Get the mirror strategy - use preserveOrgStructure for backward compatibility
  const mirrorStrategy = config.githubConfig.mirrorStrategy || 
    (config.giteaConfig.preserveOrgStructure ? "preserve" : "flat-user");
  const configuredGitHubOwner =
    (
      config.githubConfig.owner ||
      (config.githubConfig as typeof config.githubConfig & { username?: string }).username ||
      ""
    )
      .trim()
      .toLowerCase();

  switch (mirrorStrategy) {
    case "preserve":
      // Keep GitHub structure:
      // - org repos stay in the same org
      // - personal repos owned by other users keep their source owner namespace
      // - personal repos owned by the configured account go to defaultOwner
      if (repository.organization) {
        return repository.organization;
      }

      const normalizedRepoOwner = repository.owner.trim().toLowerCase();
      if (
        normalizedRepoOwner &&
        configuredGitHubOwner &&
        normalizedRepoOwner !== configuredGitHubOwner
      ) {
        return repository.owner;
      }

      // Personal repos from the configured GitHub account go to the configured default owner
      return config.giteaConfig.defaultOwner;

    case "single-org":
      // All non-starred repos go to the destination organization
      if (config.giteaConfig.organization) {
        return config.giteaConfig.organization;
      }
      // Fallback to username if no organization specified
      return config.giteaConfig.defaultOwner;

    case "flat-user":
      // All non-starred repos go under the user account
      return config.giteaConfig.defaultOwner;

    case "mixed":
      // Mixed mode: personal repos to single org, organization repos preserve structure
      if (repository.organization) {
        // Organization repos preserve their structure
        return repository.organization;
      }
      // Personal repos go to configured organization (same as single-org)
      if (config.giteaConfig.organization) {
        return config.giteaConfig.organization;
      }
      // Fallback to username if no organization specified
      return config.giteaConfig.defaultOwner;

    default:
      // Default fallback
      return config.giteaConfig.defaultOwner;
  }
};

export const isRepoPresentInGitea = async ({
  config,
  owner,
  repoName,
}: {
  config: Partial<Config>;
  owner: string;
  repoName: string;
}): Promise<boolean> => {
  try {
    if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
      throw new Error("Gitea config is required.");
    }

    // Decrypt config tokens for API usage
    const decryptedConfig = decryptConfigTokens(config as Config);

    // Check if the repository exists at the specified owner location
    const response = await fetch(
      `${config.giteaConfig.url}/api/v1/repos/${owner}/${repoName}`,
      {
        headers: {
          Authorization: `token ${decryptedConfig.giteaConfig.token}`,
        },
      }
    );

    return response.ok;
  } catch (error) {
    console.error("Error checking if repo exists in Gitea:", error);
    return false;
  }
};

/**
 * Check if a repository is currently being mirrored (in-progress state in database)
 * This prevents race conditions where multiple concurrent operations try to mirror the same repo
 */
export const isRepoCurrentlyMirroring = async ({
  config,
  repoName,
  expectedLocation,
}: {
  config: Partial<Config>;
  repoName: string;
  expectedLocation?: string; // Format: "owner/repo"
}): Promise<boolean> => {
  try {
    if (!config.userId) {
      return false;
    }

    const { or } = await import("drizzle-orm");

    // Check database for any repository with "mirroring" or "syncing" status
    const inProgressRepos = await db
      .select()
      .from(repositories)
      .where(
        and(
          eq(repositories.userId, config.userId),
          eq(repositories.name, repoName),
          // Check for in-progress statuses
          or(
            eq(repositories.status, "mirroring"),
            eq(repositories.status, "syncing")
          )
        )
      );

    if (inProgressRepos.length > 0) {
      // Check if any of the in-progress repos are stale (stuck for > 2 hours)
      const TWO_HOURS_MS = 2 * 60 * 60 * 1000;
      const now = new Date().getTime();

      const activeRepos = inProgressRepos.filter((repo) => {
        if (!repo.updatedAt) return true; // No timestamp, assume active
        const updatedTime = new Date(repo.updatedAt).getTime();
        const isStale = (now - updatedTime) > TWO_HOURS_MS;

        if (isStale) {
          console.warn(
            `[Idempotency] Repository ${repo.name} has been in "${repo.status}" status for over 2 hours. ` +
            `Considering it stale and allowing retry.`
          );
        }

        return !isStale;
      });

      if (activeRepos.length === 0) {
        console.log(
          `[Idempotency] All in-progress operations for ${repoName} are stale (>2h). Allowing retry.`
        );
        return false;
      }

      // If we have an expected location, verify it matches
      if (expectedLocation) {
        const matchingRepo = activeRepos.find(
          (repo) => repo.mirroredLocation === expectedLocation
        );
        if (matchingRepo) {
          console.log(
            `[Idempotency] Repository ${repoName} is already being mirrored at ${expectedLocation}`
          );
          return true;
        }
      } else {
        console.log(
          `[Idempotency] Repository ${repoName} is already being mirrored (${activeRepos.length} in-progress operations found)`
        );
        return true;
      }
    }

    return false;
  } catch (error) {
    console.error("Error checking if repo is currently mirroring:", error);
    console.error("Error details:", error);
    return false;
  }
};

/**
 * Helper function to check if a repository exists in Gitea.
 * First checks the recorded mirroredLocation, then falls back to the expected location.
 */
export const checkRepoLocation = async ({
  config,
  repository,
  expectedOwner,
}: {
  config: Partial<Config>;
  repository: Repository;
  expectedOwner: string;
}): Promise<{ present: boolean; actualOwner: string }> => {
  // First check if we have a recorded mirroredLocation and if the repo exists there
  if (
    repository.mirroredLocation &&
    repository.mirroredLocation.trim() !== ""
  ) {
    const [mirroredOwner] = repository.mirroredLocation.split("/");
    if (mirroredOwner) {
      const mirroredPresent = await isRepoPresentInGitea({
        config,
        owner: mirroredOwner,
        repoName: repository.name,
      });

      if (mirroredPresent) {
        console.log(
          `Repository found at recorded mirrored location: ${repository.mirroredLocation}`
        );
        return { present: true, actualOwner: mirroredOwner };
      }
    }
  }

  // If not found at the recorded location, check the expected location
  const present = await isRepoPresentInGitea({
    config,
    owner: expectedOwner,
    repoName: repository.name,
  });

  if (present) {
    return { present: true, actualOwner: expectedOwner };
  }

  // Repository not found at any location
  return { present: false, actualOwner: expectedOwner };
};

const sanitizeTopicForGitea = (topic: string): string =>
  topic
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-+/, "")
    .replace(/-+$/, "");

const normalizeTopicsForGitea = (
  topics: string[],
  topicPrefix?: string
): string[] => {
  const normalizedPrefix = topicPrefix ? sanitizeTopicForGitea(topicPrefix) : "";
  const transformedTopics = topics
    .map((topic) => sanitizeTopicForGitea(topic))
    .filter((topic) => topic.length > 0)
    .map((topic) => (normalizedPrefix ? `${normalizedPrefix}-${topic}` : topic));

  return [...new Set(transformedTopics)];
};

const getSourceRepositoryCoordinates = (repository: Repository) => {
  const delimiterIndex = repository.fullName.indexOf("/");
  if (
    delimiterIndex > 0 &&
    delimiterIndex < repository.fullName.length - 1
  ) {
    return {
      owner: repository.fullName.slice(0, delimiterIndex),
      repo: repository.fullName.slice(delimiterIndex + 1),
    };
  }

  return {
    owner: repository.owner,
    repo: repository.name,
  };
};

const fetchGitHubTopics = async ({
  octokit,
  repository,
}: {
  octokit: Octokit;
  repository: Repository;
}): Promise<string[] | null> => {
  const { owner, repo } = getSourceRepositoryCoordinates(repository);

  try {
    const response = await octokit.request("GET /repos/{owner}/{repo}/topics", {
      owner,
      repo,
      headers: {
        Accept: "application/vnd.github+json",
      },
    });

    const names = (response.data as { names?: unknown }).names;
    if (!Array.isArray(names)) {
      console.warn(
        `[Metadata] Unexpected topics payload for ${repository.fullName}; skipping topic sync.`
      );
      return null;
    }

    return names.filter((topic): topic is string => typeof topic === "string");
  } catch (error) {
    console.warn(
      `[Metadata] Failed to fetch topics from GitHub for ${repository.fullName}: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
    return null;
  }
};

const syncRepositoryMetadataToGitea = async ({
  config,
  octokit,
  repository,
  giteaOwner,
  giteaRepoName,
  giteaToken,
}: {
  config: Partial<Config>;
  octokit: Octokit;
  repository: Repository;
  giteaOwner: string;
  giteaRepoName: string;
  giteaToken: string;
}): Promise<void> => {
  const giteaBaseUrl = config.giteaConfig?.url;
  if (!giteaBaseUrl) {
    return;
  }

  const repoApiUrl = `${giteaBaseUrl}/api/v1/repos/${giteaOwner}/${giteaRepoName}`;
  const authHeaders = {
    Authorization: `token ${giteaToken}`,
  };
  const description = repository.description?.trim() || "";

  try {
    await httpPatch(
      repoApiUrl,
      { description },
      authHeaders
    );
    console.log(
      `[Metadata] Synced description for ${repository.fullName} to ${giteaOwner}/${giteaRepoName}`
    );
  } catch (error) {
    console.warn(
      `[Metadata] Failed to sync description for ${repository.fullName} to ${giteaOwner}/${giteaRepoName}: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }

  if (config.giteaConfig?.addTopics === false) {
    return;
  }

  const sourceTopics = await fetchGitHubTopics({ octokit, repository });
  if (sourceTopics === null) {
    console.warn(
      `[Metadata] Skipping topic sync for ${repository.fullName} because GitHub topics could not be fetched.`
    );
    return;
  }

  const topics = normalizeTopicsForGitea(
    sourceTopics,
    config.giteaConfig?.topicPrefix
  );

  try {
    await httpPut(
      `${repoApiUrl}/topics`,
      { topics },
      authHeaders
    );
    console.log(
      `[Metadata] Synced ${topics.length} topic(s) for ${repository.fullName} to ${giteaOwner}/${giteaRepoName}`
    );
  } catch (error) {
    console.warn(
      `[Metadata] Failed to sync topics for ${repository.fullName} to ${giteaOwner}/${giteaRepoName}: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }
};

export const mirrorGithubRepoToGitea = async ({
  octokit,
  repository,
  config,
}: {
  octokit: Octokit;
  repository: Repository;
  config: Partial<Config>;
}): Promise<any> => {
  try {
    if (!config.userId || !config.githubConfig || !config.giteaConfig) {
      throw new Error("github config and gitea config are required.");
    }

    if (!config.giteaConfig.defaultOwner) {
      throw new Error("Gitea username is required.");
    }

    // Decrypt config tokens for API usage
    const decryptedConfig = decryptConfigTokens(config as Config);

    // Get the correct owner based on the strategy (with organization overrides)
    let repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
    const mirrorStrategy = config.githubConfig.mirrorStrategy ||
      (config.giteaConfig.preserveOrgStructure ? "preserve" : "flat-user");
    const configuredGitHubOwner = (
      config.githubConfig.owner ||
      (config.githubConfig as typeof config.githubConfig & { username?: string }).username ||
      ""
    )
      .trim()
      .toLowerCase();
    const normalizedRepoOwner = repository.owner.trim().toLowerCase();
    const isExternalPersonalRepoInPreserveMode =
      mirrorStrategy === "preserve" &&
      !repository.organization &&
      !repository.isStarred &&
      normalizedRepoOwner !== "" &&
      configuredGitHubOwner !== "" &&
      normalizedRepoOwner !== configuredGitHubOwner;

    // Determine the actual repository name to use (handle duplicates for starred repos)
    let targetRepoName = repository.name;

    if (
      repository.isStarred &&
      config.githubConfig &&
      (config.githubConfig.starredReposMode || "dedicated-org") === "dedicated-org"
    ) {
      // Extract GitHub owner from full_name (format: owner/repo)
      const githubOwner = repository.fullName.split('/')[0];

      targetRepoName = await generateUniqueRepoName({
        config,
        orgName: repoOwner,
        baseName: repository.name,
        githubOwner,
        fullName: repository.fullName,
        strategy: config.githubConfig.starredDuplicateStrategy,
      });

      if (targetRepoName !== repository.name) {
        console.log(
          `Starred repo ${repository.fullName} will be mirrored as ${repoOwner}/${targetRepoName} to avoid naming conflict`
        );
      }
    }

    // IDEMPOTENCY CHECK: Check if this repo is already being mirrored
    const expectedLocation = `${repoOwner}/${targetRepoName}`;
    const isCurrentlyMirroring = await isRepoCurrentlyMirroring({
      config,
      repoName: targetRepoName,
      expectedLocation,
    });

    if (isCurrentlyMirroring) {
      console.log(
        `[Idempotency] Skipping ${repository.fullName} - already being mirrored to ${expectedLocation}`
      );

      // Don't throw an error, just return to allow other repos to continue
      return;
    }

    const isExisting = await isRepoPresentInGitea({
      config,
      owner: repoOwner,
      repoName: targetRepoName,
    });

    if (isExisting) {
      const { getGiteaRepoInfo, handleExistingNonMirrorRepo } = await import("./gitea-enhanced");
      const existingRepoInfo = await getGiteaRepoInfo({
        config,
        owner: repoOwner,
        repoName: targetRepoName,
      });

      if (existingRepoInfo && !existingRepoInfo.mirror) {
        console.log(`Repository ${targetRepoName} exists but is not a mirror. Handling...`);
        await handleExistingNonMirrorRepo({
          config,
          repository,
          repoInfo: existingRepoInfo,
          strategy: "delete", // Can be configured: "skip", "delete", or "rename"
        });
      } else if (existingRepoInfo?.mirror) {
        console.log(
          `Repository ${targetRepoName} already exists in Gitea under ${repoOwner}. Updating database status.`
        );

        await syncRepositoryMetadataToGitea({
          config,
          octokit,
          repository,
          giteaOwner: repoOwner,
          giteaRepoName: targetRepoName,
          giteaToken: decryptedConfig.giteaConfig.token,
        });

        // Update database to reflect that the repository is already mirrored
        await db
          .update(repositories)
          .set({
            status: repoStatusEnum.parse("mirrored"),
            updatedAt: new Date(),
            lastMirrored: new Date(),
            errorMessage: null,
            mirroredLocation: `${repoOwner}/${targetRepoName}`,
          })
          .where(eq(repositories.id, repository.id!));

        // Append log for "mirrored" status
        await createMirrorJob({
          userId: config.userId,
          repositoryId: repository.id,
          repositoryName: repository.name,
          message: `Repository ${repository.name} already exists in Gitea`,
          details: `Repository ${repository.name} was found to already exist in Gitea under ${repoOwner} and database status was updated.`,
          status: "mirrored",
        });

        console.log(
          `Repository ${repository.name} database status updated to mirrored`
        );
        return;
      } else {
        console.warn(
          `[Mirror] Repository ${repoOwner}/${targetRepoName} exists but mirror status could not be verified. Continuing with mirror creation flow.`
        );
      }
    }

    console.log(`Mirroring repository ${repository.name}`);

    // DOUBLE-CHECK: Final idempotency check right before updating status
    // This catches race conditions in the small window between first check and status update
    const finalCheck = await isRepoCurrentlyMirroring({
      config,
      repoName: targetRepoName,
      expectedLocation,
    });

    if (finalCheck) {
      console.log(
        `[Idempotency] Race condition detected - ${repository.fullName} is now being mirrored by another process. Skipping.`
      );
      return;
    }

    // Mark repos as "mirroring" in DB
    // CRITICAL: Set mirroredLocation NOW (not after success) so idempotency checks work
    // This becomes the "target location" - where we intend to mirror to
    // Without this, the idempotency check can't detect concurrent operations on first mirror
    await db
      .update(repositories)
      .set({
        status: repoStatusEnum.parse("mirroring"),
        mirroredLocation: expectedLocation,
        updatedAt: new Date(),
      })
      .where(eq(repositories.id, repository.id!));

    // Append log for "mirroring" status
    await createMirrorJob({
      userId: config.userId,
      repositoryId: repository.id,
      repositoryName: repository.name,
      message: `Started mirroring repository: ${repository.name}`,
      details: `Repository ${repository.name} is now in the mirroring state.`,
      status: "mirroring",
    });

    // Use clean clone URL without embedded credentials (Forgejo 12+ security requirement)
    const cloneAddress = repository.cloneUrl;

    const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;

    // Handle organization creation if needed for single-org, preserve strategies, or starred repos
    if (repoOwner !== config.giteaConfig.defaultOwner) {
      // Need to create the organization if it doesn't exist
      try {
        await getOrCreateGiteaOrg({
          orgName: repoOwner,
          config,
        });
      } catch (orgError) {
        console.error(`Failed to create/access organization ${repoOwner}: ${orgError instanceof Error ? orgError.message : String(orgError)}`);

        // Check if we should fallback to user account
        if (orgError instanceof Error &&
            (orgError.message.includes('Permission denied') ||
             orgError.message.includes('Authentication failed') ||
             orgError.message.includes('does not have permission'))) {
          if (isExternalPersonalRepoInPreserveMode) {
            throw new Error(
              `Cannot create/access namespace "${repoOwner}" for ${repository.fullName}. ` +
              `Refusing fallback to "${config.giteaConfig.defaultOwner}" in preserve mode to avoid cross-owner overwrite.`
            );
          }

          console.warn(`[Fallback] Organization creation/access failed. Attempting to mirror to user account instead.`);

          // Update the repository owner to use the user account
          repoOwner = config.giteaConfig.defaultOwner;

          // Log this fallback in the database
          await db
            .update(repositories)
            .set({
              errorMessage: `Organization creation failed, using user account. ${orgError.message}`,
              updatedAt: new Date(),
            })
            .where(eq(repositories.id, repository.id!));
        } else {
          // Re-throw if it's not a permission issue
          throw orgError;
        }
      }
    }

    // Check if repository already exists as a non-mirror
    const { getGiteaRepoInfo, handleExistingNonMirrorRepo } = await import("./gitea-enhanced");
    const existingRepo = await getGiteaRepoInfo({
      config,
      owner: repoOwner,
      repoName: targetRepoName,
    });

    if (existingRepo && !existingRepo.mirror) {
      console.log(`Repository ${targetRepoName} exists but is not a mirror. Handling...`);

      // Handle the existing non-mirror repository
      await handleExistingNonMirrorRepo({
        config,
        repository,
        repoInfo: existingRepo,
        strategy: "delete", // Can be configured: "skip", "delete", or "rename"
      });

      // After handling, proceed with mirror creation
      console.log(`Proceeding with mirror creation for ${targetRepoName}`);
    }

    // Prepare migration payload
    // For private repos, use separate auth fields instead of embedding credentials in URL
    // This is required for Forgejo 12+ which rejects URLs with embedded credentials
    // Skip wiki for starred repos if starredCodeOnly is enabled
    const shouldMirrorWiki = config.giteaConfig?.wiki &&
      !(repository.isStarred && config.githubConfig?.starredCodeOnly);

    const migratePayload: any = {
      clone_addr: cloneAddress,
      repo_name: targetRepoName,
      mirror: true,
      mirror_interval: config.giteaConfig?.mirrorInterval || "8h",
      wiki: shouldMirrorWiki || false,
      lfs: config.giteaConfig?.lfs || false,
      private: repository.isPrivate,
      repo_owner: repoOwner,
      description: repository.description?.trim() || "",
      service: "git",
    };

    // Add authentication for private repositories
    if (repository.isPrivate) {
      const githubOwner =
        (
          config.githubConfig as typeof config.githubConfig & {
            owner?: string;
          }
        ).owner || "";

      Object.assign(
        migratePayload,
        buildGithubSourceAuthPayload({
          token: decryptedConfig.githubConfig.token,
          githubOwner,
          githubUsername: config.githubConfig.username,
          repositoryOwner: repository.owner,
        })
      );
    }

    // Track whether the Gitea migrate call succeeded so the catch block
    // knows whether to clear mirroredLocation (only safe before migrate succeeds)
    let migrateSucceeded = false;

    const response = await httpPost(
      apiUrl,
      migratePayload,
      {
        Authorization: `token ${decryptedConfig.giteaConfig.token}`,
      }
    );

    migrateSucceeded = true;

    await syncRepositoryMetadataToGitea({
      config,
      octokit,
      repository,
      giteaOwner: repoOwner,
      giteaRepoName: targetRepoName,
      giteaToken: decryptedConfig.giteaConfig.token,
    });

    const metadataState = parseRepositoryMetadataState(repository.metadata);
    let metadataUpdated = false;
    const skipMetadataForStarred =
      repository.isStarred && config.githubConfig?.starredCodeOnly;

    // Mirror releases if enabled (always allowed to rerun for updates)
    const shouldMirrorReleases =
      !!config.giteaConfig?.mirrorReleases && !skipMetadataForStarred;

    console.log(
      `[Metadata] Release mirroring check: mirrorReleases=${config.giteaConfig?.mirrorReleases}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorReleases=${shouldMirrorReleases}`
    );

    if (shouldMirrorReleases) {
      try {
        await mirrorGitHubReleasesToGitea({
          config,
          octokit,
          repository,
          giteaOwner: repoOwner,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.releases = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored releases for ${repository.name}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror releases for ${repository.name}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other operations even if releases fail
      }
    }

    // Determine metadata operations to avoid duplicates
    const shouldMirrorIssuesThisRun =
      !!config.giteaConfig?.mirrorIssues &&
      !skipMetadataForStarred &&
      !metadataState.components.issues;

    console.log(
      `[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, alreadyMirrored=${metadataState.components.issues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorIssues=${shouldMirrorIssuesThisRun}`
    );

    if (shouldMirrorIssuesThisRun) {
      try {
        await mirrorGitRepoIssuesToGitea({
          config,
          octokit,
          repository,
          giteaOwner: repoOwner,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.issues = true;
        metadataState.components.labels = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored issues for ${repository.name}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror issues for ${repository.name}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other metadata operations even if issues fail
      }
    } else if (config.giteaConfig?.mirrorIssues && metadataState.components.issues) {
      console.log(
        `[Metadata] Issues already mirrored for ${repository.name}; skipping to avoid duplicates`
      );
    }

    const shouldMirrorPullRequests =
      !!config.giteaConfig?.mirrorPullRequests &&
      !skipMetadataForStarred &&
      !metadataState.components.pullRequests;

    console.log(
      `[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}, alreadyMirrored=${metadataState.components.pullRequests}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorPullRequests=${shouldMirrorPullRequests}`
    );

    if (shouldMirrorPullRequests) {
      try {
        await mirrorGitRepoPullRequestsToGitea({
          config,
          octokit,
          repository,
          giteaOwner: repoOwner,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.pullRequests = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored pull requests for ${repository.name}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror pull requests for ${repository.name}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other metadata operations even if PRs fail
      }
    } else if (
      config.giteaConfig?.mirrorPullRequests &&
      metadataState.components.pullRequests
    ) {
      console.log(
        `[Metadata] Pull requests already mirrored for ${repository.name}; skipping`
      );
    }

    const shouldMirrorLabels =
      !!config.giteaConfig?.mirrorLabels &&
      !skipMetadataForStarred &&
      !shouldMirrorIssuesThisRun &&
      !metadataState.components.labels;

    console.log(
      `[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, alreadyMirrored=${metadataState.components.labels}, issuesRunning=${shouldMirrorIssuesThisRun}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorLabels=${shouldMirrorLabels}`
    );

    if (shouldMirrorLabels) {
      try {
        await mirrorGitRepoLabelsToGitea({
          config,
          octokit,
          repository,
          giteaOwner: repoOwner,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.labels = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored labels for ${repository.name}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror labels for ${repository.name}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other metadata operations even if labels fail
      }
    } else if (config.giteaConfig?.mirrorLabels && metadataState.components.labels) {
      console.log(
        `[Metadata] Labels already mirrored for ${repository.name}; skipping`
      );
    }

    const shouldMirrorMilestones =
      !!config.giteaConfig?.mirrorMilestones &&
      !skipMetadataForStarred &&
      !metadataState.components.milestones;

    console.log(
      `[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}, alreadyMirrored=${metadataState.components.milestones}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorMilestones=${shouldMirrorMilestones}`
    );

    if (shouldMirrorMilestones) {
      try {
        await mirrorGitRepoMilestonesToGitea({
          config,
          octokit,
          repository,
          giteaOwner: repoOwner,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.milestones = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored milestones for ${repository.name}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror milestones for ${repository.name}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other metadata operations even if milestones fail
      }
    } else if (
      config.giteaConfig?.mirrorMilestones &&
      metadataState.components.milestones
    ) {
      console.log(
        `[Metadata] Milestones already mirrored for ${repository.name}; skipping`
      );
    }

    if (metadataUpdated) {
      metadataState.lastSyncedAt = new Date().toISOString();
    }

    console.log(`Repository ${repository.name} mirrored successfully as ${targetRepoName}`);

    // Mark repos as "mirrored" in DB
    await db
      .update(repositories)
      .set({
        status: repoStatusEnum.parse("mirrored"),
        updatedAt: new Date(),
        lastMirrored: new Date(),
        errorMessage: null,
        mirroredLocation: `${repoOwner}/${targetRepoName}`,
        metadata: metadataUpdated
          ? serializeRepositoryMetadataState(metadataState)
          : repository.metadata ?? null,
      })
      .where(eq(repositories.id, repository.id!));

    // Append log for "mirrored" status
    await createMirrorJob({
      userId: config.userId,
      repositoryId: repository.id,
      repositoryName: repository.name,
      message: `Successfully mirrored repository: ${repository.name}${targetRepoName !== repository.name ? ` as ${targetRepoName}` : ''}`,
      details: `Repository ${repository.fullName} was mirrored to Gitea at ${repoOwner}/${targetRepoName}.`,
      status: "mirrored",
    });

    return response.data;
  } catch (error) {
    console.error(
      `Error while mirroring repository ${repository.name}: ${
        error instanceof Error ? error.message : String(error)
      }`
    );

    // Mark repos as "failed" in DB. Only clear mirroredLocation if the Gitea
    // migrate call itself failed (repo doesn't exist in Gitea). If migrate
    // succeeded but metadata mirroring failed, preserve the location since
    // the repo physically exists and we need the location for recovery/retry.
    const failureUpdate: Record<string, any> = {
      status: repoStatusEnum.parse("failed"),
      updatedAt: new Date(),
      errorMessage: error instanceof Error ? error.message : "Unknown error",
    };
    if (!migrateSucceeded) {
      failureUpdate.mirroredLocation = "";
    }
    await db
      .update(repositories)
      .set(failureUpdate)
      .where(eq(repositories.id, repository.id!));

    // Append log for failure
    await createMirrorJob({
      userId: config.userId ?? "", // userId is going to be there anyways
      repositoryId: repository.id,
      repositoryName: repository.name,
      message: `Failed to mirror repository: ${repository.name}`,
      details: `Repository ${repository.name} failed to mirror. Error: ${
        error instanceof Error ? error.message : "Unknown error"
      }`,
      status: "failed",
    });
    if (error instanceof Error) {
      throw new Error(`Failed to mirror repository: ${error.message}`);
    }
    throw new Error("Failed to mirror repository: An unknown error occurred.");
  }
};

export async function getOrCreateGiteaOrg({
  orgName,
  orgId,
  config,
}: {
  orgId?: string; //db id
  orgName: string;
  config: Partial<Config>;
}): Promise<number> {
  // Import the enhanced version with retry logic
  const { getOrCreateGiteaOrgEnhanced } = await import("./gitea-enhanced");
  
  try {
    return await getOrCreateGiteaOrgEnhanced({
      orgName,
      orgId,
      config,
      maxRetries: 3,
      retryDelay: 100,
    });
  } catch (error) {
    // Re-throw with original function name for backward compatibility
    if (error instanceof Error) {
      throw new Error(`Error in getOrCreateGiteaOrg: ${error.message}`);
    }
    throw error;
  }
}

/**
 * Check if a candidate mirroredLocation is already claimed by another repository
 * in the local database. This prevents race conditions during concurrent batch
 * mirroring where two repos could both claim the same name before either
 * finishes creating in Gitea.
 */
async function isMirroredLocationClaimedInDb({
  userId,
  candidateLocation,
  excludeFullName,
}: {
  userId: string;
  candidateLocation: string;
  excludeFullName: string;
}): Promise<boolean> {
  try {
    const existing = await db
      .select({ id: repositories.id })
      .from(repositories)
      .where(
        and(
          eq(repositories.userId, userId),
          eq(repositories.mirroredLocation, candidateLocation),
          ne(repositories.fullName, excludeFullName)
        )
      )
      .limit(1);

    return existing.length > 0;
  } catch (error) {
    console.error(
      `Error checking DB for mirroredLocation "${candidateLocation}":`,
      error
    );
    // Fail-closed: assume claimed to be conservative and prevent collisions
    return true;
  }
}

/**
 * Generate a unique repository name for starred repos with duplicate names.
 * Checks both the Gitea instance (HTTP) and the local DB (mirroredLocation)
 * to reduce collisions during concurrent batch mirroring.
 *
 * NOTE: This function only checks availability — it does NOT claim the name.
 * The actual claim happens later when mirroredLocation is written at the
 * status="mirroring" DB update, which is protected by a unique partial index
 * on (userId, mirroredLocation) WHERE mirroredLocation != ''.
 */
async function generateUniqueRepoName({
  config,
  orgName,
  baseName,
  githubOwner,
  fullName,
  strategy,
}: {
  config: Partial<Config>;
  orgName: string;
  baseName: string;
  githubOwner: string;
  fullName: string;
  strategy?: string;
}): Promise<string> {
  if (!fullName?.includes("/")) {
    throw new Error(
      `Invalid fullName "${fullName}" for starred repo dedup — expected "owner/repo" format`
    );
  }

  const duplicateStrategy = strategy || "suffix";
  const userId = config.userId || "";

  // Helper: check both Gitea and local DB for a candidate name
  const isNameTaken = async (candidateName: string): Promise<boolean> => {
    const existsInGitea = await isRepoPresentInGitea({
      config,
      owner: orgName,
      repoName: candidateName,
    });
    if (existsInGitea) return true;

    // Also check local DB to catch concurrent batch operations
    // where another repo claimed this location but hasn't created it in Gitea yet
    if (userId) {
      const claimedInDb = await isMirroredLocationClaimedInDb({
        userId,
        candidateLocation: `${orgName}/${candidateName}`,
        excludeFullName: fullName,
      });
      if (claimedInDb) return true;
    }

    return false;
  };

  // First check if base name is available
  const baseExists = await isNameTaken(baseName);

  if (!baseExists) {
    return baseName;
  }

  // Generate name based on strategy
  let candidateName: string;
  let attempt = 0;
  const maxAttempts = 10;

  while (attempt < maxAttempts) {
    switch (duplicateStrategy) {
      case "prefix":
        // Prefix with owner: owner-reponame
        candidateName = attempt === 0
          ? `${githubOwner}-${baseName}`
          : `${githubOwner}-${baseName}-${attempt}`;
        break;

      case "owner-org":
        // This would require creating sub-organizations, not supported in this PR
        // Fall back to suffix strategy
      case "suffix":
      default:
        // Suffix with owner: reponame-owner
        candidateName = attempt === 0
          ? `${baseName}-${githubOwner}`
          : `${baseName}-${githubOwner}-${attempt}`;
        break;
    }

    const exists = await isNameTaken(candidateName);

    if (!exists) {
      console.log(`Found unique name for duplicate starred repo: ${candidateName}`);
      return candidateName;
    }

    attempt++;
  }

  // SECURITY FIX: Prevent infinite duplicate creation
  // Instead of falling back to timestamp (which creates infinite duplicates),
  // throw an error to prevent hundreds of duplicate repos
  console.error(`Failed to find unique name for ${baseName} after ${maxAttempts} attempts`);
  console.error(`Organization: ${orgName}, GitHub Owner: ${githubOwner}, Strategy: ${duplicateStrategy}`);
  throw new Error(
    `Unable to generate unique repository name for "${baseName}". ` +
    `All ${maxAttempts} naming attempts resulted in conflicts. ` +
    `Please manually resolve the naming conflict or adjust your duplicate strategy.`
  );
}

export async function mirrorGitHubRepoToGiteaOrg({
  octokit,
  config,
  repository,
  giteaOrgId,
  orgName,
}: {
  octokit: Octokit;
  config: Partial<Config>;
  repository: Repository;
  giteaOrgId: number;
  orgName: string;
}) {
  try {
    if (
      !config.giteaConfig?.url ||
      !config.giteaConfig?.token ||
      !config.userId
    ) {
      throw new Error("Gitea config is required.");
    }

    // Decrypt config tokens for API usage
    const decryptedConfig = decryptConfigTokens(config as Config);

    // Determine the actual repository name to use (handle duplicates for starred repos)
    let targetRepoName = repository.name;

    if (
      repository.isStarred &&
      config.githubConfig &&
      (config.githubConfig.starredReposMode || "dedicated-org") === "dedicated-org"
    ) {
      // Extract GitHub owner from full_name (format: owner/repo)
      const githubOwner = repository.fullName.split('/')[0];

      targetRepoName = await generateUniqueRepoName({
        config,
        orgName,
        baseName: repository.name,
        githubOwner,
        fullName: repository.fullName,
        strategy: config.githubConfig.starredDuplicateStrategy,
      });

      if (targetRepoName !== repository.name) {
        console.log(
          `Starred repo ${repository.fullName} will be mirrored as ${orgName}/${targetRepoName} to avoid naming conflict`
        );
      }
    }

    // IDEMPOTENCY CHECK: Check if this repo is already being mirrored
    const expectedLocation = `${orgName}/${targetRepoName}`;
    const isCurrentlyMirroring = await isRepoCurrentlyMirroring({
      config,
      repoName: targetRepoName,
      expectedLocation,
    });

    if (isCurrentlyMirroring) {
      console.log(
        `[Idempotency] Skipping ${repository.fullName} - already being mirrored to ${expectedLocation}`
      );

      // Don't throw an error, just return to allow other repos to continue
      return;
    }

    const isExisting = await isRepoPresentInGitea({
      config,
      owner: orgName,
      repoName: targetRepoName,
    });

    if (isExisting) {
      const { getGiteaRepoInfo, handleExistingNonMirrorRepo } = await import("./gitea-enhanced");
      const existingRepoInfo = await getGiteaRepoInfo({
        config,
        owner: orgName,
        repoName: targetRepoName,
      });

      if (existingRepoInfo && !existingRepoInfo.mirror) {
        console.log(`Repository ${targetRepoName} exists but is not a mirror. Handling...`);
        await handleExistingNonMirrorRepo({
          config,
          repository,
          repoInfo: existingRepoInfo,
          strategy: "delete", // Can be configured: "skip", "delete", or "rename"
        });
      } else if (existingRepoInfo?.mirror) {
        console.log(
          `Repository ${targetRepoName} already exists in Gitea organization ${orgName}. Updating database status.`
        );

        await syncRepositoryMetadataToGitea({
          config,
          octokit,
          repository,
          giteaOwner: orgName,
          giteaRepoName: targetRepoName,
          giteaToken: decryptedConfig.giteaConfig.token,
        });

        // Update database to reflect that the repository is already mirrored
        await db
          .update(repositories)
          .set({
            status: repoStatusEnum.parse("mirrored"),
            updatedAt: new Date(),
            lastMirrored: new Date(),
            errorMessage: null,
            mirroredLocation: `${orgName}/${targetRepoName}`,
          })
          .where(eq(repositories.id, repository.id!));

        // Create a mirror job log entry
        await createMirrorJob({
          userId: config.userId,
          repositoryId: repository.id,
          repositoryName: repository.name,
          message: `Repository ${targetRepoName} already exists in Gitea organization ${orgName}`,
          details: `Repository ${targetRepoName} was found to already exist in Gitea organization ${orgName} and database status was updated.`,
          status: "mirrored",
        });

        console.log(
          `Repository ${targetRepoName} database status updated to mirrored in organization ${orgName}`
        );
        return;
      } else {
        console.warn(
          `[Mirror] Repository ${orgName}/${targetRepoName} exists but mirror status could not be verified. Continuing with mirror creation flow.`
        );
      }
    }

    console.log(
      `Mirroring repository ${repository.fullName} to organization ${orgName} as ${targetRepoName}`
    );

    // Use clean clone URL without embedded credentials (Forgejo 12+ security requirement)
    const cloneAddress = repository.cloneUrl;

    // DOUBLE-CHECK: Final idempotency check right before updating status
    // This catches race conditions in the small window between first check and status update
    const finalCheck = await isRepoCurrentlyMirroring({
      config,
      repoName: targetRepoName,
      expectedLocation,
    });

    if (finalCheck) {
      console.log(
        `[Idempotency] Race condition detected - ${repository.fullName} is now being mirrored by another process. Skipping.`
      );
      return;
    }

    // Mark repos as "mirroring" in DB
    // CRITICAL: Set mirroredLocation NOW (not after success) so idempotency checks work
    // This becomes the "target location" - where we intend to mirror to
    // Without this, the idempotency check can't detect concurrent operations on first mirror
    await db
      .update(repositories)
      .set({
        status: repoStatusEnum.parse("mirroring"),
        mirroredLocation: expectedLocation,
        updatedAt: new Date(),
      })
      .where(eq(repositories.id, repository.id!));

    // Note: "mirroring" status events are handled by the concurrency system
    // to avoid duplicate events during batch operations

    const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;

    // Prepare migration payload
    // For private repos, use separate auth fields instead of embedding credentials in URL
    // This is required for Forgejo 12+ which rejects URLs with embedded credentials
    // Skip wiki for starred repos if starredCodeOnly is enabled
    const shouldMirrorWiki = config.giteaConfig?.wiki &&
      !(repository.isStarred && config.githubConfig?.starredCodeOnly);

    const migratePayload: any = {
      clone_addr: cloneAddress,
      uid: giteaOrgId,
      repo_name: targetRepoName,
      mirror: true,
      mirror_interval: config.giteaConfig?.mirrorInterval || "8h",
      wiki: shouldMirrorWiki || false,
      lfs: config.giteaConfig?.lfs || false,
      private: repository.isPrivate,
      description: repository.description?.trim() || "",
    };

    // Add authentication for private repositories
    if (repository.isPrivate) {
      const githubOwner =
        (
          config.githubConfig as typeof config.githubConfig & {
            owner?: string;
          }
        )?.owner || "";

      Object.assign(
        migratePayload,
        buildGithubSourceAuthPayload({
          token: decryptedConfig.githubConfig?.token,
          githubOwner,
          githubUsername: config.githubConfig?.username,
          repositoryOwner: repository.owner,
        })
      );
    }

    let migrateSucceeded = false;

    const migrateRes = await httpPost(
      apiUrl,
      migratePayload,
      {
        Authorization: `token ${decryptedConfig.giteaConfig.token}`,
      }
    );

    migrateSucceeded = true;

    await syncRepositoryMetadataToGitea({
      config,
      octokit,
      repository,
      giteaOwner: orgName,
      giteaRepoName: targetRepoName,
      giteaToken: decryptedConfig.giteaConfig.token,
    });

    const metadataState = parseRepositoryMetadataState(repository.metadata);
    let metadataUpdated = false;
    const skipMetadataForStarred =
      repository.isStarred && config.githubConfig?.starredCodeOnly;

    const shouldMirrorReleases =
      !!config.giteaConfig?.mirrorReleases && !skipMetadataForStarred;

    console.log(
      `[Metadata] Release mirroring check: mirrorReleases=${config.giteaConfig?.mirrorReleases}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorReleases=${shouldMirrorReleases}`
    );

    if (shouldMirrorReleases) {
      try {
        await mirrorGitHubReleasesToGitea({
          config,
          octokit,
          repository,
          giteaOwner: orgName,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.releases = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored releases for ${repository.name}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror releases for ${repository.name}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other operations even if releases fail
      }
    }

    const shouldMirrorIssuesThisRun =
      !!config.giteaConfig?.mirrorIssues &&
      !skipMetadataForStarred &&
      !metadataState.components.issues;

    console.log(
      `[Metadata] Issue mirroring check: mirrorIssues=${config.giteaConfig?.mirrorIssues}, alreadyMirrored=${metadataState.components.issues}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorIssues=${shouldMirrorIssuesThisRun}`
    );

    if (shouldMirrorIssuesThisRun) {
      try {
        await mirrorGitRepoIssuesToGitea({
          config,
          octokit,
          repository,
          giteaOwner: orgName,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.issues = true;
        metadataState.components.labels = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored issues for ${repository.name} to org ${orgName}/${targetRepoName}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror issues for ${repository.name} to org ${orgName}/${targetRepoName}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other metadata operations even if issues fail
      }
    } else if (
      config.giteaConfig?.mirrorIssues &&
      metadataState.components.issues
    ) {
      console.log(
        `[Metadata] Issues already mirrored for ${repository.name}; skipping`
      );
    }

    const shouldMirrorPullRequests =
      !!config.giteaConfig?.mirrorPullRequests &&
      !skipMetadataForStarred &&
      !metadataState.components.pullRequests;

    console.log(
      `[Metadata] Pull request mirroring check: mirrorPullRequests=${config.giteaConfig?.mirrorPullRequests}, alreadyMirrored=${metadataState.components.pullRequests}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorPullRequests=${shouldMirrorPullRequests}`
    );

    if (shouldMirrorPullRequests) {
      try {
        await mirrorGitRepoPullRequestsToGitea({
          config,
          octokit,
          repository,
          giteaOwner: orgName,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.pullRequests = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored pull requests for ${repository.name} to org ${orgName}/${targetRepoName}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror pull requests for ${repository.name} to org ${orgName}/${targetRepoName}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other metadata operations even if PRs fail
      }
    } else if (
      config.giteaConfig?.mirrorPullRequests &&
      metadataState.components.pullRequests
    ) {
      console.log(
        `[Metadata] Pull requests already mirrored for ${repository.name}; skipping`
      );
    }

    const shouldMirrorLabels =
      !!config.giteaConfig?.mirrorLabels &&
      !skipMetadataForStarred &&
      !shouldMirrorIssuesThisRun &&
      !metadataState.components.labels;

    console.log(
      `[Metadata] Label mirroring check: mirrorLabels=${config.giteaConfig?.mirrorLabels}, alreadyMirrored=${metadataState.components.labels}, issuesRunning=${shouldMirrorIssuesThisRun}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorLabels=${shouldMirrorLabels}`
    );

    if (shouldMirrorLabels) {
      try {
        await mirrorGitRepoLabelsToGitea({
          config,
          octokit,
          repository,
          giteaOwner: orgName,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.labels = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored labels for ${repository.name} to org ${orgName}/${targetRepoName}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror labels for ${repository.name} to org ${orgName}/${targetRepoName}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other metadata operations even if labels fail
      }
    } else if (
      config.giteaConfig?.mirrorLabels &&
      metadataState.components.labels
    ) {
      console.log(
        `[Metadata] Labels already mirrored for ${repository.name}; skipping`
      );
    }

    const shouldMirrorMilestones =
      !!config.giteaConfig?.mirrorMilestones &&
      !skipMetadataForStarred &&
      !metadataState.components.milestones;

    console.log(
      `[Metadata] Milestone mirroring check: mirrorMilestones=${config.giteaConfig?.mirrorMilestones}, alreadyMirrored=${metadataState.components.milestones}, isStarred=${repository.isStarred}, starredCodeOnly=${config.githubConfig?.starredCodeOnly}, shouldMirrorMilestones=${shouldMirrorMilestones}`
    );

    if (shouldMirrorMilestones) {
      try {
        await mirrorGitRepoMilestonesToGitea({
          config,
          octokit,
          repository,
          giteaOwner: orgName,
          giteaRepoName: targetRepoName,
        });
        metadataState.components.milestones = true;
        metadataUpdated = true;
        console.log(
          `[Metadata] Successfully mirrored milestones for ${repository.name} to org ${orgName}/${targetRepoName}`
        );
      } catch (error) {
        console.error(
          `[Metadata] Failed to mirror milestones for ${repository.name} to org ${orgName}/${targetRepoName}: ${
            error instanceof Error ? error.message : String(error)
          }`
        );
        // Continue with other metadata operations even if milestones fail
      }
    } else if (
      config.giteaConfig?.mirrorMilestones &&
      metadataState.components.milestones
    ) {
      console.log(
        `[Metadata] Milestones already mirrored for ${repository.name}; skipping`
      );
    }

    if (metadataUpdated) {
      metadataState.lastSyncedAt = new Date().toISOString();
    }

    console.log(
      `Repository ${repository.name} mirrored successfully to organization ${orgName} as ${targetRepoName}`
    );

    // Mark repos as "mirrored" in DB
    await db
      .update(repositories)
      .set({
        status: repoStatusEnum.parse("mirrored"),
        updatedAt: new Date(),
        lastMirrored: new Date(),
        errorMessage: null,
        mirroredLocation: `${orgName}/${targetRepoName}`,
        metadata: metadataUpdated
          ? serializeRepositoryMetadataState(metadataState)
          : repository.metadata ?? null,
      })
      .where(eq(repositories.id, repository.id!));

    //create a mirror job
    await createMirrorJob({
      userId: config.userId,
      repositoryId: repository.id,
      repositoryName: repository.name,
      message: `Repository ${repository.name} mirrored successfully${targetRepoName !== repository.name ? ` as ${targetRepoName}` : ''}`,
      details: `Repository ${repository.fullName} was mirrored to Gitea at ${orgName}/${targetRepoName}`,
      status: "mirrored",
    });

    return migrateRes.data;
  } catch (error) {
    console.error(
      `Error while mirroring repository ${repository.name}: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
    // Mark repos as "failed" in DB. For starred repos, clear mirroredLocation
    // to release the name claim for retry. For non-starred repos, preserve it
    // since the Gitea repo may partially exist and we need the location for recovery.
    const failureUpdate2: Record<string, any> = {
      status: repoStatusEnum.parse("failed"),
      updatedAt: new Date(),
      errorMessage: error instanceof Error ? error.message : "Unknown error",
    };
    // Only clear mirroredLocation if the Gitea migrate call itself failed.
    // If migrate succeeded but metadata mirroring failed, preserve the
    // location since the repo physically exists in Gitea.
    if (!migrateSucceeded) {
      failureUpdate2.mirroredLocation = "";
    }
    await db
      .update(repositories)
      .set(failureUpdate2)
      .where(eq(repositories.id, repository.id!));

    // Append log for failure
    await createMirrorJob({
      userId: config.userId || "", // userId is going to be there anyways
      repositoryId: repository.id,
      repositoryName: repository.name,
      message: `Failed to mirror repository: ${repository.name}`,
      details: `Repository ${repository.name} failed to mirror. Error: ${
        error instanceof Error ? error.message : "Unknown error"
      }`,
      status: "failed",
    });
    if (error instanceof Error) {
      throw new Error(`Failed to mirror repository: ${error.message}`);
    }
    throw new Error("Failed to mirror repository: An unknown error occurred.");
  }
}

export async function mirrorGitHubOrgRepoToGiteaOrg({
  config,
  octokit,
  repository,
  orgName,
}: {
  config: Partial<Config>;
  octokit: Octokit;
  repository: Repository;
  orgName: string;
}) {
  try {
    if (!config.giteaConfig?.url || !config.giteaConfig?.token) {
      throw new Error("Gitea config is required.");
    }

    const giteaOrgId = await getOrCreateGiteaOrg({
      orgName,
      config,
    });

    await mirrorGitHubRepoToGiteaOrg({
      octokit,
      config,
      repository,
      giteaOrgId,
      orgName,
    });
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(`Failed to mirror repository: ${error.message}`);
    }
    throw new Error("Failed to mirror repository: An unknown error occurred.");
  }
}

export async function mirrorGitHubOrgToGitea({
  organization,
  octokit,
  config,
}: {
  organization: Organization;
  octokit: Octokit;
  config: Partial<Config>;
}) {
  try {
    if (
      !config.userId ||
      !config.id ||
      !config.githubConfig?.token ||
      !config.giteaConfig?.url
    ) {
      throw new Error("Config, GitHub token and Gitea URL are required.");
    }

    console.log(`Mirroring organization ${organization.name}`);

    //mark the org as "mirroring" in DB
    await db
      .update(organizations)
      .set({
        isIncluded: true,
        status: repoStatusEnum.parse("mirroring"),
        updatedAt: new Date(),
      })
      .where(eq(organizations.id, organization.id!));

    // Append log for "mirroring" status
    await createMirrorJob({
      userId: config.userId,
      organizationId: organization.id,
      organizationName: organization.name,
      message: `Started mirroring organization: ${organization.name}`,
      details: `Organization ${organization.name} is now in the mirroring state.`,
      status: repoStatusEnum.parse("mirroring"),
    });

    // Get the mirror strategy - use preserveOrgStructure for backward compatibility
    const mirrorStrategy = config.githubConfig?.mirrorStrategy ||
      (config.giteaConfig?.preserveOrgStructure ? "preserve" : "flat-user");

    let giteaOrgId: number;
    let targetOrgName: string;

    // Determine the target organization based on strategy
    if (mirrorStrategy === "single-org" && config.giteaConfig?.organization) {
      // For single-org strategy, use the configured destination organization
      targetOrgName = config.giteaConfig.organization || config.giteaConfig.defaultOwner;
      giteaOrgId = await getOrCreateGiteaOrg({
        orgId: organization.id,
        orgName: targetOrgName,
        config,
      });
      console.log(`Using single organization strategy: all repos will go to ${targetOrgName}`);
    } else if (mirrorStrategy === "preserve") {
      // For preserve strategy, create/use an org with the same name as GitHub
      targetOrgName = organization.name;
      giteaOrgId = await getOrCreateGiteaOrg({
        orgId: organization.id,
        orgName: targetOrgName,
        config,
      });
    } else {
      // For flat-user strategy, we shouldn't create organizations at all
      // Skip organization creation and let individual repos be handled by getGiteaRepoOwner
      console.log(`Using flat-user strategy: repos will be placed under user account`);
      targetOrgName = config.giteaConfig?.defaultOwner || "";
    }

    //query the db with the org name and get the repos
    const orgRepos = await db
      .select()
      .from(repositories)
      .where(eq(repositories.organization, organization.name));

    if (orgRepos.length === 0) {
      console.log(
        `No repositories found for organization ${organization.name} - marking as successfully mirrored`
      );
    } else {
      console.log(
        `Mirroring ${orgRepos.length} repositories for organization ${organization.name}`
      );

      // Import the processWithRetry function
      const { processWithRetry } = await import("@/lib/utils/concurrency");

      // Process repositories in parallel with concurrency control
      await processWithRetry(
        orgRepos,
        async (repo) => {
          // Prepare repository data
          const repoData = {
            ...repo,
            status: repo.status as RepoStatus,
            visibility: repo.visibility as RepositoryVisibility,
            lastMirrored: repo.lastMirrored ?? undefined,
            errorMessage: repo.errorMessage ?? undefined,
            organization: repo.organization ?? undefined,
            forkedFrom: repo.forkedFrom ?? undefined,
            mirroredLocation: repo.mirroredLocation || "",
          };

          // Log the start of mirroring
          console.log(
            `Starting mirror for repository: ${repo.name} from GitHub org ${organization.name}`
          );

          // Mirror the repository based on strategy
          if (mirrorStrategy === "flat-user") {
            // For flat-user strategy, mirror directly to user account
            await mirrorGithubRepoToGitea({
              octokit,
              repository: repoData,
              config,
            });
          } else {
            // For preserve and single-org strategies, use organization
            await mirrorGitHubRepoToGiteaOrg({
              octokit,
              config,
              repository: repoData,
              giteaOrgId: giteaOrgId!,
              orgName: targetOrgName,
            });
          }

          return repo;
        },
        {
          concurrencyLimit: 3, // Process 3 repositories at a time
          maxRetries: 2,
          retryDelay: 2000,
          onProgress: (completed, total, result) => {
            const percentComplete = Math.round((completed / total) * 100);
            if (result) {
              console.log(
                `Mirrored repository "${result.name}" in organization ${organization.name} (${completed}/${total}, ${percentComplete}%)`
              );
            }
          },
          onRetry: (repo, error, attempt) => {
            console.log(
              `Retrying repository ${repo.name} in organization ${organization.name} (attempt ${attempt}): ${error.message}`
            );
          },
        }
      );
    }

    console.log(`Organization ${organization.name} mirrored successfully`);

    // Mark org as "mirrored" in DB
    await db
      .update(organizations)
      .set({
        status: repoStatusEnum.parse("mirrored"),
        updatedAt: new Date(),
        lastMirrored: new Date(),
        errorMessage: null,
      })
      .where(eq(organizations.id, organization.id!));

    // Append log for "mirrored" status
    await createMirrorJob({
      userId: config.userId,
      organizationId: organization.id,
      organizationName: organization.name,
      message: `Successfully mirrored organization: ${organization.name}`,
      details:
        orgRepos.length === 0
          ? `Organization ${organization.name} was processed successfully (no repositories found).`
          : `Organization ${organization.name} was mirrored to Gitea with ${orgRepos.length} repositories.`,
      status: repoStatusEnum.parse("mirrored"),
    });
  } catch (error) {
    console.error(
      `Error while mirroring organization ${organization.name}: ${
        error instanceof Error ? error.message : String(error)
      }`
    );

    // Mark org as "failed" in DB
    await db
      .update(organizations)
      .set({
        status: repoStatusEnum.parse("failed"),
        updatedAt: new Date(),
        errorMessage: error instanceof Error ? error.message : "Unknown error",
      })
      .where(eq(organizations.id, organization.id!));

    // Append log for failure
    await createMirrorJob({
      userId: config.userId || "", // userId is going to be there anyways
      organizationId: organization.id,
      organizationName: organization.name,
      message: `Failed to mirror organization: ${organization.name}`,
      details: `Organization ${organization.name} failed to mirror. Error: ${
        error instanceof Error ? error.message : "Unknown error"
      }`,
      status: repoStatusEnum.parse("failed"),
    });

    if (error instanceof Error) {
      throw new Error(`Failed to mirror repository: ${error.message}`);
    }
    throw new Error("Failed to mirror repository: An unknown error occurred.");
  }
}

export const syncGiteaRepo = async ({
  config,
  repository,
}: {
  config: Partial<Config>;
  repository: Repository;
}) => {
  // Use the enhanced sync function that handles non-mirror repos
  const { syncGiteaRepoEnhanced } = await import("./gitea-enhanced");
  
  try {
    return await syncGiteaRepoEnhanced({ config, repository });
  } catch (error) {
    // Re-throw with original function name for backward compatibility
    if (error instanceof Error) {
      throw new Error(`Failed to sync repository: ${error.message}`);
    }
    throw new Error("Failed to sync repository: An unknown error occurred.");
  }
};

export const mirrorGitRepoIssuesToGitea = async ({
  config,
  octokit,
  repository,
  giteaOwner,
  giteaRepoName,
}: {
  config: Partial<Config>;
  octokit: Octokit;
  repository: Repository;
  giteaOwner: string;
  giteaRepoName?: string;
}) => {
  //things covered here are- issue, title, body, labels, comments and assignees
  if (
    !config.githubConfig?.token ||
    !config.giteaConfig?.token ||
    !config.giteaConfig?.url ||
    !config.giteaConfig?.defaultOwner
  ) {
    throw new Error("Missing GitHub or Gitea configuration.");
  }

  // Decrypt config tokens for API usage
  const decryptedConfig = decryptConfigTokens(config as Config);
  
  // Use provided giteaRepoName or fall back to repository.name
  const repoName = giteaRepoName || repository.name;
  
  // Log configuration details for debugging
  console.log(`[Issues] Starting issue mirroring for repository ${repository.name} as ${repoName}`);
  console.log(`[Issues] Gitea URL: ${config.giteaConfig!.url}`);
  console.log(`[Issues] Gitea Owner: ${giteaOwner}`);
  console.log(`[Issues] Gitea Default Owner: ${config.giteaConfig!.defaultOwner}`);
  
  // Verify the repository exists in Gitea before attempting to mirror metadata
  console.log(`[Issues] Verifying repository ${repoName} exists at ${giteaOwner}`);
  const repoExists = await isRepoPresentInGitea({
    config,
    owner: giteaOwner,
    repoName: repoName,
  });
  
  if (!repoExists) {
    console.error(`[Issues] Repository ${repoName} not found at ${giteaOwner}. Cannot mirror issues.`);
    throw new Error(`Repository ${repoName} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`);
  }

  const [owner, repo] = repository.fullName.split("/");

  // Fetch GitHub issues
  const issues = await octokit.paginate(
    octokit.rest.issues.listForRepo,
    {
      owner,
      repo,
      state: "all",
      per_page: 100,
      sort: "created",
      direction: "asc",
    },
    (res) => res.data
  );

  // Filter out pull requests
  const filteredIssues = issues.filter((issue) => !(issue as any).pull_request);

  console.log(
    `Mirroring ${filteredIssues.length} issues from ${repository.fullName}`
  );

  if (filteredIssues.length === 0) {
    console.log(`No issues to mirror for ${repository.fullName}`);
    return;
  }

  const ghIssueMarkerRegex = /\[GH-ISSUE #(\d+)\]/i;
  const extractGitHubIssueNumber = (value: string | null | undefined): number | null => {
    if (!value) return null;
    const match = value.match(ghIssueMarkerRegex);
    if (!match?.[1]) return null;
    const parsed = Number.parseInt(match[1], 10);
    return Number.isFinite(parsed) ? parsed : null;
  };

  const existingGiteaIssues: any[] = [];
  const titleFallbackMap = new Map<string, any[]>();
  const giteaIssueByGitHubNumber = new Map<number, any>();
  let issuesPage = 1;
  const issuesPerPage = 100;

  while (true) {
    const existingIssuesRes = await httpGet(
      `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/issues?state=all&page=${issuesPage}&limit=${issuesPerPage}`,
      {
        Authorization: `token ${decryptedConfig.giteaConfig.token}`,
      }
    );

    const pageIssues = Array.isArray(existingIssuesRes.data)
      ? existingIssuesRes.data
      : [];
    if (!pageIssues.length) break;

    existingGiteaIssues.push(...pageIssues);
    if (pageIssues.length < issuesPerPage) break;
    issuesPage += 1;
  }

  for (const giteaIssue of existingGiteaIssues) {
    const mappedNumber = extractGitHubIssueNumber(giteaIssue.title);
    if (mappedNumber !== null) {
      giteaIssueByGitHubNumber.set(mappedNumber, giteaIssue);
      continue;
    }

    const title = (giteaIssue.title || "").trim();
    if (!title) continue;
    const existing = titleFallbackMap.get(title) || [];
    existing.push(giteaIssue);
    titleFallbackMap.set(title, existing);
  }

  // Get existing labels from Gitea
  const giteaLabelsRes = await httpGet(
    `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/labels`,
    {
      Authorization: `token ${decryptedConfig.giteaConfig.token}`,
    }
  );

  const giteaLabels = giteaLabelsRes.data;
  const labelMap = new Map<string, number>(
    giteaLabels.map((label: any) => [label.name, label.id])
  );

  // Import the processWithRetry function
  const { processWithRetry } = await import("@/lib/utils/concurrency");

  const rawIssueConcurrency = config.giteaConfig?.issueConcurrency ?? 3;
  const issueConcurrencyLimit =
    Number.isFinite(rawIssueConcurrency)
      ? Math.max(1, Math.floor(rawIssueConcurrency))
      : 1;

  if (issueConcurrencyLimit > 1) {
    console.warn(
      `[Issues] Concurrency is set to ${issueConcurrencyLimit}. This may lead to out-of-order issue creation in Gitea but is faster.`
    );
  }

  // Process issues in parallel with concurrency control
  await processWithRetry(
    filteredIssues,
    async (issue) => {
      const githubLabelNames =
        issue.labels
          ?.map((l) => (typeof l === "string" ? l : l.name))
          .filter((l): l is string => !!l) || [];

      const giteaLabelIds: number[] = [];

      // Resolve or create labels in Gitea
      for (const name of githubLabelNames) {
        if (labelMap.has(name)) {
          giteaLabelIds.push(labelMap.get(name)!);
        } else {
          try {
            const created = await httpPost(
              `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/labels`,
              { name, color: "#ededed" }, // Default color
              {
                Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
              }
            );

            labelMap.set(name, created.data.id);
            giteaLabelIds.push(created.data.id);
          } catch (labelErr) {
            console.error(
              `Failed to create label "${name}" in Gitea: ${labelErr}`
            );
          }
        }
      }

      const originalAssignees =
        issue.assignees && issue.assignees.length > 0
          ? `\n\nOriginally assigned to: ${issue.assignees
              .map((a) => `@${a.login}`)
              .join(", ")} on GitHub.`
          : "";

      const issueAuthor = issue.user?.login ?? "unknown";
      const issueCreatedOn = formatDateShort(issue.created_at);
      const issueOriginHeader = `Originally created by @${issueAuthor} on GitHub${
        issueCreatedOn ? ` (${issueCreatedOn})` : ""
      }.`;
      const issueMarker = `[GH-ISSUE #${issue.number}]`;
      const mirroredTitle = `${issueMarker} ${issue.title}`;
      const issueBody = `${issueOriginHeader}\nOriginal GitHub issue: ${issue.html_url}${originalAssignees}\n\n${issue.body ?? ""}`;

      const issuePayload: any = {
        title: mirroredTitle,
        body: issueBody,
        closed: issue.state === "closed",
        labels: giteaLabelIds,
      };

      let existingIssue = giteaIssueByGitHubNumber.get(issue.number);
      if (!existingIssue) {
        const titleFallbackCandidates = titleFallbackMap.get(issue.title.trim()) || [];
        if (titleFallbackCandidates.length === 1) {
          existingIssue = titleFallbackCandidates[0];
          giteaIssueByGitHubNumber.set(issue.number, existingIssue);
          console.log(
            `[Issues] Matched legacy issue by title for #${issue.number}; converting to marker-based title`
          );
        } else if (titleFallbackCandidates.length > 1) {
          const filtered = titleFallbackCandidates.filter((candidate) =>
            String(candidate.body || "").startsWith(issueOriginHeader)
          );
          if (filtered.length === 1) {
            existingIssue = filtered[0];
            giteaIssueByGitHubNumber.set(issue.number, existingIssue);
            console.log(
              `[Issues] Matched legacy issue by body prefix for #${issue.number}; converting to marker-based title`
            );
          }
        }
      }

      let targetIssueNumber: number;
      if (existingIssue) {
        targetIssueNumber = existingIssue.number;
        await httpPatch(
          `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues/${targetIssueNumber}`,
          {
            title: issuePayload.title,
            body: issuePayload.body,
            state: issue.state === "closed" ? "closed" : "open",
            labels: issuePayload.labels,
          },
          {
            Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
          }
        );
      } else {
        const createdIssue = await httpPost(
          `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues`,
          issuePayload,
          {
            Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
          }
        );
        targetIssueNumber = createdIssue.data.number;

        if (issue.state === "closed" && createdIssue.data.state !== "closed") {
          try {
            await httpPatch(
              `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues/${targetIssueNumber}`,
              { state: "closed" },
              {
                Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
              }
            );
          } catch (closeError) {
            console.error(
              `[Issues] Failed to close issue #${targetIssueNumber}: ${
                closeError instanceof Error ? closeError.message : String(closeError)
              }`
            );
          }
        }
      }

      // Clone comments
      const comments = await octokit.paginate(
        octokit.rest.issues.listComments,
        {
          owner,
          repo,
          issue_number: issue.number,
          per_page: 100,
        },
        (res) => res.data
      );

      // Ensure comments are applied in chronological order to preserve discussion flow
      const sortedComments = comments
        .slice()
        .sort(
          (a, b) =>
            new Date(a.created_at || 0).getTime() -
            new Date(b.created_at || 0).getTime()
        );

      // Process comments sequentially to preserve historical ordering
      if (sortedComments.length > 0) {
        const existingComments: any[] = [];
        let commentsPage = 1;
        const commentsPerPage = 100;
        while (true) {
          const existingCommentsRes = await httpGet(
            `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues/${targetIssueNumber}/comments?page=${commentsPage}&limit=${commentsPerPage}`,
            {
              Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
            }
          );
          const pageComments = Array.isArray(existingCommentsRes.data)
            ? existingCommentsRes.data
            : [];
          if (!pageComments.length) break;
          existingComments.push(...pageComments);
          if (pageComments.length < commentsPerPage) break;
          commentsPage += 1;
        }
        const mirroredCommentIds = new Set<number>();
        const existingCommentBodies = new Set<string>();
        for (const existingComment of existingComments) {
          const body = String(existingComment.body || "");
          if (body) existingCommentBodies.add(body);
          const marker = String(existingComment.body || "").match(
            /<!--\s*gh-comment-id:(\d+)\s*-->/i
          );
          if (marker?.[1]) {
            const parsed = Number.parseInt(marker[1], 10);
            if (Number.isFinite(parsed)) mirroredCommentIds.add(parsed);
          }
        }

        await processWithRetry(
          sortedComments,
          async (comment) => {
            if (mirroredCommentIds.has(comment.id)) {
              return comment;
            }
            const commenter = comment.user?.login ?? "unknown";
            const commentDate = formatDateShort(comment.created_at);
            const commentHeader = `@${commenter} commented on GitHub${
              commentDate ? ` (${commentDate})` : ""
            }:`;
            const legacyBody = `${commentHeader}\n\n${comment.body ?? ""}`;
            const markedBody = `<!-- gh-comment-id:${comment.id} -->\n${legacyBody}`;
            if (existingCommentBodies.has(legacyBody) || existingCommentBodies.has(markedBody)) {
              return comment;
            }

            await httpPost(
              `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues/${targetIssueNumber}/comments`,
              {
                body: markedBody,
              },
              {
                Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
              }
            );
            return comment;
          },
          {
            concurrencyLimit: 1,
            maxRetries: 2,
            retryDelay: 1000,
            onRetry: (_comment, error, attempt) => {
              console.log(
                `Retrying comment (attempt ${attempt}): ${error.message}`
              );
            },
          }
        );
      }

      return issue;
    },
    {
      concurrencyLimit: issueConcurrencyLimit,
      maxRetries: 2,
      retryDelay: 2000,
      onProgress: (completed, total, result) => {
        const percentComplete = Math.round((completed / total) * 100);
        if (result) {
          console.log(
            `Mirrored issue "${result.title}" (${completed}/${total}, ${percentComplete}%)`
          );
        }
      },
      onRetry: (issue, error, attempt) => {
        console.log(
          `Retrying issue "${issue.title}" (attempt ${attempt}): ${error.message}`
        );
      },
    }
  );

  console.log(
    `Completed mirroring ${filteredIssues.length} issues for ${repository.fullName}`
  );
};

export async function mirrorGitHubReleasesToGitea({
  octokit,
  repository,
  config,
  giteaOwner,
  giteaRepoName,
}: {
  octokit: Octokit;
  repository: Repository;
  config: Partial<Config>;
  giteaOwner?: string;
  giteaRepoName?: string;
}) {
  if (
    !config.giteaConfig?.defaultOwner ||
    !config.giteaConfig?.token ||
    !config.giteaConfig?.url
  ) {
    throw new Error("Gitea config is incomplete for mirroring releases.");
  }

  // Decrypt config tokens for API usage
  const decryptedConfig = decryptConfigTokens(config as Config);

  // Determine target owner/repo in Gitea (supports renamed repos)
  const repoOwner = giteaOwner || (await getGiteaRepoOwnerAsync({ config, repository }));
  const repoName = giteaRepoName || repository.name;

  // Verify the repository exists in Gitea before attempting to mirror releases
  console.log(`[Releases] Verifying repository ${repoName} exists at ${repoOwner}`);
  const repoExists = await isRepoPresentInGitea({
    config,
    owner: repoOwner,
    repoName: repoName,
  });
  
  if (!repoExists) {
    console.error(`[Releases] Repository ${repository.name} not found at ${repoOwner}. Cannot mirror releases.`);
    throw new Error(`Repository ${repository.name} does not exist in Gitea at ${repoOwner}. Please ensure the repository is mirrored first.`);
  }

  // Get release limit from config (default to 10)
  const releaseLimit = Math.max(1, Math.floor(config.giteaConfig?.releaseLimit || 10));

  // GitHub API max per page is 100; paginate until we reach the configured limit.
  const releases: Awaited<
    ReturnType<typeof octokit.rest.repos.listReleases>
  >["data"] = [];
  let page = 1;
  const perPage = Math.min(100, releaseLimit);

  while (releases.length < releaseLimit) {
    const response = await octokit.rest.repos.listReleases({
      owner: repository.owner,
      repo: repository.name,
      per_page: perPage,
      page,
    });

    if (response.data.length === 0) {
      break;
    }

    releases.push(...response.data);

    if (response.data.length < perPage) {
      break;
    }

    page++;
  }

  const limitedReleases = releases.slice(0, releaseLimit);

  console.log(
    `[Releases] Found ${limitedReleases.length} releases (limited to latest ${releaseLimit}) to mirror for ${repository.fullName}`
  );

  if (limitedReleases.length === 0) {
    console.log(`[Releases] No releases to mirror for ${repository.fullName}`);
    return;
  }

  let mirroredCount = 0;
  let skippedCount = 0;

  const getReleaseTimestamp = (release: (typeof limitedReleases)[number]) => {
    // Use published_at first (when the release was published on GitHub)
    // Fall back to created_at (when the git tag was created) only if published_at is missing
    // This matches GitHub's sorting behavior and handles cases where multiple tags
    // point to the same commit but have different publish dates
    const sourceDate = release.published_at ?? release.created_at ?? "";
    const timestamp = sourceDate ? new Date(sourceDate).getTime() : 0;
    return Number.isFinite(timestamp) ? timestamp : 0;
  };

  // Capture the latest releases, then process them oldest-to-newest so Gitea mirrors keep chronological order
  const releasesToProcess = limitedReleases
    .slice()
    .sort((a, b) => getReleaseTimestamp(b) - getReleaseTimestamp(a))
    .sort((a, b) => getReleaseTimestamp(a) - getReleaseTimestamp(b));

  console.log(`[Releases] Processing ${releasesToProcess.length} releases in chronological order (oldest to newest by published date)`);
  releasesToProcess.forEach((rel, idx) => {
    const publishedDate = new Date(rel.published_at || rel.created_at);
    const createdDate = new Date(rel.created_at);
    const dateInfo = rel.published_at !== rel.created_at
      ? `published ${publishedDate.toISOString()} (tag created ${createdDate.toISOString()})`
      : `published ${publishedDate.toISOString()}`;
    console.log(`[Releases] ${idx + 1}. ${rel.tag_name} - ${dateInfo}`);
  });

  // Check if existing releases in Gitea are in the wrong order
  // If so, we need to delete and recreate them to fix the ordering
  let needsRecreation = false;
  try {
    const existingReleasesResponse = await httpGet(
      `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases?per_page=100`,
      {
        Authorization: `token ${decryptedConfig.giteaConfig.token}`,
      }
    ).catch(() => null);

    if (existingReleasesResponse && existingReleasesResponse.data && Array.isArray(existingReleasesResponse.data)) {
      const existingReleases = existingReleasesResponse.data;

      if (existingReleases.length > 0) {
        console.log(`[Releases] Found ${existingReleases.length} existing releases in Gitea, checking chronological order...`);

        // Create a map of tag_name to expected chronological index (0 = oldest, n = newest)
        const expectedOrder = new Map<string, number>();
        releasesToProcess.forEach((rel, idx) => {
          expectedOrder.set(rel.tag_name, idx);
        });

        // Check if existing releases are in the correct order based on created_unix
        // Gitea sorts by created_unix DESC, so newer releases should have higher created_unix values
        const releasesThatShouldExist = existingReleases.filter(r => expectedOrder.has(r.tag_name));

        if (releasesThatShouldExist.length > 1) {
          for (let i = 0; i < releasesThatShouldExist.length - 1; i++) {
            const current = releasesThatShouldExist[i];
            const next = releasesThatShouldExist[i + 1];

            const currentExpectedIdx = expectedOrder.get(current.tag_name)!;
            const nextExpectedIdx = expectedOrder.get(next.tag_name)!;

            // Since Gitea returns releases sorted by created_unix DESC:
            // - Earlier releases in the list should have HIGHER expected indices (newer)
            // - Later releases in the list should have LOWER expected indices (older)
            if (currentExpectedIdx < nextExpectedIdx) {
              console.log(`[Releases] ⚠️  Incorrect ordering detected: ${current.tag_name} (index ${currentExpectedIdx}) appears before ${next.tag_name} (index ${nextExpectedIdx})`);
              needsRecreation = true;
              break;
            }
          }
        }

        if (needsRecreation) {
          console.log(`[Releases] ⚠️  Releases are in incorrect chronological order. Will delete and recreate all releases.`);

          // Delete all existing releases that we're about to recreate
          for (const existingRelease of releasesThatShouldExist) {
            try {
              console.log(`[Releases] Deleting incorrectly ordered release: ${existingRelease.tag_name}`);
              await httpDelete(
                `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases/${existingRelease.id}`,
                {
                  Authorization: `token ${decryptedConfig.giteaConfig.token}`,
                }
              );
            } catch (deleteError) {
              console.error(`[Releases] Failed to delete release ${existingRelease.tag_name}: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`);
            }
          }

          console.log(`[Releases] ✅ Deleted ${releasesThatShouldExist.length} releases. Will recreate in correct chronological order.`);
        } else {
          console.log(`[Releases] ✅ Existing releases are in correct chronological order.`);
        }
      }
    }
  } catch (orderCheckError) {
    console.warn(`[Releases] Could not verify release order: ${orderCheckError instanceof Error ? orderCheckError.message : String(orderCheckError)}`);
    // Continue with normal processing
  }

  for (const release of releasesToProcess) {
    try {
      // Check if release already exists (skip check if we just deleted all releases)
      const existingReleasesResponse = needsRecreation ? null : await httpGet(
        `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases/tags/${release.tag_name}`,
        {
          Authorization: `token ${decryptedConfig.giteaConfig.token}`,
        }
      ).catch(() => null);

      // Prepare release body with GitHub original date header
      const githubPublishedDate = release.published_at || release.created_at;
      const githubTagCreatedDate = release.created_at;

      let githubDateHeader = '';
      if (githubPublishedDate) {
        githubDateHeader = `> 📅 **Originally published on GitHub:** ${new Date(githubPublishedDate).toUTCString()}`;

        // If the tag was created on a different date than the release was published,
        // show both dates (helps with repos that create multiple tags from the same commit)
        if (release.published_at && release.created_at && release.published_at !== release.created_at) {
          githubDateHeader += `\n> 🏷️  **Git tag created:** ${new Date(githubTagCreatedDate).toUTCString()}`;
        }

        githubDateHeader += '\n\n';
      }

      const originalReleaseNote = release.body || "";
      const releaseNote = githubDateHeader + originalReleaseNote;

      if (existingReleasesResponse) {
        // Update existing release if the changelog/body differs
        const existingRelease = existingReleasesResponse.data;
        const existingNote = existingRelease.body || "";
        
        if (existingNote !== releaseNote || existingRelease.name !== (release.name || release.tag_name)) {
          console.log(`[Releases] Updating existing release ${release.tag_name} with new changelog/title`);
          
          await httpPut(
            `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases/${existingRelease.id}`,
            {
              tag_name: release.tag_name,
              target: release.target_commitish,
              title: release.name || release.tag_name,
              body: releaseNote,
              draft: release.draft,
              prerelease: release.prerelease,
            },
            {
              Authorization: `token ${decryptedConfig.giteaConfig.token}`,
            }
          );

          if (originalReleaseNote) {
            console.log(`[Releases] Updated changelog for ${release.tag_name} (${originalReleaseNote.length} characters + GitHub date header)`);
          } else {
            console.log(`[Releases] Updated release ${release.tag_name} with GitHub date header`);
          }
          mirroredCount++;
        } else {
          console.log(`[Releases] Release ${release.tag_name} already up-to-date, skipping`);
          skippedCount++;
        }
        continue;
      }

      // Create new release with changelog/body content (includes GitHub date header)
      if (originalReleaseNote) {
        console.log(`[Releases] Including changelog for ${release.tag_name} (${originalReleaseNote.length} characters + GitHub date header)`);
      } else {
        console.log(`[Releases] Creating release ${release.tag_name} with GitHub date header (no changelog)`);
      }
      
      const createReleaseResponse = await httpPost(
        `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases`,
        {
          tag_name: release.tag_name,
          target: release.target_commitish,
          title: release.name || release.tag_name,
          body: releaseNote,
          draft: release.draft,
          prerelease: release.prerelease,
        },
        {
          Authorization: `token ${decryptedConfig.giteaConfig.token}`,
        }
      );
      
      // Mirror release assets if they exist
      if (release.assets && release.assets.length > 0) {
        console.log(`[Releases] Mirroring ${release.assets.length} assets for release ${release.tag_name}`);
        
        for (const asset of release.assets) {
          try {
            // Download the asset from GitHub
            console.log(`[Releases] Downloading asset: ${asset.name} (${asset.size} bytes)`);
            const assetResponse = await fetch(asset.browser_download_url, {
              headers: {
                'Accept': 'application/octet-stream',
                'Authorization': `token ${decryptedConfig.githubConfig.token}`,
              },
            });
            
            if (!assetResponse.ok) {
              console.error(`[Releases] Failed to download asset ${asset.name}: ${assetResponse.statusText}`);
              continue;
            }
            
            const assetData = await assetResponse.arrayBuffer();
            
            // Upload the asset to Gitea release
            const formData = new FormData();
            formData.append('attachment', new Blob([assetData]), asset.name);
            
            const uploadResponse = await fetch(
              `${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases/${createReleaseResponse.data.id}/assets?name=${encodeURIComponent(asset.name)}`,
              {
                method: 'POST',
                headers: {
                  'Authorization': `token ${decryptedConfig.giteaConfig.token}`,
                },
                body: formData,
              }
            );
            
            if (uploadResponse.ok) {
              console.log(`[Releases] Successfully uploaded asset: ${asset.name}`);
            } else {
              const errorText = await uploadResponse.text();
              console.error(`[Releases] Failed to upload asset ${asset.name}: ${errorText}`);
            }
          } catch (assetError) {
            console.error(`[Releases] Error processing asset ${asset.name}: ${assetError instanceof Error ? assetError.message : String(assetError)}`);
          }
        }
      }
      
      mirroredCount++;
      const noteInfo = originalReleaseNote ? ` with ${originalReleaseNote.length} character changelog` : " without changelog";
      console.log(`[Releases] Successfully mirrored release: ${release.tag_name}${noteInfo}`);

      // Add delay to ensure proper timestamp ordering in Gitea
      // Gitea sorts releases by created_unix DESC, and all releases created in quick succession
      // will have nearly identical timestamps. The 1-second delay ensures proper chronological order.
      console.log(`[Releases] Waiting 1 second to ensure proper timestamp ordering in Gitea...`);
      await new Promise(resolve => setTimeout(resolve, 1000));
    } catch (error) {
      console.error(`[Releases] Failed to mirror release ${release.tag_name}: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  console.log(`✅ Mirrored/Updated ${mirroredCount} releases to Gitea (${skippedCount} already up-to-date)`);
}

export async function mirrorGitRepoPullRequestsToGitea({
  config,
  octokit,
  repository,
  giteaOwner,
  giteaRepoName,
}: {
  config: Partial<Config>;
  octokit: Octokit;
  repository: Repository;
  giteaOwner: string;
  giteaRepoName?: string;
}) {
  if (
    !config.githubConfig?.token ||
    !config.giteaConfig?.token ||
    !config.giteaConfig?.url ||
    !config.giteaConfig?.defaultOwner
  ) {
    throw new Error("Missing GitHub or Gitea configuration.");
  }

  // Decrypt config tokens for API usage
  const decryptedConfig = decryptConfigTokens(config as Config);
  
  // Use provided giteaRepoName or fall back to repository.name
  const repoName = giteaRepoName || repository.name;
  
  // Log configuration details for debugging
  console.log(`[Pull Requests] Starting PR mirroring for repository ${repository.name} as ${repoName}`);
  console.log(`[Pull Requests] Gitea URL: ${config.giteaConfig!.url}`);
  console.log(`[Pull Requests] Gitea Owner: ${giteaOwner}`);
  console.log(`[Pull Requests] Gitea Default Owner: ${config.giteaConfig!.defaultOwner}`);
  
  // Verify the repository exists in Gitea before attempting to mirror metadata
  console.log(`[Pull Requests] Verifying repository ${repoName} exists at ${giteaOwner}`);
  const repoExists = await isRepoPresentInGitea({
    config,
    owner: giteaOwner,
    repoName: repoName,
  });
  
  if (!repoExists) {
    console.error(`[Pull Requests] Repository ${repoName} not found at ${giteaOwner}. Cannot mirror PRs.`);
    throw new Error(`Repository ${repoName} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`);
  }

  const [owner, repo] = repository.fullName.split("/");

  // Fetch GitHub pull requests
  const pullRequests = await octokit.paginate(
    octokit.rest.pulls.list,
    {
      owner,
      repo,
      state: "all",
      per_page: 100,
      sort: "created",
      direction: "asc",
    },
    (res) => res.data
  );

  console.log(
    `Mirroring ${pullRequests.length} pull requests from ${repository.fullName}`
  );

  if (pullRequests.length === 0) {
    console.log(`No pull requests to mirror for ${repository.fullName}`);
    return;
  }

  // Note: Gitea doesn't have a direct API to create pull requests from external sources
  // Pull requests are typically created through Git operations
  // For now, we'll create them as issues with a special label
  
  // Get existing labels from Gitea and ensure "pull-request" label exists
  const giteaLabelsRes = await httpGet(
    `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/labels`,
    {
      Authorization: `token ${decryptedConfig.giteaConfig.token}`,
    }
  );

  const giteaLabels = giteaLabelsRes.data;
  const labelMap = new Map<string, number>(
    giteaLabels.map((label: any) => [label.name, label.id])
  );

  // Ensure "pull-request" label exists
  let pullRequestLabelId: number | null = null;
  if (labelMap.has("pull-request")) {
    pullRequestLabelId = labelMap.get("pull-request")!;
  } else {
    try {
      const created = await httpPost(
        `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/labels`,
        { 
          name: "pull-request",
          color: "#0366d6",
          description: "Mirrored from GitHub Pull Request"
        },
        {
          Authorization: `token ${decryptedConfig.giteaConfig.token}`,
        }
      );
      pullRequestLabelId = created.data.id;
    } catch (error) {
      console.error(`Failed to create "pull-request" label in Gitea: ${error}`);
      // Continue without labels if creation fails
    }
  }

  const existingPrIssuesByNumber = new Map<number, any>();
  let prIssuesPage = 1;
  const prIssuesPerPage = 100;
  while (true) {
    const existingIssuesRes = await httpGet(
      `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/issues?state=all&page=${prIssuesPage}&limit=${prIssuesPerPage}`,
      {
        Authorization: `token ${decryptedConfig.giteaConfig.token}`,
      }
    );
    const pageIssues = Array.isArray(existingIssuesRes.data)
      ? existingIssuesRes.data
      : [];
    if (!pageIssues.length) break;

    for (const issue of pageIssues) {
      const match = String(issue.title || "").match(/\[PR #(\d+)\]/i);
      if (!match?.[1]) continue;
      const prNumber = Number.parseInt(match[1], 10);
      if (Number.isFinite(prNumber)) {
        existingPrIssuesByNumber.set(prNumber, issue);
      }
    }

    if (pageIssues.length < prIssuesPerPage) break;
    prIssuesPage += 1;
  }

  const { processWithRetry } = await import("@/lib/utils/concurrency");

  const rawPullConcurrency = config.giteaConfig?.pullRequestConcurrency ?? 5;
  const pullRequestConcurrencyLimit =
    Number.isFinite(rawPullConcurrency)
      ? Math.max(1, Math.floor(rawPullConcurrency))
      : 1;

  if (pullRequestConcurrencyLimit > 1) {
    console.warn(
      `[Pull Requests] Concurrency is set to ${pullRequestConcurrencyLimit}. This may lead to out-of-order pull request mirroring in Gitea.`
    );
  }

  let successCount = 0;
  let failedCount = 0;

  await processWithRetry(
    pullRequests,
    async (pr) => {
      try {
        // Fetch additional PR data for rich metadata
        const [prDetail, commits, files] = await Promise.all([
          octokit.rest.pulls.get({ owner, repo, pull_number: pr.number }),
          octokit.rest.pulls.listCommits({ owner, repo, pull_number: pr.number, per_page: 10 }),
          octokit.rest.pulls.listFiles({ owner, repo, pull_number: pr.number, per_page: 100 })
        ]);

        // Build rich PR body with metadata
        let richBody = `## 📋 Pull Request Information\n\n`;
        richBody += `**Original PR:** ${pr.html_url}\n`;
        richBody += `**Author:** [@${pr.user?.login}](${pr.user?.html_url})\n`;
        richBody += `**Created:** ${new Date(pr.created_at).toLocaleDateString()}\n`;
        richBody += `**Status:** ${pr.state === 'closed' ? (pr.merged_at ? '✅ Merged' : '❌ Closed') : '🔄 Open'}\n`;
        
        if (pr.merged_at) {
          richBody += `**Merged:** ${new Date(pr.merged_at).toLocaleDateString()}\n`;
          richBody += `**Merged by:** [@${prDetail.data.merged_by?.login}](${prDetail.data.merged_by?.html_url})\n`;
        }

        richBody += `\n**Base:** \`${pr.base.ref}\` ← **Head:** \`${pr.head.ref}\`\n`;
        richBody += `\n---\n\n`;

        // Add commit history (up to 10 commits)
        if (commits.data.length > 0) {
          richBody += `### 📝 Commits (${commits.data.length}${commits.data.length >= 10 ? '+' : ''})\n\n`;
          commits.data.slice(0, 10).forEach(commit => {
            const shortSha = commit.sha.substring(0, 7);
            richBody += `- [\`${shortSha}\`](${commit.html_url}) ${commit.commit.message.split('\n')[0]}\n`;
          });
          if (commits.data.length > 10) {
            richBody += `\n_...and ${commits.data.length - 10} more commits_\n`;
          }
          richBody += `\n`;
        }

        // Add file changes summary
        if (files.data.length > 0) {
          const additions = prDetail.data.additions || 0;
          const deletions = prDetail.data.deletions || 0;
          const changedFiles = prDetail.data.changed_files || files.data.length;
          
          richBody += `### 📊 Changes\n\n`;
          richBody += `**${changedFiles} file${changedFiles !== 1 ? 's' : ''} changed** `;
          richBody += `(+${additions} additions, -${deletions} deletions)\n\n`;
          
          // List changed files (up to 20)
          richBody += `<details>\n<summary>View changed files</summary>\n\n`;
          files.data.slice(0, 20).forEach(file => {
            const changeIndicator = file.status === 'added' ? '➕' : 
                                   file.status === 'removed' ? '➖' : '📝';
            richBody += `${changeIndicator} \`${file.filename}\` (+${file.additions} -${file.deletions})\n`;
          });
          if (files.data.length > 20) {
            richBody += `\n_...and ${files.data.length - 20} more files_\n`;
          }
          richBody += `\n</details>\n\n`;
        }

        // Add original PR description
        richBody += `### 📄 Description\n\n`;
        richBody += pr.body || '_No description provided_';
        richBody += `\n\n---\n`;
        richBody += `\n<sub>🔄 This issue represents a GitHub Pull Request. `;
        richBody += `It cannot be merged through Gitea due to API limitations.</sub>`;

        // Prepare issue title with status indicator
        const statusPrefix = pr.merged_at ? '[MERGED] ' : (pr.state === 'closed' ? '[CLOSED] ' : '');
        const issueTitle = `[PR #${pr.number}] ${statusPrefix}${pr.title}`;

        const issueData = {
          title: issueTitle,
          body: richBody,
          labels: pullRequestLabelId ? [pullRequestLabelId] : [],
          closed: pr.state === "closed" || pr.merged_at !== null,
        };

        const existingPrIssue = existingPrIssuesByNumber.get(pr.number);
        if (existingPrIssue) {
          await httpPatch(
            `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues/${existingPrIssue.number}`,
            {
              title: issueData.title,
              body: issueData.body,
              state: issueData.closed ? "closed" : "open",
              labels: issueData.labels,
            },
            {
              Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
            }
          );
        } else {
          console.log(`[Pull Requests] Creating enriched issue for PR #${pr.number}: ${pr.title}`);
          const createdPrIssue = await httpPost(
            `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues`,
            issueData,
            {
              Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
            }
          );
          existingPrIssuesByNumber.set(pr.number, createdPrIssue.data);

          // Verify and explicitly close if the PR issue should be closed but wasn't
          const prShouldBeClosed = pr.state === "closed" || pr.merged_at !== null;
          const prIsActuallyClosed = createdPrIssue.data.state === "closed";

          if (prShouldBeClosed && !prIsActuallyClosed) {
            try {
              await httpPatch(
                `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues/${createdPrIssue.data.number}`,
                { state: "closed" },
                {
                  Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
                }
              );
            } catch (closeError) {
              console.error(
                `[Pull Requests] Failed to close issue for PR #${pr.number}: ${
                  closeError instanceof Error ? closeError.message : String(closeError)
                }`
              );
            }
          }
        }

        successCount++;
        console.log(`[Pull Requests] ✅ Successfully created issue for PR #${pr.number}`);
      } catch (apiError) {
        // If the detailed fetch fails, fall back to basic PR info
        console.log(`[Pull Requests] Falling back to basic info for PR #${pr.number} due to error: ${apiError}`);
        const basicIssueData = {
          title: `[PR #${pr.number}] ${pr.title}`,
          body: `**Original Pull Request:** ${pr.html_url}\n\n**State:** ${pr.state}\n**Merged:** ${pr.merged_at ? 'Yes' : 'No'}\n\n---\n\n${pr.body || 'No description provided'}`,
          labels: pullRequestLabelId ? [pullRequestLabelId] : [],
          closed: pr.state === "closed" || pr.merged_at !== null,
        };
        
        try {
          const existingPrIssue = existingPrIssuesByNumber.get(pr.number);
          if (existingPrIssue) {
            await httpPatch(
              `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues/${existingPrIssue.number}`,
              {
                title: basicIssueData.title,
                body: basicIssueData.body,
                state: basicIssueData.closed ? "closed" : "open",
                labels: basicIssueData.labels,
              },
              {
                Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
              }
            );
          } else {
            const createdBasicPrIssue = await httpPost(
              `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues`,
              basicIssueData,
              {
                Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
              }
            );
            existingPrIssuesByNumber.set(pr.number, createdBasicPrIssue.data);

            // Verify and explicitly close if needed
            const basicPrShouldBeClosed = pr.state === "closed" || pr.merged_at !== null;
            const basicPrIsActuallyClosed = createdBasicPrIssue.data.state === "closed";

            if (basicPrShouldBeClosed && !basicPrIsActuallyClosed) {
              try {
                await httpPatch(
                  `${config.giteaConfig!.url}/api/v1/repos/${giteaOwner}/${repoName}/issues/${createdBasicPrIssue.data.number}`,
                  { state: "closed" },
                  {
                    Authorization: `token ${decryptedConfig.giteaConfig!.token}`,
                  }
                );
              } catch (closeError) {
                console.error(
                  `[Pull Requests] Failed to close basic issue for PR #${pr.number}: ${
                    closeError instanceof Error ? closeError.message : String(closeError)
                  }`
                );
              }
            }
          }

          successCount++;
          console.log(`[Pull Requests] ✅ Created basic issue for PR #${pr.number}`);
        } catch (error) {
          failedCount++;
          console.error(
            `[Pull Requests] ❌ Failed to mirror PR #${pr.number}: ${error instanceof Error ? error.message : String(error)}`
          );
        }
      }
    },
    {
      concurrencyLimit: pullRequestConcurrencyLimit,
      maxRetries: 3,
      retryDelay: 1000,
    }
  );

  console.log(`✅ Mirrored ${successCount}/${pullRequests.length} pull requests to Gitea as enriched issues (${failedCount} failed)`);
}

export async function mirrorGitRepoLabelsToGitea({
  config,
  octokit,
  repository,
  giteaOwner,
  giteaRepoName,
}: {
  config: Partial<Config>;
  octokit: Octokit;
  repository: Repository;
  giteaOwner: string;
  giteaRepoName?: string;
}) {
  if (
    !config.githubConfig?.token ||
    !config.giteaConfig?.token ||
    !config.giteaConfig?.url
  ) {
    throw new Error("Missing GitHub or Gitea configuration.");
  }

  // Decrypt config tokens for API usage
  const decryptedConfig = decryptConfigTokens(config as Config);
  
  // Use provided giteaRepoName or fall back to repository.name
  const repoName = giteaRepoName || repository.name;
  
  // Verify the repository exists in Gitea before attempting to mirror metadata
  console.log(`[Labels] Verifying repository ${repoName} exists at ${giteaOwner}`);
  const repoExists = await isRepoPresentInGitea({
    config,
    owner: giteaOwner,
    repoName: repoName,
  });
  
  if (!repoExists) {
    console.error(`[Labels] Repository ${repoName} not found at ${giteaOwner}. Cannot mirror labels.`);
    throw new Error(`Repository ${repoName} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`);
  }

  const [owner, repo] = repository.fullName.split("/");

  // Fetch GitHub labels
  const labels = await octokit.paginate(
    octokit.rest.issues.listLabelsForRepo,
    {
      owner,
      repo,
      per_page: 100,
    },
    (res) => res.data
  );

  console.log(`Mirroring ${labels.length} labels from ${repository.fullName}`);

  if (labels.length === 0) {
    console.log(`No labels to mirror for ${repository.fullName}`);
    return;
  }

  // Get existing labels from Gitea
  const giteaLabelsRes = await httpGet(
    `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/labels`,
    {
      Authorization: `token ${decryptedConfig.giteaConfig.token}`,
    }
  );

  const existingLabels = new Set(
    giteaLabelsRes.data.map((label: any) => label.name)
  );

  let mirroredCount = 0;
  for (const label of labels) {
    if (!existingLabels.has(label.name)) {
      try {
        await httpPost(
          `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/labels`,
          {
            name: label.name,
            color: `#${label.color}`,
            description: label.description || "",
          },
          {
            Authorization: `token ${decryptedConfig.giteaConfig.token}`,
          }
        );
        mirroredCount++;
      } catch (error) {
        console.error(
          `Failed to mirror label "${label.name}": ${error instanceof Error ? error.message : String(error)}`
        );
      }
    }
  }

  console.log(`✅ Mirrored ${mirroredCount} new labels to Gitea`);
}

export async function mirrorGitRepoMilestonesToGitea({
  config,
  octokit,
  repository,
  giteaOwner,
  giteaRepoName,
}: {
  config: Partial<Config>;
  octokit: Octokit;
  repository: Repository;
  giteaOwner: string;
  giteaRepoName?: string;
}) {
  if (
    !config.githubConfig?.token ||
    !config.giteaConfig?.token ||
    !config.giteaConfig?.url
  ) {
    throw new Error("Missing GitHub or Gitea configuration.");
  }

  // Decrypt config tokens for API usage
  const decryptedConfig = decryptConfigTokens(config as Config);
  
  // Use provided giteaRepoName or fall back to repository.name
  const repoName = giteaRepoName || repository.name;
  
  // Verify the repository exists in Gitea before attempting to mirror metadata
  console.log(`[Milestones] Verifying repository ${repoName} exists at ${giteaOwner}`);
  const repoExists = await isRepoPresentInGitea({
    config,
    owner: giteaOwner,
    repoName: repoName,
  });
  
  if (!repoExists) {
    console.error(`[Milestones] Repository ${repoName} not found at ${giteaOwner}. Cannot mirror milestones.`);
    throw new Error(`Repository ${repoName} does not exist in Gitea at ${giteaOwner}. Please ensure the repository is mirrored first.`);
  }

  const [owner, repo] = repository.fullName.split("/");

  // Fetch GitHub milestones
  const milestones = await octokit.paginate(
    octokit.rest.issues.listMilestones,
    {
      owner,
      repo,
      state: "all",
      per_page: 100,
    },
    (res) => res.data
  );

  console.log(`Mirroring ${milestones.length} milestones from ${repository.fullName}`);

  if (milestones.length === 0) {
    console.log(`No milestones to mirror for ${repository.fullName}`);
    return;
  }

  // Get existing milestones from Gitea
  const giteaMilestonesRes = await httpGet(
    `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/milestones`,
    {
      Authorization: `token ${decryptedConfig.giteaConfig.token}`,
    }
  );

  const existingMilestones = new Set(
    giteaMilestonesRes.data.map((milestone: any) => milestone.title)
  );

  let mirroredCount = 0;
  for (const milestone of milestones) {
    if (!existingMilestones.has(milestone.title)) {
      try {
        await httpPost(
          `${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repoName}/milestones`,
          {
            title: milestone.title,
            description: milestone.description || "",
            due_on: milestone.due_on,
            state: milestone.state,
          },
          {
            Authorization: `token ${decryptedConfig.giteaConfig.token}`,
          }
        );
        mirroredCount++;
      } catch (error) {
        console.error(
          `Failed to mirror milestone "${milestone.title}": ${error instanceof Error ? error.message : String(error)}`
        );
      }
    }
  }

  console.log(`✅ Mirrored ${mirroredCount} new milestones to Gitea`);
}

/**
 * Create a simple Gitea client object with base URL and token
 */
export function createGiteaClient(url: string, token: string) {
  return { url, token };
}

/**
 * Delete a repository from Gitea
 */
export async function deleteGiteaRepo(
  client: { url: string; token: string },
  owner: string,
  repo: string
): Promise<void> {
  try {
    const response = await httpDelete(
      `${client.url}/api/v1/repos/${owner}/${repo}`,
      {
        Authorization: `token ${client.token}`,
      }
    );
    
    if (response.status >= 400) {
      throw new Error(`Failed to delete repository ${owner}/${repo}: ${response.status} ${response.statusText}`);
    }
    
    console.log(`Successfully deleted repository ${owner}/${repo} from Gitea`);
  } catch (error) {
    console.error(`Error deleting repository ${owner}/${repo}:`, error);
    throw error;
  }
}

/**
 * Archive a repository in Gitea
 * 
 * IMPORTANT: This function NEVER deletes data. It only marks repositories as archived.
 * - For regular repos: Uses Gitea's archive feature (makes read-only)
 * - For mirror repos: Renames with [ARCHIVED] prefix (Gitea doesn't allow archiving mirrors)
 * 
 * This ensures backups are preserved even when the GitHub source disappears.
 */
export async function archiveGiteaRepo(
  client: { url: string; token: string },
  owner: string,
  repo: string
): Promise<void> {
  try {
    // Helper: sanitize to Gitea's AlphaDashDot rule
    const sanitizeRepoNameAlphaDashDot = (name: string): string => {
      // Replace anything that's not [A-Za-z0-9.-] with '-'
      const base = name.replace(/[^A-Za-z0-9.-]+/g, "-").replace(/-+/g, "-");
      // Trim leading/trailing separators and dots for safety
      return base.replace(/^[.-]+/, "").replace(/[.-]+$/, "");
    };

    // First, check if this is a mirror repository
    const repoResponse = await httpGet(
      `${client.url}/api/v1/repos/${owner}/${repo}`,
      {
        Authorization: `token ${client.token}`,
      }
    );
    
    if (!repoResponse.data) {
      console.warn(`[Archive] Repository ${owner}/${repo} not found in Gitea. Skipping.`);
      return;
    }
    
    if (repoResponse.data?.mirror) {
      console.log(`[Archive] Repository ${owner}/${repo} is a mirror. Using safe rename strategy.`);
      
      // IMPORTANT: Gitea API doesn't allow archiving mirror repositories
      // According to Gitea source code, attempting to archive a mirror returns:
      // "repo is a mirror, cannot archive/un-archive" (422 Unprocessable Entity)
      // 
      // Our solution: Rename the repo to clearly mark it as orphaned
      // This preserves all data while indicating the repo is no longer actively synced
      
      const currentName = repoResponse.data.name;
      
      // Skip if already marked as archived
      const normalizedName = currentName.toLowerCase();
      if (
        currentName.startsWith('[ARCHIVED]') ||
        normalizedName.startsWith('archived-')
      ) {
        console.log(`[Archive] Repository ${owner}/${repo} already marked as archived. Skipping.`);
        return;
      }
      
      // Use a safe prefix and sanitize the name to satisfy AlphaDashDot rule
      let archivedName = `archived-${sanitizeRepoNameAlphaDashDot(currentName)}`;
      const currentDesc = repoResponse.data.description || '';
      const archiveNotice = `\n\n⚠️ ARCHIVED: Original GitHub repository no longer exists. Preserved as backup on ${new Date().toISOString()}`;
      
      // Only add notice if not already present
      const newDescription = currentDesc.includes('⚠️ ARCHIVED:') 
        ? currentDesc 
        : currentDesc + archiveNotice;
      
      try {
        await httpPatch(
          `${client.url}/api/v1/repos/${owner}/${repo}`,
          {
            name: archivedName,
            description: newDescription,
          },
          {
            Authorization: `token ${client.token}`,
            'Content-Type': 'application/json',
          }
        );
      } catch (e: any) {
        // If rename fails (e.g., 422 AlphaDashDot or name conflict), attempt a timestamped fallback
        const ts = new Date().toISOString().replace(/[-:T.]/g, "").slice(0, 14);
        archivedName = `archived-${ts}-${sanitizeRepoNameAlphaDashDot(currentName)}`;
        try {
          await httpPatch(
            `${client.url}/api/v1/repos/${owner}/${repo}`,
            {
              name: archivedName,
              description: newDescription,
            },
            {
              Authorization: `token ${client.token}`,
              'Content-Type': 'application/json',
            }
          );
        } catch (e2) {
          // If this also fails, log but don't throw - data remains preserved
          console.error(`[Archive] Failed to rename mirror repository ${owner}/${repo}:`, e2);
          console.log(`[Archive] Repository ${owner}/${repo} remains accessible but not marked as archived`);
          return;
        }
      }
      
      console.log(`[Archive] Successfully marked mirror repository ${owner}/${repo} as archived (renamed to ${archivedName})`);
      
      // Also try to reduce sync frequency to prevent unnecessary API calls
      // This is optional - if it fails, the repo is still preserved
      try {
        await httpPatch(
          `${client.url}/api/v1/repos/${owner}/${archivedName}`,
          {
            mirror_interval: "0h", // Disable automatic syncing; manual sync is still available
          },
          {
            Authorization: `token ${client.token}`,
            'Content-Type': 'application/json',
          }
        );
        console.log(`[Archive] Disabled automatic syncs for ${owner}/${archivedName}; manual sync only`);
      } catch (intervalError) {
        // Non-critical - repo is still preserved even if we can't change interval
        console.debug(`[Archive] Could not disable mirror interval (non-critical):`, intervalError);
      }
    } else {
      // For non-mirror repositories, use Gitea's native archive feature
      // This makes the repository read-only but preserves all data
      console.log(`[Archive] Archiving regular repository ${owner}/${repo}`);
      
      const response = await httpPatch(
        `${client.url}/api/v1/repos/${owner}/${repo}`,
        {
          archived: true,
        },
        {
          Authorization: `token ${client.token}`,
          'Content-Type': 'application/json',
        }
      );
      
      if (response.status >= 400) {
        // If archive fails, log but data is still preserved in Gitea
        console.error(`[Archive] Failed to archive repository ${owner}/${repo}: ${response.status}`);
        console.log(`[Archive] Repository ${owner}/${repo} remains accessible but not marked as archived`);
        return;
      }
      
      console.log(`[Archive] Successfully archived repository ${owner}/${repo} (now read-only)`);
    }
  } catch (error) {
    // Even on error, the repository data is preserved in Gitea
    // We just couldn't mark it as archived
    console.error(`[Archive] Could not mark repository ${owner}/${repo} as archived:`, error);
    console.log(`[Archive] Repository ${owner}/${repo} data is preserved but not marked as archived`);
    // Don't throw - we want cleanup to continue for other repos
  }
}
