import {
  type BudgetForecastRow,
  getDashboardBudgetForecast,
  getDashboardChargeabilityOverview,
  getDashboardDemand,
  getDashboardOverview,
  getDashboardPeakTimes,
  getDashboardProjectHealth,
  getDashboardSkillGaps,
  getDashboardSkillGapSummary,
  getDashboardTopValueResources,
} from "@capakraken/application";
import { z } from "zod";
import { anonymizeResources, getAnonymizationDirectory } from "../lib/anonymization.js";
import { cacheGet, cacheSet } from "../lib/cache.js";
import { fmtEur } from "../lib/format-utils.js";
import type { TRPCContext } from "../trpc.js";
import {
  type DashboardBudgetForecastDetail,
  mapBudgetForecastDetailRows,
  mapProjectHealthDetailRows,
} from "./dashboard-detail-support.js";

const DEFAULT_TTL = 60;

type DashboardProcedureContext = Pick<TRPCContext, "db" | "dbUser" | "session">;

type TopValueResourceRow = {
  id: string;
  eid: string;
  displayName: string;
  chapter: string | null;
  valueScore: number | null;
  valueScoreBreakdown: import("@capakraken/shared").ValueScoreBreakdown | null;
  valueScoreUpdatedAt: Date | null;
  lcrCents: number;
  countryCode: string | null;
  countryName: string | null;
  federalState: string | null;
  metroCityName: string | null;
};

export const dashboardPeakTimesInputSchema = z.object({
  startDate: z.string().datetime(),
  endDate: z.string().datetime(),
  granularity: z.enum(["week", "month"]).default("month"),
  groupBy: z.enum(["project", "chapter", "resource"]).default("project"),
});

export const dashboardTopValueResourcesInputSchema = z.object({
  limit: z.number().int().min(1).max(50).default(10),
});

export const dashboardDemandInputSchema = z.object({
  startDate: z.string().datetime(),
  endDate: z.string().datetime(),
  groupBy: z.enum(["project", "person", "chapter"]).default("project"),
});

export const dashboardDetailInputSchema = z.object({
  section: z.string().optional().default("all"),
});

export const dashboardChargeabilityOverviewInputSchema = z.object({
  includeProposed: z.boolean().default(false),
  topN: z.number().int().min(1).max(50).default(10),
  watchlistThreshold: z.number().default(15),
  countryIds: z.array(z.string()).optional(),
  departed: z.boolean().optional(),
});

type DashboardPeakTimesInput = z.infer<typeof dashboardPeakTimesInputSchema>;
type DashboardTopValueResourcesInput = z.infer<typeof dashboardTopValueResourcesInputSchema>;
type DashboardDemandInput = z.infer<typeof dashboardDemandInputSchema>;
type DashboardDetailInput = z.infer<typeof dashboardDetailInputSchema>;
type DashboardChargeabilityOverviewInput = z.infer<typeof dashboardChargeabilityOverviewInputSchema>;
type DashboardChargeabilityOverviewRead = Awaited<ReturnType<typeof getDashboardChargeabilityOverview>>;

function round1(value: number): number {
  return Math.round(value * 10) / 10;
}

function formatPct(value: number): string {
  return `${Math.round(value)}%`;
}

function mapStatisticsDetail(overview: Awaited<ReturnType<typeof getDashboardOverview>>) {
  return {
    activeResources: overview.activeResources,
    totalProjects: overview.totalProjects,
    activeProjects: overview.activeProjects,
    totalAllocations: overview.totalAllocations,
    approvedVacations: overview.approvedVacations,
    totalEstimates: overview.totalEstimates,
    totalBudget: overview.budgetSummary.totalBudgetCents > 0
      ? fmtEur(overview.budgetSummary.totalBudgetCents)
      : "N/A",
    projectsByStatus: Object.fromEntries(
      overview.projectsByStatus.map((entry) => [entry.status, entry.count]),
    ),
    topChapters: [...overview.chapterUtilization]
      .sort((left, right) => right.resourceCount - left.resourceCount)
      .slice(0, 10)
      .map((chapter) => ({
        chapter: chapter.chapter,
        count: chapter.resourceCount,
      })),
  };
}

