import { Prisma } from "@capakraken/db";
import {
  isChargeabilityActualBooking,
  isChargeabilityRelevantProject,
  listAssignmentBookings,
} from "@capakraken/application";
import type { WeekdayAvailability } from "@capakraken/shared";
import { createTRPCRouter, controllerProcedure } from "../trpc.js";
import {
  calculateEffectiveAvailableHours,
  calculateEffectiveBookedHours,
  countEffectiveWorkingDays,
  getAvailabilityHoursForDate,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import { TRPCError } from "@trpc/server";
import { z } from "zod";

// ─── Column Definitions ──────────────────────────────────────────────────────

interface ColumnDef {
  key: string;
  label: string;
  dataType: "string" | "number" | "date" | "boolean";
  /** Prisma select path — nested relations use dot notation */
  prismaPath?: string;
}

const RESOURCE_COLUMNS: ColumnDef[] = [
  { key: "id", label: "ID", dataType: "string" },
  { key: "eid", label: "Employee ID", dataType: "string" },
  { key: "displayName", label: "Name", dataType: "string" },
  { key: "email", label: "Email", dataType: "string" },
  { key: "chapter", label: "Chapter", dataType: "string" },
  { key: "resourceType", label: "Resource Type", dataType: "string" },
  { key: "lcrCents", label: "LCR (cents)", dataType: "number" },
  { key: "ucrCents", label: "UCR (cents)", dataType: "number" },
  { key: "currency", label: "Currency", dataType: "string" },
  { key: "chargeabilityTarget", label: "Chargeability Target (%)", dataType: "number" },
  { key: "fte", label: "FTE", dataType: "number" },
  { key: "isActive", label: "Active", dataType: "boolean" },
  { key: "chgResponsibility", label: "Chg Responsibility", dataType: "boolean" },
  { key: "rolledOff", label: "Rolled Off", dataType: "boolean" },
  { key: "departed", label: "Departed", dataType: "boolean" },
  { key: "postalCode", label: "Postal Code", dataType: "string" },
  { key: "federalState", label: "Federal State", dataType: "string" },
  { key: "country.code", label: "Country Code", dataType: "string", prismaPath: "country" },
  { key: "country.name", label: "Country", dataType: "string", prismaPath: "country" },
  { key: "metroCity.name", label: "Metro City", dataType: "string", prismaPath: "metroCity" },
  { key: "orgUnit.name", label: "Org Unit", dataType: "string", prismaPath: "orgUnit" },
  { key: "managementLevelGroup.name", label: "Mgmt Level Group", dataType: "string", prismaPath: "managementLevelGroup" },
  { key: "managementLevel.name", label: "Mgmt Level", dataType: "string", prismaPath: "managementLevel" },
  { key: "areaRole.name", label: "Area Role", dataType: "string", prismaPath: "areaRole" },
  { key: "createdAt", label: "Created At", dataType: "date" },
  { key: "updatedAt", label: "Updated At", dataType: "date" },
];

const PROJECT_COLUMNS: ColumnDef[] = [
  { key: "id", label: "ID", dataType: "string" },
  { key: "shortCode", label: "Short Code", dataType: "string" },
  { key: "name", label: "Name", dataType: "string" },
  { key: "orderType", label: "Order Type", dataType: "string" },
  { key: "allocationType", label: "Allocation Type", dataType: "string" },
  { key: "status", label: "Status", dataType: "string" },
  { key: "winProbability", label: "Win Probability (%)", dataType: "number" },
  { key: "budgetCents", label: "Budget (cents)", dataType: "number" },
  { key: "clientId", label: "Client ID", dataType: "string" },
  { key: "startDate", label: "Start Date", dataType: "date" },
  { key: "endDate", label: "End Date", dataType: "date" },
  { key: "responsiblePerson", label: "Responsible Person", dataType: "string" },
  { key: "client.name", label: "Client", dataType: "string", prismaPath: "client" },
  { key: "utilizationCategory.name", label: "Util. Category", dataType: "string", prismaPath: "utilizationCategory" },
  { key: "blueprint.name", label: "Blueprint", dataType: "string", prismaPath: "blueprint" },
  { key: "createdAt", label: "Created At", dataType: "date" },
  { key: "updatedAt", label: "Updated At", dataType: "date" },
];

const ASSIGNMENT_COLUMNS: ColumnDef[] = [
  { key: "id", label: "ID", dataType: "string" },
  { key: "resourceId", label: "Resource ID", dataType: "string" },
  { key: "projectId", label: "Project ID", dataType: "string" },
  { key: "resource.displayName", label: "Resource", dataType: "string", prismaPath: "resource" },
  { key: "resource.eid", label: "Resource EID", dataType: "string", prismaPath: "resource" },
  { key: "resource.chapter", label: "Resource Chapter", dataType: "string", prismaPath: "resource" },
  { key: "resource.country.code", label: "Resource Country Code", dataType: "string", prismaPath: "resource" },
  { key: "resource.federalState", label: "Resource State", dataType: "string", prismaPath: "resource" },
  { key: "resource.country.name", label: "Resource Country", dataType: "string", prismaPath: "resource" },
  { key: "resource.metroCity.name", label: "Resource City", dataType: "string", prismaPath: "resource" },
  { key: "project.name", label: "Project", dataType: "string", prismaPath: "project" },
  { key: "project.shortCode", label: "Project Code", dataType: "string", prismaPath: "project" },
  { key: "project.status", label: "Project Status", dataType: "string", prismaPath: "project" },
  { key: "project.client.name", label: "Project Client", dataType: "string", prismaPath: "project" },
  { key: "startDate", label: "Start Date", dataType: "date" },
  { key: "endDate", label: "End Date", dataType: "date" },
  { key: "hoursPerDay", label: "Hours/Day", dataType: "number" },
  { key: "percentage", label: "Percentage", dataType: "number" },
  { key: "role", label: "Role (legacy)", dataType: "string" },
  { key: "roleEntity.name", label: "Role", dataType: "string", prismaPath: "roleEntity" },
  { key: "dailyCostCents", label: "Daily Cost (cents)", dataType: "number" },
  { key: "status", label: "Status", dataType: "string" },
  { key: "createdAt", label: "Created At", dataType: "date" },
  { key: "updatedAt", label: "Updated At", dataType: "date" },
];

const RESOURCE_MONTH_COLUMNS: ColumnDef[] = [
  { key: "id", label: "Row ID", dataType: "string" },
  { key: "resourceId", label: "Resource ID", dataType: "string" },
  { key: "monthKey", label: "Month", dataType: "string" },
  { key: "periodStart", label: "Period Start", dataType: "date" },
  { key: "periodEnd", label: "Period End", dataType: "date" },
  { key: "eid", label: "Employee ID", dataType: "string" },
  { key: "displayName", label: "Name", dataType: "string" },
  { key: "email", label: "Email", dataType: "string" },
  { key: "chapter", label: "Chapter", dataType: "string" },
  { key: "resourceType", label: "Resource Type", dataType: "string" },
  { key: "isActive", label: "Active", dataType: "boolean" },
  { key: "chgResponsibility", label: "Chg Responsibility", dataType: "boolean" },
  { key: "rolledOff", label: "Rolled Off", dataType: "boolean" },
  { key: "departed", label: "Departed", dataType: "boolean" },
  { key: "countryCode", label: "Country Code", dataType: "string" },
  { key: "countryName", label: "Country", dataType: "string" },
  { key: "federalState", label: "Federal State", dataType: "string" },
  { key: "metroCityName", label: "Metro City", dataType: "string" },
  { key: "orgUnitName", label: "Org Unit", dataType: "string" },
  { key: "managementLevelGroupName", label: "Mgmt Level Group", dataType: "string" },
  { key: "managementLevelName", label: "Mgmt Level", dataType: "string" },
  { key: "fte", label: "FTE", dataType: "number" },
  { key: "lcrCents", label: "LCR (cents)", dataType: "number" },
  { key: "ucrCents", label: "UCR (cents)", dataType: "number" },
  { key: "currency", label: "Currency", dataType: "string" },
  { key: "monthlyChargeabilityTargetPct", label: "Target Chargeability (%)", dataType: "number" },
  { key: "monthlyTargetHours", label: "Target Hours", dataType: "number" },
  { key: "monthlyBaseWorkingDays", label: "Base Working Days", dataType: "number" },
  { key: "monthlyEffectiveWorkingDays", label: "Effective Working Days", dataType: "number" },
  { key: "monthlyBaseAvailableHours", label: "Base Available Hours", dataType: "number" },
  { key: "monthlySahHours", label: "SAH", dataType: "number" },
  { key: "monthlyPublicHolidayCount", label: "Holiday Dates", dataType: "number" },
  { key: "monthlyPublicHolidayWorkdayCount", label: "Holiday Workdays", dataType: "number" },
  { key: "monthlyPublicHolidayHoursDeduction", label: "Holiday Hours Deduction", dataType: "number" },
  { key: "monthlyAbsenceDayEquivalent", label: "Absence Day Equivalent", dataType: "number" },
  { key: "monthlyAbsenceHoursDeduction", label: "Absence Hours Deduction", dataType: "number" },
  { key: "monthlyActualBookedHours", label: "Actual Booked Hours", dataType: "number" },
  { key: "monthlyExpectedBookedHours", label: "Expected Booked Hours", dataType: "number" },
  { key: "monthlyActualChargeabilityPct", label: "Actual Chargeability (%)", dataType: "number" },
  { key: "monthlyExpectedChargeabilityPct", label: "Expected Chargeability (%)", dataType: "number" },
  { key: "monthlyUnassignedHours", label: "Unassigned Hours", dataType: "number" },
];

const COLUMN_MAP: Record<EntityKey, ColumnDef[]> = {
  resource: RESOURCE_COLUMNS,
  project: PROJECT_COLUMNS,
  assignment: ASSIGNMENT_COLUMNS,
  resource_month: RESOURCE_MONTH_COLUMNS,
};

// ─── Helpers ────────────────────────────────────────────────────────────────

const ENTITY_MAP = {
  resource: "resource",
  project: "project",
  assignment: "assignment",
  resource_month: "resource_month",
} as const;

type EntityKey = keyof typeof ENTITY_MAP;
const PERIOD_MONTH_PATTERN = /^\d{4}-(0[1-9]|1[0-2])$/;

/** Allowlist of top-level scalar fields per entity that can be filtered/sorted on. */
const ALLOWED_SCALAR_FIELDS: Record<EntityKey, Set<string>> = {
  resource: new Set([
    "id", "eid", "displayName", "email", "chapter", "resourceType",
    "lcrCents", "ucrCents", "currency", "chargeabilityTarget", "fte",
    "isActive", "chgResponsibility", "rolledOff", "departed",
    "postalCode", "federalState", "createdAt", "updatedAt",
  ]),
  project: new Set([
    "id", "shortCode", "name", "orderType", "allocationType", "status",
    "winProbability", "budgetCents", "startDate", "endDate",
    "responsiblePerson", "createdAt", "updatedAt",
  ]),
  assignment: new Set([
    "id", "startDate", "endDate", "hoursPerDay", "percentage",
    "role", "dailyCostCents", "status", "createdAt", "updatedAt",
  ]),
  resource_month: new Set(RESOURCE_MONTH_COLUMNS.map((column) => column.key)),
};

function getValidScalarField(entity: EntityKey, field: string): string | null {
  // Only allow top-level scalar fields for filter/sort (no relation traversal in where/orderBy)
  if (ALLOWED_SCALAR_FIELDS[entity].has(field)) return field;
  return null;
}

function getColumnDef(entity: EntityKey, columnKey: string): ColumnDef | undefined {
  return COLUMN_MAP[entity].find((column) => column.key === columnKey);
}

function assertKnownColumns(entity: EntityKey, columns: string[]): void {
  const invalidColumns = columns.filter((column) => !getColumnDef(entity, column));
  if (invalidColumns.length > 0) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Unknown columns for ${entity}: ${invalidColumns.join(", ")}`,
    });
  }
}

function assertValidFilterField(entity: EntityKey, field: string): string {
  if (entity === "resource_month") {
    if (!getColumnDef(entity, field)) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Unknown filter field for ${entity}: ${field}`,
      });
    }
    return field;
  }

  const validField = getValidScalarField(entity, field);
  if (!validField) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Unsupported filter field for ${entity}: ${field}`,
    });
  }
  return validField;
}

function assertValidSortField(entity: EntityKey, field: string): void {
  if (entity === "resource_month") {
    if (!getColumnDef(entity, field)) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Unknown sort field for ${entity}: ${field}`,
      });
    }
    return;
  }

  if (!getValidScalarField(entity, field)) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Unsupported sort field for ${entity}: ${field}`,
    });
  }
}

function assertValidGroupField(entity: EntityKey, field: string): void {
  const knownField =
    entity === "resource_month"
      ? getColumnDef(entity, field)?.key
      : getValidScalarField(entity, field);
  if (!knownField) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Unsupported group field for ${entity}: ${field}`,
    });
  }
}

