import { type Redactor, redactValue } from "../ports/redaction.js";

/**
 * Logical provider watcher name used to toggle groups of instrumentation
 * events.
 */
export type ProviderInstrumentationWatcherName = string;

/**
 * Common metadata attached to every provider instrumentation event.
 */
export interface BaseProviderInstrumentationEvent {
  /**
   * Optional event identifier supplied by the caller.
   */
  id?: string;
  /**
   * ISO timestamp supplied by the caller. Devtools can also assign a timestamp
   * when one is omitted.
   */
  timestamp?: string;
  /**
   * Request correlation ID for events emitted during request handling.
   */
  requestId?: string;
  /**
   * Trace identifier for distributed tracing integrations.
   */
  traceId?: string;
  /**
   * Span identifier for the current operation.
   */
  spanId?: string;
  /**
   * Parent span identifier for nested operations.
   */
  parentSpanId?: string;
  /**
   * W3C traceparent header value, when available.
   */
  traceparent?: string;
  /**
   * W3C tracestate value associated with the current operation.
   */
  tracestate?: string;
  /**
   * Watcher category used by devtools and instrumentation sinks.
   */
  watcher?: ProviderInstrumentationWatcherName;
  /**
   * Provider-specific structured details. Values are redacted before being
   * recorded by `createProviderInstrumentation(...)`.
   */
  details?: unknown;
}

/**
 * HTTP request instrumentation emitted by server adapters.
 */
export interface RequestProviderInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "request";
  /**
   * HTTP method.
   */
  method: string;
  /**
   * Request path.
   */
  path: string;
  /**
   * Matched contract name, when known.
   */
  contractName?: string;
  /**
   * Which layer produced the response.
   */
  responseOwner?: "route" | "framework" | "transport" | "unknown";
  /**
   * Response status code.
   */
  status?: number;
  /**
   * Request duration in milliseconds.
   */
  durationMs?: number;
  /**
   * Per-stage timing breakdown of the request pipeline, in milliseconds:
   * `onRequest` hooks, request parsing, context creation, route hooks plus
   * `beforeHandle` hooks, the handler, and response preparation.
   */
  stages?: {
    onRequestMs: number;
    parseMs: number;
    contextMs: number;
    beforeHandleMs: number;
    handlerMs: number;
    sendMs: number;
  };
  /**
   * Human-readable summary for devtools lists.
   */
  summary?: string;
}

/**
 * Error instrumentation emitted by framework hooks, use cases, or providers.
 */
export interface ErrorProviderInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "error";
  /**
   * Error message.
   */
  message: string;
  /**
   * Error stack trace, when available.
   */
  stack?: string;
  /**
   * Contract associated with the error.
   */
  contractName?: string;
  /**
   * Use case associated with the error.
   */
  useCaseName?: string;
  /**
   * Which layer owns the error.
   */
  owner?:
    | "route"
    | "framework"
    | "provider"
    | "job"
    | "schedule"
    | "outbox"
    | "client"
    | "devtools"
    | "unknown";
}

/**
 * Use-case lifecycle instrumentation.
 */
export interface UseCaseProviderInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "usecase";
  /**
   * Use-case name.
   */
  name: string;
  /**
   * Optional classification used by devtools.
   */
  kind?: "command" | "query";
  /**
   * Lifecycle phase.
   */
  phase: "start" | "end" | "error";
  /**
   * Duration in milliseconds for completed phases.
   */
  durationMs?: number;
  /**
   * Error summary for failed phases.
   */
  error?: string;
}

/**
 * Event bus instrumentation.
 */
export interface EventBusProviderInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "eventBus";
  /**
   * Domain or integration event name.
   */
  eventName: string;
}

/**
 * Background job instrumentation.
 */
export interface JobProviderInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "job";
  /**
   * Job name.
   */
  jobName: string;
  /**
   * Job lifecycle status.
   */
  status:
    | "scheduled"
    | "started"
    | "completed"
    | "failed"
    | "retryScheduled"
    | "deadLettered";
}

/**
 * Durable outbox delivery instrumentation.
 */
export interface OutboxProviderInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "outbox";
  /**
   * Durable outbox message ID.
   */
  messageId: string;
  /**
   * Message kind.
   */
  messageKind: "event" | "job";
  /**
   * Event or job name stored in the outbox message.
   */
  messageName: string;
  /**
   * Delivery lifecycle status.
   */
  status: "delivered" | "retryScheduled" | "deadLettered";
}

