import type { StandardSchemaV1 } from "@standard-schema/spec";
import { runWithResolvedTracingContext } from "../tracing/execution.js";
import {
  parseTraceCarrier,
  type TraceCarrier,
  type TracingPort,
} from "../tracing/index.js";

/**
 * Any Standard Schema compatible validator.
 */
export type StandardSchema = StandardSchemaV1<unknown, unknown>;

/**
 * Value or promise of that value.
 */
export type MaybePromise<T> = T | Promise<T>;

/**
 * Infer the parsed output type from a Standard Schema.
 */
export type InferSchemaOutput<T extends StandardSchemaV1> =
  StandardSchemaV1.InferOutput<T>;

/**
 * Minimal event definition shape accepted by event bus helpers.
 */
export interface EventPayloadDef<
  Name extends string = string,
  Payload extends StandardSchema = StandardSchema,
> {
  /**
   * Stable event name.
   */
  readonly name: Name;
  /**
   * Standard Schema payload validator.
   */
  readonly payload: Payload;
  /**
   * Optional human-readable description for docs and tooling.
   */
  readonly description?: string;
}

/**
 * Event definition created by `defineEvent(...)`.
 */
export interface EventDef<
  Name extends string = string,
  Payload extends StandardSchema = StandardSchema,
> extends EventPayloadDef<Name, Payload> {
  /**
   * Discriminator for event definitions.
   */
  readonly kind: "event";
}

/**
 * Infer the parsed payload type for an event definition.
 */
