import { randomUUID } from 'node:crypto';
import { createTransport } from 'nodemailer';
import type { CapabilityProvider, CapabilityRequest, ProviderResult } from '@insignia/iios-contracts';

/** One SMTP sending identity (a mailbox + how to reach its server). */
export interface SmtpIdentity {
  host: string;
  port: number;
  secure: boolean;
  user: string;
  pass: string;
  from: string;
}

export interface MailAttachment {
  filename: string;
  content: Buffer;
  contentType?: string;
}

/** Fetch an attachment's bytes by its opaque contentRef (backed by the media StoragePort). */
export type AttachmentResolver = (contentRef: string) => Promise<{ filename?: string; content: Buffer; contentType?: string } | null>;

/** Build an AttachmentResolver over the media StoragePort (contentRef = the storage object key). */
export function storageResolver(storage: { get(key: string): Promise<{ data: Buffer; mime: string } | null> }): AttachmentResolver {
  return async (contentRef) => {
    const o = await storage.get(contentRef);
    return o ? { content: o.data, contentType: o.mime } : null;
  };
}

/** The subset of a mail transport this provider needs — lets tests inject a stub (no live server). */
export interface MailTransport {
  sendMail(mail: {
    from: string;
    to: string;
    subject?: string;
    text?: string;
    html?: string;
    inReplyTo?: string;
    references?: string;
    attachments?: MailAttachment[];
  }): Promise<{ messageId: string; accepted?: unknown[] }>;
}

const bool = (v: string | undefined): boolean => v === 'true' || v === '1';

/** Build the primary SMTP identity from env, or null if the required trio is incomplete. */
export function smtpIdentityFromEnv(env: NodeJS.ProcessEnv = process.env): SmtpIdentity | null {
  return identityFrom(env, '');
}

/** Build the optional fallback identity (accounts@ → ceo@), or null if not configured. */
export function smtpFallbackFromEnv(env: NodeJS.ProcessEnv = process.env): SmtpIdentity | null {
  const fb = identityFrom(env, 'FALLBACK_');
  if (fb) return fb;
  // Fallback may reuse the primary host/port and only override the mailbox identity.
  const host = env.IIOS_SMTP_HOST;
  const user = env.IIOS_SMTP_FALLBACK_USER;
  const pass = env.IIOS_SMTP_FALLBACK_PASS;
  if (!host || !user || !pass) return null;
  return {
    host,
    port: Number(env.IIOS_SMTP_PORT ?? 587),
    secure: bool(env.IIOS_SMTP_SECURE),
    user,
    pass,
    from: env.IIOS_SMTP_FALLBACK_FROM ?? user,
  };
}

function identityFrom(env: NodeJS.ProcessEnv, prefix: string): SmtpIdentity | null {
  const host = env[`IIOS_SMTP_${prefix}HOST`] ?? (prefix ? undefined : env.IIOS_SMTP_HOST);
  const user = env[`IIOS_SMTP_${prefix}USER`];
  const pass = env[`IIOS_SMTP_${prefix}PASS`];
  if (!host || !user || !pass) return null;
  return {
    host,
    port: Number(env[`IIOS_SMTP_${prefix}PORT`] ?? env.IIOS_SMTP_PORT ?? 587),
    secure: bool(env[`IIOS_SMTP_${prefix}SECURE`] ?? env.IIOS_SMTP_SECURE),
    user,
    pass,
    from: env[`IIOS_SMTP_${prefix}FROM`] ?? user,
  };
}

/** Map a tenant's stored SMTP config (BYO) to a sending identity, or null if incomplete. */
export function smtpIdentityFromConfig(c: Record<string, unknown> | null | undefined): SmtpIdentity | null {
  if (!c) return null;
  const s = (v: unknown): string => (typeof v === 'string' ? v.trim() : '');
  const host = s(c.host);
  const user = s(c.user);
  const pass = s(c.pass);
  const fromEmail = s(c.fromEmail);
  if (!host || !user || !pass || !fromEmail) return null;
  const fromName = s(c.fromName);
  return {
    host,
    port: Number(c.port) || 587,
    secure: c.secure === true || c.secure === 'true',
    user,
    pass,
    from: fromName ? `${fromName} <${fromEmail}>` : fromEmail,
  };
}

/** Resolve a scope's own SMTP identity (BYO), decrypting the stored credential. Null when unset. */
export type SmtpCredResolver = (scopeId: string) => Promise<SmtpIdentity | null>;

interface EmailPayload {
  subject?: string;
  text?: string;
  html?: string;
  inReplyTo?: string;
  /** Attachment REFS (not bytes) — resolved to bytes at send time via the injected resolver. */
  attachments?: Array<{ filename?: string; contentRef: string; mimeType?: string }>;
}

/**
 * SMTP egress provider (nodemailer). Bound for EMAIL when the SMTP env trio is set (else the sandbox
 * stays). A transport failure surfaces as FAILED, never thrown. On a PRE-acceptance failure it retries
 * once via the fallback identity (accounts@ → ceo@); a post-acceptance failure is NOT retried, so a
 * message the server already accepted can't be double-delivered.
 */
