import { Injectable } from '@nestjs/common';

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

// The dashboard counts the issues that are in a ministry's hands. Draft,
// Saved and New Submission are earlier steps, so they stay out. A ministry
// sets these statuses from its progress report or a meeting summary, and both
// flows mirror the status onto the Issues table, so reading it here already
// includes the progress-report answers.
const COUNTED_ISSUE_STATUS_CODES = ['IN_PROGRESS', 'NOT_ADDRESSED', 'SOLVED'];

// The pivot row that marks the responsible ministry of an issue.
const PRIMARY_AGENCY_ORDER = 1;

// Stakeholder type of the government agencies (the "primary agencies" chart).
const GOVERNMENT_AGENCY_TYPE_NAME = 'Ministry';

// Stakeholder type of the working groups that own the issues.
const PRIVATE_SECTOR_TYPE_NAME = 'Private Sector';

// The child rows of a saved dashboard, with the names needed for the labels.
const snapshotRelations = {
  workingGroups: {
    include: { stakeholder: { select: { id: true, name: true } } },
  },
  primaryAgencies: {
    include: { stakeholder: { select: { id: true, name: true } } },
  },
  categories: {
    include: { category: { select: { id: true, name: true } } },
  },
} satisfies Prisma.ProgressReportDashboardInclude;

export type DashboardSnapshotRecord = Prisma.ProgressReportDashboardGetPayload<{
  include: typeof snapshotRelations;
}>;

// The published dashboard also carries the report it was published from, so
// the role dashboards can show which semester the numbers belong to.
const liveSnapshotRelations = {
  ...snapshotRelations,
  progressReport: {
    select: { id: true, title: true, year: true, semester: true },
  },
} satisfies Prisma.ProgressReportDashboardInclude;

export type LiveDashboardSnapshotRecord =
  Prisma.ProgressReportDashboardGetPayload<{
    include: typeof liveSnapshotRelations;
  }>;

// One row of a dashboard chart, before it is saved or sent to the frontend.
export type DashboardCountRow = {
  id: number;
  name: string;
  total: number;
  solved: number;
  inProgress: number;
  notAddressed: number;
};

