import {
  type ProviderInstrumentationTarget,
  resolveProviderInstrumentationPort,
} from "../providers/instrumentation.js";
import { redactValue } from "./redaction.js";

/**
 * The normalized category of actor that caused application activity.
 *
 * Actors describe who or what performed work for request context,
 * authorization, audit logs, and diagnostics. They do not authenticate the
 * request by themselves.
 */
export type ActivityActorType = "anonymous" | "service" | "system" | "user";

/**
 * Whether an audited activity completed successfully or intentionally records
 * a failed attempt.
 */
export type AuditOutcome = "success" | "failure";

/**
 * JSON-like metadata values accepted by activity and audit descriptors.
 *
 * Metadata should stay intentionally small. Prefer stable IDs and short labels
 * over full request bodies, secrets, PHI, or PII.
 */
export type ActivityMetadataValue =
  | ActivityMetadataValue[]
  | boolean
  | null
  | number
  | string
  | { [key: string]: ActivityMetadataValue | undefined };

/**
 * Additional structured metadata attached to actors, tenants, resources, or
 * audit entries.
 */
export type ActivityMetadata = Record<
  string,
  ActivityMetadataValue | undefined
>;

/**
 * A normalized descriptor for the person, service, or system process that
 * caused application activity.
 *
 * Store this on application context as `ctx.actor` so routes, use cases, jobs,
 * policies, audit logs, and devtools share one identity shape.
 */
export interface ActivityActor {
  /**
   * The actor category.
   */
  type: ActivityActorType;
  /**
   * Stable application ID for this actor, when known.
   */
  id?: string;
  /**
   * Human-readable label for diagnostics and audit views.
   */
  displayName?: string;
  /**
   * Small, redaction-safe metadata about the actor.
   */
  metadata?: ActivityMetadata;
}

/**
 * A normalized tenant/account/workspace scope for activity.
 *
 * This is a context value used by audit logs, authorization, and diagnostics.
 * It does not create, load, or persist a tenant record.
 */
export interface ActivityTenant {
  /**
   * Stable tenant/account/workspace ID.
   */
  id: string;
  /**
   * Optional human-readable tenant slug.
   */
  slug?: string;
  /**
   * Small, redaction-safe metadata about the tenant.
   */
  metadata?: ActivityMetadata;
}

/**
 * A normalized descriptor for the business object affected by an audit entry.
 */
export interface ActivityResource {
  /**
   * Resource type, usually a singular domain noun such as "post", "invoice",
   * or "appointment".
   */
  type: string;
  /**
   * Stable resource ID, when known.
   */
  id?: string;
  /**
   * Human-readable resource label for audit views.
   */
  name?: string;
  /**
   * Small, redaction-safe metadata about the resource.
   */
  metadata?: ActivityMetadata;
}

/**
 * A normalized audit/activity log entry.
 *
 * Application code usually records entries through an audit port wrapped with
 * `createAmbientAuditLog(...)` from `@beignet/core/server`, which fills
 * missing actor, tenant, request ID, and trace ID fields from the ambient
 * request context at record time. Durability depends on the `AuditLogPort`
 * implementation.
 */
export interface AuditLogEntry {
  /**
   * Stable action name, usually namespaced by feature and workflow.
   *
   * @example "posts.publish"
   */
  action: string;
  /**
   * Actor that caused the activity.
   */
  actor: ActivityActor;
  /**
   * Timestamp assigned when the activity occurred.
   */
  occurredAt: Date;
  /**
   * Whether the activity succeeded or records a failed attempt.
   */
  outcome: AuditOutcome;
  /**
   * Small, redaction-safe metadata about the activity.
   */
  metadata?: ActivityMetadata;
  /**
   * Optional human-readable audit message.
   */
  message?: string;
  /**
   * Request correlation ID, when the activity originated from a request or
   * background context.
   */
  requestId?: string;
  /**
   * Business resource affected by the activity.
   */
  resource?: ActivityResource;
  /**
   * Tenant/account/workspace scope for the activity.
   */
  tenant?: ActivityTenant;
  /**
   * Trace correlation ID, when tracing is enabled.
   */
  traceId?: string;
}

/**
 * Input accepted by `AuditLogPort.record(...)`.
 *
 * `actor`, `occurredAt`, and `outcome` are optional at call sites. Wrappers
 * such as `createAmbientAuditLog(...)` fill a missing actor from the ambient
 * request context. Adapters that store audit entries should call
 * `normalizeAuditLogEntry(...)` before persistence or otherwise apply
 * equivalent defaults; entries without an actor normalize to an anonymous
 * actor.
 */
export type AuditLogEntryInput = Omit<
  AuditLogEntry,
  "actor" | "occurredAt" | "outcome"
> & {
  actor?: ActivityActor;
  occurredAt?: Date;
  outcome?: AuditOutcome;
};

/**
 * App-facing port for audit/activity logging.
 *
 * Production implementations should usually write to a durable database table,
 * append-only log, or external audit service. Tests can use an in-memory
 * adapter. Application code should depend on this interface, not on a concrete
 * audit provider.
 */
