import 'server-only';

import { and, count, desc, eq, ilike, isNull, or } from 'drizzle-orm';
import {
  appNotificationDispatches,
  appNotificationEvents,
  appNotificationTemplates,
} from '@/db/schema';
import { auditAction } from '@/features/foundation/audit-log/service';
import { db } from '@/lib/db';
import { AuthError } from '@/lib/auth/session';
import { resolveNotificationAttachments } from './attachment-service';
import { executeNotificationDispatch } from './delivery-engine';
import { getNotificationProvider } from './provider-factory';
import { renderNotificationTemplate } from './template-renderer';
import type {
  NotificationDispatchRecord,
  NotificationDispatchRecipient,
  NotificationEventRecord,
  NotificationHistoryFilters,
  NotificationPreviewRequest,
  NotificationRenderResult,
  NotificationSendRequest,
  NotificationTemplateRecord,
  ResolvedNotificationAttachment,
} from '../types';

type TemplateOverride = NotificationPreviewRequest['templateOverride'];

function normalizeDispatchRecipients(value: unknown): NotificationDispatchRecipient[] {
  if (!Array.isArray(value)) {
    return [];
  }

  return value.flatMap((item) => {
    if (!item || typeof item !== 'object') {
      return [];
    }

    const email = typeof (item as { email?: unknown }).email === 'string'
      ? (item as { email: string }).email.trim().toLowerCase()
      : '';

    if (!email) {
      return [];
    }

    const name = typeof (item as { name?: unknown }).name === 'string'
      ? (item as { name: string }).name.trim()
      : null;

    return [{ email, name }];
  });
}

function normalizeAttachments(
  value: unknown,
): Array<Omit<ResolvedNotificationAttachment, 'buffer'>> {
  if (!Array.isArray(value)) {
    return [];
  }

  return value.flatMap((item) => {
    if (!item || typeof item !== 'object') {
      return [];
    }

    const candidate = item as Record<string, unknown>;

    if (
      typeof candidate.sourceType !== 'string' ||
      typeof candidate.sourceId !== 'string' ||
      typeof candidate.fileName !== 'string' ||
      typeof candidate.contentType !== 'string' ||
      typeof candidate.size !== 'number'
    ) {
      return [];
    }

    return [
      {
        sourceType: candidate.sourceType as ResolvedNotificationAttachment['sourceType'],
        sourceId: candidate.sourceId,
        fileName: candidate.fileName,
        contentType: candidate.contentType,
        size: candidate.size,
        checksum:
          typeof candidate.checksum === 'string' ? candidate.checksum : null,
        label: typeof candidate.label === 'string' ? candidate.label : null,
      },
    ];
  });
}

function mapEventRecord(row: typeof appNotificationEvents.$inferSelect): NotificationEventRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    eventType: row.eventType,
    entityType: row.entityType,
    entityId: row.entityId,
    actorUserId: row.actorUserId ?? null,
    payload: (row.payload as Record<string, unknown> | null) ?? null,
    dedupeKey: row.dedupeKey ?? null,
    status: row.status as NotificationEventRecord['status'],
    lastError: row.lastError ?? null,
    publishedAt: row.publishedAt.toISOString(),
    processedAt: row.processedAt?.toISOString() ?? null,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
  };
}

function mapTemplateRecord(
  row: typeof appNotificationTemplates.$inferSelect,
): NotificationTemplateRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    eventType: row.eventType,
    code: row.code,
    name: row.name,
    channel: row.channel as NotificationTemplateRecord['channel'],
    subjectTemplate: row.subjectTemplate ?? null,
    titleTemplate: row.titleTemplate,
    bodyTemplate: row.bodyTemplate,
    bodyFormat: row.bodyFormat as NotificationTemplateRecord['bodyFormat'],
    linkTemplate: row.linkTemplate ?? null,
    language: row.language,
    variables: Array.isArray(row.variables)
      ? row.variables.filter((value): value is string => typeof value === 'string')
      : [],
    version: row.version,
    isActive: row.isActive,
    metadata: (row.metadata as Record<string, unknown> | null) ?? null,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
    deletedAt: row.deletedAt?.toISOString() ?? null,
    createdBy: row.createdBy,
    updatedBy: row.updatedBy,
  };
}