// Everything saveSnapshot() needs to write one dashboard and its child rows.
export type DashboardSnapshotInput = {
  progressReportId: number;
  // When these numbers were counted. Publishing keeps the date of the final
  // dashboard it copies, so every page shows the same "last update".
  generatedAt?: Date;
  type: ProgressReportDashboardType;
  status: ProgressReportDashboardStatus;
  userId: number;
  totalIssues: number;
  solved: number;
  inProgress: number;
  notAddressed: number;
  totalPrimaryAgencies: number;
  // Only the Plenary dashboard splits its in-progress items in two.
  midProgress?: number | null;
  earlyProgress?: number | null;
  workingGroups: DashboardCountRow[];
  primaryAgencies: DashboardCountRow[];
  categories: DashboardCountRow[];
};

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

  // Only active reports have a dashboard. This lives here (instead of reusing
  // ProgressReportsRepository) so the dashboards folder stays independent.
  findActiveReport(progressReportId: number) {
    return this.prisma.progressReport.findFirst({
      where: { id: progressReportId, deletedAt: null },
      select: { id: true, semester: true, year: true },
    });
  }

  // Every active ministry in the system. It is the "/ 13" part of the
  // "Total Primary Agencies" card, so it is read fresh every time.
  countActiveMinistries() {
    return this.prisma.stakeholder.count({
      where: {
        active: true,
        deletedAt: null,
        stakeholderType: {
          name: { equals: GOVERNMENT_AGENCY_TYPE_NAME, mode: 'insensitive' },
        },
      },
    });
  }

  // Reads everything the dashboard is calculated from, in one transaction so
  // every number comes from the same snapshot of the database. `year` is the
  // progress report's year, so a Semester 1 2026 dashboard only counts the
  // issues raised that year.
  getCountedIssues(year: number) {
    return this.prisma.$transaction(async (tx) => {
      // One row per issue a ministry is responsible for, in one of the three
      // counted statuses. The status is read from the issue itself, which the
      // progress-report and meeting-summary flows both keep up to date.
      const issues = await tx.issues.findMany({
        where: {
          deletedAt: null,
          createdAt: {
            gte: new Date(`${year}-01-01T00:00:00.000Z`),
            lte: new Date(`${year}-12-31T23:59:59.999Z`),
          },
          issueStatus: {
            deletedAt: null,
            // `in` cannot be case-insensitive, so match each code on its own.
            OR: COUNTED_ISSUE_STATUS_CODES.map((code) => ({
              code: { equals: code, mode: 'insensitive' as const },
            })),
          },
          governmentAgencies: { some: { agencyOrder: PRIMARY_AGENCY_ORDER } },
        },
        select: {
          issueStatusId: true,
          stakeholderId: true,
          categoryId: true,
          governmentAgencies: {
            where: { agencyOrder: PRIMARY_AGENCY_ORDER },
            select: { stakeholderId: true },
          },
        },
      });

      // Lookup table: status id → code + name, used to bucket the counts.
      const statuses = await tx.issueStatuses.findMany({
        where: { deletedAt: null },
        orderBy: { id: 'asc' },
        select: { id: true, code: true, name: true },
      });

      // ALL active working groups, so groups without issues still show as an
      // empty bar — the same rule the other dashboards follow.
      const workingGroups = await tx.stakeholder.findMany({
        where: {
          active: true,
          deletedAt: null,
          stakeholderType: {
            name: { equals: PRIVATE_SECTOR_TYPE_NAME, mode: 'insensitive' },
          },
        },
        orderBy: { name: 'asc' },
        select: { id: true, name: true },
      });

      // ALL active ministries, for the primary-agency chart.
      const ministries = await tx.stakeholder.findMany({
        where: {
          active: true,
          deletedAt: null,
          stakeholderType: {
            name: { equals: GOVERNMENT_AGENCY_TYPE_NAME, mode: 'insensitive' },
          },
        },
        orderBy: { name: 'asc' },
        select: { id: true, name: true },
      });

      // ALL categories, for the category chart.
      const categories = await tx.categories.findMany({
        where: { deletedAt: null },
        orderBy: { name: 'asc' },
        select: { id: true, name: true },
      });

      return { issues, statuses, workingGroups, ministries, categories };
    });
  }

  // Everything the Plenary dashboard is calculated from. It counts RGC
  // decisions instead of issues: their status, the ministry responsible, the
  // measure category, the working groups of the issues they came from, and
  // CDC-GPSF's Mid / Early judgement recorded in this progress report.
  getCountedRgcDecisions(year: number, progressReportId: number) {
    return this.prisma.$transaction(async (tx) => {
      const decisions = await tx.plenaryRgcDecision.findMany({
        where: {
          deletedAt: null,
          meetingDate: {
            gte: new Date(`${year}-01-01T00:00:00.000Z`),
            lte: new Date(`${year}-12-31T23:59:59.999Z`),
          },
        },
        select: {
          id: true,
          status: true,
          stakeholderId: true,
          categoryId: true,
          decisionIssues: {
            select: { issue: { select: { stakeholderId: true } } },
          },
        },
      });

      // What CDC-GPSF judged in this report, one row per decision.
      const cdcJudgements =
        await tx.progressReportUpdatePlenaryDecision.findMany({
          where: { progressReportId, deletedAt: null },
          select: { plenaryDecisionId: true, cdcInProgressStatus: true },
        });

      // ALL active ministries, working groups and categories, so a row with no
      // decision still shows as an empty bar — the same rule the PSWG
      // dashboard follows.
      const ministries = await tx.stakeholder.findMany({
        where: {
          active: true,
          deletedAt: null,
          stakeholderType: {
            name: { equals: GOVERNMENT_AGENCY_TYPE_NAME, mode: 'insensitive' },
          },
        },
        orderBy: { name: 'asc' },
        select: { id: true, name: true },
      });

      const workingGroups = await tx.stakeholder.findMany({
        where: {
          active: true,
          deletedAt: null,
          stakeholderType: {
            name: { equals: PRIVATE_SECTOR_TYPE_NAME, mode: 'insensitive' },
          },
        },
        orderBy: { name: 'asc' },
        select: { id: true, name: true },
      });

      const categories = await tx.categories.findMany({
        where: { deletedAt: null },
        orderBy: { name: 'asc' },
        select: { id: true, name: true },
      });

      return {
        decisions,
        cdcJudgements,
        ministries,
        workingGroups,
        categories,
      };
    });
  }

  // The saved dashboard for one report, type and status (draft or final),
  // or null when that one was never generated.
  findSnapshot(
    progressReportId: number,
    type: ProgressReportDashboardType,
    status: ProgressReportDashboardStatus,
  ) {
    return this.prisma.progressReportDashboard.findFirst({
      where: { progressReportId, type, status, deletedAt: null },
      include: snapshotRelations,
    });
  }

  // The dashboard CDC-GPSF published for everybody, or null when nothing has
  // been published yet. Only one is ever live, but the newest wins in case an
  // older one was left behind.
  findLatestLiveSnapshot(type: ProgressReportDashboardType) {
    return this.prisma.progressReportDashboard.findFirst({
      where: {
        type,
        status: ProgressReportDashboardStatus.LIVE,
        deletedAt: null,
      },
      orderBy: { generatedAt: 'desc' },
      include: liveSnapshotRelations,
    });
  }

  // Clears the dashboard that is live now, so publishing a new one always
  // leaves exactly one live dashboard behind. The child rows go first because
  // they point at the dashboard row.
  async deleteLiveSnapshots(type: ProgressReportDashboardType) {
    const live = await this.prisma.progressReportDashboard.findMany({
      where: { type, status: ProgressReportDashboardStatus.LIVE },
      select: { id: true },
    });

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

    const dashboardIds = live.map((dashboard) => dashboard.id);

    // One query after another: they share a single database connection.
    await this.prisma.progressReportDashboardWorkingGroup.deleteMany({
      where: { dashboardId: { in: dashboardIds } },
    });
    await this.prisma.progressReportDashboardPrimaryAgency.deleteMany({
      where: { dashboardId: { in: dashboardIds } },
    });
    await this.prisma.progressReportDashboardCategory.deleteMany({
      where: { dashboardId: { in: dashboardIds } },
    });
    await this.prisma.progressReportDashboard.deleteMany({
      where: { id: { in: dashboardIds } },
    });
  }

  // Writes (or rewrites) one dashboard. Generating again replaces the child
  // rows of that same type and status, so the draft and the final never
  // overwrite each other.
  saveSnapshot(
    input: DashboardSnapshotInput,
  ): Promise<DashboardSnapshotRecord> {
    const summary = {
      userId: input.userId,
      totalIssues: input.totalIssues,
      solved: input.solved,
      inProgress: input.inProgress,
      notAddressed: input.notAddressed,
      totalPrimaryAgencies: input.totalPrimaryAgencies,
      midProgress: input.midProgress ?? null,
      earlyProgress: input.earlyProgress ?? null,
      generatedAt: input.generatedAt ?? new Date(),
      deletedAt: null,
    };

    return this.prisma.$transaction(async (tx) => {
      const dashboard = await tx.progressReportDashboard.upsert({
        where: {
          progressReportId_type_status: {
            progressReportId: input.progressReportId,
            type: input.type,
            status: input.status,
          },
        },
        create: {
          progressReportId: input.progressReportId,
          type: input.type,
          status: input.status,
          ...summary,
        },
        update: summary,
      });

      // Replace the child rows: delete the old ones, then write the new set.
      // The queries run one after another because they share one connection.
      await tx.progressReportDashboardWorkingGroup.deleteMany({
        where: { dashboardId: dashboard.id },
      });
      await tx.progressReportDashboardPrimaryAgency.deleteMany({
        where: { dashboardId: dashboard.id },
      });
      await tx.progressReportDashboardCategory.deleteMany({
        where: { dashboardId: dashboard.id },
      });

      await tx.progressReportDashboardWorkingGroup.createMany({
        data: input.workingGroups.map((row) => ({
          dashboardId: dashboard.id,
          stakeholderId: row.id,
          total: row.total,
          solved: row.solved,
          inProgress: row.inProgress,
          notAddressed: row.notAddressed,
        })),
      });
      await tx.progressReportDashboardPrimaryAgency.createMany({
        data: input.primaryAgencies.map((row) => ({
          dashboardId: dashboard.id,
          stakeholderId: row.id,
          total: row.total,
          solved: row.solved,
          inProgress: row.inProgress,
          notAddressed: row.notAddressed,
        })),
      });
      await tx.progressReportDashboardCategory.createMany({
        data: input.categories.map((row) => ({
          dashboardId: dashboard.id,
          categoryId: row.id,
          total: row.total,
          solved: row.solved,
          inProgress: row.inProgress,
          notAddressed: row.notAddressed,
        })),
      });

      return tx.progressReportDashboard.findFirstOrThrow({
        where: { id: dashboard.id },
        include: snapshotRelations,
      });
    });
  }
}
