import { Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { IngestService } from '../interactions/ingest.service';
import { ActorResolver, type MessagePrincipal } from '../identity/actor.resolver';
import { TemplateService } from '../templates/template.service';
import { TemplatedSender } from '../templates/templated-sender';
import type { RenderedContent } from '../templates/template.renderer';
import type { TemplateSource } from '../templates/template.model';

export interface MailPart { kind: 'HTML' | 'TEXT'; bodyText: string }

/** Turn rendered content into ingest parts (HTML + TEXT) + the thread subject. Pure. */
export function contentToParts(content: RenderedContent): { subject?: string; parts: MailPart[] } {
  const parts: MailPart[] = [];
  if (content.html != null) parts.push({ kind: 'HTML', bodyText: content.html });
  if (content.text != null) parts.push({ kind: 'TEXT', bodyText: content.text });
  // A message must carry at least one part; fall back to an empty text part rather than fail ingest.
  if (parts.length === 0) parts.push({ kind: 'TEXT', bodyText: '' });
  return { ...(content.subject != null ? { subject: content.subject } : {}), parts };
}

export interface MailAttachment { filename?: string; contentRef: string; mimeType?: string; sizeBytes?: number }

export interface PostInternalInput {
  source: TemplateSource;
  recipientUserId: string;
  vars?: Record<string, unknown>;
  locale?: string;
  idempotencyKey: string;
  /** In-app attachment refs — stored as FILE message parts alongside the body. */
  attachments?: MailAttachment[];
}

export interface SendExternalInput {
  source: TemplateSource;
  target: string;               // email address (external)
  vars?: Record<string, unknown>;
  locale?: string;
  idempotencyKey: string;
  purpose?: string;
  /** Attachment refs (bytes resolved at send time). */
  attachments?: Array<{ filename?: string; contentRef: string; mimeType?: string }>;
  /** The registered recipient to mirror to; omit for a pre-registration send (email only, no mirror). */
  mirrorToUserId?: string;
}

/**
 * Mail orchestration: render a template, then deliver it.
 *  - INTERNAL (app-to-app): create an EMAIL interaction on a per-email thread — no SMTP.
 *  - EXTERNAL: send via SMTP (TemplatedSender) AND mirror a copy into the recipient's in-app inbox,
 *    but only when they are a registered user (pre-registration sends have no inbox yet).
 *
 * ingest() writes the interaction + thread but adds no participants, and a thread is only visible to
 * its participants — so after each ingest we add BOTH the sender and the recipient as participants.
 */
@Injectable()
export class MailService {
  constructor(
    private readonly templates: TemplateService,
    private readonly ingest: IngestService,
    private readonly sender: TemplatedSender,
    private readonly actors: ActorResolver,
    private readonly prisma: PrismaService,
  ) {}

  /** Marks a thread as CRM mail so it lists separately from chat (Messenger uses source=crm-messenger). */
  static readonly SOURCE = 'crm-mail';

  async postInternal(principal: MessagePrincipal, input: PostInternalInput): Promise<{ threadId: string; interactionId: string }> {
    const { content } = await this.templates.render(input.source, input.vars ?? {}, { channel: 'INTERNAL', ...(input.locale ? { locale: input.locale } : {}) });
    return this.deposit(principal, input.recipientUserId, content, input.idempotencyKey, 'PORTAL', input.attachments);
  }

  async sendExternalWithMirror(principal: MessagePrincipal, input: SendExternalInput): Promise<{ commandId: string; mirror?: { threadId: string; interactionId: string } }> {
    const command = await this.sender.sendTemplated({
      source: input.source, channel: 'EMAIL', target: input.target, vars: input.vars ?? {},
      ...(input.locale ? { locale: input.locale } : {}), idempotencyKey: input.idempotencyKey, ...(input.purpose ? { purpose: input.purpose } : {}),
      ...(input.attachments && input.attachments.length > 0 ? { attachments: input.attachments } : {}),
    });

    // Mirror only for a registered recipient (timing rule: no inbox exists pre-registration).
    if (!input.mirrorToUserId) return { commandId: command.id };

    const { content } = await this.templates.render(input.source, input.vars ?? {}, { channel: 'EMAIL', ...(input.locale ? { locale: input.locale } : {}) });
    const mirror = await this.deposit(principal, input.mirrorToUserId, content, `mirror:${input.idempotencyKey}`, 'EMAIL', input.attachments);
    return { commandId: command.id, mirror };
  }

  /** A stored media ref → a message part; kind follows the mime (image/video → MEDIA_REF, audio → VOICE_REF, else FILE_REF). */
  private attachmentPart(a: MailAttachment): { kind: 'MEDIA_REF' | 'VOICE_REF' | 'FILE_REF'; bodyText?: string; contentRef: string; mimeType?: string; sizeBytes?: number } {
    const mime = a.mimeType ?? '';
    const kind = /^(image|video)\//.test(mime) ? 'MEDIA_REF' : /^audio\//.test(mime) ? 'VOICE_REF' : 'FILE_REF';
    return { kind, ...(a.filename ? { bodyText: a.filename } : {}), contentRef: a.contentRef, ...(a.mimeType ? { mimeType: a.mimeType } : {}), ...(a.sizeBytes != null ? { sizeBytes: a.sizeBytes } : {}) };
  }

  /** Ingest the rendered content as an EMAIL interaction on a per-email thread, visible to both parties. */
  private async deposit(principal: MessagePrincipal, recipientUserId: string, content: RenderedContent, key: string, channelType: string, attachments?: MailAttachment[]): Promise<{ threadId: string; interactionId: string }> {
    const { subject, parts } = contentToParts(content);
    const allParts = [...parts, ...(attachments ?? []).map((a) => this.attachmentPart(a))];
    const res = await this.ingest.ingest(
      {
        scope: { orgId: principal.orgId, appId: principal.appId, ...(principal.tenantId ? { tenantId: principal.tenantId } : {}) },
        channel: { type: channelType, externalChannelId: channelType.toLowerCase() },
        source: { handleKind: 'PORTAL_USER', externalId: principal.userId, ...(principal.displayName ? { displayName: principal.displayName } : {}) },
        kind: 'EMAIL',
        thread: { externalThreadId: key, ...(subject ? { subject } : {}) },
        parts: allParts,
        occurredAt: new Date().toISOString(),
        providerEventId: key,
      },
      key,
    );

    // Make the thread visible to both the sender and the recipient (ingest adds no participants).
    const scope = await this.actors.resolveScope(principal);
    const senderActor = await this.actors.resolveActor(scope.id, principal);
    const recipientActor = await this.actors.resolveActor(scope.id, {
      userId: recipientUserId, appId: principal.appId, orgId: principal.orgId, ...(principal.tenantId ? { tenantId: principal.tenantId } : {}),
    });
    await this.actors.ensureParticipant(res.threadId, senderActor.id);
    await this.actors.ensureParticipant(res.threadId, recipientActor.id);

    // Tag the thread as CRM mail (thread metadata) so it lists separately from Messenger chat.
    // ingest() puts req.metadata on the interaction, not the thread, so we set it here directly.
    await this.prisma.iiosThread.update({
      where: { id: res.threadId },
      data: { metadata: { source: MailService.SOURCE } as Prisma.InputJsonValue },
    });

    return { threadId: res.threadId, interactionId: res.interactionId };
  }
}
