import {
  countPlanningEntries,
  listAssignmentBookings,
} from "@capakraken/application";
import type { WeekdayAvailability } from "@capakraken/shared";
import { BlueprintTarget, CreateProjectSchema, FieldType, PermissionKey, ProjectStatus, UpdateProjectSchema } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { calculateShoringRatio, type ShoringAssignment } from "@capakraken/engine/allocation";
import { findUniqueOrThrow } from "../db/helpers.js";
import { paginate, paginateCursor, PaginationInputSchema, CursorInputSchema } from "../db/pagination.js";
import { assertBlueprintDynamicFields } from "./blueprint-validation.js";
import { buildDynamicFieldWhereClauses } from "./custom-field-filters.js";
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
import { adminProcedure, controllerProcedure, createTRPCRouter, managerProcedure, planningReadProcedure, protectedProcedure, requirePermission } from "../trpc.js";
import { createDalleClient, isDalleConfigured, loggedAiCall, parseAiError } from "../ai-client.js";
import { generateGeminiImage, isGeminiConfigured, parseGeminiError } from "../gemini-client.js";
import { invalidateDashboardCache } from "../lib/cache.js";
import { logger } from "../lib/logger.js";
import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
import { validateImageDataUrl } from "../lib/image-validation.js";
import type { TRPCContext } from "../trpc.js";
import {
  calculateEffectiveBookedHours,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";
import { fmtEur } from "../lib/format-utils.js";

const MAX_COVER_SIZE = 4 * 1024 * 1024; // 4 MB base64 string length limit (client compresses before upload)

const PROJECT_SUMMARY_SELECT = {
  id: true,
  shortCode: true,
  name: true,
  status: true,
  startDate: true,
  endDate: true,
  client: { select: { name: true } },
} as const;

const PROJECT_SUMMARY_DETAIL_SELECT = {
  ...PROJECT_SUMMARY_SELECT,
  budgetCents: true,
  winProbability: true,
  _count: { select: { assignments: true, estimates: true } },
} as const;

const PROJECT_IDENTIFIER_SELECT = {
  id: true,
  shortCode: true,
  name: true,
  status: true,
  startDate: true,
  endDate: true,
} as const;

const PROJECT_DETAIL_SELECT = {
  ...PROJECT_IDENTIFIER_SELECT,
  id: true,
  shortCode: true,
  name: true,
  status: true,
  orderType: true,
  allocationType: true,
  budgetCents: true,
  winProbability: true,
  startDate: true,
  endDate: true,
  responsiblePerson: true,
  client: { select: { name: true } },
  utilizationCategory: { select: { code: true, name: true } },
  _count: { select: { assignments: true, estimates: true } },
} as const;

function runProjectBackgroundEffect(
  effectName: string,
  execute: () => unknown,
  metadata: Record<string, unknown> = {},
): void {
  void Promise.resolve()
    .then(execute)
    .catch((error) => {
      logger.error(
        { err: error, effectName, ...metadata },
        "Project background side effect failed",
      );
    });
}

function invalidateDashboardCacheInBackground(): void {
  runProjectBackgroundEffect("invalidateDashboardCache", () => invalidateDashboardCache());
}

function dispatchProjectWebhookInBackground(
  db: TRPCContext["db"],
  event: string,
  payload: Record<string, unknown>,
): void {
  runProjectBackgroundEffect(
    "dispatchWebhooks",
    () => dispatchWebhooks(db, event, payload),
    { event },
  );
}

function formatDate(value: Date | null): string | null {
  return value ? value.toISOString().slice(0, 10) : null;
}

function mapProjectSummary(project: {
  id: string;
  shortCode: string;
  name: string;
  status: string;
  startDate: Date | null;
  endDate: Date | null;
  client: { name: string } | null;
}) {
  return {
    id: project.id,
    code: project.shortCode,
    name: project.name,
    status: project.status,
    start: formatDate(project.startDate),
    end: formatDate(project.endDate),
    client: project.client?.name ?? null,
  };
}

function mapProjectSummaryDetail(project: {
  id: string;
  shortCode: string;
  name: string;
  status: string;
  budgetCents: number | null;
  winProbability: number;
  startDate: Date | null;
  endDate: Date | null;
  client: { name: string } | null;
  _count: { assignments: number; estimates: number };
}) {
  return {
    id: project.id,
    code: project.shortCode,
    name: project.name,
    status: project.status,
    budget: project.budgetCents && project.budgetCents > 0 ? fmtEur(project.budgetCents) : "Not set",
    winProbability: `${project.winProbability}%`,
    start: formatDate(project.startDate),
    end: formatDate(project.endDate),
    client: project.client?.name ?? null,
    assignmentCount: project._count.assignments,
    estimateCount: project._count.estimates,
  };
}

function mapProjectDetail(
  project: {
    id: string;
    shortCode: string;
    name: string;
    status: string;
    orderType: string;
    allocationType: string;
    budgetCents: number | null;
    winProbability: number;
    startDate: Date | null;
    endDate: Date | null;
    responsiblePerson: string | null;
    client: { name: string } | null;
    utilizationCategory: { code: string; name: string } | null;
    _count: { assignments: number; estimates: number };
  },
  topAssignments: Array<{
    resource: { displayName: string; eid: string };
    role: string | null;
    status: string;
    hoursPerDay: number;
    startDate: Date;
    endDate: Date;
  }>,
) {
  return {
    id: project.id,
    code: project.shortCode,
    name: project.name,
    status: project.status,
    orderType: project.orderType,
    allocationType: project.allocationType,
    budget: project.budgetCents && project.budgetCents > 0 ? fmtEur(project.budgetCents) : "Not set",
    budgetCents: project.budgetCents,
    winProbability: `${project.winProbability}%`,
    start: formatDate(project.startDate),
    end: formatDate(project.endDate),
    responsible: project.responsiblePerson,
    client: project.client?.name ?? null,
    category: project.utilizationCategory?.name ?? null,
    assignmentCount: project._count.assignments,
    estimateCount: project._count.estimates,
    topAllocations: topAssignments.map((assignment) => ({
      resource: assignment.resource.displayName,
      eid: assignment.resource.eid,
      role: assignment.role ?? null,
      status: assignment.status,
      hoursPerDay: assignment.hoursPerDay,
      start: formatDate(assignment.startDate),
      end: formatDate(assignment.endDate),
    })),
  };
}

async function readProjectSummariesSnapshot(
  ctx: Pick<TRPCContext, "db">,
  input: {
    search?: string | undefined;
    status?: ProjectStatus | undefined;
    limit: number;
  },
) {
  const buildWhere = (search: string | undefined) => ({
    ...(input.status ? { status: input.status } : {}),
    ...(search
      ? {
          OR: [
            { name: { contains: search, mode: "insensitive" as const } },
            { shortCode: { contains: search, mode: "insensitive" as const } },
          ],
        }
      : {}),
  });

  let projects = await ctx.db.project.findMany({
    where: buildWhere(input.search),
    select: PROJECT_SUMMARY_SELECT,
    take: input.limit,
    orderBy: { name: "asc" },
  });

  if (projects.length === 0 && input.search) {
    const words = input.search.split(/[\s,._\-/]+/).filter((word) => word.length >= 2);
    if (words.length > 1) {
      const candidates = await ctx.db.project.findMany({
        where: {
          ...(input.status ? { status: input.status } : {}),
          OR: words.flatMap((word) => ([
            { name: { contains: word, mode: "insensitive" as const } },
            { shortCode: { contains: word, mode: "insensitive" as const } },
          ])),
        },
        select: PROJECT_SUMMARY_SELECT,
        take: input.limit * 2,
        orderBy: { name: "asc" },
      });

      projects = candidates
        .map((project) => {
          const haystack = `${project.name} ${project.shortCode}`.toLowerCase();
          const matchCount = words.filter((word) => haystack.includes(word.toLowerCase())).length;
          return { project, matchCount };
        })
        .sort((left, right) => right.matchCount - left.matchCount)
        .slice(0, input.limit)
        .map((entry) => entry.project);
    }
  }

  return {
    items: projects,
    exactMatch: input.search
      ? projects.some((project) =>
        project.name.toLowerCase().includes(input.search!.toLowerCase())
        || project.shortCode.toLowerCase().includes(input.search!.toLowerCase()))
      : true,
  };
}

async function readProjectSummaryDetailsSnapshot(
  ctx: Pick<TRPCContext, "db">,
  input: {
    search?: string | undefined;
    status?: ProjectStatus | undefined;
    limit: number;
  },
) {
  const buildWhere = (search: string | undefined) => ({
    ...(input.status ? { status: input.status } : {}),
    ...(search
      ? {
          OR: [
            { name: { contains: search, mode: "insensitive" as const } },
            { shortCode: { contains: search, mode: "insensitive" as const } },
          ],
        }
      : {}),
  });

  let projects = await ctx.db.project.findMany({
    where: buildWhere(input.search),
    select: PROJECT_SUMMARY_DETAIL_SELECT,
    take: input.limit,
    orderBy: { name: "asc" },
  });

  if (projects.length === 0 && input.search) {
    const words = input.search.split(/[\s,._\-/]+/).filter((word) => word.length >= 2);
    if (words.length > 1) {
      const candidates = await ctx.db.project.findMany({
        where: {
          ...(input.status ? { status: input.status } : {}),
          OR: words.flatMap((word) => ([
            { name: { contains: word, mode: "insensitive" as const } },
            { shortCode: { contains: word, mode: "insensitive" as const } },
          ])),
        },
        select: PROJECT_SUMMARY_DETAIL_SELECT,
        take: input.limit * 2,
        orderBy: { name: "asc" },
      });

      projects = candidates
        .map((project) => {
          const haystack = `${project.name} ${project.shortCode}`.toLowerCase();
          const matchCount = words.filter((word) => haystack.includes(word.toLowerCase())).length;
          return { project, matchCount };
        })
        .sort((left, right) => right.matchCount - left.matchCount)
        .slice(0, input.limit)
        .map((entry) => entry.project);
    }
  }

  return {
    items: projects,
    exactMatch: input.search
      ? projects.some((project) =>
        project.name.toLowerCase().includes(input.search!.toLowerCase())
        || project.shortCode.toLowerCase().includes(input.search!.toLowerCase()))
      : true,
  };
}

async function resolveProjectIdentifierSnapshot(
  ctx: Pick<TRPCContext, "db">,
  identifier: string,
) {
  let project = await ctx.db.project.findUnique({
    where: { id: identifier },
    select: PROJECT_IDENTIFIER_SELECT,
  });
  if (!project) {
    project = await ctx.db.project.findUnique({
      where: { shortCode: identifier },
      select: PROJECT_IDENTIFIER_SELECT,
    });
  }
  if (!project) {
    project = await ctx.db.project.findFirst({
      where: { name: { equals: identifier, mode: "insensitive" } },
      select: PROJECT_IDENTIFIER_SELECT,
    });
  }
  if (!project) {
    project = await ctx.db.project.findFirst({
      where: { name: { contains: identifier, mode: "insensitive" } },
      select: PROJECT_IDENTIFIER_SELECT,
    });
  }

  if (!project) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
  }

  return project;
}

