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

export interface SmtpOptions {
  id?: string;
  host?: string;
  port?: number;
  secure?: boolean;
  user?: string;
  pass?: string;
  from?: string;
}

export class SmtpProviderAdapter implements ProviderAdapter {
  public readonly id: string;
  public readonly channel: Channel = 'email';
  private transporter: Transporter | null = null;
  private currentCredsKey = '';

  constructor(options: SmtpOptions = {}) {
    this.id = options.id || 'smtp';
  }

  private getTransporter(creds: ResolvedCredentials): Transporter {
    const host = String(creds.host || 'localhost');
    const port = Number(creds.port || 587);
    // Port 587 = STARTTLS (plain then upgrade). Port 465 = implicit SSL.
    // Mis-selecting "ssl" on 587 causes OpenSSL "wrong version number".
    let security = creds.security ? String(creds.security).toLowerCase() : undefined;
    if (security === 'ssl' && port === 587) security = 'starttls';
    if (security === 'starttls' && port === 465) security = 'ssl';
    const secure =
      security === 'ssl'
        ? true
        : security === 'none' || security === 'starttls'
        ? false
        : Boolean(creds.secure || port === 465);
    const requireTLS = security === 'starttls';
    const userRaw = creds.username ?? creds.user;
    const passRaw = creds.password ?? creds.pass;
    const user = userRaw ? String(userRaw) : undefined;
    const pass = passRaw ? String(passRaw) : undefined;

    const key = `${host}:${port}:${secure}:${requireTLS}:${user}:${pass}`;
    if (this.transporter && this.currentCredsKey === key) {
      return this.transporter;
    }

    this.transporter = nodemailer.createTransport({
      host,
      port,
      secure,
      requireTLS,
      auth: user ? { user, pass } : undefined,
      pool: true,
      maxConnections: 5,
      maxMessages: 100,
    });
    this.currentCredsKey = key;
    return this.transporter;
  }

  public async validateConfiguration(c: ResolvedCredentials): Promise<void> {
    if (!c.host) {
      throw new Error(`SMTP configuration missing 'host' parameter`);
    }
  }

  public async send(
    m: NormalizedMessage,
    c: ResolvedCredentials,
    signal: AbortSignal
  ): Promise<ProviderResult> {
    if (signal.aborted) {
      return {
        success: false,
        error: { code: 'TIMEOUT', message: 'Send operation aborted before execution' },
      };
    }

    const transporter = this.getTransporter(c);
    const rawFrom = String(c.from || 'noreply@zyphon.local');
    const fromName = c.from_name ? String(c.from_name).trim() : '';
    const fromAddress = fromName && !rawFrom.includes('<') ? `"${fromName}" <${rawFrom}>` : rawFrom;

    const mailOptions: nodemailer.SendMailOptions = {
      from: fromAddress,
      to: m.recipient,
      subject: m.content.subject || '(No Subject)',
      text: m.content.type === 'text' ? m.content.body : undefined,
      html: m.content.type === 'html' ? m.content.body : undefined,
      attachments: m.content.attachments?.map((att) =>
        att.url && !att.content
          ? {
              filename: att.filename,
              path: att.url,
              contentType: att.contentType,
            }
          : {
              filename: att.filename,
              content: Buffer.from(String(att.content || ''), 'base64'),
              contentType: att.contentType,
            },
      ),
    };

    try {
      const info = await new Promise<nodemailer.SentMessageInfo>((resolve, reject) => {
        const onAbort = () => reject(new Error('Operation aborted by timeout'));
        signal.addEventListener('abort', onAbort);

        transporter.sendMail(mailOptions, (err, sendInfo) => {
          signal.removeEventListener('abort', onAbort);
          if (err) reject(err);
          else resolve(sendInfo);
        });
      });

      return {
        success: true,
        provider_reference: info.messageId,
        raw_response: {
          accepted: info.accepted,
          rejected: info.rejected,
          response: info.response,
        },
      };
    } catch (err: any) {
      const isTimeout = signal.aborted || err.message?.includes('aborted');
      let code: ProviderErrorCode = isTimeout ? 'TIMEOUT' : 'PROVIDER_5XX';

      if (err.responseCode && err.responseCode >= 500) {
        code = 'PROVIDER_5XX';
      } else if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
        code = 'PROVIDER_UNAVAILABLE';
      } else if (err.code === 'EAUTH') {
        code = 'CREDENTIAL_ERROR';
      }

      return {
        success: false,
        error: {
          code,
          message: err.message || 'SMTP dispatch error',
          raw: { code: err.code, responseCode: err.responseCode },
        },
      };
    }
  }

  public async healthCheck(c: ResolvedCredentials): Promise<ProviderHealth> {
    const startTime = Date.now();
    try {
      const transporter = this.getTransporter(c);
      await transporter.verify();
      return {
        status: 'healthy',
        latency_ms: Date.now() - startTime,
        checked_at: Date.now(),
        message: 'SMTP connection verified',
      };
    } catch (err: any) {
      return {
        status: 'down',
        latency_ms: Date.now() - startTime,
        checked_at: Date.now(),
        message: err.message || 'SMTP verify failed',
      };
    }
  }

  public parseWebhook(_req: WebhookRequest): DeliveryEvent | null {
    return null; // SMTP does not receive webhooks directly
  }
}