export interface AuditLogPort {
  /**
   * Persist or capture an audit entry.
   */
  record(entry: AuditLogEntryInput): Promise<void> | void;
}

/**
 * In-memory audit log port used by tests and local examples.
 */
export interface MemoryAuditLogPort extends AuditLogPort {
  /**
   * Captured, normalized, redacted audit entries.
   */
  entries: AuditLogEntry[];
}

/**
 * Options shared by audit log wrappers and in-memory audit adapters.
 */
export interface AuditLogOptions {
  /**
   * Optional final redaction/customization step applied after Beignet's default
   * metadata redaction.
   */
  redact?: (entry: AuditLogEntry) => AuditLogEntry;
}

/**
 * Create an anonymous actor descriptor for unauthenticated activity.
 *
 * This helper only creates a normalized context value. It does not perform
 * authentication.
 *
 * @example
 * ```ts
 * const actor = createAnonymousActor();
 * ```
 *
 * @param options - Optional display name or metadata to include.
 * @returns An activity actor with `type: "anonymous"`.
 */
export function createAnonymousActor(
  options: Omit<ActivityActor, "type"> = {},
): ActivityActor {
  return { type: "anonymous", ...options };
}

/**
 * Create a service actor descriptor for work initiated by another service or
 * integration.
 *
 * This is useful for webhooks, internal service calls, or integration-driven
 * background jobs.
 *
 * @example
 * ```ts
 * const actor = createServiceActor("stripe-webhook");
 * ```
 *
 * @param id - Stable service or integration ID.
 * @param options - Optional display name or metadata to include.
 * @returns An activity actor with `type: "service"`.
 */
export function createServiceActor(
  id: string,
  options: Omit<ActivityActor, "type" | "id"> = {},
): ActivityActor {
  return { type: "service", id, ...options };
}

/**
 * Create a system actor descriptor for framework or app-owned background work.
 *
 * Use this for schedules, scripts, maintenance jobs, and other work that
 * is not directly caused by a user or external service.
 *
 * @example
 * ```ts
 * const actor = createSystemActor("nightly-maintenance");
 * ```
 *
 * @param id - Stable system actor ID. Defaults to `"system"`.
 * @param options - Optional display name or metadata to include.
 * @returns An activity actor with `type: "system"`.
 */
export function createSystemActor(
  id = "system",
  options: Omit<ActivityActor, "type" | "id"> = {},
): ActivityActor {
  return { type: "system", id, ...options };
}

/**
 * Create a user actor descriptor for authenticated user activity.
 *
 * This helper only normalizes a known user ID for context, authorization,
 * audit, and diagnostics. It does not verify a session or load a user record.
 * Resolve authentication first, then call this helper with the authenticated
 * user ID.
 *
 * @example
 * ```ts
 * const actor = createUserActor(session.user.id, {
 *   displayName: session.user.name,
 * });
 * ```
 *
 * @param id - Stable application user ID.
 * @param options - Optional display name or metadata to include.
 * @returns An activity actor with `type: "user"`.
 */
export function createUserActor(
  id: string,
  options: Omit<ActivityActor, "type" | "id"> = {},
): ActivityActor {
  return { type: "user", id, ...options };
}

/**
 * Create a tenant/account/workspace descriptor for request or background
 * context.
 *
 * This helper only creates a normalized context value used by audit,
 * authorization, logs, and diagnostics. It does not create, load, or persist a
 * tenant record.
 *
 * @example
 * ```ts
 * const tenant = createTenant(session.organizationId, {
 *   slug: session.organizationSlug,
 * });
 * ```
 *
 * @param id - Stable tenant/account/workspace ID.
 * @param options - Optional slug or metadata to include.
 * @returns A normalized activity tenant descriptor.
 */
export function createTenant(
  id: string,
  options: Omit<ActivityTenant, "id"> = {},
): ActivityTenant {
  return { id, ...options };
}

/**
 * Fill default audit fields for an input entry.
 *
 * @param entry - Partial audit entry accepted by `AuditLogPort.record(...)`.
 * @returns A complete audit entry with `actor`, `occurredAt`, and `outcome`
 * populated. Entries without an actor default to an anonymous actor.
 */
export function normalizeAuditLogEntry(
  entry: AuditLogEntryInput,
): AuditLogEntry {
  return {
    ...entry,
    actor: entry.actor ?? createAnonymousActor(),
    occurredAt: entry.occurredAt ?? new Date(),
    outcome: entry.outcome ?? "success",
  };
}

/**
 * Redact metadata on an already-normalized audit entry.
 *
 * This redacts metadata values on the entry, actor, tenant, and resource using
 * the default redaction rules from `redactValue(...)`.
 *
 * @param entry - Audit entry to redact.
 * @returns A shallow copy with redacted metadata fields.
 */
