import {
  BadRequestException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';

import { PlenaryStatus, Prisma } from '@/generated/prisma/client';
import { PrismaService } from '@/prisma/prisma.service';

import { CreatePlenaryDto } from './dto/create-plenary.dto';
import { PlenaryQueryDto } from './dto/plenary-query.dto';
import { UpdatePlenaryDto } from './dto/update-plenary.dto';

type MinistryUser = {
  id: number;
  name: string | null;
  email: string;
  position: string | null;
  avatar: string | null;
};

type MinistryWithUsers = {
  id: number;
  name: string;
  logo: string | null;
  users: MinistryUser[];
};

type StakeholderWithUsersRow = {
  plenaryId: number;
  id: number;
  name: string;
  logo: string | null;
  userId: number | null;
  userName: string | null;
  userEmail: string | null;
  userPosition: string | null;
  userAvatar: string | null;
};

const MINISTRY_STAKEHOLDER_TYPE = 'Ministry';

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

  async findAll(query: PlenaryQueryDto, currentUserId?: number) {
    const page = query.page ?? 1;
    const limit = query.limit ?? 10;
    const skip = (page - 1) * limit;

    const where: Prisma.PlenaryWhereInput = {
      deletedAt: null,
    };

    const statuses = this.toPlenaryStatuses(
      query.statuses?.length
        ? query.statuses
        : query.status
          ? [query.status]
          : [],
    );

    if (statuses.length === 1) where.status = statuses[0];
    if (statuses.length > 1) where.status = { in: statuses };

    if (query.ministryOnly === true) {
      const currentUserMinistryIds =
        await this.getCurrentUserMinistryIds(currentUserId);

      where.status = PlenaryStatus.SENT;

      if (currentUserMinistryIds.length === 0) {
        where.id = -1;
      } else {
        where.ministries = {
          some: {
            stakeholderId: {
              in: currentUserMinistryIds,
            },
          },
        };
      }
    } else {
      const ministryIds = this.normalizeIds(query.ministryIds);

      if (ministryIds.length > 0) {
        where.ministries = {
          some: {
            stakeholderId: {
              in: ministryIds,
            },
          },
        };
      }
    }

    if (query.search?.trim()) {
      where.name = {
        contains: query.search.trim(),
        mode: 'insensitive',
      };
    }

    const [items, total] = await this.prisma.$transaction([
      this.prisma.plenary.findMany({
        where,
        include: {
          _count: {
            select: {
              rgcDecisions: {
                where: { deletedAt: null },
              },
            },
          },
        },
        orderBy: { meetingDate: 'desc' },
        skip,
        take: limit,
      }),
      this.prisma.plenary.count({ where }),
    ]);

    const ministriesByPlenaryId = await this.getMinistriesByPlenaryIds(
      items.map((item) => item.id),
    );

    return {
      items: items.map((item) => ({
        id: item.id,
        name: item.name,
        meetingDate: item.meetingDate,
        deadline: item.deadline,
        documentReference: item.documentReference,
        status: this.mapPlenaryStatus(item.status),
        statusCode: item.status,
        numberOfRgcDecisions: item._count.rgcDecisions,
        ministries: ministriesByPlenaryId.get(item.id) ?? [],
        createdAt: item.createdAt,
        updatedAt: item.updatedAt,
      })),
      meta: {
        total,
        page,
        limit,
        totalPages: Math.max(1, Math.ceil(total / limit)),
      },
    };
  }

  async findOne(plenaryId: number, currentUserId?: number) {
    const plenary = await this.getActivePlenary(plenaryId);

    await this.ensurePlenaryIsVisibleToCurrentUser(
      plenaryId,
      plenary.status,
      currentUserId,
    );

    const [ministryRows, rgcDecisionCount] = await Promise.all([
      this.prisma.plenaryMinistry.findMany({
        where: { plenaryId },
        select: { stakeholderId: true },
        orderBy: { stakeholderId: 'asc' },
      }),
      this.prisma.plenaryRgcDecision.count({
        where: {
          plenaryId,
          deletedAt: null,
        },
      }),
    ]);

    const ministries = await this.getStakeholdersByIds(
      ministryRows.map((item) => item.stakeholderId),
    );

    return {
      id: plenary.id,
      name: plenary.name,
      meetingDate: plenary.meetingDate,
      deadline: plenary.deadline,
      documentReference: plenary.documentReference,
      status: this.mapPlenaryStatus(plenary.status),
      statusCode: plenary.status,
      numberOfRgcDecisions: rgcDecisionCount,
      ministries,
      createdAt: plenary.createdAt,
      updatedAt: plenary.updatedAt,
    };
  }

  async create(dto: CreatePlenaryDto, userId: number) {
    await this.ensureUserExists(userId);

    const ministryIds = this.normalizeIds(dto.ministryIds);
    const status = this.toPlenaryStatus(dto.status) ?? PlenaryStatus.DRAFT;

    if (status === PlenaryStatus.SENT && ministryIds.length === 0) {
      throw new BadRequestException(
        'Please select at least one related Ministry before sending notification.',
      );
    }

    await this.ensureMinistriesExist(ministryIds);

    const plenary = await this.prisma.plenary.create({
      data: {
        name: dto.name.trim(),
        meetingDate: new Date(dto.meetingDate),
        deadline: dto.deadline ? new Date(dto.deadline) : null,
        documentReference: dto.documentReference?.trim() || null,
        status,
        userId,
        ministries:
          ministryIds.length > 0
            ? {
                create: ministryIds.map((stakeholderId) => ({
                  stakeholderId,
                })),
              }
            : undefined,
      },
    });

    return this.findOne(plenary.id);
  }

  async update(plenaryId: number, dto: UpdatePlenaryDto) {
    await this.getActivePlenary(plenaryId);

    const data: Prisma.PlenaryUpdateInput = {};

    if (dto.name !== undefined) data.name = dto.name.trim();
    if (dto.meetingDate !== undefined)
      data.meetingDate = new Date(dto.meetingDate);

    if (dto.deadline !== undefined) {
      data.deadline = dto.deadline ? new Date(dto.deadline) : null;
    }

    if (dto.documentReference !== undefined) {
      data.documentReference = dto.documentReference.trim() || null;
    }

    const status = this.toPlenaryStatus(dto.status);
    if (status) data.status = status;

    if (dto.ministryIds !== undefined) {
      const ministryIds = this.normalizeIds(dto.ministryIds);

      if (status === PlenaryStatus.SENT && ministryIds.length === 0) {
        throw new BadRequestException(
          'Please select at least one related Ministry before sending notification.',
        );
      }

      await this.ensureMinistriesExist(ministryIds);

      data.ministries = {
        deleteMany: {},
        create: ministryIds.map((stakeholderId) => ({
          stakeholderId,
        })),
      };
    }

    await this.prisma.plenary.update({
      where: { id: plenaryId },
      data,
    });

    return this.findOne(plenaryId);
  }

  async updateDocument(plenaryId: number, documentReference: string) {
    await this.getActivePlenary(plenaryId);

    const cleanDocumentReference = documentReference.trim();

    if (!cleanDocumentReference) {
      throw new BadRequestException('Document reference is required.');
    }

    await this.prisma.plenary.update({
      where: { id: plenaryId },
      data: { documentReference: cleanDocumentReference },
    });

    return this.findOne(plenaryId);
  }

  async submitToCdc(plenaryId: number) {
    await this.getActivePlenary(plenaryId);

    const ministryCount = await this.prisma.plenaryMinistry.count({
      where: { plenaryId },
    });

    if (ministryCount === 0) {
      throw new BadRequestException(
        'Select at least one related Ministry before sending this Plenary.',
      );
    }

    await this.prisma.plenary.update({
      where: { id: plenaryId },
      data: { status: PlenaryStatus.SENT },
    });

    return this.findOne(plenaryId);
  }

  async remove(plenaryId: number) {
    await this.getActivePlenary(plenaryId);

    const deletedAt = new Date();

    await this.prisma.$transaction([
      this.prisma.plenary.update({
        where: { id: plenaryId },
        data: { deletedAt },
      }),
      this.prisma.plenaryRgcDecision.updateMany({
        where: {
          plenaryId,
          deletedAt: null,
        },
        data: { deletedAt },
      }),
    ]);

    return {
      success: true,
      message: 'Plenary deleted successfully.',
    };
  }

  async getMinistries(relatedStakeholderId?: number) {
    if (
      relatedStakeholderId !== undefined &&
      (!Number.isInteger(relatedStakeholderId) || relatedStakeholderId < 1)
    ) {
      throw new BadRequestException('Invalid related stakeholder.');
    }

    const items = await this.prisma.stakeholder.findMany({
      where: {
        deletedAt: null,
        active: true,
        stakeholderType: {
          deletedAt: null,
          name: {
            equals: MINISTRY_STAKEHOLDER_TYPE,
            mode: 'insensitive',
          },
        },
        ...(relatedStakeholderId ? { relatedStakeholderId } : {}),
      },
      select: {
        id: true,
        name: true,
        logo: true,
        users: {
          select: {
            user: {
              select: {
                id: true,
                name: true,
                email: true,
                position: true,
                avatar: true,
              },
            },
          },
        },
      },
      orderBy: {
        name: 'asc',
      },
    });

    return {
      items: items.map((item) => ({
        id: item.id,
        name: item.name,
        logo: item.logo,
        users: item.users.map((row) => row.user),
      })),
    };
  }

  private async getCurrentUserMinistryIds(
    currentUserId?: number,
  ): Promise<number[]> {
    if (!currentUserId) return [];

    const rows = await this.prisma.stakeholderUser.findMany({
      where: {
        userId: currentUserId,
        stakeholder: {
          deletedAt: null,
          active: true,
          stakeholderType: {
            deletedAt: null,
            name: {
              equals: MINISTRY_STAKEHOLDER_TYPE,
              mode: 'insensitive',
            },
          },
        },
      },
      select: {
        stakeholderId: true,
      },
    });

    return rows.map((row) => row.stakeholderId);
  }

  private async ensureMinistriesExist(ministryIds: number[]) {
    if (ministryIds.length === 0) return;

    const rows = await this.prisma.stakeholder.findMany({
      where: {
        id: { in: ministryIds },
        deletedAt: null,
        active: true,
        stakeholderType: {
          deletedAt: null,
          name: {
            equals: MINISTRY_STAKEHOLDER_TYPE,
            mode: 'insensitive',
          },
        },
      },
      select: {
        id: true,
      },
    });

    if (rows.length !== ministryIds.length) {
      throw new BadRequestException(
        'One or more selected ministries do not exist or are inactive.',
      );
    }
  }

  private async getMinistriesByPlenaryIds(
    plenaryIds: number[],
  ): Promise<Map<number, MinistryWithUsers[]>> {
    const result = new Map<number, MinistryWithUsers[]>();

    if (plenaryIds.length === 0) return result;

    const rows = await this.prisma.$queryRaw<StakeholderWithUsersRow[]>(
      Prisma.sql`
        SELECT
          pm."plenary_id" AS "plenaryId",
          s."id" AS "id",
          s."name" AS "name",
          s."logo" AS "logo",
          u."id" AS "userId",
          u."name" AS "userName",
          u."email" AS "userEmail",
          u."position" AS "userPosition",
          u."avatar" AS "userAvatar"
        FROM "plenary_ministries" AS pm
        INNER JOIN "stakeholders" AS s
          ON s."id" = pm."stakeholder_id"
        LEFT JOIN "stakeholder_users" AS su
          ON su."stakeholder_id" = s."id"
        LEFT JOIN "users" AS u
          ON u."id" = su."user_id"
          AND u."deleted_at" IS NULL
          AND u."is_active" = true
        WHERE pm."plenary_id" IN (${Prisma.join(plenaryIds)})
          AND s."deleted_at" IS NULL
        ORDER BY pm."plenary_id" ASC, s."name" ASC, u."id" ASC
      `,
    );

    for (const row of rows) {
      const ministries = result.get(row.plenaryId) ?? [];

      let ministry = ministries.find((item) => item.id === row.id);

      if (!ministry) {
        ministry = {
          id: row.id,
          name: row.name,
          logo: row.logo,
          users: [],
        };

        ministries.push(ministry);
        result.set(row.plenaryId, ministries);
      }

      if (row.userId) {
        ministry.users.push({
          id: row.userId,
          name: row.userName,
          email: row.userEmail ?? '',
          position: row.userPosition,
          avatar: row.userAvatar,
        });
      }
    }

    return result;
  }

  private async getStakeholdersByIds(ids: number[]) {
    if (ids.length === 0) return [];

    const rows = await this.prisma.stakeholder.findMany({
      where: {
        id: { in: ids },
        deletedAt: null,
      },
      select: {
        id: true,
        name: true,
        logo: true,
        users: {
          select: {
            user: {
              select: {
                id: true,
                name: true,
                email: true,
                position: true,
                avatar: true,
              },
            },
          },
        },
      },
    });

    const stakeholderMap = new Map(
      rows.map((item) => [
        item.id,
        {
          id: item.id,
          name: item.name,
          logo: item.logo,
          users: item.users.map((row) => row.user),
        },
      ]),
    );

    return ids
      .map((id) => stakeholderMap.get(id))
      .filter((item): item is MinistryWithUsers => Boolean(item));
  }

  private async getActivePlenary(plenaryId: number) {
    const plenary = await this.prisma.plenary.findFirst({
      where: {
        id: plenaryId,
        deletedAt: null,
      },
    });

    if (!plenary) {
      throw new NotFoundException('Plenary not found.');
    }

    return plenary;
  }

  private async ensureUserExists(userId: number) {
    const user = await this.prisma.user.findFirst({
      where: {
        id: userId,
        deletedAt: null,
        isActive: true,
      },
      select: {
        id: true,
      },
    });

    if (!user) {
      throw new BadRequestException('Authenticated user does not exist.');
    }
  }

  private async ensurePlenaryIsVisibleToCurrentUser(
    plenaryId: number,
    plenaryStatus: PlenaryStatus,
    currentUserId?: number,
  ) {
    const ministryIds = await this.getCurrentUserMinistryIds(currentUserId);

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

    if (plenaryStatus !== PlenaryStatus.SENT) {
      throw new NotFoundException('Plenary not found.');
    }

    const matchingMinistry = await this.prisma.plenaryMinistry.findFirst({
      where: {
        plenaryId,
        stakeholderId: {
          in: ministryIds,
        },
      },
      select: {
        plenaryId: true,
      },
    });

    if (!matchingMinistry) {
      throw new NotFoundException('Plenary not found.');
    }
  }

  private normalizeIds(ids?: number[]) {
    return [
      ...new Set((ids ?? []).filter((id) => Number.isInteger(id) && id > 0)),
    ];
  }

  private toPlenaryStatuses(values: string[]): PlenaryStatus[] {
    const statuses = values
      .map((value) => this.toPlenaryStatus(value))
      .filter((status): status is PlenaryStatus => status !== undefined);

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

  private toPlenaryStatus(value?: string) {
    if (!value) return undefined;

    const normalized = value.trim().toLowerCase();

    if (normalized === 'draft') return PlenaryStatus.DRAFT;
    if (normalized === 'sent') return PlenaryStatus.SENT;

    throw new BadRequestException('Invalid Plenary status.');
  }

  private mapPlenaryStatus(status: PlenaryStatus) {
    return status === PlenaryStatus.SENT ? 'Sent' : 'Draft';
  }
}
