import { and, asc, eq, inArray, isNull } from 'drizzle-orm';
import {
  appNotificationDeliveries,
  appNotificationEvents,
  appNotificationTemplates,
  appNotifications
} from '@/db/schema';
import { auditAction } from '@/features/foundation/audit-log/service';
import { db } from '@/lib/db';
import { resolveNotificationRecipients } from './recipient-resolver';
import { renderNotificationTemplate } from './template-renderer';
import type {
  NotificationChannel,
  NotificationEventRecord,
  NotificationRecipientTarget,
  NotificationSeverity,
  NotificationTemplateRecord,
  PublishNotificationEventInput
} from '../types';

type EventRule = {
  severity: NotificationSeverity;
  recipients: NotificationRecipientTarget[];
};

const IN_APP_CHANNEL: NotificationChannel = 'in_app';

const EVENT_RULES: Record<string, EventRule> = {
  'approval.requested': {
    severity: 'warning',
    recipients: [{ type: 'approval_current_step_approvers', excludeActor: true }]
  },
  'approval.step.approved': {
    severity: 'success',
    recipients: [
      { type: 'approval_requester' },
      { type: 'approval_current_step_approvers', excludeActor: true }
    ]
  },
  'approval.step.rejected': {
    severity: 'error',
    recipients: [{ type: 'approval_requester' }]
  },
  'approval.completed': {
    severity: 'success',
    recipients: [{ type: 'approval_requester' }]
  },
  'approval.cancelled': {
    severity: 'info',
    recipients: [
      { type: 'approval_requester' },
      { type: 'approval_current_step_approvers', excludeActor: true }
    ]
  },
  'approval.returned': {
    severity: 'warning',
    recipients: [{ type: 'approval_requester' }]
  },
  'approval.reminder': {
    severity: 'warning',
    recipients: [{ type: 'approval_current_step_approvers', excludeActor: true }]
  },
  'approval.escalated': {
    severity: 'warning',
    recipients: [
      { type: 'explicit_user', excludeActor: true },
      { type: 'approval_requester' }
    ]
  },
  'approval.timeout': {
    severity: 'error',
    recipients: [
      { type: 'explicit_user', excludeActor: true },
      { type: 'approval_requester' }
    ]
  }
};

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 normalizeError(error: unknown) {
  if (error instanceof Error) {
    return error.message.slice(0, 500);
  }

  return 'Unknown notification processing error';
}

async function loadEventById(eventId: string) {
  const [row] = await db
    .select()
    .from(appNotificationEvents)
    .where(eq(appNotificationEvents.id, eventId))
    .limit(1);

  return row ? mapEventRecord(row) : null;
}

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

  return row ? mapTemplateRecord(row) : null;
}

async function markEventFailure(
  eventId: string,
  organizationId: string,
  actorUserId: string | null,
  error: unknown
) {
  const message = normalizeError(error);

  await db
    .update(appNotificationEvents)
    .set({
      status: 'failed',
      lastError: message,
      updatedAt: new Date()
    })
    .where(eq(appNotificationEvents.id, eventId));

  await auditAction({
    organizationId,
    userId: actorUserId ?? 'system',
    entityType: 'app_notification_event',
    entityId: eventId,
    action: 'notification_event_failed',
    afterData: { error: message }
  });
}

