/**
 * @beignet/core/mail
 *
 * Shared mail port and test adapters for Beignet applications.
 */

import {
  createProvider,
  createProviderInstrumentation,
} from "../providers/index.js";

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

/**
 * Email address accepted by Beignet mail helpers.
 */
export type MailAddress =
  | string
  | {
      /**
       * Email address.
       */
      email: string;
      /**
       * Optional display name.
       */
      name?: string;
    };

/**
 * Single address or address list.
 */
export type MailAddressList = MailAddress | readonly MailAddress[];

/**
 * Common mail message fields shared by all send options.
 */
export interface MailBaseMessage {
  /**
   * Required recipients.
   */
  to: MailAddressList;
  /**
   * Message subject.
   */
  subject: string;
  /**
   * Sender address. Providers or adapters may supply a default.
   */
  from?: MailAddress;
  /**
   * Carbon-copy recipients.
   */
  cc?: MailAddressList;
  /**
   * Blind-carbon-copy recipients.
   */
  bcc?: MailAddressList;
  /**
   * Reply-to recipients.
   */
  replyTo?: MailAddressList;
  /**
   * Provider-specific message headers.
   */
  headers?: Record<string, string>;
}

/**
 * Mail send options.
 *
 * A message must include at least one body format: `text` or `html`.
 */
export type SendMailOptions = MailBaseMessage &
  (
    | {
        text: string;
        html?: string;
      }
    | {
        html: string;
        text?: string;
      }
  );

/**
 * Normalized mail message with address fields converted to arrays.
 */
export interface NormalizedMailMessage extends MailBaseMessage {
  /**
   * Normalized recipients.
   */
  to: readonly MailAddress[];
  /**
   * Sender address after applying any default.
   */
  from?: MailAddress;
  /**
   * Normalized carbon-copy recipients.
   */
  cc?: readonly MailAddress[];
  /**
   * Normalized blind-carbon-copy recipients.
   */
  bcc?: readonly MailAddress[];
  /**
   * Normalized reply-to recipients.
   */
  replyTo?: readonly MailAddress[];
  /**
   * Plain text body.
   */
  text?: string;
  /**
   * HTML body.
   */
  html?: string;
}

/**
 * Result returned by a mail provider.
 */
export interface SendMailResult {
  /**
   * Provider message ID when available.
   */
  id?: string;
  /**
   * Provider name.
   */
  provider?: string;
}

/**
 * App-facing mailer port.
 */
export interface MailerPort {
  /**
   * Send one mail message.
   */
  send(message: SendMailOptions): Promise<SendMailResult>;
}

/**
 * Delivery captured by the memory mailer.
 */
export interface MemoryMailDelivery {
  /**
   * Generated delivery ID.
   */
  id: string;
  /**
   * Normalized message that would have been sent.
   */
  message: NormalizedMailMessage;
  /**
   * Timestamp assigned by the memory mailer.
   */
  sentAt: Date;
}

/**
 * In-memory mailer port for tests and local examples.
 */
export interface MemoryMailerPort extends MailerPort {
  /**
   * Captured deliveries.
   */
  readonly deliveries: readonly MemoryMailDelivery[];
  /**
   * Clear captured deliveries.
   */
  clear(): void;
}

/**
 * Options for `createMemoryMailer(...)`.
 */
export interface CreateMemoryMailerOptions {
  /**
   * Sender used when a message does not specify `from`.
   */
  defaultFrom?: MailAddress;
  /**
   * Clock used for captured deliveries.
   */
  now?: () => Date;
  /**
   * ID factory used for captured deliveries.
   */
  id?: () => string;
  /**
   * Observer called after a delivery is captured.
   */
  onSend?: (delivery: MemoryMailDelivery) => MaybePromise<void>;
}

/**
 * Error thrown by mail helpers and provider adapters.
 */
export class MailDeliveryError extends Error {
  /**
   * Provider name when known.
   */
  readonly provider?: string;
  /**
   * Original provider error when available.
   */
  readonly cause?: unknown;

  constructor(args: { provider?: string; message: string; cause?: unknown }) {
    super(args.message);
    this.name = "MailDeliveryError";
    this.provider = args.provider;
    this.cause = args.cause;
  }
}

/**
 * Normalize a single address or address list into an array.
 *
 * This helper does not validate email syntax.
 */
export function normalizeMailAddressList(
  addresses: MailAddressList | undefined,
): readonly MailAddress[] | undefined {
  if (addresses === undefined) return undefined;
  return Array.isArray(addresses) ? [...addresses] : [addresses as MailAddress];
}

function normalizeOptionalMailAddressList(
  addresses: MailAddressList | undefined,
): readonly MailAddress[] | undefined {
  const normalized = normalizeMailAddressList(addresses);
  return normalized && normalized.length > 0 ? normalized : undefined;
}

