import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createAuditEntry } from "../lib/audit.js";
import { assertCommentEntityAccess, CommentEntityTypeSchema } from "../lib/comment-entity-registry.js";
import { stripHtml } from "../lib/strip-html.js";
import { createNotification } from "../lib/create-notification.js";
import type { TRPCContext } from "../trpc.js";
import {
  assertCommentManageableByActor,
  buildCommentCreateData,
  buildCommentMentionNotifications,
  commentBelongsToEntity,
  commentThreadInclude,
  commentWithAuthorInclude,
  parseCommentMentions,
} from "./comment-support.js";

export const CommentEntityInputSchema = z.object({
  entityType: CommentEntityTypeSchema,
  entityId: z.string(),
});

export const CommentMentionCandidatesInputSchema = CommentEntityInputSchema.extend({
  query: z.string().trim().max(100).optional(),
});

export const CreateCommentInputSchema = CommentEntityInputSchema.extend({
  parentId: z.string().optional(),
  body: z.string().min(1).max(10_000),
});

export const ResolveCommentInputSchema = z.object({
  id: z.string(),
  resolved: z.boolean().default(true),
});

export const DeleteCommentInputSchema = z.object({
  id: z.string(),
});

type CommentProcedureContext = Pick<TRPCContext, "db" | "dbUser" | "roleDefaults">;
type CreateCommentProcedureContext = Pick<TRPCContext, "db" | "dbUser" | "roleDefaults">;

export async function listComments(
  ctx: CommentProcedureContext,
  input: z.infer<typeof CommentEntityInputSchema>,
) {
  await assertCommentEntityAccess(ctx, input.entityType, input.entityId);

  return ctx.db.comment.findMany({
    where: {
      entityType: input.entityType,
      entityId: input.entityId,
      parentId: null,
    },
    include: commentThreadInclude,
    orderBy: { createdAt: "asc" },
  });
}

export async function countComments(
  ctx: CommentProcedureContext,
  input: z.infer<typeof CommentEntityInputSchema>,
) {
  await assertCommentEntityAccess(ctx, input.entityType, input.entityId);

  return ctx.db.comment.count({
    where: {
      entityType: input.entityType,
      entityId: input.entityId,
    },
  });
}

export async function listCommentMentionCandidates(
  ctx: CommentProcedureContext,
  input: z.infer<typeof CommentMentionCandidatesInputSchema>,
) {
  const normalizedQuery = input.query && input.query.length > 0 ? input.query : undefined;
  const policy = await assertCommentEntityAccess(ctx, input.entityType, input.entityId);
  return policy.listMentionCandidates(ctx, input.entityId, normalizedQuery);
}

export async function createComment(
  ctx: CreateCommentProcedureContext,
  input: z.infer<typeof CreateCommentInputSchema>,
) {
  const policy = await assertCommentEntityAccess(ctx, input.entityType, input.entityId);
  const authorId = ctx.dbUser?.id;
  if (!authorId) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }

  const sanitizedBody = stripHtml(input.body);
  const mentions = parseCommentMentions(sanitizedBody);

  if (input.parentId) {
    const parent = await ctx.db.comment.findUnique({
      where: { id: input.parentId },
      select: { id: true, entityType: true, entityId: true },
    });
    if (!parent) {
      throw new TRPCError({ code: "NOT_FOUND", message: "Parent comment not found" });
    }
    if (!commentBelongsToEntity({
      comment: parent,
      entityType: input.entityType,
      entityId: input.entityId,
    })) {
      throw new TRPCError({
        code: "BAD_REQUEST",
        message: "Parent comment does not belong to the requested entity",
      });
    }
  }

  const comment = await ctx.db.comment.create({
    data: buildCommentCreateData({
      entityType: input.entityType,
      entityId: input.entityId,
      parentId: input.parentId,
      authorId,
      body: sanitizedBody,
      mentions,
    }),
    include: commentWithAuthorInclude,
  });

  const mentionedUserIds = mentions.filter((id) => id !== authorId);
  if (mentionedUserIds.length > 0) {
    const authorName = comment.author.name ?? comment.author.email;
    const notifications = buildCommentMentionNotifications({
      mentionedUserIds,
      authorName,
      body: sanitizedBody,
      entityId: input.entityId,
      entityType: input.entityType,
      senderId: authorId,
      link: policy.buildLink(input.entityId),
    });

    await Promise.all(
      notifications.map((notification) =>
        createNotification({
          db: ctx.db,
          ...notification,
        })),
    );
  }

  void createAuditEntry({
    db: ctx.db,
    entityType: "Comment",
    entityId: comment.id,
    entityName: sanitizedBody.slice(0, 50),
    action: "CREATE",
    ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
    after: comment as unknown as Record<string, unknown>,
    source: "ui",
  });

  return comment;
}

async function findManageableComment(
  ctx: CommentProcedureContext,
  id: string,
) {
  const existing = await ctx.db.comment.findUnique({
    where: { id },
    select: { id: true, authorId: true, entityType: true, entityId: true },
  });

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

  await assertCommentEntityAccess(ctx, existing.entityType, existing.entityId);

  return existing;
}

export async function resolveComment(
  ctx: CommentProcedureContext,
  input: z.infer<typeof ResolveCommentInputSchema>,
) {
  const userId = ctx.dbUser?.id;
  if (!userId) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }

  const existing = await findManageableComment(ctx, input.id);

  assertCommentManageableByActor({
    authorId: existing.authorId,
    actorUserId: userId,
    isAdmin: ctx.dbUser?.systemRole === "ADMIN",
    action: "resolve",
  });

  const updated = await ctx.db.comment.update({
    where: { id: input.id },
    data: { resolved: input.resolved },
    include: commentWithAuthorInclude,
  });

  void createAuditEntry({
    db: ctx.db,
    entityType: "Comment",
    entityId: input.id,
    action: "UPDATE",
    ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
    summary: input.resolved ? "Resolved comment" : "Unresolved comment",
    after: updated as unknown as Record<string, unknown>,
    source: "ui",
  });

  return updated;
}

export async function deleteComment(
  ctx: CommentProcedureContext,
  input: z.infer<typeof DeleteCommentInputSchema>,
) {
  const userId = ctx.dbUser?.id;
  if (!userId) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }

  const existing = await findManageableComment(ctx, input.id);

  assertCommentManageableByActor({
    authorId: existing.authorId,
    actorUserId: userId,
    isAdmin: ctx.dbUser?.systemRole === "ADMIN",
    action: "delete",
  });

  await ctx.db.comment.deleteMany({
    where: { parentId: input.id },
  });

  await ctx.db.comment.delete({
    where: { id: input.id },
  });

  void createAuditEntry({
    db: ctx.db,
    entityType: "Comment",
    entityId: input.id,
    action: "DELETE",
    ...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
    before: existing as unknown as Record<string, unknown>,
    source: "ui",
  });
}
