import { PermissionKey, parseTaskAction, resolvePermissions } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js";
import { createNotification } from "../lib/create-notification.js";
import { getTaskAction } from "../lib/task-actions.js";
import {
  emitTaskAssigned,
  emitTaskCompleted,
  emitTaskStatusChanged,
} from "../sse/event-bus.js";
import {
  AssignTaskInputSchema,
  CreateTaskInputSchema,
  getNotificationErrorCandidates,
  ListNotificationTasksInputSchema,
  NotificationIdInputSchema,
  type NotificationProcedureContext,
  rethrowNotificationReferenceError,
  requireNotificationDbUser,
  resolveUserId,
  sendNotificationEmail,
  UpdateNotificationTaskStatusInputSchema,
} from "./notification-procedure-base.js";

function requireTaskActionTransaction(
  db: NotificationProcedureContext["db"],
): NonNullable<NotificationProcedureContext["db"]["$transaction"]> {
  if (typeof db.$transaction !== "function") {
    throw new TRPCError({
      code: "INTERNAL_SERVER_ERROR",
      message: "Task action execution requires transactional persistence support.",
    });
  }

  return db.$transaction.bind(db);
}

export async function listNotificationTasks(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof ListNotificationTasksInputSchema>,
) {
  const userId = await resolveUserId(ctx);
  const userFilter = input.includeAssigned
    ? { OR: [{ userId }, { assigneeId: userId }] }
    : { userId };

  return ctx.db.notification.findMany({
    where: {
      ...userFilter,
      category: { in: ["TASK", "APPROVAL"] },
      ...(input.status !== undefined ? { taskStatus: input.status } : {}),
    },
    orderBy: [{ priority: "desc" }, { dueDate: "asc" }, { createdAt: "desc" }],
    take: input.limit,
  });
}

export async function getNotificationTaskCounts(ctx: NotificationProcedureContext) {
  const userId = await resolveUserId(ctx);
  const now = new Date();
  const where = {
    OR: [{ userId }, { assigneeId: userId }],
    category: { in: ["TASK" as const, "APPROVAL" as const] },
  };

  const [grouped, overdue] = await Promise.all([
    ctx.db.notification.groupBy({
      by: ["taskStatus"],
      where,
      _count: true,
    }),
    ctx.db.notification.count({
      where: {
        ...where,
        taskStatus: { in: ["OPEN", "IN_PROGRESS"] },
        dueDate: { lt: now },
      },
    }),
  ]);

  const counts: Record<string, number> = {};
  for (const group of grouped) {
    if (group.taskStatus) {
      counts[group.taskStatus] = group._count;
    }
  }

  return {
    open: counts.OPEN ?? 0,
    inProgress: counts.IN_PROGRESS ?? 0,
    done: counts.DONE ?? 0,
    dismissed: counts.DISMISSED ?? 0,
    overdue,
  };
}

export async function getNotificationTaskDetail(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof NotificationIdInputSchema>,
) {
  const userId = await resolveUserId(ctx);
  const task = await ctx.db.notification.findFirst({
    where: {
      id: input.id,
      OR: [{ userId }, { assigneeId: userId }],
      category: { in: ["TASK", "APPROVAL"] },
    },
    select: {
      id: true,
      title: true,
      body: true,
      type: true,
      priority: true,
      category: true,
      taskStatus: true,
      taskAction: true,
      dueDate: true,
      entityId: true,
      entityType: true,
      completedAt: true,
      completedBy: true,
      createdAt: true,
      userId: true,
      assigneeId: true,
      sender: { select: { id: true, name: true, email: true } },
    },
  });

  if (!task) {
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Task not found or you do not have permission",
    });
  }

  return task;
}

export async function updateNotificationTaskStatus(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof UpdateNotificationTaskStatusInputSchema>,
) {
  const userId = await resolveUserId(ctx);
  const existing = await ctx.db.notification.findFirst({
    where: {
      id: input.id,
      OR: [{ userId }, { assigneeId: userId }],
      category: { in: ["TASK", "APPROVAL"] },
    },
  });

  if (!existing) {
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Task not found or you do not have permission",
    });
  }

  const isCompleting = input.status === "DONE";
  const updated = await ctx.db.notification.update({
    where: { id: input.id },
    data: {
      taskStatus: input.status,
      ...(isCompleting
        ? { completedAt: new Date(), completedBy: userId }
        : { completedAt: null, completedBy: null }),
    },
  });

  if (isCompleting) {
    emitTaskCompleted(existing.userId, updated.id);
    if (existing.assigneeId && existing.assigneeId !== existing.userId) {
      emitTaskCompleted(existing.assigneeId, updated.id);
    }
  } else {
    emitTaskStatusChanged(existing.userId, updated.id);
    if (existing.assigneeId && existing.assigneeId !== existing.userId) {
      emitTaskStatusChanged(existing.assigneeId, updated.id);
    }
  }

  return updated;
}

