import {
  HttpStatus,
  Injectable,
} from '@nestjs/common';
import { AppException, ErrorCode } from '../../common/errors';
import { LabCaseActivityType, LabCaseOrigin, LabCaseTabReadTarget, Prisma } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { hasEffectivePermission } from '../../common/membership-permissions';
import {
  CLINIC_TREATMENT_TAB_ACTIVITY_TYPES,
  LAB_CASES_TAB_ACTIVITY_TYPES,
  LAB_TASKS_TAB_ACTIVITY_TYPES,
} from '../../common/lab-case-activity';
import { treatmentProviderScopeWhere } from '../../common/treatment-provider-scope';

type TxClient = Prisma.TransactionClient;

@Injectable()
export class LabCaseActivityService {
  constructor(private readonly prisma: PrismaService) {}

  async record(
    input: {
      labCaseId: string;
      type: LabCaseActivityType;
      actorUserId?: string | null;
      payload?: Prisma.InputJsonValue;
    },
    tx?: TxClient,
  ) {
    const client = tx ?? this.prisma;
    await client.labCaseActivity.create({
      data: {
        labCaseId: input.labCaseId,
        type: input.type,
        actorUserId: input.actorUserId ?? null,
        payload: input.payload ?? undefined,
      },
    });
  }

  async getTabCounts(userId: string, organizationId: string) {
    const org = await this.prisma.organization.findUnique({
      where: { id: organizationId },
      include: { type: true },
    });
    if (!org) {
      throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    await this.assertMembership(userId, organizationId);

    const orgType = org.type.name;
    const tabReads = await this.prisma.labCaseUserTabReadState.findMany({
      where: { userId, organizationId },
    });
    const tabSince = (tab: LabCaseTabReadTarget) =>
      tabReads.find((row) => row.tab === tab)?.lastReadAt ?? new Date(0);

    if (orgType === 'LAB') {
      const [cases, tasks] = await Promise.all([
        this.countUnreadLabCasesForCasesTab(userId, organizationId),
        this.countUnreadForTab(
          userId,
          organizationId,
          'LAB',
          LAB_TASKS_TAB_ACTIVITY_TYPES,
          tabSince(LabCaseTabReadTarget.TASKS),
        ),
      ]);
      return { success: true, data: { cases, tasks } };
    }

    if (orgType === 'CLINIC') {
      const treatment = await this.countUnreadLabCasesForTreatmentTab(
        userId,
        organizationId,
      );
      return { success: true, data: { treatment } };
    }

    return { success: true, data: {} };
  }

  async markTabRead(userId: string, organizationId: string, tab: LabCaseTabReadTarget) {
    await this.assertMembership(userId, organizationId);
    const now = new Date();
    await this.prisma.labCaseUserTabReadState.upsert({
      where: {
        userId_organizationId_tab: { userId, organizationId, tab },
      },
      create: { userId, organizationId, tab, lastReadAt: now },
      update: { lastReadAt: now },
    });
    return { success: true };
  }

  /** Cases with unread activity in the Cases tab bucket (per-case read cursor, not tab visit). */
  async countUnreadLabCasesForCasesTab(
    userId: string,
    organizationId: string,
  ): Promise<number> {
    return this.countUnreadLabCasesForOrg(
      userId,
      organizationId,
      LAB_CASES_TAB_ACTIVITY_TYPES,
      {
        sentAt: { not: null },
        sends: { some: { organizationId } },
      },
      'LAB',
    );
  }

  /** Clinic Treatment tab — unread sent cases (per-case read cursor, not tab visit). */
  async countUnreadLabCasesForTreatmentTab(
    userId: string,
    organizationId: string,
  ): Promise<number> {
    return this.countUnreadLabCasesForOrg(
      userId,
      organizationId,
      CLINIC_TREATMENT_TAB_ACTIVITY_TYPES,
      {
        sentAt: { not: null },
        treatment: {
          organizationId,
          ...treatmentProviderScopeWhere(userId),
        },
      },
      'CLINIC',
    );
  }

  private async countUnreadLabCasesForOrg(
    userId: string,
    organizationId: string,
    types: LabCaseActivityType[],
    labCaseScope: Prisma.LabCaseWhereInput,
    orgType: 'LAB' | 'CLINIC',
  ): Promise<number> {
    const clinicLabCommentFilter = this.clinicLabCommentFilter(orgType);
    const grouped = await this.prisma.labCaseActivity.groupBy({
      by: ['labCaseId'],
      where: {
        type: { in: types },
        labCase: labCaseScope,
        OR: [{ actorUserId: null }, { actorUserId: { not: userId } }],
        AND: [
          ...(Object.keys(clinicLabCommentFilter).length ? [clinicLabCommentFilter] : []),
        ],
      },
      _max: { createdAt: true },
    });

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

    const caseIds = grouped.map((row) => row.labCaseId);
    const readStates = await this.prisma.labCaseUserReadState.findMany({
      where: { userId, organizationId, labCaseId: { in: caseIds } },
    });
    const readMap = new Map(readStates.map((row) => [row.labCaseId, row.lastReadAt]));

    return grouped.filter((row) => {
      const since = readMap.get(row.labCaseId) ?? new Date(0);
      return (row._max.createdAt ?? new Date(0)) > since;
    }).length;
  }

  async unreadCaseIdsInBatch(
    userId: string,
    organizationId: string,
    labCaseIds: string[],
    types: LabCaseActivityType[],
    orgType: 'LAB' | 'CLINIC',
  ): Promise<Set<string>> {
    if (labCaseIds.length === 0) return new Set();

    const readStates = await this.prisma.labCaseUserReadState.findMany({
      where: { userId, organizationId, labCaseId: { in: labCaseIds } },
    });
    const sinceByCase = new Map(
      labCaseIds.map((id) => [
        id,
        readStates.find((row) => row.labCaseId === id)?.lastReadAt ?? new Date(0),
      ]),
    );

    const clinicLabCommentFilter = this.clinicLabCommentFilter(orgType);
    const activities = await this.prisma.labCaseActivity.findMany({
      where: {
        labCaseId: { in: labCaseIds },
        type: { in: types },
        AND: [
          { OR: [{ actorUserId: null }, { actorUserId: { not: userId } }] },
          ...(Object.keys(clinicLabCommentFilter).length ? [clinicLabCommentFilter] : []),
        ],
      },
      select: { labCaseId: true, createdAt: true },
    });

    const unread = new Set<string>();
    for (const activity of activities) {
      const since = sinceByCase.get(activity.labCaseId)!;
      if (activity.createdAt > since) {
        unread.add(activity.labCaseId);
      }
    }
    return unread;
  }

  async listForLabCase(
    userId: string,
    organizationId: string,
    labCaseId: string,
    limit = 50,
  ) {
    const org = await this.prisma.organization.findUnique({
      where: { id: organizationId },
      include: { type: true },
    });
    if (!org) {
      throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    await this.assertCanAccessCase(userId, organizationId, labCaseId);

    const orgType = org.type.name === 'LAB' ? 'LAB' : 'CLINIC';
    const clinicLabCommentFilter = this.clinicLabCommentFilter(orgType);
    const activities = await this.prisma.labCaseActivity.findMany({
      where: {
        labCaseId,
        AND: [
          ...(Object.keys(clinicLabCommentFilter).length ? [clinicLabCommentFilter] : []),
        ],
      },
      include: { actorUser: { select: { name: true } } },
      orderBy: { createdAt: 'desc' },
      take: Math.min(Math.max(limit, 1), 100),
    });

    const enriched = await this.enrichActivities(activities);
    return { success: true, data: enriched };
  }

  async getLastActivitiesForCases(
    labCaseIds: string[],
    orgType: 'LAB' | 'CLINIC',
  ): Promise<Map<string, Awaited<ReturnType<LabCaseActivityService['enrichActivities']>>[number]>> {
    if (labCaseIds.length === 0) return new Map();

    const clinicLabCommentFilter = this.clinicLabCommentFilter(orgType);
    const activities = await this.prisma.labCaseActivity.findMany({
      where: {
        labCaseId: { in: labCaseIds },
        AND: [
          ...(Object.keys(clinicLabCommentFilter).length ? [clinicLabCommentFilter] : []),
        ],
      },
      include: { actorUser: { select: { name: true } } },
      orderBy: { createdAt: 'desc' },
    });

    const latestByCase = new Map<string, (typeof activities)[number]>();
    for (const activity of activities) {
      if (!latestByCase.has(activity.labCaseId)) {
        latestByCase.set(activity.labCaseId, activity);
      }
    }

    const enriched = await this.enrichActivities([...latestByCase.values()]);
    const map = new Map<string, (typeof enriched)[number]>();
    for (const item of enriched) {
      map.set(item.labCaseId, item);
    }
    return map;
  }

  async markCaseRead(userId: string, organizationId: string, labCaseId: string) {
    await this.assertMembership(userId, organizationId);
    await this.assertCanAccessCase(userId, organizationId, labCaseId);

    const now = new Date();
    await this.prisma.labCaseUserReadState.upsert({
      where: {
        userId_organizationId_labCaseId: { userId, organizationId, labCaseId },
      },
      create: { userId, organizationId, labCaseId, lastReadAt: now },
      update: { lastReadAt: now },
    });
    return { success: true };
  }

  private async enrichActivities(
    activities: Array<{
      id: string;
      labCaseId: string;
      type: LabCaseActivityType;
      actorUserId: string | null;
      payload: Prisma.JsonValue | null;
      createdAt: Date;
      actorUser: { name: string | null } | null;
    }>,
  ) {
    const commentIds: string[] = [];
    const taskIds: string[] = [];

    for (const activity of activities) {
      const payload = activity.payload as Record<string, unknown> | null;
      if (
        activity.type === LabCaseActivityType.CLINIC_COMMENT ||
        activity.type === LabCaseActivityType.LAB_COMMENT
      ) {
        const commentId = payload?.commentId;
        if (typeof commentId === 'string') commentIds.push(commentId);
      }
      if (
        activity.type === LabCaseActivityType.TASK_COMPLETED ||
        activity.type === LabCaseActivityType.TASK_ASSIGNED
      ) {
        const taskId = payload?.taskId;
        if (typeof taskId === 'string') taskIds.push(taskId);
      }
    }

    const [comments, tasks] = await Promise.all([
      commentIds.length
        ? this.prisma.labCaseComment.findMany({
            where: { id: { in: commentIds } },
            select: { id: true, body: true },
          })
        : Promise.resolve([]),
      taskIds.length
        ? this.prisma.labCaseTask.findMany({
            where: { id: { in: taskIds } },
            select: { id: true, stepLabel: true },
          })
        : Promise.resolve([]),
    ]);

    const commentById = new Map<string, string>(
      comments.map((row) => [row.id, row.body] as const),
    );
    const taskById = new Map<string, string>(
      tasks.map((row) => [row.id, row.stepLabel] as const),
    );

    return activities.map((activity) => {
      const payload = activity.payload as Record<string, unknown> | null;
      const commentId =
        typeof payload?.commentId === 'string' ? payload.commentId : null;
      const taskId = typeof payload?.taskId === 'string' ? payload.taskId : null;

      return {
        id: activity.id,
        labCaseId: activity.labCaseId,
        type: activity.type,
        createdAt: activity.createdAt.toISOString(),
        actorName: activity.actorUser?.name ?? null,
        commentBody: commentId ? (commentById.get(commentId) ?? null) : null,
        stepLabel: taskId ? (taskById.get(taskId) ?? null) : null,
        visibleToClinic:
          activity.type === LabCaseActivityType.LAB_COMMENT
            ? payload?.visibleToClinic === true
            : undefined,
      };
    });
  }

  private async countUnreadForTab(
    userId: string,
    organizationId: string,
    orgType: 'LAB' | 'CLINIC',
    types: LabCaseActivityType[],
    since: Date,
  ): Promise<number> {
    const labCaseScope =
      orgType === 'LAB'
        ? {
            sentAt: { not: null },
            sends: { some: { organizationId } },
          }
        : {
            sentAt: { not: null },
            treatment: { organizationId },
          };

    const clinicLabCommentFilter = this.clinicLabCommentFilter(orgType);
    const includesTaskAssigned = types.includes(LabCaseActivityType.TASK_ASSIGNED);
    const otherTypes = types.filter((type) => type !== LabCaseActivityType.TASK_ASSIGNED);

    // TASK_ASSIGNED: count for the assignee only (including self-assign).
    // Other task-tab types: org-wide, excluding events the current user authored.
    const typeFilter: Prisma.LabCaseActivityWhereInput = includesTaskAssigned
      ? {
          OR: [
            ...(otherTypes.length > 0
              ? [
                  {
                    AND: [
                      { type: { in: otherTypes } },
                      {
                        OR: [{ actorUserId: null }, { actorUserId: { not: userId } }],
                      },
                    ],
                  },
                ]
              : []),
            {
              type: LabCaseActivityType.TASK_ASSIGNED,
              payload: { path: ['assigneeUserId'], equals: userId },
            },
          ],
        }
      : {
          AND: [
            { type: { in: types } },
            { OR: [{ actorUserId: null }, { actorUserId: { not: userId } }] },
          ],
        };

    return this.prisma.labCaseActivity.count({
      where: {
        createdAt: { gt: since },
        labCase: labCaseScope,
        AND: [clinicLabCommentFilter, typeFilter],
      },
    });
  }

  private clinicLabCommentFilter(orgType: 'LAB' | 'CLINIC'): Prisma.LabCaseActivityWhereInput {
    if (orgType !== 'CLINIC') return {};
    return {
      OR: [
        { type: { not: LabCaseActivityType.LAB_COMMENT } },
        {
          type: LabCaseActivityType.LAB_COMMENT,
          payload: { path: ['visibleToClinic'], equals: true },
        },
      ],
    };
  }

  private async assertMembership(userId: string, organizationId: string) {
    const membership = await this.prisma.membership.findFirst({
      where: {
        userId,
        organizationId,
        OR: [{ isOwner: true }, { isActive: true }],
      },
    });
    if (!membership) {
      throw new AppException(ErrorCode.PERMISSION_NOT_MEMBER, HttpStatus.FORBIDDEN);
    }
  }

  private async assertCanAccessCase(
    userId: string,
    organizationId: string,
    labCaseId: string,
  ) {
    const org = await this.prisma.organization.findUnique({
      where: { id: organizationId },
      include: { type: true },
    });
    if (!org) {
      throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    const labCase = await this.prisma.labCase.findFirst({
      where:
        org.type.name === 'LAB'
          ? {
              id: labCaseId,
              OR: [
                {
                  sentAt: { not: null },
                  sends: { some: { organizationId } },
                },
                {
                  origin: LabCaseOrigin.LAB_INTERNAL,
                  destinationOrganizationId: organizationId,
                },
              ],
            }
          : {
              id: labCaseId,
              sentAt: { not: null },
              treatment: {
                organizationId,
                ...treatmentProviderScopeWhere(userId),
              },
            },
      select: { id: true },
    });

    if (!labCase) {
      throw new AppException(ErrorCode.CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
    }

    if (org.type.name === 'LAB') {
      const membership = await this.prisma.membership.findFirst({
        where: {
          userId,
          organizationId,
          OR: [{ isOwner: true }, { isActive: true }],
        },
        include: {
          permissions: { include: { permission: true } },
          organization: { include: { type: true, plan: true } },
        },
      });
      if (
        !membership ||
        !(
          hasEffectivePermission(membership, 'TAB_CASES_READ') ||
          hasEffectivePermission(membership, 'TAB_TASKS_READ')
        )
      ) {
        throw new AppException(ErrorCode.PERMISSION_ACCESS_CASES, HttpStatus.FORBIDDEN);
      }
      return;
    }

    const membership = await this.prisma.membership.findFirst({
      where: {
        userId,
        organizationId,
        OR: [{ isOwner: true }, { isActive: true }],
      },
      include: {
        permissions: { include: { permission: true } },
        organization: { include: { type: true, plan: true } },
      },
    });
    if (
      !membership ||
      !(
        hasEffectivePermission(membership, 'TAB_TREATMENT_READ') ||
        hasEffectivePermission(membership, 'TAB_TREATMENT_EDIT')
      )
    ) {
      throw new AppException(ErrorCode.PERMISSION_ACCESS_CASES, HttpStatus.FORBIDDEN);
    }
  }
}