function mapDispatchRecord(
  row: typeof appNotificationDispatches.$inferSelect,
): NotificationDispatchRecord {
  return {
    id: row.id,
    organizationId: row.organizationId,
    eventId: row.eventId,
    templateId: row.templateId ?? null,
    entityType: row.entityType,
    entityId: row.entityId,
    channel: row.channel as NotificationDispatchRecord['channel'],
    providerKey: row.providerKey,
    status: row.status as NotificationDispatchRecord['status'],
    recipientTo: normalizeDispatchRecipients(row.recipientTo),
    recipientCc: normalizeDispatchRecipients(row.recipientCc),
    recipientBcc: normalizeDispatchRecipients(row.recipientBcc),
    replyTo: row.replyTo ?? null,
    subject: row.subject,
    bodyText: row.bodyText,
    bodyHtml: row.bodyHtml ?? null,
    attachments: normalizeAttachments(row.attachments),
    recipientUserId: row.recipientUserId ?? null,
    recipientAddress: row.recipientAddress ?? null,
    requestedBy: row.requestedBy,
    metadata: (row.metadata as Record<string, unknown> | null) ?? null,
    lastError: row.lastError ?? null,
    retryCount: row.retryCount,
    maxRetryCount: row.maxRetryCount,
    lastAttemptAt: row.lastAttemptAt?.toISOString() ?? null,
    sentAt: row.sentAt?.toISOString() ?? null,
    createdAt: row.createdAt.toISOString(),
    updatedAt: row.updatedAt.toISOString(),
  };
}

function normalizeEmailAddress(value: string) {
  return value.trim().toLowerCase();
}

function assertRecipients(recipients: NotificationDispatchRecipient[], label: string) {
  const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

  if (!recipients.length) {
    if (label === 'to') {
      throw new AuthError('At least one recipient is required.', 400);
    }

    return;
  }

  for (const recipient of recipients) {
    if (!emailPattern.test(recipient.email)) {
      throw new AuthError(`Invalid ${label} email: ${recipient.email}`, 400);
    }
  }
}

async function loadTemplate(
  organizationId: string,
  eventType: string,
  channel: NotificationPreviewRequest['channel'],
) {
  const rows = await db
    .select()
    .from(appNotificationTemplates)
    .where(
      and(
        eq(appNotificationTemplates.organizationId, organizationId),
        eq(appNotificationTemplates.eventType, eventType),
        eq(appNotificationTemplates.isActive, true),
        isNull(appNotificationTemplates.deletedAt),
      ),
    )
    .limit(10);

  const row =
    rows.find((candidate) => candidate.channel === channel) ??
    rows.find((candidate) => candidate.channel === 'in_app') ??
    rows[0];

  return row ? mapTemplateRecord(row) : null;
}

async function ensureEvent(input: {
  organizationId: string;
  eventType: string;
  entityType: string;
  entityId: string;
  actorUserId: string;
  payload: Record<string, unknown>;
}) {
  const [created] = await db
    .insert(appNotificationEvents)
    .values({
      id: crypto.randomUUID(),
      organizationId: input.organizationId,
      eventType: input.eventType,
      entityType: input.entityType,
      entityId: input.entityId,
      actorUserId: input.actorUserId,
      payload: input.payload,
      status: 'pending',
      createdAt: new Date(),
      updatedAt: new Date(),
    })
    .returning();

  await auditAction({
    organizationId: input.organizationId,
    userId: input.actorUserId,
    entityType: 'app_notification_event',
    entityId: created.id,
    action: 'notification_requested',
    afterData: {
      eventType: input.eventType,
      entityType: input.entityType,
      entityId: input.entityId,
    },
  });

  return mapEventRecord(created);
}

function buildTemplateFromOverride(
  template: NotificationTemplateRecord,
  override?: TemplateOverride,
): NotificationTemplateRecord {
  if (!override) {
    return template;
  }

  return {
    ...template,
    subjectTemplate: override.subjectTemplate ?? template.subjectTemplate,
    titleTemplate: override.titleTemplate ?? template.titleTemplate,
    bodyTemplate: override.bodyTemplate ?? template.bodyTemplate,
    bodyFormat: override.bodyFormat ?? template.bodyFormat,
    linkTemplate:
      override.linkTemplate === undefined
        ? template.linkTemplate
        : override.linkTemplate,
  };
}

