import type { TRPCContext } from "../../trpc.js";
import { AllocationStatus, PermissionKey, UpdateAssignmentSchema } from "@capakraken/shared";
import { SystemRole } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { fmtEur } from "../../lib/format-utils.js";
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";

type AssistantToolErrorResult = { error: string };

type ResolvedProject = {
  id: string;
  name: string;
  shortCode: string;
};

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

type AllocationPlanningDeps = {
  assertPermission: (ctx: ToolContext, perm: PermissionKey) => void;
  createAllocationCaller: (ctx: TRPCContext) => {
    listView: (params: {
      resourceId?: string;
      projectId?: string;
      status?: AllocationStatus;
    }) => Promise<{
      assignments: Array<{
        id: string;
        status: string;
        hoursPerDay: number;
        dailyCostCents?: number | null;
        startDate: Date | string;
        endDate: Date | string;
        role?: string | null;
        roleEntity?: { name?: string | null } | null;
        resource?: { displayName?: string | null; eid?: string | null } | null;
        project?: { name?: string | null; shortCode?: string | null } | null;
      }>;
    }>;
    ensureAssignment: (params: {
      resourceId: string;
      projectId: string;
      startDate: Date;
      endDate: Date;
      hoursPerDay: number;
      role?: string;
    }) => Promise<{
      action: "created" | "reactivated";
      assignment: {
        id: string;
        status: string;
      };
    }>;
    resolveAssignment: (params: {
      assignmentId?: string;
      resourceId?: string;
      projectId?: string;
      startDate?: Date;
      endDate?: Date;
      selectionMode: "WINDOW" | "EXACT_START";
      excludeCancelled?: boolean;
    }) => Promise<{
      id: string;
      status: string;
      startDate: Date;
      endDate: Date;
      resource: { displayName: string };
      project: { name: string; shortCode: string };
    }>;
    updateAssignment: (params: {
      id: string;
      data: z.input<typeof UpdateAssignmentSchema>;
    }) => Promise<unknown>;
  };
  createTimelineCaller: (ctx: TRPCContext) => {
    getBudgetStatus: (params: { projectId: string }) => Promise<{
      projectName: string;
      projectCode: string;
      budgetCents: number;
      confirmedCents: number;
      proposedCents: number;
      allocatedCents: number;
      remainingCents: number;
      utilizationPercent: number;
      winProbabilityWeightedCents: number;
      totalAllocations: 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;
  parseOptionalIsoDate: (value: string | undefined, fieldName: string) => Date | undefined;
  fmtDate: (value: Date | null | undefined) => string | null;
  toAssistantAllocationNotFoundError: (error: unknown) => unknown;
};

export const allocationPlanningReadToolDefinitions: ToolDef[] = withToolAccess([
  {
    type: "function",
    function: {
      name: "list_allocations",
      description: "List assignments/allocations, optionally filtered by resource or project. Shows who is assigned where, hours/day, dates, and cost.",
      parameters: {
        type: "object",
        properties: {
          resourceId: { type: "string", description: "Filter by resource ID" },
          projectId: { type: "string", description: "Filter by project ID" },
          resourceName: { type: "string", description: "Filter by resource name (partial match)" },
          projectCode: { type: "string", description: "Filter by project short code (partial match)" },
          status: { type: "string", description: "Filter by status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
          limit: { type: "integer", description: "Max results. Default: 30" },
        },
      },
    },
  },
  {
    type: "function",
    function: {
      name: "get_budget_status",
      description: "Get the budget status of a project: total budget, confirmed/proposed costs, remaining, utilization percentage.",
      parameters: {
        type: "object",
        properties: {
          projectId: { type: "string", description: "Project ID or short code" },
        },
        required: ["projectId"],
      },
    },
  },
], {
  list_allocations: {
    requiresPlanningRead: true,
  },
  get_budget_status: {
    allowedSystemRoles: [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER],
    requiresCostView: true,
  },
});

export const allocationPlanningMutationToolDefinitions: ToolDef[] = withToolAccess([
  {
    type: "function",
    function: {
      name: "create_allocation",
      description: "Create a new allocation/booking for a resource on a project. Requires manageAllocations permission. Always confirm with the user before calling this. Created with PROPOSED status.",
      parameters: {
        type: "object",
        properties: {
          resourceId: { type: "string", description: "Resource ID" },
          projectId: { type: "string", description: "Project ID" },
          startDate: { type: "string", description: "Start date YYYY-MM-DD" },
          endDate: { type: "string", description: "End date YYYY-MM-DD" },
          hoursPerDay: { type: "number", description: "Hours per day (e.g. 8)" },
          role: { type: "string", description: "Optional role name" },
        },
        required: ["resourceId", "projectId", "startDate", "endDate", "hoursPerDay"],
      },
    },
  },
  {
    type: "function",
    function: {
      name: "cancel_allocation",
      description: "Cancel an existing allocation. Can find by allocation ID, or by resource name + project code + date range. Requires manageAllocations permission. Always confirm with the user before calling this.",
      parameters: {
        type: "object",
        properties: {
          allocationId: { type: "string", description: "Allocation ID (if known)" },
          resourceName: { type: "string", description: "Resource name (partial match)" },
          projectCode: { type: "string", description: "Project short code (partial match)" },
          startDate: { type: "string", description: "Filter by start date YYYY-MM-DD" },
          endDate: { type: "string", description: "Filter by end date YYYY-MM-DD" },
        },
      },
    },
  },
  {
    type: "function",
    function: {
      name: "update_allocation_status",
      description: "Change the status of an existing allocation. Can reactivate cancelled allocations, confirm proposed ones, etc. Requires manageAllocations permission. Always confirm with the user before calling.",
      parameters: {
        type: "object",
        properties: {
          allocationId: { type: "string", description: "Allocation ID" },
          resourceName: { type: "string", description: "Resource name (partial match, used if no allocationId)" },
          projectCode: { type: "string", description: "Project short code (partial match, used if no allocationId)" },
          startDate: { type: "string", description: "Start date filter YYYY-MM-DD (used if no allocationId)" },
          newStatus: { type: "string", description: "New status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
        },
        required: ["newStatus"],
      },
    },
  },
], {
  create_allocation: {
    requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
  },
  cancel_allocation: {
    requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
  },
  update_allocation_status: {
    requiredPermissions: [PermissionKey.MANAGE_ALLOCATIONS],
  },
});

export function createAllocationPlanningExecutors(
  deps: AllocationPlanningDeps,
): Record<string, ToolExecutor> {
  return {
    async list_allocations(params: {
      resourceId?: string;
      projectId?: string;
      resourceName?: string;
      projectCode?: string;
      status?: string;
      limit?: number;
    }, ctx: ToolContext) {
      const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
      const status = params.status && Object.values(AllocationStatus).includes(params.status as AllocationStatus)
        ? params.status as AllocationStatus
        : undefined;
      const readModel = await caller.listView({
        ...(params.resourceId ? { resourceId: params.resourceId } : {}),
        ...(params.projectId ? { projectId: params.projectId } : {}),
        ...(status ? { status } : {}),
      });

      const resourceNameQuery = params.resourceName?.trim().toLowerCase();
      const projectCodeQuery = params.projectCode?.trim().toLowerCase();
      const limit = Math.min(params.limit ?? 30, 50);

      return readModel.assignments
        .filter((assignment) => {
          if (
            resourceNameQuery
            && !assignment.resource?.displayName?.toLowerCase().includes(resourceNameQuery)
          ) {
            return false;
          }
          if (
            projectCodeQuery
            && !assignment.project?.shortCode?.toLowerCase().includes(projectCodeQuery)
          ) {
            return false;
          }
          return true;
        })
        .slice(0, limit)
        .map((assignment) => ({
          id: assignment.id,
          resource: assignment.resource?.displayName ?? "Unknown",
          resourceEid: assignment.resource?.eid ?? null,
          project: assignment.project?.name ?? "Unknown",
          projectCode: assignment.project?.shortCode ?? null,
          role: assignment.role ?? assignment.roleEntity?.name ?? null,
          status: assignment.status,
          hoursPerDay: assignment.hoursPerDay,
          dailyCost: assignment.dailyCostCents == null ? null : fmtEur(assignment.dailyCostCents),
          start: deps.fmtDate(new Date(assignment.startDate)),
          end: deps.fmtDate(new Date(assignment.endDate)),
        }));
    },

    async get_budget_status(params: { projectId: string }, ctx: ToolContext) {
      const project = await deps.resolveProjectIdentifier(ctx, params.projectId);
      if ("error" in project) {
        return project;
      }

      const caller = deps.createTimelineCaller(deps.createScopedCallerContext(ctx));
      const budgetStatus = await caller.getBudgetStatus({ projectId: project.id });

      if (budgetStatus.budgetCents <= 0) {
        return {
          project: budgetStatus.projectName,
          code: budgetStatus.projectCode,
          budget: "Not set",
          note: "No budget defined for this project",
          totalAllocations: budgetStatus.totalAllocations,
        };
      }

      return {
        project: budgetStatus.projectName,
        code: budgetStatus.projectCode,
        budget: fmtEur(budgetStatus.budgetCents),
        confirmed: fmtEur(budgetStatus.confirmedCents),
        proposed: fmtEur(budgetStatus.proposedCents),
        allocated: fmtEur(budgetStatus.allocatedCents),
        remaining: fmtEur(budgetStatus.remainingCents),
        utilization: `${budgetStatus.utilizationPercent.toFixed(1)}%`,
        winWeighted: fmtEur(budgetStatus.winProbabilityWeightedCents),
      };
    },

    async create_allocation(params: {
      resourceId: string;
      projectId: string;
      startDate: string;
      endDate: string;
      hoursPerDay: number;
      role?: string;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
      const [resource, project] = await Promise.all([
        deps.resolveResourceIdentifier(ctx, params.resourceId),
        deps.resolveProjectIdentifier(ctx, params.projectId),
      ]);
      if ("error" in resource) {
        return resource;
      }
      if ("error" in project) {
        return project;
      }

      const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
      try {
        const result = await caller.ensureAssignment({
          resourceId: resource.id,
          projectId: project.id,
          startDate: deps.parseIsoDate(params.startDate, "startDate"),
          endDate: deps.parseIsoDate(params.endDate, "endDate"),
          hoursPerDay: params.hoursPerDay,
          ...(params.role ? { role: params.role } : {}),
        });

        return {
          __action: "invalidate",
          scope: ["allocation", "timeline"],
          success: true,
          message: `${result.action === "reactivated" ? "Reactivated" : "Created"} allocation: ${resource.displayName} → ${project.name} (${project.shortCode}), ${params.hoursPerDay}h/day, ${params.startDate} to ${params.endDate}`,
          allocationId: result.assignment.id,
          status: result.assignment.status,
        };
      } catch (error) {
        if (error instanceof TRPCError && error.code === "CONFLICT") {
          return { error: "Allocation already exists for this resource/project/dates. No new allocation created." };
        }
        throw error;
      }
    },

    async cancel_allocation(params: {
      allocationId?: string;
      resourceName?: string;
      projectCode?: string;
      startDate?: string;
      endDate?: string;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);

      const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
      let resourceId: string | undefined;
      let projectId: string | undefined;
      if (!params.allocationId && params.resourceName && params.projectCode) {
        const [resource, project] = await Promise.all([
          deps.resolveResourceIdentifier(ctx, params.resourceName),
          deps.resolveProjectIdentifier(ctx, params.projectCode),
        ]);
        if ("error" in resource) {
          return resource;
        }
        if ("error" in project) {
          return project;
        }
        resourceId = resource.id;
        projectId = project.id;
      }

      const startDate = deps.parseOptionalIsoDate(params.startDate, "startDate");
      const endDate = deps.parseOptionalIsoDate(params.endDate, "endDate");
      let assignment;
      try {
        assignment = await caller.resolveAssignment({
          ...(params.allocationId ? { assignmentId: params.allocationId } : {}),
          ...(resourceId ? { resourceId } : {}),
          ...(projectId ? { projectId } : {}),
          ...(startDate ? { startDate } : {}),
          ...(endDate ? { endDate } : {}),
          selectionMode: "WINDOW",
          excludeCancelled: true,
        });
      } catch (error) {
        const mapped = deps.toAssistantAllocationNotFoundError(error);
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      try {
        await caller.updateAssignment({
          id: assignment.id,
          data: UpdateAssignmentSchema.parse({ status: AllocationStatus.CANCELLED }),
        });
      } catch (error) {
        const mapped = deps.toAssistantAllocationNotFoundError(error);
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      return {
        __action: "invalidate",
        scope: ["allocation", "timeline"],
        success: true,
        message: `Cancelled allocation: ${assignment.resource.displayName} → ${assignment.project.name} (${assignment.project.shortCode}), ${deps.fmtDate(assignment.startDate)} to ${deps.fmtDate(assignment.endDate)}`,
      };
    },

    async update_allocation_status(params: {
      allocationId?: string;
      resourceName?: string;
      projectCode?: string;
      startDate?: string;
      newStatus: string;
    }, ctx: ToolContext) {
      deps.assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);

      const validStatuses = ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"];
      if (!validStatuses.includes(params.newStatus)) {
        return { error: `Invalid status: ${params.newStatus}. Valid: ${validStatuses.join(", ")}` };
      }

      const caller = deps.createAllocationCaller(deps.createScopedCallerContext(ctx));
      let resourceId: string | undefined;
      let projectId: string | undefined;
      if (!params.allocationId && params.resourceName && params.projectCode) {
        const [resource, project] = await Promise.all([
          deps.resolveResourceIdentifier(ctx, params.resourceName),
          deps.resolveProjectIdentifier(ctx, params.projectCode),
        ]);
        if ("error" in resource) {
          return resource;
        }
        if ("error" in project) {
          return project;
        }
        resourceId = resource.id;
        projectId = project.id;
      }

      const startDate = deps.parseOptionalIsoDate(params.startDate, "startDate");
      let assignment;
      try {
        assignment = await caller.resolveAssignment({
          ...(params.allocationId ? { assignmentId: params.allocationId } : {}),
          ...(resourceId ? { resourceId } : {}),
          ...(projectId ? { projectId } : {}),
          ...(startDate ? { startDate } : {}),
          selectionMode: "EXACT_START",
        });
      } catch (error) {
        const mapped = deps.toAssistantAllocationNotFoundError(error);
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      const oldStatus = assignment.status;
      try {
        await caller.updateAssignment({
          id: assignment.id,
          data: UpdateAssignmentSchema.parse({
            status: params.newStatus as AllocationStatus,
          }),
        });
      } catch (error) {
        const mapped = deps.toAssistantAllocationNotFoundError(error);
        if (mapped) {
          return mapped;
        }
        throw error;
      }

      return {
        __action: "invalidate",
        scope: ["allocation", "timeline"],
        success: true,
        message: `Updated allocation status: ${assignment.resource.displayName} → ${assignment.project.name} (${assignment.project.shortCode}), ${deps.fmtDate(assignment.startDate)} to ${deps.fmtDate(assignment.endDate)}: ${oldStatus} → ${params.newStatus}`,
      };
    },
  };
}
