import { prisma } from "@capakraken/db";
import { resolvePermissions, PermissionKey, SystemRole } from "@capakraken/shared";
import { initTRPC, TRPCError } from "@trpc/server";
import { ZodError } from "zod";
import { loggingMiddleware } from "./middleware/logging.js";
import { apiRateLimiter } from "./middleware/rate-limit.js";

// Minimal Session type to avoid next-auth peer-dep in this package
interface Session {
  user?: { email?: string | null; name?: string | null; image?: string | null } | null;
  expires: string;
}

// ─── Context ──────────────────────────────────────────────────────────────────

export interface TRPCContext {
  session: Session | null;
  db: typeof prisma;
  dbUser: { id: string; systemRole: string; permissionOverrides: unknown } | null;
  roleDefaults: Record<string, PermissionKey[]> | null;
  requestId?: string;
}

// Cache role defaults for 60 seconds to avoid DB hit on every request
let _roleDefaultsCache: Record<string, PermissionKey[]> | null = null;
let _roleDefaultsCacheTime = 0;
const ROLE_DEFAULTS_TTL = 60_000;

export async function loadRoleDefaults(): Promise<Record<string, PermissionKey[]>> {
  const now = Date.now();
  if (_roleDefaultsCache && now - _roleDefaultsCacheTime < ROLE_DEFAULTS_TTL) {
    return _roleDefaultsCache;
  }
  const configs = await prisma.systemRoleConfig.findMany({
    select: { role: true, defaultPermissions: true },
  });
  const map: Record<string, PermissionKey[]> = {};
  for (const c of configs) {
    map[c.role] = c.defaultPermissions as PermissionKey[];
  }
  _roleDefaultsCache = map;
  _roleDefaultsCacheTime = now;
  return map;
}

/** Invalidate the role defaults cache (call after updating SystemRoleConfig) */
export function invalidateRoleDefaultsCache(): void {
  _roleDefaultsCache = null;
  _roleDefaultsCacheTime = 0;
}

export function createTRPCContext(opts: {
  session: Session | null;
  dbUser?: { id: string; systemRole: string; permissionOverrides: unknown } | null;
  roleDefaults?: Record<string, PermissionKey[]> | null;
}): TRPCContext {
  return {
    session: opts.session,
    db: prisma,
    dbUser: opts.dbUser ?? null,
    roleDefaults: opts.roleDefaults ?? null,
  };
}

// ─── tRPC Init ───────────────────────────────────────────────────────────────

const t = initTRPC.context<TRPCContext>().create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

// ─── Procedures ──────────────────────────────────────────────────────────────

export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;

/**
 * Public procedure — no authentication required.
 */
export const publicProcedure = t.procedure;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const withLogging = t.middleware(loggingMiddleware as any);
const isE2eTestMode = process.env["E2E_TEST_MODE"] === "true";

/**
 * Protected procedure — requires authenticated session AND a valid DB user record.
 * This prevents stale sessions from accessing data after the DB user is deleted.
 */
export const protectedProcedure = t.procedure.use(withLogging).use(async ({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: "UNAUTHORIZED", message: "Authentication required" });
  }
  if (!ctx.dbUser) {
    throw new TRPCError({ code: "UNAUTHORIZED", message: "User account not found" });
  }

  // Rate limit by user ID
  if (!isE2eTestMode) {
    const rateLimitResult = await apiRateLimiter(ctx.dbUser.id);
    if (!rateLimitResult.allowed) {
      throw new TRPCError({
        code: "TOO_MANY_REQUESTS",
        message: `Rate limit exceeded. Try again after ${rateLimitResult.resetAt.toISOString()}`,
      });
    }
  }

  return next({
    ctx: {
      ...ctx,
      session: ctx.session,
      user: ctx.session.user,
      dbUser: ctx.dbUser,
    },
  });
});

/**
 * Resource overview procedure — requires broad people-directory visibility.
 * Accepts explicit permission grants, not just elevated roles.
 */