/**
 * Schedule instrumentation.
 */
export interface ScheduleProviderInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "schedule";
  /**
   * Schedule name.
   */
  scheduleName: string;
  /**
   * Schedule run lifecycle status.
   */
  status: "started" | "completed" | "failed";
  /**
   * Cron expression for the schedule, when known.
   */
  cron?: string;
  /**
   * Time zone used by the schedule, when known.
   */
  timezone?: string;
}

/**
 * Provider lifecycle instrumentation emitted during setup, start, and stop.
 */
export interface ProviderLifecycleInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "provider";
  /**
   * Provider name.
   */
  providerName: string;
  /**
   * Lifecycle action.
   */
  action: "setup" | "start" | "stop";
}

/**
 * Custom provider instrumentation event for provider-owned operations.
 */
export interface CustomProviderInstrumentationEvent
  extends BaseProviderInstrumentationEvent {
  type: "custom";
  /**
   * Stable event name.
   */
  name: string;
  /**
   * Short display label for devtools.
   */
  label?: string;
  /**
   * Human-readable summary for devtools lists.
   */
  summary?: string;
}

/**
 * Union of instrumentation events that provider sinks can receive.
 */
export type ProviderInstrumentationEvent =
  | RequestProviderInstrumentationEvent
  | ErrorProviderInstrumentationEvent
  | UseCaseProviderInstrumentationEvent
  | EventBusProviderInstrumentationEvent
  | JobProviderInstrumentationEvent
  | OutboxProviderInstrumentationEvent
  | ScheduleProviderInstrumentationEvent
  | ProviderLifecycleInstrumentationEvent
  | CustomProviderInstrumentationEvent;

/**
 * Input accepted by provider instrumentation recorders.
 */
export type ProviderInstrumentationEventInput = ProviderInstrumentationEvent;

/**
 * Custom instrumentation input. `type: "custom"` is added by the helper.
 */
export type ProviderCustomInstrumentationEventInput = Omit<
  CustomProviderInstrumentationEvent,
  "type"
>;

/**
 * Sink for provider instrumentation events.
 */
export interface ProviderInstrumentationPort<
  EventInput extends
    ProviderInstrumentationEventInput = ProviderInstrumentationEventInput,
  WatcherName extends string = ProviderInstrumentationWatcherName,
  RecordResult = unknown,
> {
  /**
   * Record an instrumentation event.
   */
  record(event: EventInput): RecordResult;
  /**
   * Return whether a watcher is enabled. Missing methods are treated as
   * enabled so simple sinks only need to implement `record`.
   */
  isWatcherEnabled?(name: WatcherName): boolean;
}

/**
 * Options for creating a provider instrumentation helper.
 */
export interface ProviderInstrumentationOptions {
  /**
   * Provider name to attach to custom event details.
   */
  providerName: string;
  /**
   * Default watcher name for events emitted by the helper.
   */
  watcher?: ProviderInstrumentationWatcherName;
  /**
   * Optional redactor applied after Beignet's default redaction pass.
   */
  redact?: Redactor<ProviderInstrumentationEventInput>;
}

/**
 * Convenience wrapper used by providers to emit safe, watcher-aware events.
 */
export interface ProviderInstrumentation {
  /**
   * Resolved instrumentation port, when one is available.
   */
  port: ProviderInstrumentationPort | undefined;
  /**
   * Return whether the given watcher is enabled.
   */
  isEnabled(watcher?: ProviderInstrumentationWatcherName): boolean;
  /**
   * Record a fully formed provider instrumentation event.
   */
  record(event: ProviderInstrumentationEventInput): unknown;
  /**
   * Record a provider-owned custom event.
   */
  custom(event: ProviderCustomInstrumentationEventInput): unknown;
}

/**
 * Values accepted by `createProviderInstrumentation(...)`.
 *
 * Plain ports objects are accepted so callers can pass `ctx.ports` (or a
 * provider's `ports` argument) directly; the resolver reads
 * `ports.instrumentation`, then `ports.devtools`.
 */
export type ProviderInstrumentationTarget =
  | ProviderInstrumentationPort
  | ProviderInstrumentation
  | {
      devtools?: ProviderInstrumentationPort | ProviderInstrumentation;
      instrumentation?: ProviderInstrumentationPort | ProviderInstrumentation;
    }
  | Record<string, unknown>
  | undefined;

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null;
}

