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

import { QueryDashboardSummaryDto } from './dto/query-dashboard-summary.dto';
import { DashboardRepository } from './dashboard.repository';

// --- Response shapes -------------------------------------------------------
// These describe the JSON the endpoint returns. The frontend gets raw counts
// and computes percentages itself. Every row carries both an id and a name so
// the UI can render labels without extra lookups.

// One status slice. Reused by the donut chart and by each stacked bar.
export type DashboardStatusCount = {
  statusId: number;
  code: string; // e.g. 'SOLVED'
  name: string; // e.g. 'Solved'
  count: number;
};

// One bar in the "issues per primary government agency" chart.
export type DashboardAgencyRow = {
  agencyId: number;
  agencyName: string;
  total: number;
  byStatus: DashboardStatusCount[];
};

// One bar in the "issues per working group" chart.
export type DashboardWorkingGroupRow = {
  workingGroupId: number;
  workingGroupName: string;
  total: number;
  byStatus: DashboardStatusCount[];
};

// One bar in the "issues per category" chart.
export type DashboardCategoryRow = {
  categoryId: number;
  categoryName: string;
  count: number;
};

// The full payload returned by GET /dashboard/working-group.
export type DashboardSummary = {
  cards: {
    totalIssues: number;
    solved: number;
    inProgress: number;
    notAddressed: number;
    totalPrimaryAgencies: number;
  };
  statusBreakdown: DashboardStatusCount[]; // donut chart
  byPrimaryAgency: DashboardAgencyRow[]; // bar chart, sorted total desc
  byWorkingGroup: DashboardWorkingGroupRow[]; // bar chart, sorted total desc
  byCategory: DashboardCategoryRow[]; // bar chart, sorted count desc
};

// Status names that feed the summary cards. Matched case-insensitively against
// the IssueStatuses table. These are the SAME names the working-group-issues
// summary uses, so both dashboards always agree.
const SUMMARY_STATUS_NAMES = {
  solved: 'Solved',
  inProgress: 'In Progress',
  notAddressed: 'Not Addressed',
};

// A lightweight status lookup entry (id → code/name).
type StatusLookup = { id: number; code: string; name: string };

@Injectable()
export class DashboardService {
  constructor(private readonly dashboardRepository: DashboardRepository) {}