export const resourceOverviewProcedure = protectedProcedure.use(({ ctx, next }) => {
  const user = ctx.dbUser;
  if (!user) throw new TRPCError({ code: "UNAUTHORIZED" });

  const permissions = resolvePermissions(
    user.systemRole as SystemRole,
    user.permissionOverrides as import("@capakraken/shared").PermissionOverrides | null,
    ctx.roleDefaults ?? undefined,
  );

  if (
    !permissions.has(PermissionKey.VIEW_ALL_RESOURCES)
    && !permissions.has(PermissionKey.MANAGE_RESOURCES)
  ) {
    throw new TRPCError({
      code: "FORBIDDEN",
      message: "Resource overview access required",
    });
  }

  return next({ ctx: { ...ctx, user, permissions } });
});

/**
 * Planning read procedure — requires the explicit broad planning read audience.
 */
export const planningReadProcedure = protectedProcedure.use(({ ctx, next }) => {
  const user = ctx.dbUser;
  if (!user) throw new TRPCError({ code: "UNAUTHORIZED" });

  const permissions = resolvePermissions(
    user.systemRole as SystemRole,
    user.permissionOverrides as import("@capakraken/shared").PermissionOverrides | null,
    ctx.roleDefaults ?? undefined,
  );

  if (!permissions.has(PermissionKey.VIEW_PLANNING)) {
    throw new TRPCError({
      code: "FORBIDDEN",
      message: "Planning read access required",
    });
  }

  return next({ ctx: { ...ctx, user, permissions } });
});

/**
 * Manager procedure — requires MANAGER or ADMIN role.
 */
export const managerProcedure = protectedProcedure.use(({ ctx, next }) => {
  const user = ctx.dbUser;
  if (!user) throw new TRPCError({ code: "UNAUTHORIZED" });
  const allowedRoles: string[] = [SystemRole.ADMIN, SystemRole.MANAGER];
  if (!allowedRoles.includes(user.systemRole)) {
    throw new TRPCError({ code: "FORBIDDEN", message: "Manager or Admin role required" });
  }
  const permissions = resolvePermissions(
    user.systemRole as SystemRole,
    user.permissionOverrides as import("@capakraken/shared").PermissionOverrides | null,
    ctx.roleDefaults ?? undefined,
  );
  return next({ ctx: { ...ctx, user, permissions } });
});

/**
 * Controller procedure — requires CONTROLLER, MANAGER, or ADMIN role.
 * Grants read-only access to financial and export data.
 */
export const controllerProcedure = protectedProcedure.use(({ ctx, next }) => {
  const user = ctx.dbUser;
  if (!user) throw new TRPCError({ code: "UNAUTHORIZED" });
  const allowed: SystemRole[] = [SystemRole.ADMIN, SystemRole.MANAGER, SystemRole.CONTROLLER];
  if (!allowed.includes(user.systemRole as SystemRole)) {
    throw new TRPCError({ code: "FORBIDDEN", message: "Controller access required" });
  }
  const permissions = resolvePermissions(
    user.systemRole as SystemRole,
    user.permissionOverrides as import("@capakraken/shared").PermissionOverrides | null,
    ctx.roleDefaults ?? undefined,
  );
  return next({ ctx: { ...ctx, user, permissions } });
});

/**
 * Admin procedure — requires ADMIN role only.
 */
export const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
  const user = ctx.dbUser;
  if (!user || user.systemRole !== SystemRole.ADMIN) {
    throw new TRPCError({ code: "FORBIDDEN", message: "Admin role required" });
  }
  const permissions = resolvePermissions(SystemRole.ADMIN, null, ctx.roleDefaults ?? undefined);
  return next({ ctx: { ...ctx, user, permissions } });
});

/**
 * requirePermission — throws FORBIDDEN if the ctx lacks the given permission.
 */
export function requirePermission(
  ctx: { permissions: Set<PermissionKey> },
  key: PermissionKey
): void {
  if (!ctx.permissions.has(key)) {
    throw new TRPCError({ code: "FORBIDDEN", message: `Permission required: ${key}` });
  }
}
