import type { Channel } from '@wa-ticketing/shared';
import type { WhatsAppMediaPayload, WhatsAppProviderAdapter } from './whatsappProvider.js';

const SEND_URL = 'https://chat.africastalking.com/whatsapp/message/send';
const REQUEST_TIMEOUT_MS = 15_000;

const MEDIA_TYPES = { image: 'Image', voice_note: 'Voice', document: 'Document' } as const;

export type AfricasTalkingErrorType =
  | 'config' | 'authentication' | 'http' | 'timeout' | 'network'
  | 'rejected' | 'invalid_response' | 'media_url_required';

const STATUS_CODES: Record<AfricasTalkingErrorType, number> = {
  config: 503,
  authentication: 502,
  http: 502,
  timeout: 504,
  network: 502,
  rejected: 502,
  invalid_response: 502,
  media_url_required: 400,
};

/**
 * Safe to log or return: carries only a fixed message, error type and HTTP status.
 * Never includes the API key, request headers, or provider response bodies.
 */
export class WhatsAppProviderError extends Error {
  readonly statusCode: number;

  constructor(
    readonly provider: 'africas_talking',
    readonly type: AfricasTalkingErrorType,
    message: string,
    readonly httpStatus?: number,
  ) {
    super(message);
    this.name = 'WhatsAppProviderError';
    this.statusCode = STATUS_CODES[type];
  }
}

export type AfricasTalkingCredentials = { username: string; apiKey: string };

export class AfricasTalkingWhatsAppProvider implements WhatsAppProviderAdapter {
  constructor(
    private readonly credentials: AfricasTalkingCredentials,
    private readonly fetchImpl: typeof fetch = fetch,
  ) {}

  sendText(channel: Channel, recipient: string, text: string): Promise<string | null> {
    return this.deliver(channel, recipient, { message: text });
  }

  async sendMedia(channel: Channel, recipient: string, media: WhatsAppMediaPayload): Promise<string | null> {
    if (!media.mediaUrl) {
      throw new WhatsAppProviderError(
        'africas_talking', 'media_url_required',
        "Africa's Talking media requires a hosted media URL",
      );
    }
    return this.deliver(channel, recipient, {
      mediaType: MEDIA_TYPES[media.kind],
      url: media.mediaUrl,
      caption: media.caption,
    });
  }

  private async deliver(channel: Channel, recipient: string, body: Record<string, unknown>): Promise<string | null> {
    let res: Response;
    try {
      res = await this.fetchImpl(SEND_URL, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          apiKey: this.credentials.apiKey,
        },
        body: JSON.stringify({
          username: this.credentials.username,
          waNumber: channel.phone_number,
          phoneNumber: recipient,
          body,
        }),
        signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
      });
    } catch (err) {
      const name = (err as { name?: string } | null)?.name;
      if (name === 'TimeoutError' || name === 'AbortError') {
        throw new WhatsAppProviderError('africas_talking', 'timeout', "Africa's Talking request timed out");
      }
      throw new WhatsAppProviderError('africas_talking', 'network', "Africa's Talking request failed (network error)");
    }

    if (!res.ok) {
      const auth = res.status === 401 || res.status === 403;
      throw new WhatsAppProviderError(
        'africas_talking',
        auth ? 'authentication' : 'http',
        `Africa's Talking ${auth ? 'authentication failed' : 'request failed'} (HTTP ${res.status})`,
        res.status,
      );
    }

    const data = await res.json().catch(() => null) as { status?: unknown; messageId?: unknown } | null;
    if (!data || typeof data !== 'object' || typeof data.status !== 'string') {
      throw new WhatsAppProviderError('africas_talking', 'invalid_response', "Africa's Talking returned an unreadable response");
    }
    if (data.status.toUpperCase() !== 'SENT') {
      throw new WhatsAppProviderError('africas_talking', 'rejected', "Africa's Talking did not send the message");
    }
    return typeof data.messageId === 'string' ? data.messageId : null;
  }
}

/** Reads the deployment-level account from env; secrets are never taken from the browser or DB. */
export function createAfricasTalkingWhatsAppProvider(
  env: NodeJS.ProcessEnv = process.env,
  fetchImpl?: typeof fetch,
): AfricasTalkingWhatsAppProvider {
  const username = env.AFRICASTALKING_USERNAME?.trim();
  const apiKey = env.AFRICASTALKING_API_KEY?.trim();
  if (!username || !apiKey) {
    throw new WhatsAppProviderError(
      'africas_talking', 'config',
      "Africa's Talking is not configured (AFRICASTALKING_USERNAME / AFRICASTALKING_API_KEY)",
    );
  }
  return new AfricasTalkingWhatsAppProvider({ username, apiKey }, fetchImpl);
}
