import { createHash } from 'node:crypto';
import {
  NotificationDeliveryError,
  type Notification,
  type NotificationEnvelope,
  type NotificationReceipt,
  type Notifier,
} from './notifier';

interface SlackNotifierOptions {
  botToken: string;
  fetchImpl?: typeof fetch;
  apiBase?: string;
}

interface SlackResponse {
  ok?: boolean;
  error?: string;
  ts?: string;
}

export class SlackNotifier implements Notifier {
  readonly provider = 'slack';
  private readonly fetchImpl: typeof fetch;
  private readonly apiBase: string;

  constructor(private readonly options: SlackNotifierOptions) {
    this.fetchImpl = options.fetchImpl ?? fetch;
    this.apiBase = options.apiBase ?? 'https://slack.com/api';
  }

  async send(
    envelope: NotificationEnvelope,
    signal?: AbortSignal,
  ): Promise<NotificationReceipt> {
    let response: Response;
    try {
      response = await this.fetchImpl(`${this.apiBase}/chat.postMessage`, {
        method: 'POST',
        headers: {
          authorization: `Bearer ${this.options.botToken}`,
          'content-type': 'application/json; charset=utf-8',
        },
        body: JSON.stringify({
          channel: envelope.destination,
          text: formatSlackNotification(envelope.notification),
          client_msg_id: slackClientMessageId(envelope.idempotencyKey),
          unfurl_links: false,
          unfurl_media: false,
        }),
        signal,
      });
    } catch {
      throw new NotificationDeliveryError('network_error', true);
    }

    if (response.status === 429) {
      const retrySeconds = Number(response.headers.get('retry-after'));
      throw new NotificationDeliveryError(
        'rate_limited',
        true,
        Number.isFinite(retrySeconds) && retrySeconds > 0 ? retrySeconds * 1_000 : undefined,
      );
    }
    if (response.status >= 500) {
      throw new NotificationDeliveryError('slack_unavailable', true);
    }
    if (!response.ok) {
      throw new NotificationDeliveryError('slack_http_error', false);
    }

    let result: SlackResponse;
    try {
      result = (await response.json()) as SlackResponse;
    } catch {
      throw new NotificationDeliveryError('invalid_slack_response', true);
    }
    if (!result.ok) {
      const code = normaliseSlackError(result.error);
      throw new NotificationDeliveryError(code, isRetryableSlackError(code));
    }
    return { externalId: result.ts ?? null };
  }
}

export function slackClientMessageId(idempotencyKey: string): string {
  const digest = createHash('sha256').update(idempotencyKey).digest('hex');
  return `${digest.slice(0, 8)}-${digest.slice(8, 12)}-4${digest.slice(13, 16)}-a${digest.slice(17, 20)}-${digest.slice(20, 32)}`;
}

function formatSlackNotification(notification: Notification): string {
  if (notification.kind === 'stage_change') {
    return [
      `*${notification.dealName}* moved from \`${notification.fromStage}\` to \`${notification.toStage}\`.`,
      `${notification.dealSide === 'demand' ? 'Demand' : 'Supply'} pipeline stage changed in PIG.`,
    ].join('\n');
  }

  return [
    `*Idle capacity: ${notification.commitmentName}*`,
    `${notification.gpuType} has ${formatNumber(notification.idleGpuHours)} unsold GPU-hours (${Math.round(notification.utilisation * 100)}% utilised).`,
    `Idle committed cost: ${formatMoney(notification.idleCostCents)}.`,
  ].join('\n');
}

function formatNumber(value: number): string {
  return new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 }).format(value);
}

function formatMoney(cents: number): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
    maximumFractionDigits: 0,
  }).format(cents / 100);
}

function normaliseSlackError(error: string | undefined): string {
  return error && /^[a-z0-9_]+$/i.test(error) ? error : 'unknown_slack_error';
}

function isRetryableSlackError(code: string): boolean {
  return ['ratelimited', 'internal_error', 'fatal_error', 'request_timeout'].includes(code);
}
