/**
 * @beignet/core/error-reporting
 *
 * Provider-neutral error reporting primitives for Beignet applications.
 */

import { type RedactionOptions, redactValue } from "../ports/redaction.js";
import {
  DEFAULT_ERROR_REPORTING_TIMEOUT_MS,
  errorReportingObserverTimeout,
  runErrorReportingOperation,
} from "./internal.js";

export {
  DEFAULT_ERROR_REPORTING_TIMEOUT_MS,
  ErrorReportingTimeoutError,
} from "./internal.js";

type MaybePromise<T> = T | Promise<T>;

/**
 * JSON-compatible value accepted by error reporting context, tags, and extras.
 */
export type ErrorReportJsonValue =
  | null
  | boolean
  | number
  | string
  | readonly ErrorReportJsonValue[]
  | { readonly [key: string]: ErrorReportJsonValue };

/**
 * Error severity level shared by common reporting providers.
 */
export type ErrorReportLevel = "fatal" | "error" | "warning" | "info" | "debug";

/**
 * User or actor attached to a reported error.
 */
export type ErrorReportUser = {
  id?: string;
  email?: string;
  username?: string;
  ipAddress?: string;
} & Record<string, ErrorReportJsonValue | undefined>;

/**
 * Provider-neutral structured context attached to a reported error.
 */
export type ErrorReportContext = Record<
  string,
  ErrorReportJsonValue | undefined
>;

/**
 * Tags used for searching, grouping, and alert routing.
 */
export type ErrorReportTags = Record<
  string,
  string | number | boolean | null | undefined
>;

/**
 * Options accepted by exception and message capture calls.
 */
export type ErrorReportOptions = {
  level?: ErrorReportLevel;
  user?: ErrorReportUser | null;
  tags?: ErrorReportTags;
  contexts?: Record<string, ErrorReportContext | undefined>;
  extra?: Record<string, ErrorReportJsonValue | undefined>;
  fingerprint?: readonly string[];
  mechanism?: string;
  handled?: boolean;
  requestId?: string;
  traceId?: string;
  spanId?: string;
  parentSpanId?: string;
  traceparent?: string;
};

/**
 * Arguments accepted by `tryReportException(...)`.
 */
export interface TryReportExceptionOptions {
  /** Reporter that owns the capture. Omit it to make reporting a no-op. */
  reporter?: ErrorReporterPort | ErrorReporterResolver;
  /** Original application or infrastructure error. */
  error: unknown;
  /** Structured metadata attached to the report. */
  reportOptions?: ErrorReportOptions;
  /**
   * Maximum time allowed for capture and, separately, the failure observer.
   * Set to `false` only when the reporting implementation is intentionally
   * unbounded.
   *
   * @default 1000
   */
  timeoutMs?: number | false;
  /**
   * Observer for reporter failures. Observer failures are also isolated.
   */
  onReporterError?: (args: {
    error: unknown;
    reportingError: unknown;
  }) => MaybePromise<void>;
}

/**
 * Result returned by a reporting provider after capture.
 */
export type ErrorReportResult = {
  id?: string;
};

/**
 * Flush options accepted by providers that buffer events.
 */
export type ErrorReporterFlushOptions = {
  timeoutMs?: number;
};

/**
 * App-facing error reporting port.
 */
export type ErrorReporterPort = {
  captureException(
    error: unknown,
    options?: ErrorReportOptions,
  ): Promise<ErrorReportResult>;
  captureMessage(
    message: string,
    options?: ErrorReportOptions,
  ): Promise<ErrorReportResult>;
  setUser(user: ErrorReportUser | null): MaybePromise<void>;
  setTags(tags: ErrorReportTags): MaybePromise<void>;
  setContext(
    name: string,
    context: ErrorReportContext | null,
  ): MaybePromise<void>;
  flush(options?: ErrorReporterFlushOptions): Promise<boolean>;
};

/** Lazy reporter resolver evaluated inside the best-effort capture deadline. */
export type ErrorReporterResolver = () => MaybePromise<
  ErrorReporterPort | undefined
>;

/**
 * Captured exception stored by `createMemoryErrorReporter(...)`.
 */
export type MemoryReportedException = {
  type: "exception";
  error: unknown;
  options?: ErrorReportOptions;
  id: string;
};

/**
 * Captured message stored by `createMemoryErrorReporter(...)`.
 */
export type MemoryReportedMessage = {
  type: "message";
  message: string;
  options?: ErrorReportOptions;
  id: string;
};

/**
 * Captured report stored by `createMemoryErrorReporter(...)`.
 */
export type MemoryErrorReport = MemoryReportedException | MemoryReportedMessage;

/**
 * In-memory reporter state exposed for tests.
 */
export type MemoryErrorReporterPort = ErrorReporterPort & {
  reports: MemoryErrorReport[];
  user: ErrorReportUser | null;
  tags: ErrorReportTags;
  contexts: Map<string, ErrorReportContext>;
  reset(): void;
};

/**
 * Options accepted by `createMemoryErrorReporter(...)`.
 */
