import type { TRPCContext } from "../../trpc.js";
import { AllocationStatus, PermissionKey, SystemRole } from "@capakraken/shared";
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";

export const advancedTimelineToolDefinitions: ToolDef[] = withToolAccess([
  {
    type: "function",
    function: {
      name: "find_best_project_resource",
      description: "Advanced assistant tool: find the best already-assigned resource on a project for a given period, ranked by remaining capacity or LCR. Holiday- and vacation-aware. Requires viewCosts and advanced assistant permissions.",
      parameters: {
        type: "object",
        properties: {
          projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
          startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
          endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
          durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
          minHoursPerDay: { type: "number", description: "Minimum remaining availability per effective working day. Default: 3." },
          rankingMode: { type: "string", description: "Ranking mode: lowest_lcr, highest_remaining_hours_per_day, or highest_remaining_hours. Default: lowest_lcr." },
          chapter: { type: "string", description: "Optional chapter filter for candidate resources." },
          roleName: { type: "string", description: "Optional role filter for candidate resources." },
        },
        required: ["projectIdentifier"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "get_timeline_entries_view",
      description: "Advanced assistant tool: read-only timeline entries view with the same timeline/disposition readmodel used by the app. Returns allocations, demands, assignments, and matching holiday overlays for a period.",
      parameters: {
        type: "object",
        properties: {
          startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
          endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
          durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
          resourceIds: { type: "array", items: { type: "string" }, description: "Optional resource IDs to scope the view." },
          projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to scope the view." },
          clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to scope the view." },
          chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters." },
          eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs to scope the view." },
          countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes such as DE or ES." },
        },
      },
    },
  },
  {
    type: "function",
    function: {
      name: "get_timeline_holiday_overlays",
      description: "Advanced assistant tool: read-only holiday overlays for the timeline, resolved with the same holiday logic as the app. Useful to explain regional holiday differences for assigned or filtered resources.",
      parameters: {
        type: "object",
        properties: {
          startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
          endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
          durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
          resourceIds: { type: "array", items: { type: "string" }, description: "Optional resource IDs to scope the overlays." },
          projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to scope the overlays via matching assignments." },
          clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to scope the overlays via matching projects." },
          chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters." },
          eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs to scope the overlays." },
          countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes such as DE or ES." },
        },
      },
    },
  },
  {
    type: "function",
    function: {
      name: "get_project_timeline_context",
      description: "Advanced assistant tool: read-only project timeline/disposition context. Reuses the same project context readmodel as the app and adds holiday overlays plus cross-project overlap summaries for assigned resources.",
      parameters: {
        type: "object",
        properties: {
          projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
          startDate: { type: "string", description: "Optional holiday/conflict window start date in YYYY-MM-DD. Defaults to the project start date when available." },
          endDate: { type: "string", description: "Optional holiday/conflict window end date in YYYY-MM-DD. Defaults to the project end date when available." },
          durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted." },
        },
        required: ["projectIdentifier"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "preview_project_shift",
      description: "Advanced assistant tool: read-only preview of the timeline shift validation for a project. Uses the same preview logic as the timeline router and does not write changes.",
      parameters: {
        type: "object",
        properties: {
          projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
          newStartDate: { type: "string", description: "New start date in YYYY-MM-DD." },
          newEndDate: { type: "string", description: "New end date in YYYY-MM-DD." },
        },
        required: ["projectIdentifier", "newStartDate", "newEndDate"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "update_timeline_allocation_inline",
      description: "Advanced assistant mutation: update a timeline allocation inline with the same manager/admin + manageAllocations validation as the timeline API. Supports hours/day, dates, includeSaturday, and role changes. Requires useAssistantAdvancedTools.",
      parameters: {
        type: "object",
        properties: {
          allocationId: { type: "string", description: "Allocation, assignment, or demand row ID to update." },
          hoursPerDay: { type: "number", description: "Optional new booked hours per day." },
          startDate: { type: "string", description: "Optional new start date in YYYY-MM-DD." },
          endDate: { type: "string", description: "Optional new end date in YYYY-MM-DD." },
          includeSaturday: { type: "boolean", description: "Optional Saturday-working flag stored in metadata." },
          role: { type: "string", description: "Optional new role label." },
        },
        required: ["allocationId"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "apply_timeline_project_shift",
      description: "Advanced assistant mutation: apply the real timeline project shift mutation, including validation, date movement, cost recalculation, audit logging, and SSE. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
      parameters: {
        type: "object",
        properties: {
          projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
          newStartDate: { type: "string", description: "New project start date in YYYY-MM-DD." },
          newEndDate: { type: "string", description: "New project end date in YYYY-MM-DD." },
        },
        required: ["projectIdentifier", "newStartDate", "newEndDate"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "quick_assign_timeline_resource",
      description: "Advanced assistant mutation: create a timeline quick-assignment with the same manager/admin + manageAllocations rules as the timeline UI. Resolves resource and project identifiers before calling the real mutation. Requires useAssistantAdvancedTools.",
      parameters: {
        type: "object",
        properties: {
          resourceIdentifier: { type: "string", description: "Resource ID, eid, or display name." },
          projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
          startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
          endDate: { type: "string", description: "End date in YYYY-MM-DD." },
          hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
          role: { type: "string", description: "Role label. Default: Team Member." },
          roleId: { type: "string", description: "Optional concrete role ID." },
          status: { type: "string", enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"], description: "Assignment status. Default: PROPOSED." },
        },
        required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "batch_quick_assign_timeline_resources",
      description: "Advanced assistant mutation: batch-create timeline quick-assignments using the same timeline router logic, permission checks, and audit/SSE side effects as the app. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
      parameters: {
        type: "object",
        properties: {
          assignments: {
            type: "array",
            minItems: 1,
            maxItems: 50,
            items: {
              type: "object",
              properties: {
                resourceIdentifier: { type: "string", description: "Resource ID, eid, or display name." },
                projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
                startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
                endDate: { type: "string", description: "End date in YYYY-MM-DD." },
                hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
                role: { type: "string", description: "Role label. Default: Team Member." },
                status: { type: "string", enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"], description: "Assignment status. Default: PROPOSED." },
              },
              required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
            },
            description: "Assignment rows to create in one batch.",
          },
        },
        required: ["assignments"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "batch_shift_timeline_allocations",
      description: "Advanced assistant mutation: shift multiple timeline allocations by a shared day delta using the real timeline batch move/resize mutation. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
      parameters: {
        type: "object",
        properties: {
          allocationIds: { type: "array", items: { type: "string" }, description: "Allocation IDs to shift." },
          daysDelta: { type: "integer", description: "Signed day delta to apply." },
          mode: { type: "string", enum: ["move", "resize-start", "resize-end"], description: "Shift mode. Default: move." },
        },
        required: ["allocationIds", "daysDelta"],
      },
    },
  },
], {
  find_best_project_resource: {
    requiresPlanningRead: true,
    requiresCostView: true,
    requiresAdvancedAssistant: true,
  },
  get_timeline_entries_view: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
    requiresAdvancedAssistant: true,
  },
  get_timeline_holiday_overlays: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
    requiresAdvancedAssistant: true,
  },
  get_project_timeline_context: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
    requiresAdvancedAssistant: true,
  },
  preview_project_shift: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
    requiresAdvancedAssistant: true,
  },
  update_timeline_allocation_inline: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
    requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
    requiresAdvancedAssistant: true,
  },
  apply_timeline_project_shift: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
    requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
    requiresAdvancedAssistant: true,
  },
  quick_assign_timeline_resource: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
    requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
    requiresAdvancedAssistant: true,
  },
  batch_quick_assign_timeline_resources: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
    requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
    requiresAdvancedAssistant: true,
  },
  batch_shift_timeline_allocations: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER],
    requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
    requiresAdvancedAssistant: true,
  },
});

type AssistantToolErrorResult = { error: string };

type ResolvedProject = {
  id: string;
  name?: string | null;
  shortCode?: string | null;
};

type ResolvedResource = {
  id: string;
  displayName: string;
};

type TimelineMutationContext = "updateInline" | "applyShift" | "quickAssign" | "batchShift";

type BatchQuickAssignmentInput = {
  resourceId: string;
  projectId: string;
  startDate: Date;
  endDate: Date;
  hoursPerDay?: number;
  role?: string;
  status?: AllocationStatus;
};

type AdvancedTimelineDeps = {
  assertPermission: (ctx: ToolContext, perm: PermissionKey) => void;
  createStaffingCaller: (ctx: TRPCContext) => {
    getBestProjectResourceDetail: (params: {
      projectId: string;
      startDate?: Date;
      endDate?: Date;
      durationDays?: number;
      minHoursPerDay?: number;
      rankingMode?: "lowest_lcr" | "highest_remaining_hours_per_day" | "highest_remaining_hours";
      chapter?: string;
      roleName?: string;
    }) => Promise<unknown>;
  };
  createTimelineCaller: (ctx: TRPCContext) => {
    getEntriesDetail: (params: {
      startDate?: string;
      endDate?: string;
      durationDays?: number;
      resourceIds?: string[];
      projectIds?: string[];
      clientIds?: string[];
      chapters?: string[];
      eids?: string[];
      countryCodes?: string[];
    }) => Promise<unknown>;
    getHolidayOverlayDetail: (params: {
      startDate?: string;
      endDate?: string;
      durationDays?: number;
      resourceIds?: string[];
      projectIds?: string[];
      clientIds?: string[];
      chapters?: string[];
      eids?: string[];
      countryCodes?: string[];
    }) => Promise<unknown>;
    getProjectContextDetail: (params: {
      projectId: string;
      startDate?: string;
      endDate?: string;
      durationDays?: number;
    }) => Promise<unknown>;
    getShiftPreviewDetail: (params: {
      projectId: string;
      newStartDate: Date;
      newEndDate: Date;
    }) => Promise<unknown>;
    updateAllocationInline: (params: {
      allocationId: string;
      hoursPerDay?: number;
      startDate?: Date;
      endDate?: Date;
      includeSaturday?: boolean;
      role?: string;
    }) => Promise<{
      id: string;
      projectId: string;
      resourceId?: string | null;
      startDate: Date;
      endDate: Date;
      hoursPerDay: number;
      role?: string | null;
      status: string;
    }>;
    applyShift: (params: {
      projectId: string;
      newStartDate: Date;
      newEndDate: Date;
    }) => Promise<{
      project: {
        id: string;
        startDate: Date;
        endDate: Date;
      };
      validation: unknown;
    }>;
    quickAssign: (params: {
      resourceId: string;
      projectId: string;
      startDate: Date;
      endDate: Date;
      hoursPerDay?: number;
      role?: string;
      roleId?: string;
      status?: AllocationStatus;
    }) => Promise<{
      id: string;
      projectId: string;
      resourceId?: string | null;
      startDate: Date | string;
      endDate: Date | string;
      hoursPerDay: number;
      role?: string | null;
      status: string;
    }>;
    batchQuickAssign: (params: { assignments: BatchQuickAssignmentInput[] }) => Promise<{ count: number }>;
    batchShiftAllocations: (params: {
      allocationIds: string[];
      daysDelta: number;
      mode?: "move" | "resize-start" | "resize-end";
    }) => Promise<{ count: number }>;
  };
  createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
  resolveProjectIdentifier: (
    ctx: ToolContext,
    identifier: string,
  ) => Promise<ResolvedProject | AssistantToolErrorResult>;
  resolveResourceIdentifier: (
    ctx: ToolContext,
    identifier: string,
  ) => Promise<ResolvedResource | AssistantToolErrorResult>;
  parseIsoDate: (value: string, fieldName: string) => Date;
  fmtDate: (value: Date | null | undefined) => string | null;
  isAssistantToolErrorResult: (value: unknown) => value is AssistantToolErrorResult;
  toAssistantIndexedFieldError: (index: number, field: string, message: string) => unknown;
  toAssistantTimelineMutationError: (error: unknown, context: TimelineMutationContext) => unknown;
};

function toDate(value: Date | string): Date {
  return value instanceof Date ? value : new Date(value);
}

export function createAdvancedTimelineExecutors(
  deps: AdvancedTimelineDeps,
): Record<string, ToolExecutor> {
  return {
    async find_best_project_resource(params: {
      projectIdentifier: string;
      startDate?: string;
      endDate?: string;
      durationDays?: number;
      minHoursPerDay?: number;
      rankingMode?: "lowest_lcr" | "highest_remaining_hours_per_day" | "highest_remaining_hours";
      chapter?: string;
      roleName?: string;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
      deps.assertPermission(ctx, PermissionKey.VIEW_COSTS);

      const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
      if ("error" in project) {
        return project;
      }

      const caller = deps.createStaffingCaller(deps.createScopedCallerContext(ctx));
      return caller.getBestProjectResourceDetail({
        projectId: project.id,
        ...(params.startDate ? { startDate: deps.parseIsoDate(params.startDate, "startDate") } : {}),
        ...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
        ...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
        ...(params.minHoursPerDay !== undefined ? { minHoursPerDay: params.minHoursPerDay } : {}),
        ...(params.rankingMode ? { rankingMode: params.rankingMode } : {}),
        ...(params.chapter ? { chapter: params.chapter } : {}),
        ...(params.roleName ? { roleName: params.roleName } : {}),
      });
    },

    async get_timeline_entries_view(params: {
      startDate?: string;
      endDate?: string;
      durationDays?: number;
      resourceIds?: string[];
      projectIds?: string[];
      clientIds?: string[];
      chapters?: string[];
      eids?: string[];
      countryCodes?: string[];
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      return caller.getEntriesDetail({ ...params });
    },

    async get_timeline_holiday_overlays(params: {
      startDate?: string;
      endDate?: string;
      durationDays?: number;
      resourceIds?: string[];
      projectIds?: string[];
      clientIds?: string[];
      chapters?: string[];
      eids?: string[];
      countryCodes?: string[];
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      return caller.getHolidayOverlayDetail({ ...params });
    },

    async get_project_timeline_context(params: {
      projectIdentifier: string;
      startDate?: string;
      endDate?: string;
      durationDays?: number;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
      if ("error" in project) {
        return project;
      }

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      return caller.getProjectContextDetail({
        projectId: project.id,
        ...(params.startDate ? { startDate: params.startDate } : {}),
        ...(params.endDate ? { endDate: params.endDate } : {}),
        ...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
      });
    },

    async preview_project_shift(params: {
      projectIdentifier: string;
      newStartDate: string;
      newEndDate: string;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
      if ("error" in project) {
        return project;
      }

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      return caller.getShiftPreviewDetail({
        projectId: project.id,
        newStartDate: deps.parseIsoDate(params.newStartDate, "newStartDate"),
        newEndDate: deps.parseIsoDate(params.newEndDate, "newEndDate"),
      });
    },

    async update_timeline_allocation_inline(params: {
      allocationId: string;
      hoursPerDay?: number;
      startDate?: string;
      endDate?: string;
      includeSaturday?: boolean;
      role?: string;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      let updated;
      try {
        updated = await caller.updateAllocationInline({
          allocationId: params.allocationId,
          ...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
          ...(params.startDate ? { startDate: deps.parseIsoDate(params.startDate, "startDate") } : {}),
          ...(params.endDate ? { endDate: deps.parseIsoDate(params.endDate, "endDate") } : {}),
          ...(params.includeSaturday !== undefined ? { includeSaturday: params.includeSaturday } : {}),
          ...(params.role !== undefined ? { role: params.role } : {}),
        });
      } catch (error) {
        const mapped = deps.toAssistantTimelineMutationError(error, "updateInline");
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      return {
        __action: "invalidate",
        scope: ["allocation", "timeline", "project"],
        success: true,
        message: `Updated timeline allocation ${updated.id}.`,
        allocation: {
          id: updated.id,
          projectId: updated.projectId,
          resourceId: updated.resourceId ?? null,
          startDate: deps.fmtDate(updated.startDate),
          endDate: deps.fmtDate(updated.endDate),
          hoursPerDay: updated.hoursPerDay,
          role: updated.role ?? null,
          status: updated.status,
        },
      };
    },

    async apply_timeline_project_shift(params: {
      projectIdentifier: string;
      newStartDate: string;
      newEndDate: string;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const project = await deps.resolveProjectIdentifier(ctx, params.projectIdentifier);
      if ("error" in project) {
        return project;
      }

      const newStartDate = deps.parseIsoDate(params.newStartDate, "newStartDate");
      const newEndDate = deps.parseIsoDate(params.newEndDate, "newEndDate");
      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      let result;
      try {
        result = await caller.applyShift({
          projectId: project.id,
          newStartDate,
          newEndDate,
        });
      } catch (error) {
        const mapped = deps.toAssistantTimelineMutationError(error, "applyShift");
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      return {
        __action: "invalidate",
        scope: ["allocation", "timeline", "project"],
        success: true,
        message: `Shifted project ${project.shortCode ?? project.name ?? project.id} to ${deps.fmtDate(newStartDate)} - ${deps.fmtDate(newEndDate)}.`,
        project: {
          id: result.project.id,
          startDate: deps.fmtDate(result.project.startDate),
          endDate: deps.fmtDate(result.project.endDate),
        },
        validation: result.validation,
      };
    },

    async quick_assign_timeline_resource(params: {
      resourceIdentifier: string;
      projectIdentifier: string;
      startDate: string;
      endDate: string;
      hoursPerDay?: number;
      role?: string;
      roleId?: string;
      status?: AllocationStatus;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const [resource, project] = await Promise.all([
        deps.resolveResourceIdentifier(ctx, params.resourceIdentifier),
        deps.resolveProjectIdentifier(ctx, params.projectIdentifier),
      ]);
      if ("error" in resource) {
        return resource;
      }
      if ("error" in project) {
        return project;
      }

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      let allocation;
      try {
        allocation = await caller.quickAssign({
          resourceId: resource.id,
          projectId: project.id,
          startDate: deps.parseIsoDate(params.startDate, "startDate"),
          endDate: deps.parseIsoDate(params.endDate, "endDate"),
          ...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
          ...(params.role !== undefined ? { role: params.role } : {}),
          ...(params.roleId !== undefined ? { roleId: params.roleId } : {}),
          ...(params.status !== undefined ? { status: params.status } : {}),
        });
      } catch (error) {
        const mapped = deps.toAssistantTimelineMutationError(error, "quickAssign");
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      return {
        __action: "invalidate",
        scope: ["allocation", "timeline", "project"],
        success: true,
        message: `Quick-assigned ${resource.displayName} to ${project.name} (${project.shortCode ?? project.id}).`,
        allocation: {
          id: allocation.id,
          projectId: allocation.projectId,
          resourceId: allocation.resourceId ?? null,
          startDate: deps.fmtDate(toDate(allocation.startDate)),
          endDate: deps.fmtDate(toDate(allocation.endDate)),
          hoursPerDay: allocation.hoursPerDay,
          role: allocation.role ?? null,
          status: allocation.status,
        },
      };
    },

    async batch_quick_assign_timeline_resources(params: {
      assignments: Array<{
        resourceIdentifier: string;
        projectIdentifier: string;
        startDate: string;
        endDate: string;
        hoursPerDay?: number;
        role?: string;
        status?: AllocationStatus;
      }>;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const resolvedAssignments = await Promise.all(params.assignments.map(async (assignment, index) => {
        const [resource, project] = await Promise.all([
          deps.resolveResourceIdentifier(ctx, assignment.resourceIdentifier),
          deps.resolveProjectIdentifier(ctx, assignment.projectIdentifier),
        ]);
        if ("error" in resource) {
          return deps.toAssistantIndexedFieldError(index, "resourceIdentifier", resource.error);
        }
        if ("error" in project) {
          return deps.toAssistantIndexedFieldError(index, "projectIdentifier", project.error);
        }
        return {
          resourceId: resource.id,
          projectId: project.id,
          startDate: deps.parseIsoDate(assignment.startDate, `assignments[${index}].startDate`),
          endDate: deps.parseIsoDate(assignment.endDate, `assignments[${index}].endDate`),
          ...(assignment.hoursPerDay !== undefined ? { hoursPerDay: assignment.hoursPerDay } : {}),
          ...(assignment.role !== undefined ? { role: assignment.role } : {}),
          ...(assignment.status !== undefined ? { status: assignment.status } : {}),
        };
      }));

      const resolutionError = resolvedAssignments.find(deps.isAssistantToolErrorResult);
      if (resolutionError) {
        return resolutionError;
      }
      const validAssignments = resolvedAssignments.filter(
        (assignment): assignment is BatchQuickAssignmentInput => !deps.isAssistantToolErrorResult(assignment),
      );

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      let result;
      try {
        result = await caller.batchQuickAssign({
          assignments: validAssignments,
        });
      } catch (error) {
        const mapped = deps.toAssistantTimelineMutationError(error, "quickAssign");
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      return {
        __action: "invalidate",
        scope: ["allocation", "timeline", "project"],
        success: true,
        message: `Created ${result.count} timeline quick-assignment(s).`,
        count: result.count,
      };
    },

    async batch_shift_timeline_allocations(params: {
      allocationIds: string[];
      daysDelta: number;
      mode?: "move" | "resize-start" | "resize-end";
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      deps.assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      let result;
      try {
        result = await caller.batchShiftAllocations({
          allocationIds: params.allocationIds,
          daysDelta: params.daysDelta,
          ...(params.mode !== undefined ? { mode: params.mode } : {}),
        });
      } catch (error) {
        const mapped = deps.toAssistantTimelineMutationError(error, "batchShift");
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      return {
        __action: "invalidate",
        scope: ["allocation", "timeline", "project"],
        success: true,
        message: `Shifted ${result.count} allocation(s) by ${params.daysDelta} day(s).`,
        count: result.count,
      };
    },
  };
}