async function buildDispatchPreview(
  input: NotificationPreviewRequest,
): Promise<{
  template: NotificationTemplateRecord;
  attachments: ResolvedNotificationAttachment[];
  rendered: NotificationRenderResult;
  providerKey: string;
}> {
  const to = input.recipientOverride.to.map((recipient) => ({
    ...recipient,
    email: normalizeEmailAddress(recipient.email),
  }));
  const cc = (input.recipientOverride.cc ?? []).map((recipient) => ({
    ...recipient,
    email: normalizeEmailAddress(recipient.email),
  }));
  const bcc = (input.recipientOverride.bcc ?? []).map((recipient) => ({
    ...recipient,
    email: normalizeEmailAddress(recipient.email),
  }));

  assertRecipients(to, 'to');
  assertRecipients(cc, 'cc');
  assertRecipients(bcc, 'bcc');

  const template = await loadTemplate(
    input.organizationId,
    input.eventType,
    input.channel,
  );

  if (!template) {
    throw new AuthError(
      `Notification template not found for ${input.eventType} (${input.channel}).`,
      404,
    );
  }

  const nextTemplate = buildTemplateFromOverride(template, input.templateOverride);
  const rendered = renderNotificationTemplate(
    {
      subjectTemplate: nextTemplate.subjectTemplate,
      titleTemplate: nextTemplate.titleTemplate,
      bodyTemplate: nextTemplate.bodyTemplate,
      bodyFormat: nextTemplate.bodyFormat,
      linkTemplate: nextTemplate.linkTemplate,
    },
    input.payload ?? {},
  );

  const attachments = input.attachments?.length
    ? await resolveNotificationAttachments({
        organizationId: input.organizationId,
        userId: input.actorUserId,
        attachments: input.attachments,
      })
    : [];

  const providerKey = input.providerKey ?? (input.channel === 'email' ? 'smtp' : input.channel);

  return { template: nextTemplate, attachments, rendered, providerKey };
}

export async function previewNotification(input: NotificationPreviewRequest) {
  const preview = await buildDispatchPreview(input);

  return {
    eventType: input.eventType,
    entityType: input.entityType,
    entityId: input.entityId,
    channel: input.channel,
    providerKey: preview.providerKey,
    template: preview.template,
    rendered: preview.rendered,
    recipients: input.recipientOverride,
    attachments: preview.attachments.map(({ buffer: _buffer, ...attachment }) => attachment),
  };
}

async function persistDispatch(input: {
  request: NotificationSendRequest;
  event: NotificationEventRecord;
  template: NotificationTemplateRecord;
  rendered: NotificationRenderResult;
  attachments: ResolvedNotificationAttachment[];
  providerKey: string;
}) {
  const [created] = await db
    .insert(appNotificationDispatches)
    .values({
      id: crypto.randomUUID(),
      organizationId: input.request.organizationId,
      eventId: input.event.id,
      templateId: input.template.id,
      entityType: input.request.entityType,
      entityId: input.request.entityId,
      channel: input.request.channel,
      providerKey: input.providerKey,
      status: 'pending',
      recipientTo: input.request.recipientOverride.to,
      recipientCc: input.request.recipientOverride.cc ?? [],
      recipientBcc: input.request.recipientOverride.bcc ?? [],
      replyTo: input.request.recipientOverride.replyTo ?? null,
      subject: input.rendered.subject,
      bodyText: input.rendered.bodyText,
      bodyHtml: input.rendered.bodyHtml,
      attachments: input.attachments.map(({ buffer: _buffer, ...attachment }) => attachment),
      recipientUserId: input.request.recipientOverride.recipientUserId ?? null,
      recipientAddress: input.request.recipientOverride.to[0]?.email ?? null,
      requestedBy: input.request.actorUserId,
      metadata: {
        payload: input.request.payload ?? {},
        linkUrl: input.rendered.linkUrl,
        attachmentRefs: input.request.attachments ?? [],
        templateOverride: input.request.templateOverride ?? null,
      },
      retryCount: 0,
      maxRetryCount: input.request.maxRetryCount ?? 3,
      createdAt: new Date(),
      updatedAt: new Date(),
    })
    .returning();

  await auditAction({
    organizationId: input.request.organizationId,
    userId: input.request.actorUserId,
    entityType: 'app_notification_dispatch',
    entityId: created.id,
    action: 'notification_provider_selected',
    afterData: {
      providerKey: input.providerKey,
      channel: input.request.channel,
      recipientAddress: created.recipientAddress,
    },
  });

  return mapDispatchRecord(created);
}