function parseFilterValueOrThrow(def: ColumnDef, value: string): unknown {
  if (def.dataType === "number") {
    const parsed = Number(value);
    if (Number.isNaN(parsed)) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Invalid numeric filter value for ${def.key}: ${value}`,
      });
    }
    return parsed;
  }

  if (def.dataType === "boolean") {
    if (value !== "true" && value !== "false") {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Invalid boolean filter value for ${def.key}: ${value}`,
      });
    }
    return value === "true";
  }

  if (def.dataType === "date") {
    const parsed = new Date(value);
    if (Number.isNaN(parsed.getTime())) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Invalid date filter value for ${def.key}: ${value}`,
      });
    }
    return parsed;
  }

  return value;
}

function validateReportInput(input: ReportInput | z.infer<typeof ReportTemplateConfigSchema>): void {
  assertKnownColumns(input.entity, input.columns);

  if (input.periodMonth && !PERIOD_MONTH_PATTERN.test(input.periodMonth)) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Invalid periodMonth: ${input.periodMonth}. Expected YYYY-MM with a month between 01 and 12.`,
    });
  }

  if (input.entity !== "resource_month" && input.periodMonth) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "periodMonth is only supported for resource_month reports",
    });
  }

  if (input.sortBy) {
    assertValidSortField(input.entity, input.sortBy);
  }

  if (input.groupBy) {
    assertValidGroupField(input.entity, input.groupBy);
  }

  for (const filter of input.filters) {
    const field = assertValidFilterField(input.entity, filter.field);
    const def = getColumnDef(input.entity, field);
    if (!def) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Unknown filter field for ${input.entity}: ${filter.field}`,
      });
    }

    if (filter.op === "contains" || filter.op === "in") {
      if (def.dataType !== "string") {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: `Filter operator ${filter.op} is only supported for string fields like ${def.key}`,
        });
      }
      continue;
    }

    void parseFilterValueOrThrow(def, filter.value);
  }
}

/**
 * Build a Prisma `select` object from the requested columns.
 * Always includes `id`. For relation columns like "country.name",
 * we include the relation with `select: { name: true }`.
 */
function buildSelect(entity: EntityKey, columns: string[]): Record<string, unknown> {
  const entityColumns = COLUMN_MAP[entity];
  const select: Record<string, unknown> = { id: true };

  for (const colKey of columns) {
    const def = entityColumns.find((c) => c.key === colKey);
    if (!def) continue;

    if (colKey.includes(".")) {
      const relationName = def.prismaPath ?? colKey.split(".")[0]!;
      const existing = select[relationName];
      const fieldSegments = colKey.split(".").slice(1);
      const relationSelect = existing && typeof existing === "object" && existing !== null && "select" in existing
        ? (existing as { select: Record<string, unknown> }).select
        : {};
      mergeSelectPath(relationSelect, fieldSegments);
      select[relationName] = { select: relationSelect };
    } else {
      select[colKey] = true;
    }
  }

  return select;
}

function mergeSelectPath(
  target: Record<string, unknown>,
  segments: string[],
): void {
  const [head, ...tail] = segments;
  if (!head) {
    return;
  }

  if (tail.length === 0) {
    target[head] = true;
    return;
  }

  const existing = target[head];
  const nestedSelect = existing && typeof existing === "object" && existing !== null && "select" in existing
    ? (existing as { select: Record<string, unknown> }).select
    : {};

  mergeSelectPath(nestedSelect, tail);
  target[head] = { select: nestedSelect };
}

/**
 * Build a Prisma `where` from the filter array.
 * Only scalar top-level fields are allowed for safety.
 */
function buildWhere(
  entity: EntityKey,
  filters: Array<{ field: string; op: string; value: string }>,
): Record<string, unknown> {
  const where: Record<string, unknown> = {};

  for (const filter of filters) {
    const field = assertValidFilterField(entity, filter.field);
    const colDef = getColumnDef(entity, field);
    if (!colDef) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Unknown filter field for ${entity}: ${filter.field}`,
      });
    }
    const parsedValue = parseFilterValueOrThrow(colDef, filter.value);

    switch (filter.op) {
      case "eq":
        where[field] = parsedValue;
        break;
      case "neq":
        where[field] = { not: parsedValue };
        break;
      case "gt":
        where[field] = { gt: parsedValue };
        break;
      case "lt":
        where[field] = { lt: parsedValue };
        break;
      case "gte":
        where[field] = { gte: parsedValue };
        break;
      case "lte":
        where[field] = { lte: parsedValue };
        break;
      case "contains":
        if (colDef.dataType !== "string") {
          throw new TRPCError({
            code: "BAD_REQUEST",
            message: `Filter operator contains is only supported for string fields like ${field}`,
          });
        }
        where[field] = { contains: filter.value, mode: "insensitive" };
        break;
      case "in":
        if (colDef.dataType !== "string") {
          throw new TRPCError({
            code: "BAD_REQUEST",
            message: `Filter operator in is only supported for string fields like ${field}`,
          });
        }
        where[field] = { in: filter.value.split(",").map((v) => v.trim()) };
        break;
      default:
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: `Unsupported filter operator: ${filter.op}`,
        });
        break;
    }
  }

  return where;
}