export type InferEventPayload<E extends EventPayloadDef> =
  E["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;

/** Metadata propagated with an event delivery. */
export interface EventPublishOptions {
  /** Versioned trace context captured by the event producer. */
  trace?: TraceCarrier;
}

/**
 * Options for `defineEvent(...)`.
 */
export interface DefineEventOptions<Payload extends StandardSchema> {
  /**
   * Standard Schema payload validator.
   */
  payload: Payload;
  /**
   * Optional human-readable description for docs and tooling.
   */
  description?: string;
}

/**
 * Arguments passed to a listener handler.
 */
export interface ListenerHandleArgs<E extends EventDef, Ctx> {
  /**
   * Event definition being handled.
   */
  event: E;
  /**
   * Parsed event payload.
   */
  payload: InferEventPayload<E>;
  /**
   * Listener context.
   */
  ctx: Ctx;
}

/**
 * Listener definition created by `defineListener(...)`.
 */
export interface ListenerDef<
  E extends EventDef = EventDef,
  Ctx = unknown,
  Name extends string = string,
> {
  /**
   * Discriminator for listener definitions.
   */
  readonly kind: "listener";
  /**
   * Stable listener name.
   */
  readonly name: Name;
  /**
   * Event this listener handles.
   */
  readonly event: E;
  /**
   * Handle a parsed event payload.
   */
  handle(args: ListenerHandleArgs<E, Ctx>): MaybePromise<void>;
}

/**
 * Options for `defineListener(...)`.
 */
export interface DefineListenerOptions<E extends EventDef, Ctx> {
  /**
   * Event this listener handles.
   */
  event: E;
  /**
   * Handle a parsed event payload.
   */
  handle(args: ListenerHandleArgs<E, Ctx>): MaybePromise<void>;
}

/**
 * Event bus shape required by Beignet listener registration helpers.
 */
export interface EventBusLike {
  /**
   * Publish an event payload.
   */
  publish<E extends EventPayloadDef>(
    event: E,
    payload: InferEventPayload<E>,
    options?: EventPublishOptions,
  ): MaybePromise<void>;
  /**
   * Subscribe to an event and return an unsubscribe function.
   */
  subscribe<E extends EventPayloadDef>(
    event: E,
    handler: (
      payload: InferEventPayload<E>,
      options?: EventPublishOptions,
    ) => MaybePromise<void>,
  ): () => void;
}

/**
 * Options for `registerListeners(...)`.
 */
export interface RegisterListenersOptions<Ctx> {
  /**
   * Static listener context or factory evaluated for each delivered event.
   */
  ctx?: Ctx | (() => MaybePromise<Ctx>);
  /**
   * Runtime tracing port used to start the listener span before a lazy context
   * factory runs.
   */
  tracing?: TracingPort;
  /**
   * Called when a listener fails. When omitted, listener errors are rethrown to
   * the event bus subscription callback.
   */
  onError?: (error: unknown, listener: ListenerDef<EventDef, Ctx>) => void;
}

/**
 * Context-bound listener helper factory.
 */
export interface Listeners<Ctx> {
  /**
   * Define a listener with the bound context type.
   */
  defineListener<Name extends string, E extends EventDef>(
    name: Name,
    options: DefineListenerOptions<E, Ctx>,
  ): ListenerDef<E, Ctx, Name>;
}

/**
 * Error thrown when event payload validation fails.
 */
export class EventValidationError extends Error {
  /**
   * Raw Standard Schema validation issues.
   */
  readonly issues: readonly StandardSchemaV1.Issue[];

  constructor(args: {
    name: string;
    issues: readonly StandardSchemaV1.Issue[];
  }) {
    super(
      `Event "${args.name}" payload validation failed: ${formatIssues(args.issues)}`,
    );
    this.name = "EventValidationError";
    this.issues = args.issues;
  }
}

function formatPath(path: StandardSchemaV1.Issue["path"]): string {
  if (!path?.length) return "";

  return path
    .map((segment) =>
      typeof segment === "object" && segment !== null && "key" in segment
        ? String(segment.key)
        : String(segment),
    )
    .join(".");
}

function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string {
  return issues
    .map((issue) => {
      const path = formatPath(issue.path);
      return path ? `${path}: ${issue.message}` : issue.message;
    })
    .join("; ");
}

async function parsePayload<Schema extends StandardSchemaV1>(
  schema: Schema,
  input: unknown,
  args: { name: string },
): Promise<InferSchemaOutput<Schema>> {
  const result = await schema["~standard"].validate(input);

  if (result.issues?.length) {
    throw new EventValidationError({
      name: args.name,
      issues: result.issues,
    });
  }

  if ("value" in result) {
    return result.value as InferSchemaOutput<Schema>;
  }

  throw new Error("Invalid Standard Schema result: missing value");
}

/**
 * Define a typed event.
 *
 * Event payloads are validated before publishing through `publishEvent(...)`
 * and before registered listeners run.
 */
export function defineEvent<
  Name extends string,
  Payload extends StandardSchema,
>(name: Name, options: DefineEventOptions<Payload>): EventDef<Name, Payload> {
  return {
    kind: "event",
    name,
    payload: options.payload,
    description: options.description,
  };
}

function defineListenerImpl<
  Name extends string = string,
  E extends EventDef = EventDef,
  Ctx = unknown,
>(
  name: Name,
  options: DefineListenerOptions<E, Ctx>,
): ListenerDef<E, Ctx, Name> {
  return {
    kind: "listener",
    name,
    event: options.event,
    handle: options.handle,
  };
}

/**
 * Validate and parse an event payload with the event's Standard Schema.
 */
export async function parseEventPayload<E extends EventPayloadDef>(
  event: E,
  payload: unknown,
): Promise<InferEventPayload<E>> {
  return (await parsePayload(event.payload, payload, {
    name: event.name,
  })) as InferEventPayload<E>;
}

/**
 * Validate an event payload and publish it through an event bus.
 */
export async function publishEvent<E extends EventPayloadDef>(
  eventBus: EventBusLike,
  event: E,
  payload: InferEventPayload<E>,
  options?: EventPublishOptions,
): Promise<void> {
  await parseEventPayload(event, payload);
  await eventBus.publish(event, payload, options);
}

/**
 * Register listeners against an event bus and return an unsubscribe function.
 *
 * Payloads are validated before listener handlers run. Listener context is
 * resolved per delivery when `options.ctx` is a factory.
 */
export function registerListeners<Ctx>(
  eventBus: EventBusLike,
  listeners: readonly ListenerDef<EventDef, Ctx>[],
  options: RegisterListenersOptions<Ctx> = {},
): () => void {
  const unsubscribes = listeners.map((listener) =>
    eventBus.subscribe(listener.event, async (rawPayload, publishOptions) => {
      try {
        const payload = await parseEventPayload(listener.event, rawPayload);
        const traceAttributes = {
          "beignet.listener.name": listener.name,
          "beignet.event.name": listener.event.name,
        } as const;
        await runWithResolvedTracingContext({
          tracing: options.tracing,
          ctx: options.ctx as Ctx | (() => MaybePromise<Ctx>),
          operation: {
            name: `beignet.listener ${listener.name}`,
            type: "listener",
            kind: "consumer",
            parent: parseTraceCarrier(publishOptions?.trace),
            attributes: traceAttributes,
            metricAttributes: traceAttributes,
          },
          run: (ctx) =>
            listener.handle({
              event: listener.event,
              payload,
              ctx,
            }),
        });
      } catch (error) {
        options.onError?.(error, listener);
        if (!options.onError) throw error;
      }
    }),
  );

  return () => {
    for (const unsubscribe of [...unsubscribes].reverse()) {
      unsubscribe();
    }
  };
}

/**
 * Create listener helper methods bound to an application context type.
 *
 * Call it once in `lib/listeners.ts`:
 *
 * ```ts
 * export const { defineListener } = createListeners<AppContext>();
 * ```
 */
export function createListeners<Ctx>(): Listeners<Ctx> {
  return {
    defineListener<Name extends string, E extends EventDef>(
      name: Name,
      options: DefineListenerOptions<E, Ctx>,
    ): ListenerDef<E, Ctx, Name> {
      return defineListenerImpl(name, options);
    },
  };
}
