import { TRPCError } from "@trpc/server";
import { z } from "zod";

import * as memberRepo from "@kan/db/repository/member.repo";
import * as permissionRepo from "@kan/db/repository/permission.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import type { Permission } from "@kan/shared";
import { allPermissions } from "@kan/shared";

import { createTRPCRouter, protectedProcedure } from "../trpc";
import {
  assertCanManageMember,
  assertPermission,
  getMemberEffectivePermissions,
  getUserPermissions,
} from "../utils/permissions";

const permissionsList = [...allPermissions] as [string, ...string[]];

export const permissionRouter = createTRPCRouter({
  getMyPermissions: protectedProcedure
    .meta({
      openapi: {
        summary: "Get my permissions",
        method: "GET",
        path: "/workspaces/{workspacePublicId}/permissions/me",
        description: "Get the current user's permissions in a workspace",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
      }),
    )
    .output(
      z.object({
        permissions: z.array(z.string()),
        role: z.string(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      const result = await getUserPermissions(ctx.db, userId, workspace.id);

      if (!result) {
        throw new TRPCError({
          message: "You are not a member of this workspace",
          code: "FORBIDDEN",
        });
      }

      return result;
    }),
  getMemberPermissions: protectedProcedure
    .meta({
      openapi: {
        summary: "Get member permissions",
        method: "GET",
        path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/permissions",
        description: "Get a specific member's permissions in a workspace",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
        memberPublicId: z.string().min(12),
      }),
    )
    .output(
      z.object({
        memberPublicId: z.string(),
        role: z.string(),
        permissions: z.array(z.string()),
        overrides: z.array(
          z.object({
            permission: z.string(),
            granted: z.boolean(),
          }),
        ),
      }),
    )
    .query(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      // Check user has permission to view member permissions
      await assertPermission(ctx.db, userId, workspace.id, "member:view");

      const member = await memberRepo.getByPublicId(
        ctx.db,
        input.memberPublicId,
      );

      if (!member || member.workspaceId !== workspace.id) {
        throw new TRPCError({
          message: "Member not found",
          code: "NOT_FOUND",
        });
      }

      const effectivePermissions = await getMemberEffectivePermissions(
        ctx.db,
        member.id,
        member.roleId ?? null,
        member.role,
      );
      const overrides = await permissionRepo.getMemberPermissionOverrides(
        ctx.db,
        member.id,
      );

      return {
        memberPublicId: member.publicId,
        role: member.role,
        permissions: effectivePermissions,
        overrides: overrides.map((o) => ({
          permission: o.permission,
          granted: o.granted,
        })),
      };
    }),
  grantPermission: protectedProcedure
    .meta({
      openapi: {
        summary: "Grant permission to member",
        method: "POST",
        path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/permissions/grant",
        description: "Grant a specific permission to a member",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
        memberPublicId: z.string().min(12),
        permission: z.enum(permissionsList),
      }),
    )
    .output(z.object({ success: z.boolean() }))
    .mutation(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      await assertPermission(ctx.db, userId, workspace.id, "member:edit");

      const member = await memberRepo.getByPublicId(
        ctx.db,
        input.memberPublicId,
      );

      if (!member || member.workspaceId !== workspace.id) {
        throw new TRPCError({
          message: "Member not found",
          code: "NOT_FOUND",
        });
      }

      await assertCanManageMember(ctx.db, userId, workspace.id, member.id);

      await permissionRepo.grantPermission(
        ctx.db,
        member.id,
        input.permission as Permission,
      );

      return { success: true };
    }),
  revokePermission: protectedProcedure
    .meta({
      openapi: {
        summary: "Revoke permission from member",
        method: "POST",
        path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/permissions/revoke",
        description: "Revoke a specific permission from a member",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
        memberPublicId: z.string().min(12),
        permission: z.enum(permissionsList),
      }),
    )
    .output(z.object({ success: z.boolean() }))
    .mutation(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      await assertPermission(ctx.db, userId, workspace.id, "member:edit");

      const member = await memberRepo.getByPublicId(
        ctx.db,
        input.memberPublicId,
      );

      if (!member || member.workspaceId !== workspace.id) {
        throw new TRPCError({
          message: "Member not found",
          code: "NOT_FOUND",
        });
      }

      await assertCanManageMember(ctx.db, userId, workspace.id, member.id);

      await permissionRepo.revokePermission(
        ctx.db,
        member.id,
        input.permission as Permission,
      );

      return { success: true };
    }),
  resetMemberPermissions: protectedProcedure
    .meta({
      openapi: {
        summary: "Reset member permissions to role defaults",
        method: "POST",
        path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/permissions/reset",
        description:
          "Clears all custom permission overrides for a member so their effective permissions come only from their role",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
        memberPublicId: z.string().min(12),
      }),
    )
    .output(z.object({ success: z.boolean() }))
    .mutation(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      await assertPermission(ctx.db, userId, workspace.id, "member:edit");

      const member = await memberRepo.getByPublicId(
        ctx.db,
        input.memberPublicId,
      );

      if (!member || member.workspaceId !== workspace.id) {
        throw new TRPCError({
          message: "Member not found",
          code: "NOT_FOUND",
        });
      }

      await assertCanManageMember(ctx.db, userId, workspace.id, member.id);

      await permissionRepo.clearMemberPermissionOverrides(ctx.db, member.id);

      return { success: true };
    }),
  resetWorkspaceMemberPermissions: protectedProcedure
    .meta({
      openapi: {
        summary: "Reset all member permission overrides in a workspace",
        method: "POST",
        path: "/workspaces/{workspacePublicId}/members/permissions/reset",
        description:
          "Clears all custom permission overrides for all members in a workspace so their effective permissions come only from their roles",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
      }),
    )
    .output(z.object({ success: z.boolean() }))
    .mutation(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      await assertPermission(ctx.db, userId, workspace.id, "member:edit");

      await permissionRepo.clearAllMemberPermissionOverridesForWorkspace(
        ctx.db,
        workspace.id,
      );

      return { success: true };
    }),
  getWorkspaceRoles: protectedProcedure
    .meta({
      openapi: {
        summary: "Get workspace roles",
        method: "GET",
        path: "/workspaces/{workspacePublicId}/roles",
        description: "Get all roles for a workspace",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
      }),
    )
    .output(
      z.object({
        roles: z.array(
          z.object({
            publicId: z.string().min(12),
            name: z.string(),
            description: z.string().nullable(),
            hierarchyLevel: z.number(),
            isSystem: z.boolean(),
          }),
        ),
      }),
    )
    .query(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      await assertPermission(ctx.db, userId, workspace.id, "member:view");

      const roles = await permissionRepo.getRolesByWorkspaceId(
        ctx.db,
        workspace.id,
      );

      return {
        roles: roles.map((role) => ({
          publicId: role.publicId,
          name: role.name,
          description: role.description ?? null,
          hierarchyLevel: role.hierarchyLevel,
          isSystem: role.isSystem,
        })),
      };
    }),
  getWorkspaceRolePermissions: protectedProcedure
    .meta({
      openapi: {
        summary: "Get workspace role permissions",
        method: "GET",
        path: "/workspaces/{workspacePublicId}/roles/permissions",
        description:
          "Get all roles for a workspace with their granted permissions",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
      }),
    )
    .output(
      z.object({
        roles: z.array(
          z.object({
            publicId: z.string().min(12),
            name: z.string(),
            permissions: z.array(z.string()),
          }),
        ),
      }),
    )
    .query(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      await assertPermission(ctx.db, userId, workspace.id, "member:view");

      const roles = await permissionRepo.getRolesByWorkspaceId(
        ctx.db,
        workspace.id,
      );

      const rolesWithPermissions = await Promise.all(
        roles.map(async (role) => {
          const permissionsForRole = await permissionRepo.getPermissionsByRoleId(
            ctx.db,
            role.id,
          );

          return {
            publicId: role.publicId,
            name: role.name,
            permissions: permissionsForRole,
          };
        }),
      );

      return {
        roles: rolesWithPermissions,
      };
    }),
  getRolePermissions: protectedProcedure
    .meta({
      openapi: {
        summary: "Get role permissions",
        method: "GET",
        path: "/workspaces/{workspacePublicId}/roles/{rolePublicId}/permissions",
        description: "Get permissions granted to a specific role in a workspace",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
        rolePublicId: z.string().min(12),
      }),
    )
    .output(
      z.object({
        rolePublicId: z.string(),
        name: z.string(),
        permissions: z.array(z.string()),
      }),
    )
    .query(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      await assertPermission(ctx.db, userId, workspace.id, "member:view");

      const role = await permissionRepo.getRoleByWorkspaceIdAndPublicId(
        ctx.db,
        workspace.id,
        input.rolePublicId,
      );

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

      const permissionsForRole = await permissionRepo.getPermissionsByRoleId(
        ctx.db,
        role.id,
      );

      return {
        rolePublicId: role.publicId,
        name: role.name,
        permissions: permissionsForRole,
      };
    }),
  grantRolePermission: protectedProcedure
    .meta({
      openapi: {
        summary: "Grant permission to role",
        method: "POST",
        path: "/workspaces/{workspacePublicId}/roles/{rolePublicId}/permissions/grant",
        description: "Grant a specific permission to a role",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
        rolePublicId: z.string().min(12),
        permission: z.enum(permissionsList),
      }),
    )
    .output(z.object({ success: z.boolean() }))
    .mutation(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      // Require ability to edit members/roles
      await assertPermission(ctx.db, userId, workspace.id, "member:edit");

      const role = await permissionRepo.getRoleByWorkspaceIdAndPublicId(
        ctx.db,
        workspace.id,
        input.rolePublicId,
      );

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

      if (role.name === "admin" && role.isSystem) {
        throw new TRPCError({
          message: "Admin role permissions cannot be modified",
          code: "FORBIDDEN",
        });
      }

      // Never allow non-admin roles to manage billing or delete workspace
      if (
        (input.permission === "workspace:manage" ||
          input.permission === "workspace:delete") &&
        role.name !== "admin"
      ) {
        throw new TRPCError({
          message:
            "Only the admin role can manage billing or delete the workspace",
          code: "FORBIDDEN",
        });
      }

      await permissionRepo.grantRolePermission(
        ctx.db,
        role.id,
        input.permission as Permission,
      );

      return { success: true };
    }),
  revokeRolePermission: protectedProcedure
    .meta({
      openapi: {
        summary: "Revoke permission from role",
        method: "POST",
        path: "/workspaces/{workspacePublicId}/roles/{rolePublicId}/permissions/revoke",
        description: "Revoke a specific permission from a role",
        tags: ["Permissions"],
        protect: true,
      },
    })
    .input(
      z.object({
        workspacePublicId: z.string().min(12),
        rolePublicId: z.string().min(12),
        permission: z.enum(permissionsList),
      }),
    )
    .output(z.object({ success: z.boolean() }))
    .mutation(async ({ ctx, input }) => {
      const userId = ctx.user?.id;

      if (!userId) {
        throw new TRPCError({
          message: "User not authenticated",
          code: "UNAUTHORIZED",
        });
      }

      const workspace = await workspaceRepo.getByPublicId(
        ctx.db,
        input.workspacePublicId,
      );

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

      await assertPermission(ctx.db, userId, workspace.id, "member:edit");

      const role = await permissionRepo.getRoleByWorkspaceIdAndPublicId(
        ctx.db,
        workspace.id,
        input.rolePublicId,
      );

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

      if (role.name === "admin" && role.isSystem) {
        throw new TRPCError({
          message: "Admin role permissions cannot be modified",
          code: "FORBIDDEN",
        });
      }

      await permissionRepo.revokeRolePermission(
        ctx.db,
        role.id,
        input.permission as Permission,
      );

      return { success: true };
    }),
});
