import type { CollectionAfterReadHook } from "payload";
import type { AuditEntry } from "@repo/core-shared/audit";
import { truncateIp } from "@repo/core-shared/audit";
import type { IAuditLog } from "../audit-log.interface";

export type AuditAfterReadHookOpts = {
  auditLog: IAuditLog;
  /** Resource type for AuditEntry.resource.type (e.g., "users"). */
  resourceType: string;
  /** Feature attribution for AuditEntry.scope.feature. */
  feature: string;
  /** Deployment environment. */
  environment: string;
  /** Tenant resolver — single-tenant projects return "default". */
  resolveTenant: (req: { user?: { id: string; tenantId?: string } | null }) => string;
  /** Whether this collection contains PII. Propagates to every entry. */
  containsPii: boolean;
  /** Optional PII categories applicable to all entries from this collection. */
  piiCategories?: string[];
  /** Optional predicate; return true to skip emitting an entry. */
  shouldSkip?: (args: { req: unknown; doc: { id: string | number } }) => boolean;
};

/**
 * Payload afterRead hook factory. Emits a VIEW AuditEntry per document read.
 * Per-collection opt-in: install via `hooks.afterRead: [createAuditAfterReadHook(...)]`
 * on the collection config.
 *
 * Fire-and-forget: a failing audit sink does NOT propagate up to break the
 * user-facing read. Failures emit a structured error to stderr (visible to
 * the same log shipper as audit entries themselves).
 *
 * Combine with use-case-level record() calls for app-facing reads; this hook
 * covers direct CMS/admin/programmatic reads. The use-case path captures
 * "why" (reason); this hook captures "the system saw this doc".
 */
export function createAuditAfterReadHook(
  opts: AuditAfterReadHookOpts,
): CollectionAfterReadHook {
  return async ({ doc, req }) => {
    if (opts.shouldSkip?.({ req, doc: doc as { id: string | number } })) {
      return doc;
    }

    const actor = (req as { user?: { id: string; roles?: string[]; tenantId?: string } | null }).user;
    const entry: AuditEntry = {
      actorId: actor?.id ?? "system",
      actorType: actor ? "user" : "system",
      actorRoles: actor?.roles ?? [],
      action: "VIEW",
      resource: {
        type: opts.resourceType,
        id: typeof doc.id === "string" || typeof doc.id === "number" ? String(doc.id) : undefined,
      },
      at: new Date(),
      scope: {
        feature: opts.feature,
        environment: opts.environment,
        tenant: opts.resolveTenant(req as { user?: { id: string; tenantId?: string } | null }),
      },
      reason: "payload-afterRead-hook",
      from: {
        ipTruncated: extractIpTruncated(req) ?? "internal",
        userAgent: extractUserAgent(req) ?? "payload-internal",
      },
      containsPii: opts.containsPii,
      piiCategories: opts.piiCategories,
      outcome: "success",
    };

    // Fire-and-forget — never break the read.
    void opts.auditLog.record(entry).catch((err: unknown) => {
      process.stderr.write(
        JSON.stringify({
          _type: "audit-hook-error",
          hook: "afterRead",
          resourceType: opts.resourceType,
          error: String(err),
          at: new Date().toISOString(),
        }) + "\n",
      );
    });

    return doc;
  };
}

function extractIpTruncated(req: unknown): string | undefined {
  const r = req as { ip?: string; headers?: Record<string, string | string[] | undefined> };
  const rawIp = r.ip ?? r.headers?.["x-forwarded-for"];
  if (!rawIp) return undefined;
  const candidate = Array.isArray(rawIp) ? rawIp[0]! : rawIp.split(",")[0]!.trim();
  try {
    return truncateIp(candidate);
  } catch {
    return undefined;
  }
}

function extractUserAgent(req: unknown): string | undefined {
  const r = req as { headers?: Record<string, string | string[] | undefined> };
  const ua = r.headers?.["user-agent"];
  if (!ua) return undefined;
  return Array.isArray(ua) ? ua[0] : ua;
}
