import type { SanitizedConfig } from "payload";
import type { IJobQueue } from "../../jobs/job-queue.interface";
import type { AuditLogProtocol } from "../../di/bind-protocols";

/**
 * Minimal Payload API surface needed by the retention purge job.
 * Injected via getPayload for testability.
 */
export type PayloadPurgeApi = {
  find(args: {
    collection: string;
    where: Record<string, unknown>;
    limit: number;
    overrideAccess: true;
  }): Promise<{ docs: Array<Record<string, unknown>> }>;
  update(args: {
    collection: string;
    id: string | number;
    data: Record<string, unknown>;
    overrideAccess: true;
  }): Promise<unknown>;
  delete(args: {
    collection: string;
    id: string | number;
    overrideAccess: true;
  }): Promise<unknown>;
};

export type GetPayloadFn = (args: {
  config: SanitizedConfig;
}) => Promise<PayloadPurgeApi>;

export type RetentionPurgeJobDeps = {
  queue: IJobQueue;
  config: SanitizedConfig;
  getPayload: GetPayloadFn;
  auditLog?: AuditLogProtocol;
};

const MS_PER_DAY = 86_400_000;

/**
 * Parse a subset of ISO 8601 duration notation (date components: Y, M, W, D)
 * to milliseconds. Returns 0 for unrecognised patterns.
 * Approximations: 1 year = 365 days, 1 month = 30 days.
 */
export function parseDurationMs(iso: string): number {
  const match = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?$/.exec(iso);
  if (!match) return 0;
  const years = parseInt(match[1] ?? "0", 10);
  const months = parseInt(match[2] ?? "0", 10);
  const weeks = parseInt(match[3] ?? "0", 10);
  const days = parseInt(match[4] ?? "0", 10);
  return (
    years * 365 * MS_PER_DAY +
    months * 30 * MS_PER_DAY +
    weeks * 7 * MS_PER_DAY +
    days * MS_PER_DAY
  );
}

/**
 * Convert a PurgeSchedule value to the delay in ms before the next run.
 * Cron-style strings fall back to daily cadence; the Payload scheduler handles
 * actual cron-aligned firing.
 */
export function scheduleDelayMs(schedule: string): number {
  if (schedule === "weekly") return 7 * MS_PER_DAY;
  if (schedule === "monthly") return 30 * MS_PER_DAY;
  return MS_PER_DAY; // "daily" and cron fallback
}

/**
 * Build the purge handler for a single collection. The returned async function
 * is intended to be registered as a Payload job task handler.
 *
 * Per run:
 *  1. Query rows past their activeRetention period.
 *  2. Apply postDeletion.action (pseudonymize | hard-delete).
 *  3. Emit one audit entry per processed row (skipped when auditLog is absent).
 *  4. Re-enqueue itself for the next purge cycle.
 *
 * `from-last-access` uses updatedAt as a proxy; a dedicated lastAccessedAt hook
 * is deferred to Q2 per the PRD.
 */
export function buildPurgeHandler(
  collectionSlug: string,
  deps: RetentionPurgeJobDeps,
): () => Promise<void> {
  const { queue, config, getPayload, auditLog } = deps;

  const collection = config.collections.find((c) => c.slug === collectionSlug);
  if (!collection) {
    throw new Error(`retention-purge: collection not found: ${collectionSlug}`);
  }

  const retention = collection.custom?.retention;
  if (!retention) {
    throw new Error(
      `retention-purge: no retention config on collection: ${collectionSlug}`,
    );
  }

  const taskSlug = `retention-purge--${collectionSlug}`;

  return async () => {
    const payload = await getPayload({ config });
    const now = Date.now();

    if (retention.activeRetention) {
      const { duration, trigger } = retention.activeRetention;
      const retentionMs = parseDurationMs(duration);
      const cutoff = new Date(now - retentionMs).toISOString();
      const dateField = trigger === "from-creation" ? "createdAt" : "updatedAt";

      const { docs } = await payload.find({
        collection: collectionSlug,
        where: { [dateField]: { less_than: cutoff } },
        limit: 1000,
        overrideAccess: true,
      });

      const action = retention.postDeletion?.action ?? "hard-delete";

      for (const doc of docs) {
        const id = doc["id"] as string | number;

        if (action === "pseudonymize") {
          const piiFields: Record<string, null> = {};
          for (const field of collection.fields) {
            const f = field as { name?: string; custom?: { pii?: unknown } };
            if (f.name && f.custom?.pii) {
              piiFields[f.name] = null;
            }
          }
          if (Object.keys(piiFields).length > 0) {
            await payload.update({
              collection: collectionSlug,
              id,
              data: piiFields,
              overrideAccess: true,
            });
          }
        } else {
          await payload.delete({
            collection: collectionSlug,
            id,
            overrideAccess: true,
          });
        }

        if (auditLog) {
          await auditLog.record({
            actorId: "system",
            actorType: "system",
            actorRoles: [],
            action: "DELETE",
            resource: { type: collectionSlug, id: String(id) },
            at: new Date(),
            scope: {
              feature: "core-shared",
              environment: process.env["NODE_ENV"] ?? "production",
              tenant: "default",
            },
            reason: "retention-policy",
            from: { ipTruncated: "system", userAgent: "background-job" },
            containsPii: false,
            outcome: "success",
          });
        }
      }
    }

    const delay = scheduleDelayMs(retention.purgeSchedule);
    await queue.enqueue(taskSlug, {}, { runAt: new Date(now + delay) });
  };
}

/**
 * Walk all Payload collections that declare `custom.retention.purgeSchedule`
 * and schedule the first purge run for each via the provided IJobQueue.
 *
 * Call once at app startup (inside bindAll or equivalent). Idempotent per
 * queue implementation — duplicate enqueues are the queue's responsibility.
 */
export async function registerRetentionPurgeJobs(
  deps: RetentionPurgeJobDeps,
): Promise<void> {
  const { queue, config } = deps;
  const now = Date.now();

  for (const collection of config.collections) {
    const retention = collection.custom?.retention;
    if (!retention?.purgeSchedule) continue;

    const taskSlug = `retention-purge--${collection.slug}`;
    const delay = scheduleDelayMs(retention.purgeSchedule);
    await queue.enqueue(taskSlug, {}, { runAt: new Date(now + delay) });
  }
}
