import db from "../db.server";
import shopify from "../shopify.server";
import { adminLog } from "./admin-logger.server";
import {
  JobType,
  enqueueJob,
  registerWorker,
  type EmergencyCleanupData,
  type GenerateUrlMappingsData,
  type ProcessBulkImportData,
} from "./jobs.server";
import { syncRulesToMetafield, clearShopMetafields } from "./metafield-sync.server";
import { urlMappingService } from "./url-mapping.server";

/**
 * Workers (CDC §X, §XVI, QJ-07).
 *
 * IMPORTANT: workers run with no incoming `request`, so we cannot call
 * `authenticate.admin(request)`. Instead we use `shopify.unauthenticated.admin(shop)`
 * which pulls the offline session out of PrismaSessionStorage. The shop must be
 * installed for this to succeed.
 */

const BULK_IMPORT_CHUNK_SIZE = 25;
const BULK_IMPORT_MAX_ENTRIES = 100_000;
const BULK_IMPORT_CHUNK_DELAY_MS = 1_000;
const BULK_IMPORT_RULE_KEYS = new Set([
  "noindex_handles",
  "noindex_specific_url",
  "noindex_custom_url_parameters",
]);

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function parseCsvEntries(raw: string): string[] {
  const seen = new Set<string>();
  const out: string[] = [];
  for (const part of raw.split(/[,\n\r]+/)) {
    const trimmed = part.trim();
    if (!trimmed) continue;
    if (seen.has(trimmed)) continue;
    seen.add(trimmed);
    out.push(trimmed);
  }
  return out;
}

async function getOfflineAdmin(shop: string) {
  const { admin } = await shopify.unauthenticated.admin(shop);
  return admin;
}

async function handleGenerateUrlMappings({
  businessData,
}: {
  businessData: GenerateUrlMappingsData;
}): Promise<unknown> {
  const { shopId } = businessData;
  // generate() reads the DB. sync() needs the admin for the metafields mutation.
  const map = await urlMappingService.generate(shopId);
  const admin = await getOfflineAdmin(shopId);
  const result = await urlMappingService.sync(shopId, admin);
  if (!result.success) {
    throw new Error(result.error ?? "url_map sync failed");
  }
  return {
    handles: Object.keys(map.handles).length,
    paths: map.paths.length,
    size: result.size ?? null,
  };
}

async function handleEmergencyCleanup({
  businessData,
}: {
  businessData: EmergencyCleanupData;
}): Promise<unknown> {
  const { shopId } = businessData;
  const admin = await getOfflineAdmin(shopId);

  // 1. Disable all rules in DB.
  const updated = await db.shopRule.updateMany({
    where: { shopId },
    data: { enabled: false, customValue: null },
  });

  // 2. Sync the now-empty config so the storefront stops applying directives.
  await syncRulesToMetafield(admin, shopId);

  // 3. Flip the `active` metafield off for belt-and-braces.
  await clearShopMetafields(admin);

  await adminLog("warn", `Emergency cleanup completed`, {
    shopId,
    rulesDisabled: updated.count,
  });

  return { rulesDisabled: updated.count };
}

async function handleProcessBulkImport({
  businessData,
}: {
  businessData: ProcessBulkImportData;
}): Promise<unknown> {
  const { shopId, ruleKey, csvText } = businessData;

  if (!BULK_IMPORT_RULE_KEYS.has(ruleKey)) {
    throw new Error(`Unsupported ruleKey for bulk import: ${ruleKey}`);
  }

  const all = parseCsvEntries(csvText);
  if (all.length === 0) {
    return { processed: 0, accepted: 0 };
  }

  const capped = all.slice(0, BULK_IMPORT_MAX_ENTRIES);
  const accepted: string[] = [];

  // Process in chunks of 25 with a 1s pause between chunks (CDC QJ-07).
  // The pause guards against overloading downstream metafield sync if the
  // worker is later extended to push partial state.
  for (let i = 0; i < capped.length; i += BULK_IMPORT_CHUNK_SIZE) {
    const chunk = capped.slice(i, i + BULK_IMPORT_CHUNK_SIZE);
    for (const entry of chunk) {
      // Per-entry validation: drop entries with control chars or > 200 chars.
      // Anything more aggressive belongs to the rule-specific Liquid logic.
      if (entry.length > 200) continue;
      // eslint-disable-next-line no-control-regex
      if (/[\x00-\x1f\x7f]/.test(entry)) continue;
      accepted.push(entry);
    }
    if (i + BULK_IMPORT_CHUNK_SIZE < capped.length) {
      await sleep(BULK_IMPORT_CHUNK_DELAY_MS);
    }
  }

  const merged = accepted.join(", ");

  await db.shopRule.upsert({
    where: { shopId_ruleKey: { shopId, ruleKey } },
    create: {
      shopId,
      ruleKey,
      enabled: accepted.length > 0,
      customValue: merged || null,
    },
    update: {
      enabled: accepted.length > 0,
      customValue: merged || null,
    },
  });

  // Re-generate the URL map after the bulk import settles.
  await enqueueJob(JobType.GENERATE_URL_MAPPINGS, { shopId });

  return {
    submitted: all.length,
    capped: capped.length,
    accepted: accepted.length,
    rejected: capped.length - accepted.length,
  };
}

export async function registerAllWorkers(): Promise<void> {
  await registerWorker<GenerateUrlMappingsData>(
    JobType.GENERATE_URL_MAPPINGS,
    handleGenerateUrlMappings,
  );
  await registerWorker<EmergencyCleanupData>(
    JobType.EMERGENCY_CLEANUP,
    handleEmergencyCleanup,
  );
  await registerWorker<ProcessBulkImportData>(
    JobType.PROCESS_BULK_IMPORT,
    handleProcessBulkImport,
  );
}