/**
 * Flatten a Prisma result row so nested relations become dot-notation keys.
 * E.g. { country: { name: "DE" } } => { "country.name": "DE" }
 */
function flattenRow(row: Record<string, unknown>, prefix = ""): Record<string, unknown> {
  const result: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(row)) {
    const fullKey = prefix ? `${prefix}.${key}` : key;
    if (value !== null && typeof value === "object" && !(value instanceof Date) && !Array.isArray(value)) {
      Object.assign(result, flattenRow(value as Record<string, unknown>, fullKey));
    } else {
      result[fullKey] = value;
    }
  }
  return result;
}

/**
 * Format a value for CSV output.
 */
function csvEscape(value: unknown): string {
  if (value === null || value === undefined) return "";
  if (value instanceof Date) return value.toISOString();
  const str = String(value);
  if (str.includes(",") || str.includes('"') || str.includes("\n")) {
    return `"${str.replace(/"/g, '""')}"`;
  }
  return str;
}

// ─── Input Schema ───────────────────────────────────────────────────────────

const reportEntitySchema = z.enum(["resource", "project", "assignment", "resource_month"]);

const FilterSchema = z.object({
  field: z.string().min(1),
  op: z.enum(["eq", "neq", "gt", "lt", "gte", "lte", "contains", "in"]),
  value: z.string(),
});