export function redactAuditLogEntry(entry: AuditLogEntry): AuditLogEntry {
  return {
    ...entry,
    actor: {
      ...entry.actor,
      metadata: entry.actor.metadata
        ? redactValue(entry.actor.metadata)
        : entry.actor.metadata,
    },
    tenant: entry.tenant
      ? {
          ...entry.tenant,
          metadata: entry.tenant.metadata
            ? redactValue(entry.tenant.metadata)
            : entry.tenant.metadata,
        }
      : entry.tenant,
    resource: entry.resource
      ? {
          ...entry.resource,
          metadata: entry.resource.metadata
            ? redactValue(entry.resource.metadata)
            : entry.resource.metadata,
        }
      : entry.resource,
    metadata: entry.metadata ? redactValue(entry.metadata) : entry.metadata,
  };
}

/**
 * Wrap an audit log port with default audit metadata redaction.
 *
 * Use this around durable adapters so application code can record entries
 * without each call site remembering to redact metadata.
 *
 * @param audit - Underlying audit log port to write to after redaction.
 * @param options - Optional final redaction/customization hook.
 * @returns An audit log port that normalizes and redacts before writing.
 */
export function createRedactedAuditLog(
  audit: AuditLogPort,
  options: AuditLogOptions = {},
): AuditLogPort {
  return {
    record(entry) {
      const normalized = normalizeAuditLogEntry(entry);
      const redacted = options.redact
        ? options.redact(redactAuditLogEntry(normalized))
        : redactAuditLogEntry(normalized);
      return audit.record(redacted);
    },
  };
}

/**
 * Options for wrapping an audit log with instrumentation emission.
 */
export interface InstrumentedAuditLogOptions {
  /**
   * Durable audit log to write first.
   */
  audit: AuditLogPort;
  /**
   * Instrumentation sink, port, or ports object. Pass the app ports object so
   * the sink (`ports.instrumentation`, then `ports.devtools`) is resolved
   * lazily on each write and observes provider startup order.
   */
  instrumentation?: ProviderInstrumentationTarget;
  /**
   * Whether to emit instrumentation events. Defaults to true.
   */
  emit?: boolean;
  /**
   * Optional app-owned redactor applied after Beignet's audit redaction.
   */
  redact?: (entry: AuditLogEntry) => AuditLogEntry;
}

function prepareInstrumentedEntry(
  input: AuditLogEntryInput,
  redact?: (entry: AuditLogEntry) => AuditLogEntry,
): AuditLogEntry {
  const redacted = redactAuditLogEntry(normalizeAuditLogEntry(input));
  return redact ? redact(redacted) : redacted;
}

function auditSummary(entry: AuditLogEntry): string {
  const resource = entry.resource?.id
    ? `${entry.resource.type}:${entry.resource.id}`
    : entry.resource?.type;
  const outcome = entry.outcome === "failure" ? "failed" : "succeeded";
  return resource
    ? `${entry.action} ${outcome} for ${resource}`
    : `${entry.action} ${outcome}`;
}

/**
 * Wrap an audit log so durable audit writes also appear in instrumentation
 * sinks such as devtools.
 *
 * Instrumentation failures are ignored so audit persistence remains the
 * source of truth.
 *
 * @example
 * ```ts
 * const audit = createInstrumentedAuditLog({
 *   audit: createDrizzleSqliteAuditLogPort(db),
 *   instrumentation: ports,
 * });
 * ```
 */
export function createInstrumentedAuditLog(
  options: InstrumentedAuditLogOptions,
): AuditLogPort {
  return {
    async record(input) {
      const entry = prepareInstrumentedEntry(input, options.redact);

      await options.audit.record(entry);

      if (options.emit === false) return;

      const port = resolveProviderInstrumentationPort(options.instrumentation);
      if (!port) return;

      try {
        port.record({
          type: "custom",
          watcher: "audit",
          name: entry.action,
          label: "Audit",
          summary: auditSummary(entry),
          requestId: entry.requestId,
          traceId: entry.traceId,
          details: {
            action: entry.action,
            actor: entry.actor,
            tenant: entry.tenant,
            resource: entry.resource,
            outcome: entry.outcome,
            message: entry.message,
            metadata: entry.metadata,
            occurredAt: entry.occurredAt.toISOString(),
          },
        });
      } catch {
        // Instrumentation is an observer; durable audit writes must not depend on it.
      }
    },
  };
}

/**
 * Create an in-memory audit log for tests and local examples.
 *
 * Entries are normalized and redacted before being pushed into the shared
 * `entries` array.
 *
 * @example
 * ```ts
 * const audit = createMemoryAuditLog();
 * await audit.record({
 *   action: "posts.publish",
 *   actor: createUserActor("user_1"),
 * });
 * expect(audit.entries).toHaveLength(1);
 * ```
 *
 * @param entries - Optional backing array, useful when tests need shared state.
 * @param options - Optional final redaction/customization hook.
 * @returns An in-memory audit log port with captured `entries`.
 */
export function createMemoryAuditLog(
  entries: AuditLogEntry[] = [],
  options: AuditLogOptions = {},
): MemoryAuditLogPort {
  return {
    entries,
    record(entry) {
      const normalized = normalizeAuditLogEntry(entry);
      entries.push(
        options.redact
          ? options.redact(redactAuditLogEntry(normalized))
          : redactAuditLogEntry(normalized),
      );
    },
  };
}