export async function processNotificationEvent(eventId: string) {
  const event = await loadEventById(eventId);
  if (!event) {
    throw new Error('Notification event not found');
  }

  if (event.status === 'processed') {
    return event;
  }

  try {
    const rule = EVENT_RULES[event.eventType];
    if (!rule) {
      throw new Error(`Unsupported notification event type: ${event.eventType}`);
    }

    const template = await loadTemplate(event.organizationId, event.eventType);
    if (!template) {
      throw new Error(`Notification template not found for ${event.eventType}`);
    }

    const payload = event.payload ?? {};
    const recipientLists = await Promise.all(
      rule.recipients.map((recipient) =>
        resolveNotificationRecipients({
          organizationId: event.organizationId,
          actorUserId: event.actorUserId,
          recipient,
          payload
        })
      )
    );

    const recipients = [...new Set(recipientLists.flat())];
    const existingRows = recipients.length
      ? await db
          .select({ recipientUserId: appNotifications.recipientUserId })
          .from(appNotifications)
          .where(
            and(
              eq(appNotifications.eventId, event.id),
              inArray(appNotifications.recipientUserId, recipients)
            )
          )
      : [];
    const existingRecipientIds = new Set(existingRows.map((row) => row.recipientUserId));
    const recipientsToCreate = recipients.filter((userId) => !existingRecipientIds.has(userId));

    const createdIds: string[] = [];

    for (const recipientUserId of recipientsToCreate) {
      const rendered = renderNotificationTemplate(template, payload);
      const notificationId = crypto.randomUUID();

      await db.insert(appNotifications).values({
        id: notificationId,
        organizationId: event.organizationId,
        recipientUserId,
        eventId: event.id,
        title: rendered.title,
    body: rendered.bodyText,
        linkUrl: rendered.linkUrl,
        severity: rule.severity,
        status: 'unread',
        metadata: payload,
        createdAt: new Date(),
        updatedAt: new Date()
      });

      await db.insert(appNotificationDeliveries).values({
        id: crypto.randomUUID(),
        organizationId: event.organizationId,
        notificationId,
        channel: IN_APP_CHANNEL,
        recipientAddress: recipientUserId,
        status: 'sent',
        attemptCount: 1,
        sentAt: new Date(),
        createdAt: new Date(),
        updatedAt: new Date()
      });

      createdIds.push(notificationId);

      await auditAction({
        organizationId: event.organizationId,
        userId: event.actorUserId ?? recipientUserId,
        entityType: 'app_notification',
        entityId: notificationId,
        action: 'notification_created',
        afterData: {
          recipientUserId,
          eventId: event.id,
          eventType: event.eventType
        }
      });
    }

    await db
      .update(appNotificationEvents)
      .set({
        status: 'processed',
        processedAt: new Date(),
        lastError: null,
        updatedAt: new Date()
      })
      .where(eq(appNotificationEvents.id, event.id));

    await auditAction({
      organizationId: event.organizationId,
      userId: event.actorUserId ?? 'system',
      entityType: 'app_notification_event',
      entityId: event.id,
      action: 'notification_event_processed',
      afterData: { createdNotificationIds: createdIds, recipientCount: recipients.length }
    });

    return await loadEventById(event.id);
  } catch (error) {
    await markEventFailure(event.id, event.organizationId, event.actorUserId, error);
    throw error;
  }
}

export async function processPendingNotificationEvents() {
  const rows = await db
    .select({ id: appNotificationEvents.id })
    .from(appNotificationEvents)
    .where(eq(appNotificationEvents.status, 'pending'))
    .orderBy(asc(appNotificationEvents.publishedAt))
    .limit(100);

  for (const row of rows) {
    try {
      await processNotificationEvent(row.id);
    } catch {
      // Event failures are persisted for inspection.
    }
  }
}

export async function publishNotificationEvent(input: PublishNotificationEventInput) {
  const existing =
    input.dedupeKey
      ? await db
          .select()
          .from(appNotificationEvents)
          .where(
            and(
              eq(appNotificationEvents.organizationId, input.organizationId),
              eq(appNotificationEvents.dedupeKey, input.dedupeKey)
            )
          )
          .limit(1)
      : [];

  const row =
    existing[0] ??
    (
      await db
        .insert(appNotificationEvents)
        .values({
          id: crypto.randomUUID(),
          organizationId: input.organizationId,
          eventType: input.eventType,
          entityType: input.entityType,
          entityId: input.entityId,
          actorUserId: input.actorUserId ?? null,
          payload: input.payload ?? null,
          dedupeKey: input.dedupeKey ?? null,
          status: 'pending',
          publishedAt: new Date(),
          createdAt: new Date(),
          updatedAt: new Date()
        })
        .returning()
    )[0];

  await auditAction({
    organizationId: row.organizationId,
    userId: row.actorUserId ?? 'system',
    entityType: 'app_notification_event',
    entityId: row.id,
    action: 'notification_event_published',
    afterData: {
      eventType: row.eventType,
      entityType: row.entityType,
      entityId: row.entityId,
      dedupeKey: row.dedupeKey
    }
  });

  try {
    await processNotificationEvent(row.id);
  } catch {
    // Event failures are recorded and must not block callers.
  }

  return mapEventRecord(row);
}
