import { TRPCError } from "@trpc/server";
import {
  COMMENT_ENTITY_LABELS,
  COMMENT_ENTITY_TYPE_VALUES,
  PermissionKey,
  resolvePermissions,
  SystemRole,
  type PermissionOverrides,
} from "@capakraken/shared";
import type { CommentEntityType } from "@capakraken/shared";
import { z } from "zod";
import type { TRPCContext } from "../trpc.js";
import { assertCanReadResource } from "./resource-access.js";

export const CommentEntityTypeSchema = z.enum(COMMENT_ENTITY_TYPE_VALUES);

export type CommentMentionCandidate = {
  id: string;
  name: string | null;
  email: string;
};

type CommentEntityPolicy = {
  assertAccess: (ctx: Pick<TRPCContext, "db" | "dbUser" | "roleDefaults">, entityId: string) => Promise<void>;
  buildLink: (entityId: string) => string;
  listMentionCandidates: (
    ctx: Pick<TRPCContext, "db" | "roleDefaults">,
    entityId: string,
    query?: string,
  ) => Promise<CommentMentionCandidate[]>;
};

const CONTROLLER_COMMENT_ROLES = new Set<SystemRole>([
  SystemRole.ADMIN,
  SystemRole.MANAGER,
  SystemRole.CONTROLLER,
]);
const RESOURCE_STAFF_PERMISSION_KEYS = new Set<PermissionKey>([
  PermissionKey.VIEW_ALL_RESOURCES,
  PermissionKey.MANAGE_RESOURCES,
]);
const MENTION_CANDIDATE_LIMIT = 20;

function buildMentionSearchWhere(query?: string) {
  if (!query) {
    return undefined;
  }

  return {
    OR: [
      { name: { contains: query, mode: "insensitive" as const } },
      { email: { contains: query, mode: "insensitive" as const } },
    ],
  };
}

async function listEstimateMentionCandidates(
  ctx: Pick<TRPCContext, "db">,
  _entityId: string,
  query?: string,
): Promise<CommentMentionCandidate[]> {
  return ctx.db.user.findMany({
    where: {
      systemRole: { in: Array.from(CONTROLLER_COMMENT_ROLES) },
      ...buildMentionSearchWhere(query),
    },
    select: {
      id: true,
      name: true,
      email: true,
    },
    orderBy: [{ name: "asc" }, { email: "asc" }],
    take: MENTION_CANDIDATE_LIMIT,
  });
}

async function listResourceMentionCandidates(
  ctx: Pick<TRPCContext, "db" | "roleDefaults">,
  entityId: string,
  query?: string,
): Promise<CommentMentionCandidate[]> {
  const resource = await ctx.db.resource.findUnique({
    where: { id: entityId },
    select: { userId: true },
  });
  if (!resource) {
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Resource not found",
    });
  }

  const mentionWhere = buildMentionSearchWhere(query);
  const users = await ctx.db.user.findMany({
    ...(mentionWhere ? { where: mentionWhere } : {}),
    select: {
      id: true,
      name: true,
      email: true,
      systemRole: true,
      permissionOverrides: true,
    },
    orderBy: [{ name: "asc" }, { email: "asc" }],
  });

  return users
    .filter((user) => {
      if (user.id === resource.userId) {
        return true;
      }

      const permissions = resolvePermissions(
        user.systemRole as SystemRole,
        user.permissionOverrides as PermissionOverrides | null,
        ctx.roleDefaults ?? undefined,
      );

      return Array.from(RESOURCE_STAFF_PERMISSION_KEYS).some((permission) =>
        permissions.has(permission),
      );
    })
    .slice(0, MENTION_CANDIDATE_LIMIT)
    .map(({ id, name, email }) => ({ id, name, email }));
}

async function assertEstimateCommentAccess(
  ctx: Pick<TRPCContext, "db" | "dbUser" | "roleDefaults">,
  entityId: string,
) {
  const role = ctx.dbUser?.systemRole as SystemRole | undefined;
  if (!role || !CONTROLLER_COMMENT_ROLES.has(role)) {
    throw new TRPCError({
      code: "FORBIDDEN",
      message: "Controller access required",
    });
  }

  const estimate = await ctx.db.estimate.findUnique({
    where: { id: entityId },
    select: { id: true },
  });
  if (!estimate) {
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Estimate not found",
    });
  }
}

async function assertResourceCommentAccess(
  ctx: Pick<TRPCContext, "db" | "dbUser" | "roleDefaults">,
  entityId: string,
) {
  const resource = await ctx.db.resource.findUnique({
    where: { id: entityId },
    select: { id: true },
  });
  if (!resource) {
    throw new TRPCError({
      code: "NOT_FOUND",
      message: "Resource not found",
    });
  }

  await assertCanReadResource(
    ctx,
    entityId,
    "You can only comment on your own resource unless you have staff access",
  );
}

const COMMENT_ENTITY_POLICIES: Record<CommentEntityType, CommentEntityPolicy> = {
  estimate: {
    assertAccess: assertEstimateCommentAccess,
    buildLink: (entityId) => `/estimates/${entityId}?tab=comments`,
    listMentionCandidates: listEstimateMentionCandidates,
  },
  resource: {
    assertAccess: assertResourceCommentAccess,
    buildLink: (entityId) => `/resources/${entityId}#comments`,
    listMentionCandidates: listResourceMentionCandidates,
  },
};

export function getSupportedCommentEntityTypes(): CommentEntityType[] {
  return [...COMMENT_ENTITY_TYPE_VALUES];
}

export function getSupportedCommentEntityLabelList(): string {
  return getSupportedCommentEntityTypes()
    .map((entityType) => COMMENT_ENTITY_LABELS[entityType])
    .join(", ");
}

export function getCommentToolEntityDescription(): string {
  return `Supported entity type. One of: ${getSupportedCommentEntityLabelList()}.`;
}

export function getCommentToolScopeSentence(): string {
  return `Supported comment entities: ${getSupportedCommentEntityLabelList()}. Estimate comments require controller/manager/admin visibility; resource comments follow resource detail visibility.`;
}

export function isSupportedCommentEntityType(value: string): value is CommentEntityType {
  return (COMMENT_ENTITY_TYPE_VALUES as readonly string[]).includes(value);
}

export function getCommentEntityPolicy(entityType: string): CommentEntityPolicy {
  if (!isSupportedCommentEntityType(entityType)) {
    throw new TRPCError({
      code: "BAD_REQUEST",
      message: `Comments are not supported for entity type: ${entityType}`,
    });
  }

  return COMMENT_ENTITY_POLICIES[entityType];
}

export async function assertCommentEntityAccess(
  ctx: Pick<TRPCContext, "db" | "dbUser" | "roleDefaults">,
  entityType: string,
  entityId: string,
) {
  const policy = getCommentEntityPolicy(entityType);
  await policy.assertAccess(ctx, entityId);
  return policy;
}
