// Client-safe shim for the dashboard route builder. The actual server
// implementation lives in dashboardBuilder.server.ts; the wrappers here
// just return closures that lazily import that impl on first invocation.
//
// Why split: routes use `export const loader = dashboardLoader(...)` at
// module top-level. Remix's dev build preserves the top-level call when
// resolving the loader export, so the import target needs to exist on
// the client even though the closure body never executes there. A
// `.server.ts` file is excluded from the client bundle, which would
// resolve `dashboardLoader` to undefined and crash with
// "dashboardLoader is not a function" on first navigation. Keeping this
// file non-`.server` puts the wrappers in the client bundle as
// effectively no-op closures (they're never called there), and the
// closure body's dynamic import only resolves at server runtime.

import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import type { RbacAbility, RbacResource } from "@trigger.dev/rbac";
import type { z } from "zod";

type AnyZodSchema = z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;

type InferZod<T> = T extends z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>
  ? z.infer<T>
  : undefined;

export type SessionUser = {
  id: string;
  email: string;
  name: string | null;
  displayName: string | null;
  avatarUrl: string | null;
  admin: boolean;
  confirmedBasicDetails: boolean;
  isImpersonating: boolean;
};

// `requireSuper: true` enforces ability.canSuper(). Otherwise an explicit
// action + resource pair is checked via ability.can(...). On failure the
// builder throws a permission-denied 403, rendered as the permission panel by
// the nearest route ErrorBoundary (RouteErrorDisplay), unless
// `unauthorizedRedirect` is set; `message` customizes the panel copy.
export type AuthorizationOption =
  | { requireSuper: true; message?: string }
  | {
      action: string;
      resource: RbacResource | RbacResource[];
      message?: string;
    };

// Plugin-side scope: whatever the route's `context` returns must include
// these (or just be `{}` when the route doesn't scope by org/project).
// rbac.authenticateSession reads them off the value to filter UserRole.
type AuthScope = { organizationId?: string; projectId?: string };

export type DashboardLoaderOptions<TParams, TSearchParams, TContext extends AuthScope> = {
  params?: TParams;
  searchParams?: TSearchParams;
  // Resolves any per-request data the handler + auth check both need
  // (typically org/project lookups from URL params). The returned object
  // is fed to `rbac.authenticateSession` as the auth scope AND passed
  // through to the handler in `args.context`, so the route does each
  // lookup once.
  context?: (params: InferZod<TParams>, request: Request) => TContext | Promise<TContext>;
  authorization?: AuthorizationOption;
  // Where to send unauthenticated requests. Defaults to /login with a
  // redirectTo back to the original path.
  loginRedirect?: string;
  // Where to send users who pass auth but fail the ability check. Defaults
  // to "/" (the home page).
  unauthorizedRedirect?: string;
};

export type DashboardLoaderHandlerArgs<TParams, TSearchParams, TContext> = {
  params: InferZod<TParams>;
  searchParams: InferZod<TSearchParams>;
  user: SessionUser;
  ability: RbacAbility;
  context: TContext;
  request: Request;
};

export function dashboardLoader<
  TParams extends AnyZodSchema | undefined = undefined,
  TSearchParams extends AnyZodSchema | undefined = undefined,
  TContext extends AuthScope = AuthScope,
  TReturn extends Response = Response,
>(
  options: DashboardLoaderOptions<TParams, TSearchParams, TContext>,
  handler: (args: DashboardLoaderHandlerArgs<TParams, TSearchParams, TContext>) => Promise<TReturn>
) {
  return async function loader({ request, params }: LoaderFunctionArgs): Promise<TReturn> {
    // Server-only — see comment at top. Node caches the module after the
    // first call, so the dynamic import is effectively free past warmup.
    const { authenticateAndAuthorize } = await import("./dashboardBuilder.server");
    const result = await authenticateAndAuthorize(request, params, options);
    if (!result.ok) throw result.response;

    return handler({
      params: result.params as InferZod<TParams>,
      searchParams: result.searchParams as InferZod<TSearchParams>,
      user: result.user,
      ability: result.ability,
      context: result.context as TContext,
      request,
    });
  };
}

export type DashboardActionOptions<
  TParams,
  TSearchParams,
  TContext extends AuthScope,
> = DashboardLoaderOptions<TParams, TSearchParams, TContext>;

export type DashboardActionHandlerArgs<TParams, TSearchParams, TContext> =
  DashboardLoaderHandlerArgs<TParams, TSearchParams, TContext>;

export function dashboardAction<
  TParams extends AnyZodSchema | undefined = undefined,
  TSearchParams extends AnyZodSchema | undefined = undefined,
  TContext extends AuthScope = AuthScope,
  TReturn extends Response = Response,
>(
  options: DashboardActionOptions<TParams, TSearchParams, TContext>,
  handler: (args: DashboardActionHandlerArgs<TParams, TSearchParams, TContext>) => Promise<TReturn>
) {
  return async function action({ request, params }: ActionFunctionArgs): Promise<TReturn> {
    const { authenticateAndAuthorize } = await import("./dashboardBuilder.server");
    const result = await authenticateAndAuthorize(request, params, options);
    if (!result.ok) throw result.response;

    return handler({
      params: result.params as InferZod<TParams>,
      searchParams: result.searchParams as InferZod<TSearchParams>,
      user: result.user,
      ability: result.ability,
      context: result.context as TContext,
      request,
    });
  };
}