async function runDispatch(input: {
  organizationId: string;
  actorUserId: string;
  dispatch: NotificationDispatchRecord;
  attachments: ResolvedNotificationAttachment[];
}) {
  const provider = getNotificationProvider(
    input.dispatch.channel,
    input.organizationId,
    input.dispatch.providerKey
  );

  await db
    .update(appNotificationDispatches)
    .set({
      status: 'sending',
      lastAttemptAt: new Date(),
      updatedAt: new Date(),
    })
    .where(eq(appNotificationDispatches.id, input.dispatch.id));

  await auditAction({
    organizationId: input.organizationId,
    userId: input.actorUserId,
    entityType: 'app_notification_dispatch',
    entityId: input.dispatch.id,
    action: 'notification_send_started',
    afterData: {
      providerKey: input.dispatch.providerKey,
      channel: input.dispatch.channel,
    },
  });

  const result = await executeNotificationDispatch({
    provider,
    retryCount: input.dispatch.retryCount,
    maxRetryCount: input.dispatch.maxRetryCount,
    message: {
      dispatchId: input.dispatch.id,
      subject: input.dispatch.subject,
      bodyText: input.dispatch.bodyText,
      bodyHtml: input.dispatch.bodyHtml,
      to: input.dispatch.recipientTo,
      cc: input.dispatch.recipientCc,
      bcc: input.dispatch.recipientBcc,
      replyTo: input.dispatch.replyTo,
      attachments: input.dispatch.attachments,
      attachmentBuffers: input.attachments.map((attachment) => attachment.buffer),
    },
  });

  const [updated] = await db
    .update(appNotificationDispatches)
    .set({
      status: result.status,
      retryCount: result.retryCount,
      lastError: result.errorMessage,
      lastAttemptAt: result.attemptedAt,
      sentAt: result.sentAt,
      metadata: {
        ...(input.dispatch.metadata ?? {}),
        providerResult: result.providerResult,
      },
      updatedAt: new Date(),
    })
    .where(eq(appNotificationDispatches.id, input.dispatch.id))
    .returning();

  await db
    .update(appNotificationEvents)
    .set({
      status: result.status === 'sent' ? 'processed' : 'failed',
      lastError: result.errorMessage,
      processedAt: result.sentAt,
      updatedAt: new Date(),
    })
    .where(eq(appNotificationEvents.id, input.dispatch.eventId));

  await auditAction({
    organizationId: input.organizationId,
    userId: input.actorUserId,
    entityType: 'app_notification_dispatch',
    entityId: input.dispatch.id,
    action:
      result.status === 'sent'
        ? 'notification_send_succeeded'
        : 'notification_send_failed',
    afterData: {
      status: result.status,
      retryCount: result.retryCount,
      error: result.errorMessage,
    },
  });

  return mapDispatchRecord(updated);
}

export async function sendNotification(input: NotificationSendRequest) {
  const preview = await buildDispatchPreview(input);
  const event = await ensureEvent({
    organizationId: input.organizationId,
    eventType: input.eventType,
    entityType: input.entityType,
    entityId: input.entityId,
    actorUserId: input.actorUserId,
    payload: input.payload ?? {},
  });

  const dispatch = await persistDispatch({
    request: input,
    event,
    template: preview.template,
    rendered: preview.rendered,
    attachments: preview.attachments,
    providerKey: preview.providerKey,
  });

  return runDispatch({
    organizationId: input.organizationId,
    actorUserId: input.actorUserId,
    dispatch,
    attachments: preview.attachments,
  });
}

export async function listNotificationHistory(
  organizationId: string,
  filters: NotificationHistoryFilters,
) {
  const page = Math.max(filters.page ?? 1, 1);
  const limit = Math.min(Math.max(filters.limit ?? 20, 1), 100);

  const where = and(
    eq(appNotificationDispatches.organizationId, organizationId),
    ...(filters.channel && filters.channel !== 'all'
      ? [eq(appNotificationDispatches.channel, filters.channel)]
      : []),
    ...(filters.status && filters.status !== 'all'
      ? [eq(appNotificationDispatches.status, filters.status)]
      : []),
    ...(filters.eventType ? [eq(appNotificationEvents.eventType, filters.eventType)] : []),
    ...(filters.entityType ? [eq(appNotificationDispatches.entityType, filters.entityType)] : []),
    ...(filters.entityId ? [eq(appNotificationDispatches.entityId, filters.entityId)] : []),
    ...(filters.search
      ? [
          or(
            ilike(appNotificationDispatches.subject, `%${filters.search}%`),
            ilike(appNotificationDispatches.recipientAddress, `%${filters.search}%`),
          )!,
        ]
      : []),
  );

  const [rows, totalRow] = await Promise.all([
    db
      .select({
        dispatch: appNotificationDispatches,
      })
      .from(appNotificationDispatches)
      .innerJoin(
        appNotificationEvents,
        eq(appNotificationEvents.id, appNotificationDispatches.eventId),
      )
      .where(where)
      .orderBy(desc(appNotificationDispatches.createdAt))
      .limit(limit)
      .offset((page - 1) * limit),
    db
      .select({ value: count() })
      .from(appNotificationDispatches)
      .innerJoin(
        appNotificationEvents,
        eq(appNotificationEvents.id, appNotificationDispatches.eventId),
      )
      .where(where),
  ]);

  return {
    items: rows.map((row) => mapDispatchRecord(row.dispatch)),
    page,
    limit,
    totalItems: totalRow[0]?.value ?? 0,
    totalPages: Math.max(Math.ceil((totalRow[0]?.value ?? 0) / limit), 1),
  };
}

