import type {
  Channel,
  NormalizedMessage,
  ResolvedCredentials,
  ProviderErrorCode,
} from '@zyphon/shared-types';
import type {
  ProviderAdapter,
  ProviderResult,
  ProviderHealth,
  WebhookRequest,
  DeliveryEvent,
} from '@zyphon/provider-contracts';

/**
 * Twilio trial accounts reject free-form SMS bodies. `Body` must be one of these
 * template keys (same values the Twilio Console "Try out SMS" page sends).
 * @see https://www.twilio.com/docs/usage/trials/try-out-sms
 */
export const TWILIO_TRIAL_SMS_TEMPLATES = [
  'sms_2fa',
  'sms_appointment_reminders',
  'sms_order_confirmation',
  'sms_delivery_updates',
  'sms_customer_support',
  'sms_marketing_promotions',
  'sms_event_notifications',
  'sms_account_alerts',
  'sms_feedback_surveys',
  'sms_internal_alerts',
] as const;

const TWILIO_TRIAL_SMS_TEMPLATE_SET = new Set<string>(TWILIO_TRIAL_SMS_TEMPLATES);

function resolveTwilioSmsBody(m: NormalizedMessage, c: ResolvedCredentials): string {
  const candidates = [
    m.content.template ? String(m.content.template) : '',
    m.metadata?.dlt_template_id ? String(m.metadata.dlt_template_id) : '',
    c.template_id ? String(c.template_id) : '',
    c.default_template ? String(c.default_template) : '',
    m.content.body ? String(m.content.body) : '',
  ]
    .map((v) => v.trim())
    .filter(Boolean);

  for (const value of candidates) {
    if (TWILIO_TRIAL_SMS_TEMPLATE_SET.has(value)) return value;
  }

  // Trial accounts cannot send free-form text; prefer configured default key.
  const trialMode = c.trial_mode === true || c.trial_mode === 'true' || c.account_mode === 'trial';
  if (trialMode) {
    return 'sms_2fa';
  }

  return String(m.content.body || '').trim();
}

export class TwilioSmsProviderAdapter implements ProviderAdapter {
  public readonly id: string;
  public readonly channel: Channel = 'sms';
  private fetchFn: typeof fetch;

  constructor(options: { id?: string; fetchFn?: typeof fetch } = {}) {
    this.id = options.id || 'twilio-sms';
    this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
  }

  public async validateConfiguration(c: ResolvedCredentials): Promise<void> {
    const sid = c.sid || c.account_sid || c.accountSid;
    const token = c.token || c.auth_token || c.authToken;
    if (!sid || !token) {
      throw new Error("Twilio SMS configuration requires 'sid' and 'token'");
    }
  }

  public async send(
    m: NormalizedMessage,
    c: ResolvedCredentials,
    signal: AbortSignal
  ): Promise<ProviderResult> {
    const sid = String(c.sid || c.account_sid || c.accountSid || '');
    const token = String(c.token || c.auth_token || c.authToken || '');
    const fromNumber = String(c.from || c.fromNumber || '');

    const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(sid)}/Messages.json`;

    const smsBody = resolveTwilioSmsBody(m, c);
    if (!smsBody) {
      return {
        success: false,
        error: {
          code: 'PROVIDER_REJECTED',
          message:
            'Twilio SMS requires a message body. Trial accounts must use a template key such as sms_2fa (same as Twilio Console Try out SMS).',
        },
      };
    }

    const bodyParams = new URLSearchParams();
    bodyParams.set('To', m.recipient);
    bodyParams.set('From', fromNumber);
    bodyParams.set('Body', smsBody);
    const firstAtt = m.content.attachments?.[0];
    if (firstAtt?.url) {
      bodyParams.set('MediaUrl', firstAtt.url);
    }

    const authHeader = 'Basic ' + Buffer.from(`${sid}:${token}`).toString('base64');

    try {
      const res = await this.fetchFn(url, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          Authorization: authHeader,
        },
        body: bodyParams.toString(),
        signal,
      });

      const resData: any = await res.json().catch(() => ({}));

      if (res.status === 201 || res.status === 200) {
        return {
          success: true,
          provider_reference: resData.sid || `tw_${Date.now()}`,
          raw_response: resData,
        };
      }

      let code: ProviderErrorCode = 'PROVIDER_5XX';
      if (res.status === 401 || res.status === 403) code = 'CREDENTIAL_ERROR';
      else if (res.status === 400) code = 'PROVIDER_REJECTED';
      else if (res.status === 429) code = 'PROVIDER_UNAVAILABLE';

      let message = resData.message || `Twilio SMS error status: ${res.status}`;
      if (/invalid template name/i.test(String(message))) {
        message =
          `${message} Twilio trial accounts only accept Body values like sms_2fa / sms_order_confirmation (not free-form text). Pick a trial template in Zyphon, or upgrade the Twilio account for custom SMS.`;
      }

      return {
        success: false,
        error: {
          code,
          message,
          raw: resData,
        },
      };
    } catch (err: any) {
      const isTimeout = signal.aborted || err.name === 'AbortError';
      return {
        success: false,
        error: {
          code: isTimeout ? 'TIMEOUT' : 'PROVIDER_5XX',
          message: err.message || 'Twilio SMS dispatch failed',
        },
      };
    }
  }

  public async healthCheck(c: ResolvedCredentials): Promise<ProviderHealth> {
    const startTime = Date.now();
    const sid = String(c.sid || c.account_sid || c.accountSid || '');
    const token = String(c.token || c.auth_token || c.authToken || '');

    if (!sid || !token) {
      return { status: 'down', latency_ms: 0, checked_at: Date.now(), message: 'Missing credentials' };
    }

    const url = `https://api.twilio.com/2010-04-01/Accounts/${encodeURIComponent(sid)}.json`;
    const authHeader = 'Basic ' + Buffer.from(`${sid}:${token}`).toString('base64');

    try {
      const res = await this.fetchFn(url, {
        method: 'GET',
        headers: { Authorization: authHeader },
      });

      const latency = Date.now() - startTime;
      if (res.ok) {
        return { status: 'healthy', latency_ms: latency, checked_at: Date.now() };
      }
      return { status: 'down', latency_ms: latency, checked_at: Date.now(), message: `HTTP ${res.status}` };
    } catch (err: any) {
      return { status: 'down', latency_ms: Date.now() - startTime, checked_at: Date.now(), message: err.message };
    }
  }

  public parseWebhook(req: WebhookRequest): DeliveryEvent | null {
    const body = (req.body as Record<string, string>) || {};
    const messageSid = body.MessageSid || body.SmsSid;
    const messageStatus = body.MessageStatus || body.SmsStatus;

    if (messageSid && messageStatus) {
      const isDelivered = messageStatus.toLowerCase() === 'delivered';
      return {
        message_id: messageSid,
        status: isDelivered ? 'delivered' : 'undelivered',
        timestamp: Date.now(),
        provider: this.id,
        raw: body,
      };
    }

    return null;
  }
}