export type CreateMemoryErrorReporterOptions = {
  onCapture?: (report: MemoryErrorReport) => MaybePromise<void>;
};

/**
 * Create a no-op reporter for apps that want to bind the port without sending
 * events.
 */
export function createNoopErrorReporter(): ErrorReporterPort {
  return {
    async captureException() {
      return {};
    },
    async captureMessage() {
      return {};
    },
    setUser() {},
    setTags() {},
    setContext() {},
    async flush() {
      return true;
    },
  };
}

/**
 * Create an in-memory reporter for tests and local assertions.
 */
export function createMemoryErrorReporter(
  options: CreateMemoryErrorReporterOptions = {},
): MemoryErrorReporterPort {
  let nextId = 1;
  const reports: MemoryErrorReport[] = [];
  const contexts = new Map<string, ErrorReportContext>();
  const port: MemoryErrorReporterPort = {
    reports,
    user: null,
    tags: {},
    contexts,
    async captureException(error, reportOptions) {
      const report: MemoryReportedException = {
        type: "exception",
        error,
        options: withAmbientState(port, reportOptions),
        id: String(nextId++),
      };
      reports.push(report);
      await options.onCapture?.(report);
      return { id: report.id };
    },
    async captureMessage(message, reportOptions) {
      const report: MemoryReportedMessage = {
        type: "message",
        message,
        options: withAmbientState(port, reportOptions),
        id: String(nextId++),
      };
      reports.push(report);
      await options.onCapture?.(report);
      return { id: report.id };
    },
    setUser(user) {
      port.user = user;
    },
    setTags(tags) {
      port.tags = { ...port.tags, ...tags };
    },
    setContext(name, context) {
      if (context === null) {
        contexts.delete(name);
        return;
      }
      contexts.set(name, context);
    },
    async flush() {
      return true;
    },
    reset() {
      reports.length = 0;
      contexts.clear();
      port.user = null;
      port.tags = {};
      nextId = 1;
    },
  };

  return port;
}

/**
 * Report an exception through any `ErrorReporterPort`.
 */
export function reportException(
  reporter: ErrorReporterPort,
  error: unknown,
  options?: ErrorReportOptions,
): Promise<ErrorReportResult> {
  return reporter.captureException(error, options);
}

/**
 * Report a message through any `ErrorReporterPort`.
 */
export function reportMessage(
  reporter: ErrorReporterPort,
  message: string,
  options?: ErrorReportOptions,
): Promise<ErrorReportResult> {
  return reporter.captureMessage(message, options);
}

/**
 * Best-effort exception capture for runtime boundaries.
 *
 * Missing reporters, reporter failures, and reporter-failure observer errors
 * resolve to `undefined` so diagnostics cannot replace application behavior.
 */
export async function tryReportException(
  options: TryReportExceptionOptions,
): Promise<ErrorReportResult | undefined> {
  const reporterSource = options.reporter;
  if (!reporterSource) return undefined;

  const timeoutMs = options.timeoutMs ?? DEFAULT_ERROR_REPORTING_TIMEOUT_MS;

  try {
    return await runErrorReportingOperation(async () => {
      const reporter =
        typeof reporterSource === "function"
          ? await reporterSource()
          : reporterSource;
      if (!reporter) return undefined;
      return reporter.captureException(options.error, options.reportOptions);
    }, timeoutMs);
  } catch (reportingError) {
    try {
      if (options.onReporterError) {
        await runErrorReportingOperation(
          () =>
            options.onReporterError?.({
              error: options.error,
              reportingError,
            }),
          errorReportingObserverTimeout(timeoutMs),
        );
      }
    } catch {
      // Reporter failure observers must not replace application behavior.
    }
    return undefined;
  }
}

/**
 * Redact structured error-report metadata with Beignet's shared sensitive-key
 * rules. The original exception is intentionally not part of this operation.
 */
export function redactErrorReportOptions(
  options: ErrorReportOptions,
  redactionOptions: RedactionOptions = {},
): ErrorReportOptions {
  return {
    ...options,
    user: options.user
      ? redactValue(options.user, redactionOptions)
      : options.user,
    tags: options.tags
      ? redactValue(options.tags, redactionOptions)
      : options.tags,
    contexts: options.contexts
      ? redactValue(options.contexts, redactionOptions)
      : options.contexts,
    extra: options.extra
      ? redactValue(options.extra, redactionOptions)
      : options.extra,
  };
}

function withAmbientState(
  port: MemoryErrorReporterPort,
  options: ErrorReportOptions | undefined,
): ErrorReportOptions | undefined {
  const contexts =
    port.contexts.size > 0 ? Object.fromEntries(port.contexts) : undefined;
  const hasTags = Object.keys(port.tags).length > 0;
  if (!port.user && !hasTags && !contexts) return options;

  return {
    ...options,
    user: options?.user ?? port.user ?? undefined,
    tags: hasTags ? { ...port.tags, ...options?.tags } : options?.tags,
    contexts:
      contexts || options?.contexts
        ? { ...contexts, ...options?.contexts }
        : undefined,
  };
}