export async function getNotificationDispatchDetail(
  organizationId: string,
  dispatchId: string,
) {
  const [row] = await db
    .select({
      dispatch: appNotificationDispatches,
      event: appNotificationEvents,
      template: appNotificationTemplates,
    })
    .from(appNotificationDispatches)
    .innerJoin(
      appNotificationEvents,
      eq(appNotificationEvents.id, appNotificationDispatches.eventId),
    )
    .leftJoin(
      appNotificationTemplates,
      eq(appNotificationTemplates.id, appNotificationDispatches.templateId),
    )
    .where(
      and(
        eq(appNotificationDispatches.id, dispatchId),
        eq(appNotificationDispatches.organizationId, organizationId),
      ),
    )
    .limit(1);

  if (!row) {
    throw new AuthError('Notification history item not found.', 404);
  }

  return {
    dispatch: mapDispatchRecord(row.dispatch),
    event: mapEventRecord(row.event),
    template: row.template ? mapTemplateRecord(row.template) : null,
  };
}

export async function resendNotification(input: {
  organizationId: string;
  actorUserId: string;
  dispatchId: string;
}) {
  const detail = await getNotificationDispatchDetail(
    input.organizationId,
    input.dispatchId,
  );

  const attachmentRefs = Array.isArray(detail.dispatch.metadata?.attachmentRefs)
    ? detail.dispatch.metadata?.attachmentRefs
    : [];

  const preview = await buildDispatchPreview({
    organizationId: input.organizationId,
    actorUserId: input.actorUserId,
    eventType: detail.event.eventType,
    entityType: detail.dispatch.entityType,
    entityId: detail.dispatch.entityId,
    channel: detail.dispatch.channel,
    payload:
      (detail.dispatch.metadata?.payload as Record<string, unknown> | undefined) ?? {},
    templateOverride:
      (detail.dispatch.metadata?.templateOverride as TemplateOverride | undefined) ??
      undefined,
    recipientOverride: {
      to: detail.dispatch.recipientTo,
      cc: detail.dispatch.recipientCc,
      bcc: detail.dispatch.recipientBcc,
      replyTo: detail.dispatch.replyTo,
      recipientUserId: detail.dispatch.recipientUserId,
    },
    attachments: attachmentRefs,
    providerKey: detail.dispatch.providerKey,
  });

  const [created] = await db
    .insert(appNotificationDispatches)
    .values({
      id: crypto.randomUUID(),
      organizationId: input.organizationId,
      eventId: detail.event.id,
      templateId: detail.template?.id ?? null,
      entityType: detail.dispatch.entityType,
      entityId: detail.dispatch.entityId,
      channel: detail.dispatch.channel,
      providerKey: detail.dispatch.providerKey,
      status: 'pending',
      recipientTo: detail.dispatch.recipientTo,
      recipientCc: detail.dispatch.recipientCc,
      recipientBcc: detail.dispatch.recipientBcc,
      replyTo: detail.dispatch.replyTo,
      subject: preview.rendered.subject,
      bodyText: preview.rendered.bodyText,
      bodyHtml: preview.rendered.bodyHtml,
      attachments: preview.attachments.map(({ buffer: _buffer, ...attachment }) => attachment),
      recipientUserId: detail.dispatch.recipientUserId,
      recipientAddress: detail.dispatch.recipientAddress,
      requestedBy: input.actorUserId,
      metadata: detail.dispatch.metadata,
      retryCount: 0,
      maxRetryCount: detail.dispatch.maxRetryCount,
      createdAt: new Date(),
      updatedAt: new Date(),
    })
    .returning();

  await auditAction({
    organizationId: input.organizationId,
    userId: input.actorUserId,
    entityType: 'app_notification_dispatch',
    entityId: created.id,
    action: 'notification_resend',
    afterData: {
      originalDispatchId: detail.dispatch.id,
      eventId: detail.event.id,
    },
  });

  return runDispatch({
    organizationId: input.organizationId,
    actorUserId: input.actorUserId,
    dispatch: mapDispatchRecord(created),
    attachments: preview.attachments,
  });
}