/**
 * Format one address for providers that accept RFC-like address strings.
 *
 * Rejects carriage returns and line feeds so address values cannot inject
 * additional mail header lines. This helper does not validate email syntax.
 */
export function formatMailAddress(address: MailAddress): string {
  if (typeof address === "string") {
    assertMailAddressLineSafe(address);
    return address;
  }

  assertMailAddressLineSafe(address.email);
  if (!address.name) return address.email;

  assertMailAddressLineSafe(address.name);
  const escapedName = address.name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
  return `"${escapedName}" <${address.email}>`;
}

function assertMailAddressLineSafe(value: string): void {
  if (/[\r\n]/.test(value)) {
    throw new MailDeliveryError({
      message:
        "Mail addresses must not include carriage returns or line feeds.",
    });
  }
}

/**
 * Format one or more addresses for providers that accept string address fields.
 */
export function formatMailAddressList(
  addresses: MailAddressList,
): string | string[] {
  const normalized = normalizeMailAddressList(addresses) ?? [];
  const formatted = normalized.map(formatMailAddress);
  return formatted.length === 1 ? formatted[0] : formatted;
}

/**
 * Normalize a mail message and apply a default sender.
 *
 * Throws when the message has no recipients.
 */
export function normalizeMailMessage(
  message: SendMailOptions,
  options: { defaultFrom?: MailAddress } = {},
): NormalizedMailMessage {
  const to = normalizeMailAddressList(message.to) ?? [];

  if (to.length === 0) {
    throw new MailDeliveryError({
      message: "Cannot send email without at least one recipient.",
    });
  }

  return {
    ...message,
    to,
    from: message.from ?? options.defaultFrom,
    cc: normalizeOptionalMailAddressList(message.cc),
    bcc: normalizeOptionalMailAddressList(message.bcc),
    replyTo: normalizeOptionalMailAddressList(message.replyTo),
    text: "text" in message ? message.text : undefined,
    html: "html" in message ? message.html : undefined,
  };
}

/**
 * Create an in-memory mailer for tests, local development, and examples.
 *
 * The memory mailer does not send real email or validate address syntax.
 */
export function createMemoryMailer(
  options: CreateMemoryMailerOptions = {},
): MemoryMailerPort {
  return createMemoryMailerInternal(options);
}

function createMemoryMailerInternal(
  options: CreateMemoryMailerOptions,
  onDelivery?: (delivery: MemoryMailDelivery, durationMs: number) => void,
): MemoryMailerPort {
  const deliveries: MemoryMailDelivery[] = [];
  const now = options.now ?? (() => new Date());
  const id = options.id ?? (() => crypto.randomUUID());

  return {
    get deliveries() {
      return deliveries;
    },

    async send(message) {
      const startedAt = Date.now();
      const delivery: MemoryMailDelivery = {
        id: id(),
        message: normalizeMailMessage(message, {
          defaultFrom: options.defaultFrom,
        }),
        sentAt: now(),
      };

      deliveries.push(delivery);
      onDelivery?.(delivery, Date.now() - startedAt);
      await options.onSend?.(delivery);

      return {
        id: delivery.id,
        provider: "memory",
      };
    },

    clear() {
      deliveries.length = 0;
    },
  };
}

/**
 * Options for the memory mailer provider.
 */
export interface MemoryMailerProviderOptions extends CreateMemoryMailerOptions {
  /**
   * Provider name. Defaults to "memory-mailer".
   */
  name?: string;
}

/**
 * Ports contributed by the memory mailer provider.
 */
export interface MemoryMailerProviderPorts {
  /**
   * Beignet mailer port.
   */
  mailer: MailerPort;
}

/**
 * Create a provider that contributes an in-memory mailer.
 *
 * Use it as the dev-default `mailer` port in `server/providers.ts` until a
 * real mail provider such as Resend or SMTP takes over. Deliveries are
 * captured in memory and recorded as `mail.sent` devtools events through the
 * `mail` watcher when an instrumentation port is installed.
 */
export function createMemoryMailerProvider(
  options: MemoryMailerProviderOptions = {},
) {
  const { name = "memory-mailer", onSend, ...mailerOptions } = options;

  return createProvider({
    name,
    setup({ ports }) {
      const instrumentation = createProviderInstrumentation(ports, {
        providerName: name,
        watcher: "mail",
      });
      const mailer: MailerPort = createMemoryMailerInternal(
        {
          ...mailerOptions,
          onSend,
        },
        (delivery, durationMs) => {
          const recipientCount =
            delivery.message.to.length +
            (delivery.message.cc?.length ?? 0) +
            (delivery.message.bcc?.length ?? 0);
          instrumentation.custom({
            name: "mail.sent",
            label: "Mail sent",
            summary: "Mail sent",
            details: {
              provider: "memory",
              recipientCount,
              id: delivery.id,
              durationMs,
            },
          });
        },
      );

      return {
        ports: {
          mailer,
        } satisfies MemoryMailerProviderPorts,
      };
    },
  });
}