  // Reads the raw counts from the repository and reshapes them into the
  // working group dashboard payload (cards + one dataset per chart).
  async getWorkingGroupSummary(
    query: QueryDashboardSummaryDto,
  ): Promise<DashboardSummary> {
    const raw = await this.dashboardRepository.getSummaryData({
      year: query.year,
      issueStatusId: query.issueStatusId,
      statusCode: query.statusCode,
      primaryAgencyId: query.primaryAgencyId,
      categoryId: query.categoryId,
      workingGroupId: query.workingGroupId,
    });

    // Status id → { code, name }. Used to label every chart. Statuses that
    // were soft-deleted are absent here, so any issue pointing at one is
    // skipped from the charts (but still counted in totalIssues).
    const statusById = new Map<number, StatusLookup>(
      raw.statuses.map((status) => [status.id, status]),
    );

    // Donut chart: one entry per status that actually has issues.
    const statusBreakdown: DashboardStatusCount[] = raw.statusCounts
      .map((row) =>
        this.toStatusCount(row.issueStatusId, row._count._all, statusById),
      )
      .filter((entry): entry is DashboardStatusCount => entry !== null)
      .sort((a, b) => a.statusId - b.statusId);

    // Working group bar chart: group the (workingGroup, status) counts by
    // working group.
    const countsByWorkingGroup = new Map<number, Map<number, number>>();
    for (const row of raw.workingGroupStatusCounts) {
      const counts = countsByWorkingGroup.get(row.stakeholderId) ?? new Map();
      counts.set(row.issueStatusId, row._count._all);
      countsByWorkingGroup.set(row.stakeholderId, counts);
    }

    // One bar per working group — EVERY active working group appears on the
    // chart, even with zero issues (it just renders an empty bar). Sorted
    // by busiest first, then by name.
    const byWorkingGroup: DashboardWorkingGroupRow[] = raw.workingGroups.map(
      (group) => {
        const counts =
          countsByWorkingGroup.get(group.id) ?? new Map<number, number>();
        const { total, byStatus } = this.buildStatusRows(counts, statusById);
        return {
          workingGroupId: group.id,
          workingGroupName: group.name,
          total,
          byStatus,
        };
      },
    );
    byWorkingGroup.sort(
      (a, b) =>
        b.total - a.total ||
        a.workingGroupName.localeCompare(b.workingGroupName),
    );

    // Primary agency bar chart: tally the pivot rows (one row = one issue)
    // into agency → status → count.
    const countsByAgency = new Map<number, Map<number, number>>();
    for (const row of raw.primaryAgencyRows) {
      const counts = countsByAgency.get(row.stakeholderId) ?? new Map();
      const statusId = row.issue.issueStatusId;
      counts.set(statusId, (counts.get(statusId) ?? 0) + 1);
      countsByAgency.set(row.stakeholderId, counts);
    }

    // One bar per ministry — EVERY active ministry appears on the chart,
    // even with zero issues (it just renders an empty bar). Sorted by
    // busiest first, then by name.
    const byPrimaryAgency: DashboardAgencyRow[] = raw.ministries.map(
      (ministry) => {
        const counts =
          countsByAgency.get(ministry.id) ?? new Map<number, number>();
        const { total, byStatus } = this.buildStatusRows(counts, statusById);
        return {
          agencyId: ministry.id,
          agencyName: ministry.name,
          total,
          byStatus,
        };
      },
    );
    byPrimaryAgency.sort(
      (a, b) => b.total - a.total || a.agencyName.localeCompare(b.agencyName),
    );

    // Category bar chart: one bar per category — EVERY category appears,
    // even with zero issues (it just renders an empty bar). Sorted by
    // busiest first, then by name.
    const countByCategoryId = new Map(
      raw.categoryCounts.map((row) => [row.categoryId, row._count._all]),
    );
    const byCategory: DashboardCategoryRow[] = raw.categories
      .map((category) => ({
        categoryId: category.id,
        categoryName: category.name,
        count: countByCategoryId.get(category.id) ?? 0,
      }))
      .sort(
        (a, b) =>
          b.count - a.count || a.categoryName.localeCompare(b.categoryName),
      );

    // Summary cards. The solved/inProgress/notAddressed numbers are the donut
    // counts summed by status name; totalPrimaryAgencies is the number of
    // active Ministry-type stakeholders in the system (all agencies, whether
    // or not they have issues assigned).
    const cards = {
      totalIssues: raw.totalIssues,
      solved: this.sumByStatusName(
        statusBreakdown,
        SUMMARY_STATUS_NAMES.solved,
      ),
      inProgress: this.sumByStatusName(
        statusBreakdown,
        SUMMARY_STATUS_NAMES.inProgress,
      ),
      notAddressed: this.sumByStatusName(
        statusBreakdown,
        SUMMARY_STATUS_NAMES.notAddressed,
      ),
      totalPrimaryAgencies: raw.ministries.length,
    };

    return {
      cards,
      statusBreakdown,
      byPrimaryAgency,
      byWorkingGroup,
      byCategory,
    };
  }

  // Turns a single (statusId, count) pair into a labelled status slice, or
  // null when the status is unknown (e.g. it was soft-deleted).
  private toStatusCount(
    statusId: number,
    count: number,
    statusById: Map<number, StatusLookup>,
  ): DashboardStatusCount | null {
    const status = statusById.get(statusId);
    if (!status) return null;
    return { statusId: status.id, code: status.code, name: status.name, count };
  }

  // Turns a "status id → count" map into a sorted list of status slices plus
  // the row total. Unknown statuses are skipped, so the total is the sum of
  // the slices that could be labelled.
  private buildStatusRows(
    countsByStatusId: Map<number, number>,
    statusById: Map<number, StatusLookup>,
  ): { total: number; byStatus: DashboardStatusCount[] } {
    const byStatus: DashboardStatusCount[] = [];
    let total = 0;
    for (const [statusId, count] of countsByStatusId) {
      const slice = this.toStatusCount(statusId, count, statusById);
      if (!slice) continue;
      byStatus.push(slice);
      total += count;
    }
    byStatus.sort((a, b) => a.statusId - b.statusId);
    return { total, byStatus };
  }

  // Adds up the counts of every status slice whose name matches (ignoring
  // case). There can be more than one status with the same name, so we sum.
  private sumByStatusName(
    statusBreakdown: DashboardStatusCount[],
    bucketName: string,
  ): number {
    return statusBreakdown
      .filter((slice) => slice.name.toLowerCase() === bucketName.toLowerCase())
      .reduce((sum, slice) => sum + slice.count, 0);
  }
}