export class SmtpProvider implements CapabilityProvider {
  readonly name = 'smtp';
  readonly channelTypes = ['EMAIL'];
  readonly capabilities = { canSend: true };

  private readonly makeTransport: (id: SmtpIdentity) => MailTransport;

  constructor(
    private readonly primary: SmtpIdentity | undefined,
    private readonly fallback?: SmtpIdentity,
    makeTransport?: (id: SmtpIdentity) => MailTransport,
    private readonly resolveAttachment?: AttachmentResolver,
    /** BYO: resolve the tenant's own SMTP identity per scope; used before the env identity. */
    private readonly credResolver?: SmtpCredResolver,
  ) {
    this.makeTransport = makeTransport ?? defaultTransport;
  }

  async send(req: CapabilityRequest): Promise<ProviderResult> {
    const started = Date.now();
    const p = (req.payload ?? {}) as EmailPayload;

    // BYO SMTP: a tenant's own identity wins over the platform env identity. The env fallback
    // (accounts@ → ceo@) applies ONLY to the platform identity, never to a tenant's own server.
    const tenant = req.scopeId && this.credResolver ? await this.credResolver(req.scopeId).catch(() => null) : null;
    const identity = tenant ?? this.primary;
    if (!identity) return this.failed('NOT_CONFIGURED', started);
    const fallback = tenant ? undefined : this.fallback;

    // Resolve attachment bytes up front. Fail CLOSED — never send an invoice/receipt email missing
    // its file; a FAILED command retries instead. Resolved once so a fallback retry doesn't re-fetch.
    let attachments: MailAttachment[] | undefined;
    if (p.attachments && p.attachments.length > 0) {
      if (!this.resolveAttachment) return this.failed('NO_ATTACHMENT_RESOLVER', started);
      const out: MailAttachment[] = [];
      for (const a of p.attachments) {
        const r = await this.resolveAttachment(a.contentRef).catch(() => null);
        if (!r) return this.failed('ATTACHMENT_UNRESOLVED', started);
        out.push({ filename: a.filename ?? r.filename ?? 'attachment', content: r.content, ...(a.mimeType ?? r.contentType ? { contentType: a.mimeType ?? r.contentType } : {}) });
      }
      attachments = out;
    }

    const attempt = async (id: SmtpIdentity): Promise<{ messageId: string }> =>
      this.makeTransport(id).sendMail({
        from: id.from,
        to: req.target,
        subject: p.subject ?? '(no subject)',
        text: p.text,
        html: p.html,
        ...(p.inReplyTo ? { inReplyTo: p.inReplyTo, references: p.inReplyTo } : {}),
        ...(attachments ? { attachments } : {}),
      });

    try {
      const info = await attempt(identity);
      return { providerRef: info.messageId, outcome: 'SENT', latencyMs: Date.now() - started };
    } catch (err) {
      // Retry via the fallback identity ONLY if the primary never got the message accepted.
      if (fallback && isPreAcceptanceFailure(err)) {
        try {
          const info = await attempt(fallback);
          return { providerRef: `fallback:${info.messageId}`, outcome: 'SENT', latencyMs: Date.now() - started };
        } catch (err2) {
          return { providerRef: `smtp-error-${randomUUID().slice(0, 8)}`, outcome: 'FAILED', errorCode: codeOf(err2), latencyMs: Date.now() - started };
        }
      }
      return { providerRef: `smtp-error-${randomUUID().slice(0, 8)}`, outcome: 'FAILED', errorCode: codeOf(err), latencyMs: Date.now() - started };
    }
  }

  private failed(errorCode: string, started: number): ProviderResult {
    return { providerRef: `smtp-error-${randomUUID().slice(0, 8)}`, outcome: 'FAILED', errorCode, latencyMs: Date.now() - started };
  }
}

/** True for connect/auth/timeout errors (server never accepted); false once the server responded 2xx. */
function isPreAcceptanceFailure(err: unknown): boolean {
  const e = err as { code?: string; responseCode?: number };
  const preCodes = ['ECONNECTION', 'ETIMEDOUT', 'ECONNREFUSED', 'EDNS', 'EAUTH', 'ESOCKET', 'EENVELOPE'];
  if (e.code && preCodes.includes(e.code)) return true;
  // A responseCode present means the server spoke — treat 5xx after acceptance as terminal (no retry).
  return e.responseCode == null && e.code == null;
}

function codeOf(err: unknown): string {
  const e = err as { code?: string; message?: string };
  return (e.code ?? e.message ?? 'SMTP_ERROR').slice(0, 60);
}

function defaultTransport(id: SmtpIdentity): MailTransport {
  return createTransport({ host: id.host, port: id.port, secure: id.secure, auth: { user: id.user, pass: id.pass } }) as unknown as MailTransport;
}