const ReportInputSchema = z.object({
  entity: reportEntitySchema,
  columns: z.array(z.string()).min(1),
  filters: z.array(FilterSchema).default([]),
  groupBy: z.string().optional(),
  sortBy: z.string().optional(),
  sortDir: z.enum(["asc", "desc"]).default("asc"),
  periodMonth: z.string().regex(PERIOD_MONTH_PATTERN).optional(),
  limit: z.number().int().min(1).max(5000).default(50),
  offset: z.number().int().min(0).default(0),
});

const ReportTemplateConfigSchema = ReportInputSchema.omit({ limit: true, offset: true });

const ReportTemplateEntity = {
  RESOURCE: "RESOURCE",
  PROJECT: "PROJECT",
  ASSIGNMENT: "ASSIGNMENT",
  RESOURCE_MONTH: "RESOURCE_MONTH",
} as const;

type ReportTemplateEntity = (typeof ReportTemplateEntity)[keyof typeof ReportTemplateEntity];

type ReportTemplateRecord = {
  id: string;
  name: string;
  description: string | null;
  entity: ReportTemplateEntity;
  config: unknown;
  isShared: boolean;
  ownerId: string;
  updatedAt: Date;
};

function getReportTemplateDelegate(db: unknown) {
  return (db as {
    reportTemplate: {
      findMany: (args: unknown) => Promise<ReportTemplateRecord[]>;
      findUnique: (args: unknown) => Promise<{ ownerId: string } | null>;
      update: (args: unknown) => Promise<{ id: string; updatedAt: Date }>;
      upsert: (args: unknown) => Promise<{ id: string; updatedAt: Date }>;
      delete: (args: unknown) => Promise<unknown>;
    };
  }).reportTemplate;
}

