import { Injectable } from '@nestjs/common';
import { Prisma, UserNotificationType } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { hasEffectivePermission } from '../../common/membership-permissions';
import { isActorTreatmentProvider } from '../../common/treatment-provider-scope';
import { RealtimeEmitter } from '../../realtime/realtime.emitter';

const membershipInclude = {
  permissions: { include: { permission: true } },
  organization: { include: { type: true, plan: true } },
} satisfies Prisma.MembershipInclude;

export type UserNotificationDto = {
  id: string;
  type: UserNotificationType;
  href: string;
  payload: Record<string, unknown> | null;
  readAt: string | null;
  createdAt: string;
  actorUserId: string | null;
};

type FanoutInput = {
  organizationId: string;
  type: UserNotificationType;
  href: string;
  actorUserId?: string | null;
  payload?: Record<string, unknown> | null;
  /** Explicit recipient user ids (e.g. task assignee). Skips permission fan-out. */
  recipientUserIds?: string[];
  /** Required tab permission when resolving org members. */
  requiredPermission?: string;
  /** When set, only clinic members who are the treatment provider for this case. */
  labCaseIdForProviderScope?: string;
};

@Injectable()
export class UserNotificationService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly realtime: RealtimeEmitter,
  ) {}

  /**
   * Lean lab-case snapshot for inbox cards. Used only inside `notify()` so
   * inbox list/read never joins patients/orgs/tasks.
   */
  private async labCaseInboxPayload(
    labCaseId: string,
    extra?: Record<string, unknown>,
  ): Promise<Record<string, unknown>> {
    const labCase = await this.prisma.labCase.findUnique({
      where: { id: labCaseId },
      select: {
        id: true,
        destinationOrganizationId: true,
        origin: true,
        referringClinicName: true,
        patientDisplayName: true,
        partnerClinic: { select: { name: true } },
        treatment: {
          select: {
            organization: { select: { name: true } },
            patient: { select: { firstName: true, lastName: true } },
          },
        },
        toothProsthesis: { select: { prosthesisTypeCode: true } },
        tasks: { select: { prosthesisTypeCode: true }, take: 40 },
        sends: {
          orderBy: { sentAt: 'asc' },
          take: 1,
          select: { organization: { select: { name: true } } },
        },
      },
    });

    if (!labCase) {
      return { labCaseId, ...(extra ?? {}) };
    }

    let labName: string | null = labCase.sends[0]?.organization.name ?? null;
    if (!labName && labCase.destinationOrganizationId) {
      const dest = await this.prisma.organization.findUnique({
        where: { id: labCase.destinationOrganizationId },
        select: { name: true },
      });
      labName = dest?.name ?? null;
    }

    const prosthesisTypeCodes = [
      ...new Set(
        [
          ...labCase.toothProsthesis.map((row) => row.prosthesisTypeCode),
          ...labCase.tasks.map((row) => row.prosthesisTypeCode),
        ].filter(Boolean),
      ),
    ];

    const patient = labCase.treatment?.patient;
    const patientName = patient
      ? `${patient.firstName} ${patient.lastName}`.trim()
      : labCase.patientDisplayName?.trim() || '';
    const clinicName =
      labCase.treatment?.organization.name ??
      labCase.partnerClinic?.name ??
      labCase.referringClinicName?.trim() ??
      '';

    return {
      ...(extra ?? {}),
      labCaseId,
      patientName,
      clinicName,
      labName,
      prosthesisTypeCodes,
    };
  }

  /** Denormalize display fields into payload once at write time. */
  private async enrichInboxPayload(
    input: FanoutInput,
  ): Promise<Record<string, unknown> | null> {
    const base =
      input.payload && typeof input.payload === 'object' && !Array.isArray(input.payload)
        ? { ...(input.payload as Record<string, unknown>) }
        : {};

    try {
      const labCaseId =
        (typeof base.labCaseId === 'string' && base.labCaseId) ||
        input.labCaseIdForProviderScope ||
        null;

      let enriched: Record<string, unknown> = { ...base };

      if (labCaseId) {
        enriched = await this.labCaseInboxPayload(labCaseId, enriched);
      }

      const taskId = typeof enriched.taskId === 'string' ? enriched.taskId : null;
      if (taskId && typeof enriched.taskName !== 'string') {
        const task = await this.prisma.labCaseTask.findUnique({
          where: { id: taskId },
          select: { stepLabel: true, prosthesisTypeCode: true },
        });
        if (task) {
          enriched = {
            ...enriched,
            taskName: task.stepLabel,
            prosthesisTypeCode: task.prosthesisTypeCode,
          };
        }
      }

      const fromOrganizationId =
        typeof enriched.fromOrganizationId === 'string' ? enriched.fromOrganizationId : null;
      if (fromOrganizationId && typeof enriched.fromOrganizationName !== 'string') {
        const org = await this.prisma.organization.findUnique({
          where: { id: fromOrganizationId },
          select: { name: true },
        });
        if (org) {
          enriched = { ...enriched, fromOrganizationName: org.name };
        }
      }

      const membershipId =
        typeof enriched.membershipId === 'string' ? enriched.membershipId : null;
      if (membershipId && typeof enriched.inviteeName !== 'string') {
        const membership = await this.prisma.membership.findUnique({
          where: { id: membershipId },
          select: { user: { select: { name: true, email: true } } },
        });
        if (membership?.user) {
          enriched = {
            ...enriched,
            inviteeName: membership.user.name,
            email:
              typeof enriched.email === 'string' && enriched.email
                ? enriched.email
                : membership.user.email,
          };
        }
      }

      return Object.keys(enriched).length > 0 ? enriched : null;
    } catch {
      // Never block inbox write if enrichment fails — store the thin payload.
      return Object.keys(base).length > 0 ? base : null;
    }
  }

  async notify(input: FanoutInput): Promise<void> {
    // Explicit recipients keep the actor (e.g. self-assign). Fan-out still skips the actor.
    const recipientIds = input.recipientUserIds?.length
      ? [...new Set(input.recipientUserIds.filter((id): id is string => Boolean(id)))]
      : await this.resolveRecipients(input);

    if (recipientIds.length === 0) return;

    const payload = await this.enrichInboxPayload(input);

    const rows = await this.prisma.userNotification.createManyAndReturn({
      data: recipientIds.map((userId) => ({
        userId,
        organizationId: input.organizationId,
        type: input.type,
        actorUserId: input.actorUserId ?? null,
        payload: (payload ?? Prisma.JsonNull) as Prisma.InputJsonValue,
        href: input.href,
      })),
    });

    for (const row of rows) {
      const dto = this.mapRow(row);
      this.realtime.emitToUserOrg(row.userId, row.organizationId, 'notification.created', {
        notification: dto,
      });
      const unreadCount = await this.countUnread(row.userId, row.organizationId);
      this.realtime.emitToUserOrg(row.userId, row.organizationId, 'notification.unreadCount', {
        count: unreadCount,
      });
    }
  }

  async listInbox(
    userId: string,
    organizationId: string,
    options?: { limit?: number; cursor?: string },
  ) {
    const limit = Math.min(Math.max(options?.limit ?? 20, 1), 50);
    const rows = await this.prisma.userNotification.findMany({
      where: { userId, organizationId },
      orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
      take: limit + 1,
      ...(options?.cursor
        ? {
            cursor: { id: options.cursor },
            skip: 1,
          }
        : {}),
    });

    const hasMore = rows.length > limit;
    const page = hasMore ? rows.slice(0, limit) : rows;
    const nextCursor = hasMore ? page[page.length - 1]?.id ?? null : null;

    return {
      success: true as const,
      data: {
        items: page.map((row) => this.mapRow(row)),
        nextCursor,
      },
    };
  }

  async unreadCount(userId: string, organizationId: string) {
    const count = await this.countUnread(userId, organizationId);
    return { success: true as const, data: { count } };
  }

  async markRead(userId: string, organizationId: string, notificationId: string) {
    const existing = await this.prisma.userNotification.findFirst({
      where: { id: notificationId, userId, organizationId },
    });
    if (!existing) {
      return { success: true as const, data: null };
    }
    if (!existing.readAt) {
      const updated = await this.prisma.userNotification.update({
        where: { id: notificationId },
        data: { readAt: new Date() },
      });
      const count = await this.countUnread(userId, organizationId);
      this.realtime.emitToUserOrg(userId, organizationId, 'notification.unreadCount', {
        count,
      });
      return { success: true as const, data: this.mapRow(updated) };
    }
    return { success: true as const, data: this.mapRow(existing) };
  }

  async markAllRead(userId: string, organizationId: string) {
    await this.prisma.userNotification.updateMany({
      where: { userId, organizationId, readAt: null },
      data: { readAt: new Date() },
    });
    this.realtime.emitToUserOrg(userId, organizationId, 'notification.unreadCount', {
      count: 0,
    });
    return { success: true as const };
  }

  private async countUnread(userId: string, organizationId: string): Promise<number> {
    return this.prisma.userNotification.count({
      where: { userId, organizationId, readAt: null },
    });
  }

  private mapRow(row: {
    id: string;
    type: UserNotificationType;
    href: string;
    payload: Prisma.JsonValue;
    readAt: Date | null;
    createdAt: Date;
    actorUserId: string | null;
  }): UserNotificationDto {
    return {
      id: row.id,
      type: row.type,
      href: row.href,
      payload:
        row.payload && typeof row.payload === 'object' && !Array.isArray(row.payload)
          ? (row.payload as Record<string, unknown>)
          : null,
      readAt: row.readAt?.toISOString() ?? null,
      createdAt: row.createdAt.toISOString(),
      actorUserId: row.actorUserId,
    };
  }

  private async resolveRecipients(input: FanoutInput): Promise<string[]> {
    if (!input.requiredPermission) return [];

    const memberships = await this.prisma.membership.findMany({
      where: {
        organizationId: input.organizationId,
        OR: [{ isOwner: true }, { isActive: true }],
      },
      include: membershipInclude,
    });

    let userIds = memberships
      .filter((m) => hasEffectivePermission(m, input.requiredPermission!))
      .map((m) => m.userId)
      .filter((id) => id !== input.actorUserId);

    if (input.labCaseIdForProviderScope) {
      const labCase = await this.prisma.labCase.findUnique({
        where: { id: input.labCaseIdForProviderScope },
        select: {
          treatment: {
            select: {
              providerUserId: true,
              appointment: { select: { providerUserId: true } },
            },
          },
        },
      });
      if (!labCase?.treatment) return [];
      const treatment = labCase.treatment;
      userIds = userIds.filter((userId) =>
        isActorTreatmentProvider(treatment, userId),
      );
    }

    return [...new Set(userIds)];
  }
}
