import { getPayload as _getPayload } from "payload";
import type { SanitizedConfig } from "payload";
import type { AuditLogProtocol } from "@repo/core-shared/di";
import type { IConsent } from "./consent.interface";
import type {
  ConsentCategory,
  ConsentGrantMeta,
  ConsentState,
  UserConsentState,
} from "./consent-types";

type PayloadAPI = {
  findByID(args: {
    collection: string;
    id: string;
    overrideAccess: true;
  }): Promise<Record<string, unknown>>;
  update(args: {
    collection: string;
    id: string;
    data: Record<string, unknown>;
    overrideAccess: true;
  }): Promise<unknown>;
};

type GetPayload = (args: { config: SanitizedConfig }) => Promise<PayloadAPI>;

/**
 * Payload-backed IConsent. Reads and writes the `users.consentState` JSON
 * field on the Payload `users` collection. Emits CONSENT_GRANT /
 * CONSENT_WITHDRAW audit entries via the injected auditLog.
 *
 * Instantiated per-user: the userId is provided at construction time.
 * Call `load()` before first use in production to hydrate the in-memory
 * cache from Payload. Mutations (grant / withdraw) update the cache
 * synchronously and persist to Payload asynchronously in the same call.
 *
 * The getPayload param is injectable for tests; production code omits it
 * and gets the real `getPayload` from `payload`.
 */
export class PayloadConsent implements IConsent {
  private cache = new Map<ConsentCategory, UserConsentState>();

  constructor(
    private readonly userId: string,
    private readonly config: SanitizedConfig,
    private readonly auditLog: AuditLogProtocol,
    private readonly getPayloadFn: GetPayload = _getPayload as unknown as GetPayload,
  ) {}

  /** Hydrate the in-memory cache from Payload. */
  async load(): Promise<void> {
    const payload = await this.getPayloadFn({ config: this.config });
    const doc = await payload.findByID({
      collection: "users",
      id: this.userId,
      overrideAccess: true,
    });
    const rawState = doc["consentState"];
    if (!Array.isArray(rawState)) return;
    this.cache.clear();
    for (const raw of rawState) {
      if (raw && typeof raw === "object") {
        const entry = deserializeEntry(raw as Record<string, unknown>);
        this.cache.set(entry.category, entry);
      }
    }
  }

  isGranted(category: ConsentCategory): boolean {
    return this.cache.get(category)?.state === "granted";
  }

  async grant(
    category: ConsentCategory,
    meta?: ConsentGrantMeta,
  ): Promise<void> {
    const now = new Date();
    const existing = this.cache.get(category);
    const entry: UserConsentState = {
      category,
      state: "granted",
      grantedAt: now,
      withdrawnAt: existing?.withdrawnAt,
      bannerVersion: meta?.bannerVersion,
      policyVersion: meta?.policyVersion,
      method: meta?.method,
    };
    this.cache.set(category, entry);
    await this.persist();
    await this.auditLog.record({
      actorId: this.userId,
      actorType: "user",
      actorRoles: [],
      action: "CONSENT_GRANT",
      resource: { type: "consent", id: category },
      at: now,
      scope: {
        feature: "core-consent",
        environment: process.env["NODE_ENV"] ?? "development",
        tenant: "default",
      },
      from: { ipTruncated: "system", userAgent: "system" },
      containsPii: false,
      outcome: "success",
    });
  }

  async withdraw(category: ConsentCategory): Promise<void> {
    const now = new Date();
    const existing = this.cache.get(category);
    const entry: UserConsentState = {
      ...existing,
      category,
      state: "denied",
      withdrawnAt: now,
    };
    this.cache.set(category, entry);
    await this.persist();
    await this.auditLog.record({
      actorId: this.userId,
      actorType: "user",
      actorRoles: [],
      action: "CONSENT_WITHDRAW",
      resource: { type: "consent", id: category },
      at: now,
      scope: {
        feature: "core-consent",
        environment: process.env["NODE_ENV"] ?? "development",
        tenant: "default",
      },
      from: { ipTruncated: "system", userAgent: "system" },
      containsPii: false,
      outcome: "success",
    });
  }

  getCategories(): UserConsentState[] {
    return Array.from(this.cache.values());
  }

  private async persist(): Promise<void> {
    const payload = await this.getPayloadFn({ config: this.config });
    const state = Array.from(this.cache.values()).map((entry) => ({
      category: entry.category,
      state: entry.state,
      grantedAt: entry.grantedAt?.toISOString() ?? null,
      withdrawnAt: entry.withdrawnAt?.toISOString() ?? null,
      bannerVersion: entry.bannerVersion ?? null,
      policyVersion: entry.policyVersion ?? null,
      method: entry.method ?? null,
    }));
    await payload.update({
      collection: "users",
      id: this.userId,
      data: { consentState: state },
      overrideAccess: true,
    });
  }
}

function deserializeEntry(raw: Record<string, unknown>): UserConsentState {
  return {
    category: String(raw["category"]) as ConsentCategory,
    state: (raw["state"] as ConsentState) ?? "pending",
    grantedAt: raw["grantedAt"]
      ? new Date(raw["grantedAt"] as string)
      : undefined,
    withdrawnAt: raw["withdrawnAt"]
      ? new Date(raw["withdrawnAt"] as string)
      : undefined,
    bannerVersion:
      raw["bannerVersion"] != null ? String(raw["bannerVersion"]) : undefined,
    policyVersion:
      raw["policyVersion"] != null ? String(raw["policyVersion"]) : undefined,
    method: raw["method"] != null ? String(raw["method"]) : undefined,
  };
}