// ─── Router ──────────────────────────────────────────────────────────────────

export const reportRouter = createTRPCRouter({
  listTemplates: controllerProcedure.query(async ({ ctx }) => {
    const reportTemplate = getReportTemplateDelegate(ctx.db);
    const templates = await reportTemplate.findMany({
      where: {
        OR: [
          { ownerId: ctx.dbUser!.id },
          { isShared: true },
        ],
      },
      orderBy: [{ name: "asc" }],
      select: {
        id: true,
        name: true,
        description: true,
        entity: true,
        config: true,
        isShared: true,
        ownerId: true,
        updatedAt: true,
      },
    });

    return templates.map((template: ReportTemplateRecord) => ({
      id: template.id,
      name: template.name,
      description: template.description,
      entity: fromTemplateEntity(template.entity),
      config: ReportTemplateConfigSchema.parse(template.config),
      isShared: template.isShared,
      isOwner: template.ownerId === ctx.dbUser!.id,
      updatedAt: template.updatedAt,
    }));
  }),

  saveTemplate: controllerProcedure
    .input(z.object({
      id: z.string().optional(),
      name: z.string().trim().min(1).max(120),
      description: z.string().trim().max(500).optional(),
      isShared: z.boolean().default(false),
      config: ReportTemplateConfigSchema,
    }))
    .mutation(async ({ ctx, input }) => {
      validateReportInput(input.config);
      const reportTemplate = getReportTemplateDelegate(ctx.db);
      const payload = input.config as unknown as Prisma.InputJsonValue;
      const entity = toTemplateEntity(input.config.entity);

      if (input.id) {
        const existing = await reportTemplate.findUnique({
          where: { id: input.id },
          select: { ownerId: true },
        });

        if (!existing || existing.ownerId !== ctx.dbUser!.id) {
          throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be updated" });
        }

        return reportTemplate.update({
          where: { id: input.id },
          data: {
            name: input.name,
            description: input.description,
            entity,
            config: payload,
            isShared: input.isShared,
          },
          select: { id: true, updatedAt: true },
        });
      }

      return reportTemplate.upsert({
        where: {
          ownerId_name: {
            ownerId: ctx.dbUser!.id,
            name: input.name,
          },
        },
        update: {
          description: input.description,
          entity,
          config: payload,
          isShared: input.isShared,
        },
        create: {
          ownerId: ctx.dbUser!.id,
          name: input.name,
          description: input.description,
          entity,
          config: payload,
          isShared: input.isShared,
        },
        select: { id: true, updatedAt: true },
      });
    }),

  deleteTemplate: controllerProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const reportTemplate = getReportTemplateDelegate(ctx.db);
      const existing = await reportTemplate.findUnique({
        where: { id: input.id },
        select: { ownerId: true },
      });

      if (!existing || existing.ownerId !== ctx.dbUser!.id) {
        throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be deleted" });
      }

      await reportTemplate.delete({ where: { id: input.id } });
      return { ok: true };
    }),

  /**
   * Return available columns for a given entity type.
   */
  getAvailableColumns: controllerProcedure
    .input(z.object({ entity: reportEntitySchema }))
    .query(({ input }) => {
      const columns = COLUMN_MAP[input.entity];
      if (!columns) {
        throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${input.entity}` });
      }
      return columns.map(({ key, label, dataType }) => ({ key, label, dataType }));
    }),

  /**
   * Fetch report data with dynamic columns, filters, sorting and pagination.
   */
  getReportData: controllerProcedure
    .input(ReportInputSchema)
    .query(async ({ ctx, input }) => {
      return executeReportQuery(ctx.db, input);
    }),

  /**
   * Same as getReportData but returns a CSV string for download.
   */
  exportReport: controllerProcedure
    .input(ReportInputSchema.omit({ offset: true }).extend({
      limit: z.number().int().min(1).max(50000).default(5000),
    }))
    .mutation(async ({ ctx, input }) => {
      const result = await executeReportQuery(ctx.db, { ...input, offset: 0 });
      const rows = result.rows;
      const outputColumns = result.columns;

      // Build CSV
      const entityColumns = COLUMN_MAP[input.entity];
      const headerLabels = outputColumns.map((key) => {
        const def = entityColumns.find((c) => c.key === key);
        return def?.label ?? key;
      });

      const csvLines = [
        headerLabels.map(csvEscape).join(","),
        ...rows.map((row) =>
          outputColumns.map((col) => csvEscape(row[col])).join(","),
        ),
      ];

      return { csv: csvLines.join("\n"), rowCount: rows.length };
    }),
});

type ReportInput = z.infer<typeof ReportInputSchema>;
type FilterInput = z.infer<typeof FilterSchema>;

async function executeReportQuery(
  db: any,
  input: ReportInput,
): Promise<{ rows: Record<string, unknown>[]; columns: string[]; totalCount: number }> {
  validateReportInput(input);

  if (input.entity === "resource_month") {
    return executeResourceMonthReport(db, input);
  }

  const { entity, columns, filters, sortBy, sortDir, limit, offset } = input;
  const select = buildSelect(entity, columns);
  const where = buildWhere(entity, filters);

  let orderBy: Record<string, string> | undefined;
  if (sortBy) {
    const validField = getValidScalarField(entity, sortBy);
    if (!validField) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: `Unsupported sort field for ${entity}: ${sortBy}`,
      });
    }
    orderBy = { [validField]: sortDir };
  }

  const modelDelegate = getModelDelegate(db, entity);
  const [rawRows, totalCount] = await Promise.all([
    (modelDelegate as any).findMany({
      select,
      where,
      ...(orderBy ? { orderBy } : {}),
      take: limit,
      skip: offset,
    }),
    (modelDelegate as any).count({ where }),
  ]);

  const rows = (rawRows as Record<string, unknown>[]).map((row) => flattenRow(row));
  const outputColumns = ["id", ...columns.filter((column) => column !== "id")];

  return {
    rows: rows.map((row) => pickColumns(row, outputColumns)),
    columns: outputColumns,
    totalCount,
  };
}

async function executeResourceMonthReport(
  db: any,
  input: ReportInput,
): Promise<{ rows: Record<string, unknown>[]; columns: string[]; totalCount: number }> {
  const periodMonth = input.periodMonth ?? new Date().toISOString().slice(0, 7);
  const [year, month] = periodMonth.split("-").map(Number) as [number, number];
  const periodStart = new Date(Date.UTC(year, month - 1, 1));
  const periodEnd = new Date(Date.UTC(year, month, 0));

  const resources = await db.resource.findMany({
    select: {
      id: true,
      eid: true,
      displayName: true,
      email: true,
      chapter: true,
      resourceType: true,
      isActive: true,
      chgResponsibility: true,
      rolledOff: true,
      departed: true,
      lcrCents: true,
      ucrCents: true,
      currency: true,
      fte: true,
      availability: true,
      chargeabilityTarget: true,
      federalState: true,
      countryId: true,
      metroCityId: true,
      country: { select: { code: true, name: true } },
      metroCity: { select: { name: true } },
      orgUnit: { select: { name: true } },
      managementLevelGroup: { select: { name: true, targetPercentage: true } },
      managementLevel: { select: { name: true } },
    },
    orderBy: { displayName: "asc" },
  });

  const resourceIds = resources.map((resource: any) => resource.id);
  const [bookings, contexts] = await Promise.all([
    resourceIds.length > 0
      ? listAssignmentBookings(db, {
        startDate: periodStart,
        endDate: periodEnd,
        resourceIds,
      })
      : Promise.resolve([]),
    loadResourceDailyAvailabilityContexts(
      db,
      resources.map((resource: any) => ({
        id: resource.id,
        availability: resource.availability as WeekdayAvailability,
        countryId: resource.countryId,
        countryCode: resource.country?.code,
        federalState: resource.federalState,
        metroCityId: resource.metroCityId,
        metroCityName: resource.metroCity?.name,
      })),
      periodStart,
      periodEnd,
    ),
  ]);

  const rows = resources.map((resource: any) => {
    const availability = resource.availability as WeekdayAvailability;
    const context = contexts.get(resource.id);
    const resourceBookings = bookings.filter((booking) => booking.resourceId === resource.id);
    const baseWorkingDays = countEffectiveWorkingDays({
      availability,
      periodStart,
      periodEnd,
      context: undefined,
    });
    const effectiveWorkingDays = countEffectiveWorkingDays({
      availability,
      periodStart,
      periodEnd,
      context,
    });
    const baseAvailableHours = calculateEffectiveAvailableHours({
      availability,
      periodStart,
      periodEnd,
      context: undefined,
    });
    const sahHours = calculateEffectiveAvailableHours({
      availability,
      periodStart,
      periodEnd,
      context,
    });

    const holidayDates = [...(context?.holidayDates ?? new Set<string>())];
    const publicHolidayWorkdayCount = holidayDates.reduce((count, isoDate) => (
      count + (getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`)) > 0 ? 1 : 0)
    ), 0);
    const publicHolidayHoursDeduction = holidayDates.reduce((sum, isoDate) => (
      sum + getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`))
    ), 0);

    let absenceDayEquivalent = 0;
    let absenceHoursDeduction = 0;
    for (const [isoDate, fraction] of context?.vacationFractionsByDate ?? []) {
      const dayHours = getAvailabilityHoursForDate(availability, new Date(`${isoDate}T00:00:00.000Z`));
      if (dayHours <= 0 || context?.holidayDates.has(isoDate)) {
        continue;
      }
      absenceDayEquivalent += fraction;
      absenceHoursDeduction += dayHours * fraction;
    }

    const actualBookedHours = resourceBookings
      .filter((booking) => isChargeabilityActualBooking(booking, false))
      .reduce((sum, booking) => sum + calculateEffectiveBookedHours({
        availability,
        startDate: booking.startDate,
        endDate: booking.endDate,
        hoursPerDay: booking.hoursPerDay,
        periodStart,
        periodEnd,
        context,
      }), 0);
    const expectedBookedHours = resourceBookings
      .filter((booking) => isChargeabilityRelevantProject(booking.project, true))
      .reduce((sum, booking) => sum + calculateEffectiveBookedHours({
        availability,
        startDate: booking.startDate,
        endDate: booking.endDate,
        hoursPerDay: booking.hoursPerDay,
        periodStart,
        periodEnd,
        context,
      }), 0);

    const targetPct = resource.managementLevelGroup?.targetPercentage != null
      ? resource.managementLevelGroup.targetPercentage * 100
      : resource.chargeabilityTarget;

    return {
      id: `${resource.id}:${periodMonth}`,
      resourceId: resource.id,
      monthKey: periodMonth,
      periodStart: periodStart.toISOString(),
      periodEnd: periodEnd.toISOString(),
      eid: resource.eid,
      displayName: resource.displayName,
      email: resource.email,
      chapter: resource.chapter,
      resourceType: resource.resourceType,
      isActive: resource.isActive,
      chgResponsibility: resource.chgResponsibility,
      rolledOff: resource.rolledOff,
      departed: resource.departed,
      countryCode: resource.country?.code ?? null,
      countryName: resource.country?.name ?? null,
      federalState: resource.federalState,
      metroCityName: resource.metroCity?.name ?? null,
      orgUnitName: resource.orgUnit?.name ?? null,
      managementLevelGroupName: resource.managementLevelGroup?.name ?? null,
      managementLevelName: resource.managementLevel?.name ?? null,
      fte: roundMetric(resource.fte),
      lcrCents: resource.lcrCents,
      ucrCents: resource.ucrCents,
      currency: resource.currency,
      monthlyChargeabilityTargetPct: roundMetric(targetPct),
      monthlyTargetHours: roundMetric((sahHours * targetPct) / 100),
      monthlyBaseWorkingDays: roundMetric(baseWorkingDays),
      monthlyEffectiveWorkingDays: roundMetric(effectiveWorkingDays),
      monthlyBaseAvailableHours: roundMetric(baseAvailableHours),
      monthlySahHours: roundMetric(sahHours),
      monthlyPublicHolidayCount: holidayDates.length,
      monthlyPublicHolidayWorkdayCount: publicHolidayWorkdayCount,
      monthlyPublicHolidayHoursDeduction: roundMetric(publicHolidayHoursDeduction),
      monthlyAbsenceDayEquivalent: roundMetric(absenceDayEquivalent),
      monthlyAbsenceHoursDeduction: roundMetric(absenceHoursDeduction),
      monthlyActualBookedHours: roundMetric(actualBookedHours),
      monthlyExpectedBookedHours: roundMetric(expectedBookedHours),
      monthlyActualChargeabilityPct: roundMetric(sahHours > 0 ? (actualBookedHours / sahHours) * 100 : 0),
      monthlyExpectedChargeabilityPct: roundMetric(sahHours > 0 ? (expectedBookedHours / sahHours) * 100 : 0),
      monthlyUnassignedHours: roundMetric(Math.max(0, sahHours - actualBookedHours)),
    };
  });

  const filteredRows = rows.filter((row: Record<string, unknown>) => input.filters.every((filter) => matchesInMemoryFilter(
    row,
    filter,
    RESOURCE_MONTH_COLUMNS,
  )));
  const sortedRows = sortInMemoryRows(filteredRows, input.sortBy, input.sortDir, RESOURCE_MONTH_COLUMNS);
  const totalCount = sortedRows.length;
  const pagedRows = sortedRows.slice(input.offset, input.offset + input.limit);
  const outputColumns = ["id", ...input.columns.filter((column) => column !== "id")];

  return {
    rows: pagedRows.map((row) => pickColumns(row, outputColumns)),
    columns: outputColumns,
    totalCount,
  };
}

function parseFilterValue(def: ColumnDef | undefined, value: string): unknown {
  if (!def) return value;
  if (def.dataType === "number") {
    const parsed = Number(value);
    return Number.isNaN(parsed) ? null : parsed;
  }
  if (def.dataType === "boolean") {
    return value === "true";
  }
  if (def.dataType === "date") {
    const parsed = new Date(value);
    return Number.isNaN(parsed.getTime()) ? null : parsed.getTime();
  }
  return value;
}

function matchesInMemoryFilter(
  row: Record<string, unknown>,
  filter: FilterInput,
  columns: ColumnDef[],
): boolean {
  const def = columns.find((column) => column.key === filter.field);
  if (!def) {
    return true;
  }

  const rowValueRaw = row[filter.field];
  const rowValue = def.dataType === "date" && typeof rowValueRaw === "string"
    ? new Date(rowValueRaw).getTime()
    : rowValueRaw;
  const parsedFilterValue = parseFilterValue(def, filter.value);

  if (parsedFilterValue === null) {
    return false;
  }

  switch (filter.op) {
    case "eq":
      return rowValue === parsedFilterValue;
    case "neq":
      return rowValue !== parsedFilterValue;
    case "gt":
      return typeof rowValue === "number" && typeof parsedFilterValue === "number" && rowValue > parsedFilterValue;
    case "lt":
      return typeof rowValue === "number" && typeof parsedFilterValue === "number" && rowValue < parsedFilterValue;
    case "gte":
      return typeof rowValue === "number" && typeof parsedFilterValue === "number" && rowValue >= parsedFilterValue;
    case "lte":
      return typeof rowValue === "number" && typeof parsedFilterValue === "number" && rowValue <= parsedFilterValue;
    case "contains":
      return typeof rowValue === "string" && rowValue.toLowerCase().includes(filter.value.toLowerCase());
    case "in":
      return filter.value.split(",").map((value) => value.trim()).includes(String(rowValue ?? ""));
    default:
      return true;
  }
}

function sortInMemoryRows(
  rows: Record<string, unknown>[],
  sortBy: string | undefined,
  sortDir: "asc" | "desc",
  columns: ColumnDef[],
): Record<string, unknown>[] {
  if (!sortBy) {
    return rows;
  }

  const def = columns.find((column) => column.key === sortBy);
  if (!def) {
    return rows;
  }

  const direction = sortDir === "asc" ? 1 : -1;
  return [...rows].sort((left, right) => {
    const leftValue = left[sortBy];
    const rightValue = right[sortBy];

    if (leftValue == null && rightValue == null) return 0;
    if (leftValue == null) return 1;
    if (rightValue == null) return -1;

    if (def.dataType === "number") {
      return direction * (Number(leftValue) - Number(rightValue));
    }
    if (def.dataType === "boolean") {
      return direction * (Number(Boolean(leftValue)) - Number(Boolean(rightValue)));
    }
    if (def.dataType === "date") {
      return direction * (new Date(String(leftValue)).getTime() - new Date(String(rightValue)).getTime());
    }
    return direction * String(leftValue).localeCompare(String(rightValue), "de");
  });
}

function pickColumns(row: Record<string, unknown>, columns: string[]): Record<string, unknown> {
  return Object.fromEntries(columns.map((column) => [column, row[column]]));
}

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

function toTemplateEntity(entity: EntityKey): ReportTemplateEntity {
  switch (entity) {
    case "resource":
      return ReportTemplateEntity.RESOURCE;
    case "project":
      return ReportTemplateEntity.PROJECT;
    case "assignment":
      return ReportTemplateEntity.ASSIGNMENT;
    case "resource_month":
      return ReportTemplateEntity.RESOURCE_MONTH;
    default:
      throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` });
  }
}

function fromTemplateEntity(entity: ReportTemplateEntity): EntityKey {
  switch (entity) {
    case ReportTemplateEntity.RESOURCE:
      return "resource";
    case ReportTemplateEntity.PROJECT:
      return "project";
    case ReportTemplateEntity.ASSIGNMENT:
      return "assignment";
    case ReportTemplateEntity.RESOURCE_MONTH:
      return "resource_month";
    default:
      throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` });
  }
}

/** Resolve the Prisma model delegate from entity key. */
function getModelDelegate(db: any, entity: EntityKey) {
  switch (entity) {
    case "resource":
      return db.resource;
    case "project":
      return db.project;
    case "assignment":
      return db.assignment;
    default:
      throw new TRPCError({ code: "BAD_REQUEST", message: `Unknown entity: ${entity}` });
  }
}
