/** Inbini SDK source preview. No package registry installation required. */
export type Lifetime = `${number}${'s' | 'm' | 'h' | 'd'}`;
export type InboxData = {
  id: string;
  address: string;
  createdAt: string;
  expiresAt: string;
  domainId: string | null;
  mode: 'sandbox' | 'live';
};
export type Message = {
  id: string;
  inboxId: string;
  from: string;
  subject: string;
  text: string;
  receivedAt: string;
  code: string | null;
  links: string[];
  source: 'sample' | 'inbound';
};
export type Domain = {
  id: string;
  hostname: string;
  visibility: 'public' | 'private';
  status: 'pending_dns' | 'pending_receiver' | 'ready';
  ownershipVerified: boolean;
  mxVerified: boolean;
  receiverConnected: boolean;
  checkedAt: string | null;
  dns: { type: 'TXT'; name: string; value: string };
  testAddress: string;
  inboundPath: string;
};
export type WaitOptions = {
  timeout?: number;
  from?: string;
  subject?: string;
  after?: string;
  signal?: AbortSignal;
};
export class InbiniError extends Error {
  constructor(
    message: string,
    public status: number,
    public code: string,
    public retryAfter = 1,
  ) {
    super(message);
    this.name = 'InbiniError';
  }
}
export class Inbini {
  readonly baseUrl: string;
  readonly workspaceToken?: string;
  constructor(options: { baseUrl: string; workspaceToken?: string }) {
    this.workspaceToken = options.workspaceToken;
    const url = new URL(options.baseUrl);
    if (!['http:', 'https:'].includes(url.protocol))
      throw new Error('Use an HTTP(S) API base URL.');
    this.baseUrl = url.href.replace(/\/$/, '');
  }
  readonly workspaces = {
    create: () =>
      this.request<{ token: string }>('/workspaces', 'POST', undefined, {}),
  };
  readonly domains = {
    list: () => this.request<Domain[]>('/domains', 'GET', this.workspaceToken),
    add: (hostname: string) =>
      this.request<Domain>('/domains', 'POST', this.workspaceToken, {
        hostname,
      }),
    get: (id: string) =>
      this.request<Domain>(
        '/domains/' + encodeURIComponent(id),
        'GET',
        this.workspaceToken,
      ),
    verify: (id: string) =>
      this.request<Domain>(
        '/domains/' + encodeURIComponent(id) + '/verify',
        'POST',
        this.workspaceToken,
        {},
      ),
    receiverKey: (id: string) =>
      this.request<{ token: string; domain: Domain }>(
        '/domains/' + encodeURIComponent(id) + '/receiver-key',
        'POST',
        this.workspaceToken,
        {},
      ),
    remove: (id: string) =>
      this.request<void>(
        '/domains/' + encodeURIComponent(id),
        'DELETE',
        this.workspaceToken,
      ),
  };
  readonly inboxes = {
    create: async (
      options: { expiresIn?: Lifetime; domainId?: string } = {},
    ) => {
      const data = await this.request<InboxData & { token: string }>(
        '/inboxes',
        'POST',
        options.domainId ? this.workspaceToken : undefined,
        {
          expiresIn: options.expiresIn ?? '1h',
          ...(options.domainId ? { domainId: options.domainId } : {}),
        },
      );
      return new InbiniInbox(this, data, data.token);
    },
    resume: async (credentials: { id: string; token: string }) => {
      const data = await this.request<InboxData>(
        '/inboxes/' + encodeURIComponent(credentials.id),
        'GET',
        credentials.token,
      );
      return new InbiniInbox(this, data, credentials.token);
    },
  };
  async request<T>(
    path: string,
    method = 'GET',
    token?: string,
    body?: unknown,
    signal?: AbortSignal,
  ): Promise<T> {
    const options: RequestInit = {
      method,
      headers: {
        ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
        ...(token ? { Authorization: 'Bearer ' + token } : {}),
      },
      signal: signal
        ? AbortSignal.any([signal, AbortSignal.timeout(10000)])
        : AbortSignal.timeout(10000),
      cache: 'no-store',
    };
    if (body !== undefined && method !== 'GET' && method !== 'HEAD')
      options.body = JSON.stringify(body);
    const response = await fetch(this.baseUrl + path, options);
    if (response.status === 204) return undefined as T;
    const result = (await response.json()) as {
      data: T;
      error?: { message: string; code: string };
    };
    if (!response.ok)
      throw new InbiniError(
        result.error?.message ?? 'API request failed',
        response.status,
        result.error?.code ?? 'API_ERROR',
        Number(response.headers.get('retry-after')) || 1,
      );
    return result.data;
  }
}
export class InbiniInbox {
  constructor(
    private client: Inbini,
    readonly data: InboxData,
    readonly token: string,
  ) {}
  get id() {
    return this.data.id;
  }
  get address() {
    return this.data.address;
  }
  get expiresAt() {
    return this.data.expiresAt;
  }
  private get path() {
    return '/inboxes/' + encodeURIComponent(this.id);
  }
  async messages(
    filters: Omit<WaitOptions, 'timeout' | 'signal'> = {},
    signal?: AbortSignal,
  ) {
    const params = new URLSearchParams();
    for (const key of ['from', 'subject', 'after'] as const)
      if (filters[key]) params.set(key, filters[key]!);
    return this.client.request<Message[]>(
      this.path + '/messages?' + params,
      'GET',
      this.token,
      undefined,
      signal,
    );
  }
  async getMessage(id: string) {
    return this.client.request<Message>(
      this.path + '/messages/' + encodeURIComponent(id),
      'GET',
      this.token,
    );
  }
  async deleteMessage(id: string) {
    await this.client.request(
      this.path + '/messages/' + encodeURIComponent(id),
      'DELETE',
      this.token,
    );
  }
  async delete() {
    await this.client.request(this.path, 'DELETE', this.token);
  }
  async sample() {
    return this.client.request<{ id: string }>(
      this.path + '/sample',
      'POST',
      this.token,
      {},
    );
  }
  private async wait<T>(
    select: (message: Message) => T | undefined,
    options: WaitOptions = {},
  ): Promise<T> {
    const timeout = options.timeout ?? 60000;
    if (!Number.isFinite(timeout) || timeout < 1 || timeout > 300000)
      throw new Error('timeout must be between 1 and 300000 milliseconds.');
    const signal = options.signal
      ? AbortSignal.any([options.signal, AbortSignal.timeout(timeout)])
      : AbortSignal.timeout(timeout);
    try {
      while (true) {
        signal.throwIfAborted();
        let delay = 1000;
        try {
          for (const m of await this.messages(options, signal)) {
            const result = select(m);
            if (result !== undefined) return result;
          }
        } catch (e) {
          if (e instanceof InbiniError && e.status === 429)
            delay = Math.max(1000, e.retryAfter * 1000);
          else throw e;
        }
        await new Promise<void>((resolve, reject) => {
          const onAbort = () => {
            clearTimeout(timer);
            reject(signal.reason);
          };
          const timer = setTimeout(() => {
            signal.removeEventListener('abort', onAbort);
            resolve();
          }, delay);
          signal.addEventListener('abort', onAbort, { once: true });
          if (signal.aborted) onAbort();
        });
      }
    } catch (e) {
      if (signal.aborted && !options.signal?.aborted)
        throw new InbiniError(
          'No matching message arrived before the timeout.',
          408,
          'WAIT_TIMEOUT',
        );
      throw e;
    }
  }
  waitForMessage(options: WaitOptions = {}) {
    return this.wait((m) => m, options);
  }
  waitForCode(options: WaitOptions = {}) {
    return this.wait((m) => m.code ?? undefined, options);
  }
  waitForLink(options: WaitOptions = {}) {
    return this.wait((m) => m.links[0], options);
  }
}
