import { countPlanningEntries } 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 { projectCostReadProcedures } from "./project-cost-read.js";
import { projectCoverProcedures } from "./project-cover.js";
import { projectIdentifierReadProcedures } from "./project-identifier-read.js";
import { createProjectLifecycleProcedures } from "./project-lifecycle.js";
import { loadProjectPlanningReadModel } from "./project-planning-read-model.js";
import { controllerProcedure, createTRPCRouter, managerProcedure, requirePermission } from "../trpc.js";
import { invalidateDashboardCache } from "../lib/cache.js";
import { logger } from "../lib/logger.js";
import { dispatchWebhooks } from "../lib/webhook-dispatcher.js";
import type { TRPCContext } from "../trpc.js";
import {
  calculateEffectiveBookedHours,
  loadResourceDailyAvailabilityContexts,
} from "../lib/resource-capacity.js";

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 },
  );
}

export const projectRouter = createTRPCRouter({
  ...projectCostReadProcedures,
  ...projectCoverProcedures,
  ...projectIdentifierReadProcedures,
  ...createProjectLifecycleProcedures({
    invalidateDashboardCacheInBackground,
    dispatchProjectWebhookInBackground,
  }),

  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,
      };
    }),

  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;
    }),

});