/**
 * Return whether a value is a provider instrumentation helper.
 *
 * Checks key presence with `in` before reading so probing proxy-backed
 * objects, such as test-context port guards that throw on unknown reads,
 * stays safe.
 */
export function isProviderInstrumentation(
  value: unknown,
): value is ProviderInstrumentation {
  return (
    isObject(value) &&
    "record" in value &&
    "custom" in value &&
    "isEnabled" in value &&
    typeof value.record === "function" &&
    typeof value.custom === "function" &&
    typeof value.isEnabled === "function"
  );
}

/**
 * Return whether a value is a provider instrumentation port.
 *
 * Checks key presence with `in` before reading so probing proxy-backed
 * objects stays safe.
 */
export function isProviderInstrumentationPort(
  value: unknown,
): value is ProviderInstrumentationPort {
  return (
    isObject(value) && "record" in value && typeof value.record === "function"
  );
}

function resolveProviderInstrumentationValue(
  value: unknown,
): ProviderInstrumentationPort | undefined {
  if (isProviderInstrumentation(value)) return value.port;
  if (isProviderInstrumentationPort(value)) return value;
  return undefined;
}

/**
 * Resolve an instrumentation port from a direct port, helper, or context-like
 * object.
 */
export function resolveProviderInstrumentationPort(
  target: ProviderInstrumentationTarget,
): ProviderInstrumentationPort | undefined {
  if (!target) return undefined;

  const directPort = resolveProviderInstrumentationValue(target);
  if (directPort) return directPort;

  if (!isObject(target)) return undefined;

  return (
    resolveProviderInstrumentationValue(
      "instrumentation" in target ? target.instrumentation : undefined,
    ) ??
    resolveProviderInstrumentationValue(
      "devtools" in target ? target.devtools : undefined,
    )
  );
}

function withProviderDetails(providerName: string, details: unknown): unknown {
  if (details === undefined) {
    return { providerName };
  }

  if (isObject(details) && !Array.isArray(details)) {
    return { ...details, providerName };
  }

  return { providerName, value: details };
}

function withInstrumentationProviderDetails(
  event: ProviderInstrumentationEventInput,
  providerName: string,
): ProviderInstrumentationEventInput {
  if (event.type === "provider") return event;

  return {
    ...event,
    details: withProviderDetails(providerName, event.details),
  } as ProviderInstrumentationEventInput;
}

/**
 * Create a provider instrumentation helper that handles watcher checks,
 * default watcher assignment, redaction, and sink failures.
 */
export function createProviderInstrumentation(
  target: ProviderInstrumentationTarget,
  options: ProviderInstrumentationOptions,
): ProviderInstrumentation {
  if (isProviderInstrumentation(target)) {
    return target;
  }

  const port = resolveProviderInstrumentationPort(target);

  function isEnabled(watcher = options.watcher): boolean {
    if (!port) return false;
    if (!watcher) return true;
    try {
      return port.isWatcherEnabled?.(watcher) ?? true;
    } catch {
      return false;
    }
  }

  function record(event: ProviderInstrumentationEventInput): unknown {
    if (!port) return undefined;

    const watcher = event.watcher ?? options.watcher;
    if (watcher && !isEnabled(watcher)) return undefined;

    const eventWithWatcher =
      options.watcher && event.watcher === undefined
        ? { ...event, watcher: options.watcher }
        : event;
    const eventWithProvider = withInstrumentationProviderDetails(
      eventWithWatcher,
      options.providerName,
    );

    let redacted: ProviderInstrumentationEventInput;
    try {
      redacted = options.redact
        ? options.redact(redactValue(eventWithProvider))
        : redactValue(eventWithProvider);
    } catch {
      return undefined;
    }

    try {
      const result = port.record(redacted);
      if (
        result !== null &&
        (typeof result === "object" || typeof result === "function") &&
        "then" in result &&
        typeof result.then === "function"
      ) {
        return Promise.resolve(result).catch(() => undefined);
      }
      return result;
    } catch {
      return undefined;
    }
  }

  function custom(event: ProviderCustomInstrumentationEventInput): unknown {
    return record({
      ...event,
      type: "custom",
      watcher: event.watcher ?? options.watcher,
      details: withProviderDetails(options.providerName, event.details),
    });
  }

  return {
    port,
    isEnabled,
    record,
    custom,
  };
}