export async function executeNotificationTaskAction(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof NotificationIdInputSchema>,
) {
  const userId = await resolveUserId(ctx);
  const task = await ctx.db.notification.findFirst({
    where: {
      id: input.id,
      OR: [{ userId }, { assigneeId: userId }],
      category: { in: ["TASK", "APPROVAL"] },
    },
    select: {
      id: true,
      userId: true,
      assigneeId: true,
      taskAction: true,
      taskStatus: true,
    },
  });

  if (!task) {
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Task not found or you do not have permission",
    });
  }
  if (!task.taskAction) {
    throw new TRPCError({
      code: "PRECONDITION_FAILED",
      message: "This task has no executable action",
    });
  }
  if (task.taskStatus === "DONE") {
    throw new TRPCError({
      code: "PRECONDITION_FAILED",
      message: "This task is already completed",
    });
  }
  if (task.taskStatus === "DISMISSED") {
    throw new TRPCError({
      code: "PRECONDITION_FAILED",
      message: "This task has been dismissed",
    });
  }

  const parsed = parseTaskAction(task.taskAction);
  if (!parsed) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Invalid taskAction format: ${task.taskAction}`,
    });
  }

  const handler = getTaskAction(parsed.action);
  if (!handler) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Unknown action: ${parsed.action}`,
    });
  }

  const currentUser = requireNotificationDbUser(ctx);
  const permissions = resolvePermissions(
    currentUser.systemRole as import("@capakraken/shared").SystemRole,
    currentUser.permissionOverrides as import("@capakraken/shared").PermissionOverrides | null,
    ctx.roleDefaults ?? undefined,
  );
  if (handler.permission && !permissions.has(handler.permission as PermissionKey)) {
    throw new TRPCError({
      code: "FORBIDDEN",
      message: `Permission denied: you need "${handler.permission}" to perform this action`,
    });
  }

  const transaction = requireTaskActionTransaction(ctx.db);
  const { completedTask, actionResult } = await transaction(async (tx) => {
    const actionResult = await handler.execute(parsed.entityId, tx as typeof ctx.db, userId);
    if (!actionResult.success) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: actionResult.message,
      });
    }

    const completedTask = await tx.notification.update({
      where: { id: input.id },
      data: {
        taskStatus: "DONE",
        completedAt: new Date(),
        completedBy: userId,
      },
    });

    return {
      completedTask,
      actionResult,
    };
  });

  emitTaskCompleted(task.userId, task.id);
  if (task.assigneeId && task.assigneeId !== task.userId) {
    emitTaskCompleted(task.assigneeId, task.id);
  }

  return {
    task: completedTask,
    actionResult,
  };
}

export async function createTask(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof CreateTaskInputSchema>,
) {
  const senderId = requireNotificationDbUser(ctx).id;
  let notificationId: string;
  try {
    notificationId = await createNotification({
      db: ctx.db,
      userId: input.userId,
      type: "TASK_CREATED",
      category: "TASK",
      taskStatus: "OPEN",
      title: input.title,
      priority: input.priority,
      senderId,
      channel: input.channel,
      body: input.body,
      dueDate: input.dueDate,
      taskAction: input.taskAction,
      entityId: input.entityId,
      entityType: input.entityType,
      link: input.link,
    });
  } catch (error) {
    rethrowNotificationReferenceError(error, "task");
  }

  emitTaskAssigned(input.userId, notificationId);

  if (input.channel === "email" || input.channel === "both") {
    void sendNotificationEmail(ctx.db, input.userId, input.title, input.body);
  }

  return ctx.db.notification.findUnique({ where: { id: notificationId } });
}

export async function assignTask(
  ctx: NotificationProcedureContext,
  input: z.infer<typeof AssignTaskInputSchema>,
) {
  const existing = await findUniqueOrThrow(
    ctx.db.notification.findUnique({ where: { id: input.id } }),
    "Task",
  );

  if (existing.category !== "TASK" && existing.category !== "APPROVAL") {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: "Only tasks and approvals can be assigned",
    });
  }

  let updated;
  try {
    updated = await ctx.db.notification.update({
      where: { id: input.id },
      data: { assigneeId: input.assigneeId },
    });
  } catch (error) {
    for (const candidate of getNotificationErrorCandidates(error)) {
      if (candidate.code === "P2025") {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "Task not found",
          cause: error,
        });
      }
    }
    rethrowNotificationReferenceError(error, "task");
  }

  emitTaskAssigned(input.assigneeId, updated.id);
  return updated;
}