async function readProjectByIdentifierDetailSnapshot(
  ctx: Pick<TRPCContext, "db">,
  identifier: string,
) {
  const projectIdentity = await resolveProjectIdentifierSnapshot(ctx, identifier);
  const project = await ctx.db.project.findUnique({
    where: { id: projectIdentity.id },
    select: PROJECT_DETAIL_SELECT,
  });

  if (!project) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
  }

  const topAssignments = await ctx.db.assignment.findMany({
    where: {
      projectId: project.id,
      status: { not: "CANCELLED" },
    },
    select: {
      resource: { select: { displayName: true, eid: true } },
      role: true,
      status: true,
      hoursPerDay: true,
      startDate: true,
      endDate: true,
    },
    take: 10,
    orderBy: { startDate: "desc" },
  });

  return {
    ...project,
    topAssignments,
  };
}

export const projectRouter = createTRPCRouter({
  resolveByIdentifier: planningReadProcedure
    .input(z.object({ identifier: z.string() }))
    .query(async ({ ctx, input }) => {
      const select = {
        id: true,
        shortCode: true,
        name: true,
        status: true,
        responsiblePerson: true,
        startDate: true,
        endDate: true,
      } as const;

      let project = await ctx.db.project.findUnique({
        where: { id: input.identifier },
        select,
      });
      if (!project) {
        project = await ctx.db.project.findUnique({
          where: { shortCode: input.identifier },
          select,
        });
      }
      if (!project) {
        project = await ctx.db.project.findFirst({
          where: { name: { equals: input.identifier, mode: "insensitive" } },
          select,
        });
      }
      if (!project) {
        project = await ctx.db.project.findFirst({
          where: { name: { contains: input.identifier, mode: "insensitive" } },
          select,
        });
      }

      if (!project) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
      }

      return project;
    }),

  searchSummaries: planningReadProcedure
    .input(z.object({
      search: z.string().optional(),
      status: z.nativeEnum(ProjectStatus).optional(),
      limit: z.number().int().min(1).max(50).default(20),
    }))
    .query(async ({ ctx, input }) => {
      const { items, exactMatch } = await readProjectSummariesSnapshot(ctx, input);
      const formatted = items.map(mapProjectSummary);
      if (items.length > 0 && input.search && !exactMatch) {
        return {
          suggestions: formatted,
          note: `No exact match for "${input.search}". These projects match some of the search terms:`,
        };
      }
      return formatted;
    }),

  searchSummariesDetail: controllerProcedure
    .input(z.object({
      search: z.string().optional(),
      status: z.nativeEnum(ProjectStatus).optional(),
      limit: z.number().int().min(1).max(50).default(20),
    }))
    .query(async ({ ctx, input }) => {
      const { items, exactMatch } = await readProjectSummaryDetailsSnapshot(ctx, input);
      const formatted = items.map(mapProjectSummaryDetail);
      if (items.length > 0 && input.search && !exactMatch) {
        return {
          suggestions: formatted,
          note: `No exact match for "${input.search}". These projects match some of the search terms:`,
        };
      }
      return formatted;
    }),

  list: controllerProcedure
    .input(
      PaginationInputSchema.extend({
        status: z.nativeEnum(ProjectStatus).optional(),
        search: z.string().optional(),
        // Custom field JSONB filters
        customFieldFilters: z.array(z.object({
          key: z.string(),
          value: z.string(),
          type: z.nativeEnum(FieldType),
        })).optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const { status, search, cursor, customFieldFilters } = input;

      const cfConditions = buildDynamicFieldWhereClauses(customFieldFilters).map((dynamicFields) => ({ dynamicFields }));

      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const where: any = {
        ...(status ? { status } : {}),
        ...(search
          ? {
              OR: [
                { name: { contains: search, mode: "insensitive" as const } },
                { shortCode: { contains: search, mode: "insensitive" as const } },
              ],
            }
          : {}),
        ...(cfConditions.length > 0 ? { AND: cfConditions } : {}),
      };

      const whereWithCursor = cursor ? { ...where, id: { gt: cursor } } : where;

      const result = await paginate(
        ({ skip, take }) =>
          ctx.db.project.findMany({
            where: whereWithCursor,
            skip,
            take,
            orderBy: [{ startDate: "asc" }, { id: "asc" }],
          }),
        () => ctx.db.project.count({ where }),
        input,
      );

      const { countsByProjectId } = await countPlanningEntries(ctx.db, {
        projectIds: result.items.map((project) => project.id),
      });

      return {
        projects: result.items.map((project) => ({
          ...project,
          _count: {
            allocations: countsByProjectId.get(project.id) ?? 0,
          },
        })),
        total: result.total,
        page: result.page,
        limit: result.limit,
        nextCursor: result.nextCursor,
      };
    }),

  getById: controllerProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ ctx, input }) => {
      const [project, planningRead] = await Promise.all([
        ctx.db.project.findUnique({
          where: { id: input.id },
          include: { blueprint: true },
        }),
        loadProjectPlanningReadModel(ctx.db, { projectId: input.id }),
      ]);

      if (!project) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
      }

      return {
        ...project,
        allocations: planningRead.readModel.assignments,
        demands: planningRead.readModel.demands,
        assignments: planningRead.readModel.assignments,
      };
    }),

  getByIdentifier: planningReadProcedure
    .input(z.object({ identifier: z.string() }))
    .query(async ({ ctx, input }) => resolveProjectIdentifierSnapshot(ctx, input.identifier)),

  getByIdentifierDetail: controllerProcedure
    .input(z.object({ identifier: z.string() }))
    .query(async ({ ctx, input }) => {
      const project = await readProjectByIdentifierDetailSnapshot(ctx, input.identifier);
      return mapProjectDetail(project, project.topAssignments);
    }),

  getShoringRatio: controllerProcedure
    .input(z.object({ projectId: z.string() }))
    .query(async ({ ctx, input }) => {
      const project = await ctx.db.project.findUnique({
        where: { id: input.projectId },
        select: {
          id: true,
          name: true,
          shoringThreshold: true,
          onshoreCountryCode: true,
        },
      });

      if (!project) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });
      }

      const assignments = await ctx.db.assignment.findMany({
        where: { projectId: input.projectId, status: { not: "CANCELLED" } },
        include: {
          resource: {
            include: {
              country: { select: { id: true, code: true } },
              metroCity: { select: { id: true, name: true } },
            },
          },
        },
      });
      const periodStart = assignments.length > 0
        ? new Date(Math.min(...assignments.map((assignment) => assignment.startDate.getTime())))
        : new Date();
      const periodEnd = assignments.length > 0
        ? new Date(Math.max(...assignments.map((assignment) => assignment.endDate.getTime())))
        : new Date();
      const contexts = await loadResourceDailyAvailabilityContexts(
        ctx.db,
        assignments.map((assignment) => ({
          id: assignment.resource.id,
          availability: assignment.resource.availability as unknown as WeekdayAvailability,
          countryId: assignment.resource.country?.id ?? assignment.resource.countryId,
          countryCode: assignment.resource.country?.code,
          federalState: assignment.resource.federalState,
          metroCityId: assignment.resource.metroCity?.id ?? assignment.resource.metroCityId,
          metroCityName: assignment.resource.metroCity?.name,
        })),
        periodStart,
        periodEnd,
      );

      const mapped: ShoringAssignment[] = assignments.map((a) => {
        const workingDays = a.hoursPerDay > 0
          ? calculateEffectiveBookedHours({
            availability: a.resource.availability as unknown as WeekdayAvailability,
            startDate: a.startDate,
            endDate: a.endDate,
            hoursPerDay: a.hoursPerDay,
            periodStart,
            periodEnd,
            context: contexts.get(a.resourceId ?? a.resource.id),
          }) / a.hoursPerDay
          : 0;
        return {
          resourceId: a.resourceId,
          countryCode: a.resource.country?.code ?? null,
          hoursPerDay: a.hoursPerDay,
          workingDays: Math.max(0, workingDays),
        };
      });

      return calculateShoringRatio(
        mapped,
        project.shoringThreshold ?? 55,
        project.onshoreCountryCode ?? "DE",
      );
    }),

  create: managerProcedure
    .input(CreateProjectSchema)
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
      const existing = await ctx.db.project.findUnique({
        where: { shortCode: input.shortCode },
      });

      if (existing) {
        throw new TRPCError({
          code: "CONFLICT",
          message: `Project with short code "${input.shortCode}" already exists`,
        });
      }

      await assertBlueprintDynamicFields({
        db: ctx.db,
        blueprintId: input.blueprintId,
        dynamicFields: input.dynamicFields,
        target: BlueprintTarget.PROJECT,
      });

      const project = await ctx.db.project.create({
        data: {
          shortCode: input.shortCode,
          name: input.name,
          orderType: input.orderType,
          allocationType: input.allocationType,
          winProbability: input.winProbability,
          budgetCents: input.budgetCents,
          startDate: input.startDate,
          endDate: input.endDate,
          status: input.status,
          responsiblePerson: input.responsiblePerson,
          ...(input.color !== undefined ? { color: input.color } : {}),
          staffingReqs: input.staffingReqs as unknown as import("@capakraken/db").Prisma.InputJsonValue,
          dynamicFields: input.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue,
          blueprintId: input.blueprintId,
          ...(input.utilizationCategoryId !== undefined ? { utilizationCategoryId: input.utilizationCategoryId || null } : {}),
          ...(input.clientId !== undefined ? { clientId: input.clientId || null } : {}),
        } as unknown as Parameters<typeof ctx.db.project.create>[0]["data"],
      });

      await ctx.db.auditLog.create({
        data: {
          entityType: "Project",
          entityId: project.id,
          action: "CREATE",
          changes: { after: project },
        },
      });

      invalidateDashboardCacheInBackground();
      dispatchProjectWebhookInBackground(ctx.db, "project.created", {
        id: project.id,
        shortCode: project.shortCode,
        name: project.name,
        status: project.status,
      });
      return project;
    }),

  update: managerProcedure
    .input(z.object({ id: z.string(), data: UpdateProjectSchema }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
      const existing = await findUniqueOrThrow(
        ctx.db.project.findUnique({ where: { id: input.id } }),
        "Project",
      );

      const nextBlueprintId = input.data.blueprintId ?? existing.blueprintId ?? undefined;
      const nextDynamicFields = (input.data.dynamicFields ?? existing.dynamicFields ?? {}) as Record<string, unknown>;

      await assertBlueprintDynamicFields({
        db: ctx.db,
        blueprintId: nextBlueprintId,
        dynamicFields: nextDynamicFields,
        target: BlueprintTarget.PROJECT,
      });

      const updated = await ctx.db.project.update({
        where: { id: input.id },
        data: {
          ...(input.data.name !== undefined ? { name: input.data.name } : {}),
          ...(input.data.orderType !== undefined ? { orderType: input.data.orderType } : {}),
          ...(input.data.allocationType !== undefined ? { allocationType: input.data.allocationType } : {}),
          ...(input.data.winProbability !== undefined ? { winProbability: input.data.winProbability } : {}),
          ...(input.data.budgetCents !== undefined ? { budgetCents: input.data.budgetCents } : {}),
          ...(input.data.startDate !== undefined ? { startDate: input.data.startDate } : {}),
          ...(input.data.endDate !== undefined ? { endDate: input.data.endDate } : {}),
          ...(input.data.status !== undefined ? { status: input.data.status } : {}),
          ...(input.data.responsiblePerson !== undefined ? { responsiblePerson: input.data.responsiblePerson } : {}),
          ...(input.data.color !== undefined ? { color: input.data.color } : {}),
          ...(input.data.staffingReqs !== undefined ? { staffingReqs: input.data.staffingReqs as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
          ...(input.data.dynamicFields !== undefined ? { dynamicFields: input.data.dynamicFields as unknown as import("@capakraken/db").Prisma.InputJsonValue } : {}),
          ...(input.data.blueprintId !== undefined ? { blueprintId: input.data.blueprintId } : {}),
          ...(input.data.utilizationCategoryId !== undefined ? { utilizationCategoryId: input.data.utilizationCategoryId || null } : {}),
          ...(input.data.clientId !== undefined ? { clientId: input.data.clientId || null } : {}),
          ...(input.data.shoringThreshold !== undefined ? { shoringThreshold: input.data.shoringThreshold } : {}),
          ...(input.data.onshoreCountryCode !== undefined ? { onshoreCountryCode: input.data.onshoreCountryCode } : {}),
        } as unknown as Parameters<typeof ctx.db.project.update>[0]["data"],
      });

      await ctx.db.auditLog.create({
        data: {
          entityType: "Project",
          entityId: input.id,
          action: "UPDATE",
          changes: { before: existing, after: updated },
        },
      });

      invalidateDashboardCacheInBackground();
      return updated;
    }),

  updateStatus: managerProcedure
    .input(z.object({ id: z.string(), status: z.nativeEnum(ProjectStatus) }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
      const result = await ctx.db.project.update({
        where: { id: input.id },
        data: { status: input.status },
      });
      invalidateDashboardCacheInBackground();
      dispatchProjectWebhookInBackground(ctx.db, "project.status_changed", {
        id: result.id,
        shortCode: result.shortCode,
        name: result.name,
        status: result.status,
      });
      return result;
    }),

  batchUpdateStatus: managerProcedure
    .input(
      z.object({
        ids: z.array(z.string()).min(1).max(100),
        status: z.nativeEnum(ProjectStatus),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
      const updated = await ctx.db.$transaction(
        input.ids.map((id) =>
          ctx.db.project.update({ where: { id }, data: { status: input.status } }),
        ),
      );

      await ctx.db.auditLog.create({
        data: {
          entityType: "Project",
          entityId: input.ids.join(","),
          action: "UPDATE",
          changes: { after: { status: input.status, ids: input.ids } },
        },
      });

      invalidateDashboardCacheInBackground();
      return { count: updated.length };
    }),

  listWithCosts: controllerProcedure
    .input(
      CursorInputSchema.extend({
        status: z.nativeEnum(ProjectStatus).optional(),
        search: z.string().optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const { status, search, cursor } = input;
      const where = {
        ...(status ? { status } : {}),
        ...(search
          ? {
              OR: [
                { name: { contains: search, mode: "insensitive" as const } },
                { shortCode: { contains: search, mode: "insensitive" as const } },
              ],
            }
          : {}),
      };
      const whereWithCursor = cursor ? { ...where, id: { gt: cursor } } : where;

      const result = await paginateCursor(
        ({ take }) =>
          ctx.db.project.findMany({
            where: whereWithCursor,
            take,
            orderBy: [{ startDate: "asc" }, { id: "asc" }],
          }),
        input,
      );

      const projectIds = result.items.map((project) => project.id);
      const bookings = projectIds.length
        ? await listAssignmentBookings(ctx.db, {
            startDate: new Date("1900-01-01T00:00:00.000Z"),
            endDate: new Date("2100-12-31T23:59:59.999Z"),
            projectIds,
          })
        : [];

      // Compute cost + person days per project
      const projects = result.items.map((p) => {
        const projectBookings = bookings.filter((booking) => booking.projectId === p.id);
        let totalCostCents = 0;
        let totalPersonDays = 0;
        for (const a of projectBookings) {
          const days =
            (new Date(a.endDate).getTime() - new Date(a.startDate).getTime()) /
              (1000 * 60 * 60 * 24) +
            1;
          totalCostCents += a.dailyCostCents * days;
          totalPersonDays += (a.hoursPerDay * days) / 8;
        }
        const utilizationPercent = p.budgetCents > 0
          ? Math.round((totalCostCents / p.budgetCents) * 100)
          : 0;
        return {
          ...p,
          totalCostCents: Math.round(totalCostCents),
          totalPersonDays: Math.round(totalPersonDays * 10) / 10,
          utilizationPercent,
        };
      });

      return { projects, nextCursor: result.nextCursor };
    }),

  delete: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      const project = await ctx.db.project.findUnique({
        where: { id: input.id },
        select: { id: true, name: true, shortCode: true },
      });
      if (!project) throw new TRPCError({ code: "NOT_FOUND", message: "Project not found" });

      // Delete all related records in a transaction
      await ctx.db.$transaction(async (tx) => {
        // Delete assignments (which reference demandRequirements)
        await tx.assignment.deleteMany({ where: { projectId: input.id } });
        // Delete demand requirements
        await tx.demandRequirement.deleteMany({ where: { projectId: input.id } });
        // Unlink calculation rules
        await tx.calculationRule.updateMany({
          where: { projectId: input.id },
          data: { projectId: null },
        });
        // Delete the project
        await tx.project.delete({ where: { id: input.id } });
        // Audit log
        await tx.auditLog.create({
          data: {
            entityType: "Project",
            entityId: input.id,
            action: "DELETE",
            ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
            changes: { before: { id: project.id, name: project.name, shortCode: project.shortCode } } as never,
          },
        });
      });

      invalidateDashboardCacheInBackground();
      return { id: input.id, name: project.name };
    }),

  batchDelete: adminProcedure
    .input(
      z.object({
        ids: z.array(z.string()).min(1).max(50),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const projects = await ctx.db.project.findMany({
        where: { id: { in: input.ids } },
        select: { id: true, name: true, shortCode: true },
      });

      if (projects.length === 0) {
        throw new TRPCError({ code: "NOT_FOUND", message: "No projects found" });
      }

      await ctx.db.$transaction(async (tx) => {
        const ids = projects.map((p) => p.id);
        await tx.assignment.deleteMany({ where: { projectId: { in: ids } } });
        await tx.demandRequirement.deleteMany({ where: { projectId: { in: ids } } });
        await tx.calculationRule.updateMany({
          where: { projectId: { in: ids } },
          data: { projectId: null },
        });
        await tx.project.deleteMany({ where: { id: { in: ids } } });
        await tx.auditLog.create({
          data: {
            entityType: "Project",
            entityId: ids.join(","),
            action: "DELETE",
            ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
            changes: { before: projects } as never,
          },
        });
      });

      invalidateDashboardCacheInBackground();
      return { count: projects.length };
    }),

  // ─── Cover Art ──────────────────────────────────────────────────────────────

  generateCover: managerProcedure
    .input(z.object({
      projectId: z.string(),
      prompt: z.string().max(500).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);

      const project = await findUniqueOrThrow(
        ctx.db.project.findUnique({
          where: { id: input.projectId },
          include: { client: { select: { name: true } } },
        }),
        "Project",
      );

      const settings = await ctx.db.systemSettings.findUnique({
        where: { id: "singleton" },
      });

      const imageProvider = settings?.imageProvider ?? "dalle";
      const useGemini = imageProvider === "gemini" && isGeminiConfigured(settings);
      const useDalle = imageProvider === "dalle" && isDalleConfigured(settings);

      if (!useGemini && !useDalle) {
        throw new TRPCError({
          code: "PRECONDITION_FAILED",
          message: "No image provider configured. Set up DALL-E or Gemini in Admin → Settings.",
        });
      }

      const clientName = project.client?.name ? ` for ${project.client.name}` : "";
      const basePrompt = `Professional cover art for a 3D automotive visualization project: "${project.name}"${clientName}. Style: cinematic, modern, photorealistic CGI rendering, dramatic lighting, studio environment. No text or typography in the image.`;
      const finalPrompt = input.prompt
        ? `${basePrompt} Additional direction: ${input.prompt}`
        : basePrompt;

      let coverImageUrl: string;

      if (useGemini) {
        try {
          coverImageUrl = await generateGeminiImage(
            settings!.geminiApiKey!,
            finalPrompt,
            settings!.geminiModel ?? undefined,
          );
        } catch (err) {
          throw new TRPCError({
            code: "INTERNAL_SERVER_ERROR",
            message: `Gemini error: ${parseGeminiError(err)}`,
          });
        }
      } else {
        const dalleClient = createDalleClient(settings!);
        const model = settings!.aiProvider === "azure" ? settings!.azureDalleDeployment! : "dall-e-3";

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        let response: any;
        try {
          response = await loggedAiCall("dalle", model, finalPrompt.length, () =>
            dalleClient.images.generate({
              model,
              prompt: finalPrompt,
              size: "1024x1024",
              n: 1,
              response_format: "b64_json",
            }),
          );
        } catch (err) {
          throw new TRPCError({
            code: "INTERNAL_SERVER_ERROR",
            message: `DALL-E error: ${parseAiError(err)}`,
          });
        }

        const b64 = response.data?.[0]?.b64_json;
        if (!b64) {
          throw new TRPCError({
            code: "INTERNAL_SERVER_ERROR",
            message: "No image data returned from DALL-E",
          });
        }

        coverImageUrl = `data:image/png;base64,${b64}`;
      }

      await ctx.db.project.update({
        where: { id: input.projectId },
        data: { coverImageUrl },
      });

      return { coverImageUrl };
    }),

  uploadCover: managerProcedure
    .input(z.object({
      projectId: z.string(),
      imageDataUrl: z.string(),
    }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);

      if (!input.imageDataUrl.startsWith("data:image/")) {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "Invalid image format. Must be a data URL starting with 'data:image/'.",
        });
      }

      // Validate magic bytes match declared MIME type
      const magicCheck = validateImageDataUrl(input.imageDataUrl);
      if (!magicCheck.valid) {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: `File validation failed: ${magicCheck.reason}`,
        });
      }

      if (input.imageDataUrl.length > MAX_COVER_SIZE) {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "Image too large. Maximum compressed size is 4 MB.",
        });
      }

      await findUniqueOrThrow(
        ctx.db.project.findUnique({ where: { id: input.projectId } }),
        "Project",
      );

      await ctx.db.project.update({
        where: { id: input.projectId },
        data: { coverImageUrl: input.imageDataUrl },
      });

      return { coverImageUrl: input.imageDataUrl };
    }),

  removeCover: managerProcedure
    .input(z.object({ projectId: z.string() }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);

      await findUniqueOrThrow(
        ctx.db.project.findUnique({ where: { id: input.projectId } }),
        "Project",
      );

      await ctx.db.project.update({
        where: { id: input.projectId },
        data: { coverImageUrl: null },
      });

      return { ok: true };
    }),

  updateCoverFocus: managerProcedure
    .input(z.object({
      projectId: z.string(),
      coverFocusY: z.number().int().min(0).max(100),
    }))
    .mutation(async ({ ctx, input }) => {
      requirePermission(ctx, PermissionKey.MANAGE_PROJECTS);
      await ctx.db.project.update({
        where: { id: input.projectId },
        data: { coverFocusY: input.coverFocusY },
      });
      return { ok: true };
    }),

  isImageGenConfigured: protectedProcedure
    .query(async ({ ctx }) => {
      const settings = await ctx.db.systemSettings.findUnique({
        where: { id: "singleton" },
      });
      const imageProvider = settings?.imageProvider ?? "dalle";
      const configured = imageProvider === "gemini"
        ? isGeminiConfigured(settings)
        : isDalleConfigured(settings);
      return { configured, provider: imageProvider };
    }),

  /** @deprecated Use isImageGenConfigured instead */
  isDalleConfigured: protectedProcedure
    .query(async ({ ctx }) => {
      const settings = await ctx.db.systemSettings.findUnique({
        where: { id: "singleton" },
      });
      const imageProvider = settings?.imageProvider ?? "dalle";
      const configured = imageProvider === "gemini"
        ? isGeminiConfigured(settings)
        : isDalleConfigured(settings);
      return { configured };
    }),
});