async function getOverviewCached(db: Parameters<typeof getDashboardOverview>[0]) {
  const cacheKey = "overview";
  const cached = await cacheGet<Awaited<ReturnType<typeof getDashboardOverview>>>(cacheKey);
  if (cached) return cached;

  const result = await getDashboardOverview(db);
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

async function getPeakTimesCached(
  db: Parameters<typeof getDashboardPeakTimes>[0],
  input: DashboardPeakTimesInput,
) {
  const cacheKey = `peakTimes:${input.startDate}:${input.endDate}:${input.granularity}:${input.groupBy}`;
  const cached = await cacheGet<Awaited<ReturnType<typeof getDashboardPeakTimes>>>(cacheKey);
  if (cached) return cached;

  const result = await getDashboardPeakTimes(db, {
    startDate: new Date(input.startDate),
    endDate: new Date(input.endDate),
    granularity: input.granularity,
    groupBy: input.groupBy,
  });
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

async function getDemandCached(
  db: Parameters<typeof getDashboardDemand>[0],
  input: DashboardDemandInput,
) {
  const cacheKey = `demand:${input.startDate}:${input.endDate}:${input.groupBy}`;
  const cached = await cacheGet<Awaited<ReturnType<typeof getDashboardDemand>>>(cacheKey);
  if (cached) return cached;

  const result = await getDashboardDemand(db, {
    startDate: new Date(input.startDate),
    endDate: new Date(input.endDate),
    groupBy: input.groupBy,
  });
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

async function getChargeabilityOverviewCached(
  db: Parameters<typeof getDashboardChargeabilityOverview>[0],
  input: DashboardChargeabilityOverviewInput,
): Promise<DashboardChargeabilityOverviewRead> {
  const cacheKey = `chargeability:${input.includeProposed}:${input.topN}:${input.watchlistThreshold}:${(input.countryIds ?? []).join(",")}:${input.departed ?? ""}`;
  const cached = await cacheGet<DashboardChargeabilityOverviewRead>(cacheKey);
  if (cached) return cached;

  const result = await getDashboardChargeabilityOverview(db, {
    includeProposed: input.includeProposed,
    topN: input.topN,
    watchlistThreshold: input.watchlistThreshold,
    ...(input.countryIds !== undefined ? { countryIds: input.countryIds } : {}),
    ...(input.departed !== undefined ? { departed: input.departed } : {}),
  });
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

async function getTopValueResourcesCached(
  db: Parameters<typeof getDashboardTopValueResources>[0],
  input: { limit: number; userRole: string },
): Promise<TopValueResourceRow[]> {
  const cacheKey = `topValue:${input.limit}:${input.userRole}`;
  const cached = await cacheGet<TopValueResourceRow[]>(cacheKey);
  if (cached) return cached;

  const [resources, directory] = await Promise.all([
    getDashboardTopValueResources(db, {
      limit: input.limit,
      userRole: input.userRole,
    }),
    getAnonymizationDirectory(db),
  ]);
  const result: TopValueResourceRow[] = anonymizeResources(resources, directory);
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

function getUserRole(ctx: DashboardProcedureContext) {
  return (ctx.session?.user as { role?: string } | undefined)?.role
    ?? ctx.dbUser?.systemRole
    ?? "USER";
}

function mapChargeabilityByChapter(
  rows: DashboardChargeabilityOverviewRead["rows"],
  month: string,
) {
  const chapterMap = new Map<string, {
    headcount: number;
    avgTargetSum: number;
    avgActualSum: number;
    avgExpectedSum: number;
    derivedHeadcount: number;
    baseAvailableHours: number;
    effectiveAvailableHours: number;
    actualBookedHours: number;
    expectedBookedHours: number;
    targetBookedHours: number;
    publicHolidayHoursDeduction: number;
    absenceDayEquivalent: number;
    absenceHoursDeduction: number;
    unassignedHours: number;
  }>();

  for (const row of rows) {
    const chapter = row.chapter ?? "Unassigned";
    const summary = chapterMap.get(chapter) ?? {
      headcount: 0,
      avgTargetSum: 0,
      avgActualSum: 0,
      avgExpectedSum: 0,
      derivedHeadcount: 0,
      baseAvailableHours: 0,
      effectiveAvailableHours: 0,
      actualBookedHours: 0,
      expectedBookedHours: 0,
      targetBookedHours: 0,
      publicHolidayHoursDeduction: 0,
      absenceDayEquivalent: 0,
      absenceHoursDeduction: 0,
      unassignedHours: 0,
    };

    summary.headcount += 1;
    summary.avgTargetSum += row.chargeabilityTarget;
    summary.avgActualSum += row.actualChargeability;
    summary.avgExpectedSum += row.expectedChargeability;

    if (row.derivation) {
      summary.derivedHeadcount += 1;
      summary.baseAvailableHours += row.derivation.baseAvailableHours;
      summary.effectiveAvailableHours += row.derivation.effectiveAvailableHours;
      summary.actualBookedHours += row.derivation.actualBookedHours;
      summary.expectedBookedHours += row.derivation.expectedBookedHours;
      summary.targetBookedHours += row.derivation.targetBookedHours;
      summary.publicHolidayHoursDeduction += row.derivation.publicHolidayHoursDeduction;
      summary.absenceDayEquivalent += row.derivation.absenceDayEquivalent;
      summary.absenceHoursDeduction += row.derivation.absenceHoursDeduction;
      summary.unassignedHours += row.derivation.unassignedHours;
    }

    chapterMap.set(chapter, summary);
  }

  return [...chapterMap.entries()]
    .map(([chapter, summary]) => ({
      chapter,
      headcount: summary.headcount,
      avgTargetPct: Math.round(summary.avgTargetSum / summary.headcount),
      avgActualPct: Math.round(summary.avgActualSum / summary.headcount),
      avgExpectedPct: Math.round(summary.avgExpectedSum / summary.headcount),
      gapToTargetPct: Math.round((summary.avgTargetSum - summary.avgActualSum) / summary.headcount),
      avgTarget: formatPct(summary.avgTargetSum / summary.headcount),
      avgActual: formatPct(summary.avgActualSum / summary.headcount),
      avgExpected: formatPct(summary.avgExpectedSum / summary.headcount),
      explainability: summary.derivedHeadcount > 0
        ? {
            month,
            resourceCount: summary.headcount,
            derivedHeadcount: summary.derivedHeadcount,
            baseAvailableHours: round1(summary.baseAvailableHours),
            effectiveAvailableHours: round1(summary.effectiveAvailableHours),
            actualBookedHours: round1(summary.actualBookedHours),
            expectedBookedHours: round1(summary.expectedBookedHours),
            targetBookedHours: round1(summary.targetBookedHours),
            publicHolidayHoursDeduction: round1(summary.publicHolidayHoursDeduction),
            absenceDayEquivalent: round1(summary.absenceDayEquivalent),
            absenceHoursDeduction: round1(summary.absenceHoursDeduction),
            unassignedHours: round1(summary.unassignedHours),
          }
        : null,
    }))
    .sort((left, right) => (
      right.headcount - left.headcount
      || left.chapter.localeCompare(right.chapter)
    ));
}

function getProjectHealthRating(overall: number): "healthy" | "at_risk" | "critical" {
  if (overall >= 80) return "healthy";
  if (overall >= 50) return "at_risk";
  return "critical";
}

export async function getDashboardOverviewRead(ctx: DashboardProcedureContext) {
  return getOverviewCached(ctx.db);
}

export async function getDashboardStatisticsDetail(ctx: DashboardProcedureContext) {
  const overview = await getOverviewCached(ctx.db);
  return mapStatisticsDetail(overview);
}

export async function getDashboardPeakTimesRead(
  ctx: DashboardProcedureContext,
  input: DashboardPeakTimesInput,
) {
  return getPeakTimesCached(ctx.db, input);
}

export async function getDashboardTopValueResourcesRead(
  ctx: DashboardProcedureContext,
  input: DashboardTopValueResourcesInput,
) {
  return getTopValueResourcesCached(ctx.db, { limit: input.limit, userRole: getUserRole(ctx) });
}

export async function getDashboardDemandRead(
  ctx: DashboardProcedureContext,
  input: DashboardDemandInput,
) {
  return getDemandCached(ctx.db, input);
}

export async function getDashboardDetail(ctx: DashboardProcedureContext, input: DashboardDetailInput) {
  const section = input.section;
  const result: Record<string, unknown> = {};
  const needsOverview = (
    section === "all"
    || section === "peak_times"
    || section === "demand_pipeline"
  );
  const overview = needsOverview ? await getOverviewCached(ctx.db) : null;
  const now = new Date();
  const rangeStart = overview?.budgetBasis.windowStart
    ? new Date(overview.budgetBasis.windowStart)
    : new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
  const rangeEnd = overview?.budgetBasis.windowEnd
    ? new Date(overview.budgetBasis.windowEnd)
    : new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 5, 0));
  const userRole = getUserRole(ctx);

  if (section === "all" || section === "peak_times") {
    const peakTimes = await getPeakTimesCached(ctx.db, {
      startDate: rangeStart.toISOString(),
      endDate: rangeEnd.toISOString(),
      granularity: "month",
      groupBy: "project",
    });

    result.peakTimes = [...peakTimes]
      .sort((left, right) => right.totalHours - left.totalHours)
      .slice(0, 6)
      .map((entry) => ({
        month: entry.period,
        totalHours: round1(entry.totalHours),
        totalHoursPerDay: round1(entry.totalHours),
        capacityHours: round1(entry.capacityHours),
        utilizationPct: entry.utilizationPct ?? null,
        calendarContextCount: entry.derivation?.calendarContextCount ?? 0,
        calendarLocations: entry.derivation?.calendarLocations ?? [],
        explainability: entry.derivation
          ? {
              periodStart: entry.derivation.periodStart,
              periodEnd: entry.derivation.periodEnd,
              resourceCount: entry.derivation.resourceCount,
              groupCount: entry.derivation.groupCount,
              baseAvailableHours: entry.derivation.baseAvailableHours,
              effectiveAvailableHours: entry.derivation.effectiveAvailableHours,
              publicHolidayHoursDeduction: entry.derivation.publicHolidayHoursDeduction,
              absenceDayEquivalent: entry.derivation.absenceDayEquivalent,
              absenceHoursDeduction: entry.derivation.absenceHoursDeduction,
              remainingCapacityHours: entry.derivation.remainingCapacityHours,
              overbookedHours: entry.derivation.overbookedHours,
            }
          : null,
      }));
  }

  if (section === "all" || section === "top_resources") {
    const resources = await getTopValueResourcesCached(ctx.db, { limit: 10, userRole });
    result.topResources = resources.map((resource) => ({
      name: resource.displayName,
      eid: resource.eid,
      chapter: resource.chapter ?? null,
      lcr: fmtEur(resource.lcrCents),
      valueScore: resource.valueScore ?? null,
      valueScoreBreakdown: resource.valueScoreBreakdown ?? null,
      valueScoreUpdatedAt: resource.valueScoreUpdatedAt?.toISOString() ?? null,
      countryCode: resource.countryCode ?? null,
      countryName: resource.countryName ?? null,
      federalState: resource.federalState ?? null,
      metroCityName: resource.metroCityName ?? null,
    }));
  }

  if (section === "all" || section === "demand_pipeline") {
    const demandRows = await getDemandCached(ctx.db, {
      startDate: rangeStart.toISOString(),
      endDate: rangeEnd.toISOString(),
      groupBy: "project",
    });
    result.demandPipeline = demandRows
      .map((row) => ({
        project: `${row.name} (${row.shortCode})`,
        needed: Math.max(0, round1(row.requiredFTEs - row.resourceCount)),
        requiredFTEs: row.requiredFTEs,
        allocatedResources: row.resourceCount,
        allocatedHours: row.allocatedHours,
        calendarLocations: row.derivation?.calendarLocations ?? [],
        explainability: row.derivation
          ? {
              periodStart: row.derivation.periodStart,
              periodEnd: row.derivation.periodEnd,
              periodWorkingHoursBase: row.derivation.periodWorkingHoursBase,
              requiredHours: row.derivation.requiredHours,
              fillPct: row.derivation.fillPct,
              demandSource: row.derivation.demandSource,
              calendarContextCount: row.derivation.calendarLocations.length,
            }
          : null,
      }))
      .filter((row) => row.needed > 0)
      .sort((left, right) => right.needed - left.needed)
      .slice(0, 15);
  }

  if (section === "all" || section === "chargeability_overview") {
    const chargeabilityOverview = await getChargeabilityOverviewCached(ctx.db, {
      includeProposed: false,
      topN: 10,
      watchlistThreshold: 15,
    });
    result.chargeabilityByChapter = mapChargeabilityByChapter(
      chargeabilityOverview.rows,
      chargeabilityOverview.month,
    );
  }

  if (section === "all" || section === "project_health") {
    const projectHealth = await getDashboardProjectHealthRead(ctx);
    result.projectHealth = [...projectHealth]
      .sort((left, right) => left.compositeScore - right.compositeScore)
      .slice(0, 10)
      .map((project) => ({
        project: `${project.projectName} (${project.shortCode})`,
        status: project.status,
        overall: project.compositeScore,
        rating: getProjectHealthRating(project.compositeScore),
        budget: project.budgetHealth,
        staffing: project.staffingHealth,
        timeline: project.timelineHealth,
        timelineStatus: project.timelineStatus ?? "UNSCHEDULED",
        daysUntilEndDate: project.daysUntilEndDate ?? null,
        demandHeadcountOpen: project.demandHeadcountOpen ?? 0,
        explainability: {
          demandHeadcountTotal: project.demandHeadcountTotal ?? 0,
          demandHeadcountFilled: project.demandHeadcountFilled ?? 0,
          demandHeadcountOpen: project.demandHeadcountOpen ?? 0,
          demandRequirementCount: project.demandRequirementCount ?? 0,
          plannedEndDate: project.plannedEndDate?.toISOString() ?? null,
          budgetUtilizationPercent: project.budgetUtilizationPercent ?? null,
          remainingBudgetCents: project.remainingBudgetCents ?? null,
          calendarContextCount: project.derivation?.calendarContextCount ?? 0,
          holidayAwareAssignmentCount: project.derivation?.holidayAwareAssignmentCount ?? 0,
          publicHolidayCostDeductionCents:
            project.derivation?.publicHolidayCostDeductionCents ?? 0,
          absenceCostDeductionCents: project.derivation?.absenceCostDeductionCents ?? 0,
        },
      }));
  }

  if (section === "all" || section === "skill_gaps") {
    const skillGapSummary = await getDashboardSkillGapSummaryRead(ctx);
    result.skillGaps = {
      totalOpenPositions: skillGapSummary.totalOpenPositions,
      roleGaps: skillGapSummary.roleGaps
        .slice(0, 10)
        .map((gap) => ({
          role: gap.role,
          gap: gap.gap,
          needed: gap.needed,
          filled: gap.filled,
          fillRate: gap.fillRate,
        })),
      topSkillsInSupply: skillGapSummary.skillSupplyTop10.slice(0, 5),
      resourcesByRole: skillGapSummary.resourcesByRole.slice(0, 5),
    };
  }

  return result;
}

export async function getDashboardChargeabilityOverviewRead(
  ctx: DashboardProcedureContext,
  input: DashboardChargeabilityOverviewInput,
) {
  const [overview, directory] = await Promise.all([
    getChargeabilityOverviewCached(ctx.db, input),
    getAnonymizationDirectory(ctx.db),
  ]);
  const { rows: _rows, top, watchlist, ...rest } = overview;

  return {
    ...rest,
    top: anonymizeResources(top, directory),
    watchlist: anonymizeResources(watchlist, directory),
  };
}

export async function getDashboardBudgetForecastRead(
  ctx: DashboardProcedureContext,
): Promise<BudgetForecastRow[]> {
  const cacheKey = "budgetForecast";
  const cached = await cacheGet<BudgetForecastRow[]>(cacheKey);
  if (cached) return cached;

  const result = await getDashboardBudgetForecast(ctx.db);
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

export async function getDashboardBudgetForecastDetail(
  ctx: DashboardProcedureContext,
): Promise<DashboardBudgetForecastDetail> {
  const budgetForecast: BudgetForecastRow[] = await getDashboardBudgetForecastRead(ctx);
  return mapBudgetForecastDetailRows(budgetForecast);
}

export async function getDashboardSkillGapsRead(ctx: DashboardProcedureContext) {
  const cacheKey = "skillGaps";
  const cached = await cacheGet<Awaited<ReturnType<typeof getDashboardSkillGaps>>>(cacheKey);
  if (cached) return cached;

  const result = await getDashboardSkillGaps(ctx.db);
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

export async function getDashboardSkillGapSummaryRead(ctx: DashboardProcedureContext) {
  const cacheKey = "skillGapSummary";
  const cached = await cacheGet<Awaited<ReturnType<typeof getDashboardSkillGapSummary>>>(cacheKey);
  if (cached) return cached;

  const result = await getDashboardSkillGapSummary(ctx.db);
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

export async function getDashboardProjectHealthRead(ctx: DashboardProcedureContext) {
  const cacheKey = "projectHealth";
  const cached = await cacheGet<Awaited<ReturnType<typeof getDashboardProjectHealth>>>(cacheKey);
  if (cached) return cached;

  const result = await getDashboardProjectHealth(ctx.db);
  await cacheSet(cacheKey, result, DEFAULT_TTL);
  return result;
}

export async function getDashboardProjectHealthDetail(ctx: DashboardProcedureContext) {
  const projectHealth = await getDashboardProjectHealthRead(ctx);
  return mapProjectHealthDetailRows(projectHealth);
}
