import type { ActivityTenant } from "./audit.js";

/**
 * Minimal request shape consumed by Beignet auth ports.
 *
 * Framework adapters can pass richer request objects, but auth providers should
 * only rely on headers and the optional raw platform request unless they
 * declare a more specific `RequestLike` type.
 */
export interface AuthRequestLike {
  /**
   * Request headers used for cookies, bearer tokens, API keys, or provider
   * specific auth data.
   */
  headers: Headers;
  /**
   * Raw platform request, when the runtime has one available.
   */
  raw?: Request;
}

/**
 * Normalized authenticated session returned by an `AuthPort`.
 *
 * `user` is the app/provider user object. `session` can carry provider-specific
 * session state such as cookie session metadata or token claims.
 */
export interface AuthSession<User = unknown, Session = unknown> {
  user: User;
  session?: Session;
}

/**
 * Error thrown by auth helpers when a route or workflow requires a user but no
 * authenticated user is available.
 */
export class AuthUnauthorizedError extends Error {
  readonly code = "UNAUTHORIZED";
  /** HTTP status used when the server maps this framework error. */
  readonly status = 401;

  constructor(message = "Unauthorized") {
    super(message);
    this.name = "AuthUnauthorizedError";
  }
}

/**
 * Error thrown by tenant helpers when a workflow requires a tenant scope but
 * the current context has none.
 *
 * The server maps this to a framework-owned 403 response, mirroring how
 * `AuthUnauthorizedError` maps to a framework-owned 401.
 */
export class TenantRequiredError extends Error {
  readonly code = "TENANT_REQUIRED";
  readonly status = 403;

  constructor(message = "A tenant is required for this action.") {
    super(message);
    this.name = "TenantRequiredError";
  }
}

/**
 * Options accepted by the `requireX(ctx)` context helpers.
 */
export interface RequireOptions {
  /**
   * Create the error to throw instead of the framework default.
   */
  error?: () => unknown;
}

function throwRequired(
  options: RequireOptions | undefined,
  fallback: () => Error,
): never {
  throw options?.error ? options.error() : fallback();
}

/**
 * Return the authenticated session from `ctx.auth` or throw.
 *
 * Throws `AuthUnauthorizedError` (a framework-owned 401) by default. Pass
 * `options.error` to throw an app-owned error instead.
 *
 * @example
 * ```ts
 * const session = requireSession(ctx);
 * ```
 */
export function requireSession<Session extends AuthSession>(
  ctx: { auth?: Session | null },
  options?: RequireOptions,
): Session {
  if (!ctx.auth) {
    throwRequired(options, () => new AuthUnauthorizedError());
  }

  return ctx.auth;
}

/**
 * Return the authenticated user from `ctx.auth` or throw.
 *
 * The user type is inferred from the app's `ctx.auth` session. Throws
 * `AuthUnauthorizedError` (a framework-owned 401) by default.
 *
 * @example
 * ```ts
 * const user = requireUser(ctx);
 * ```
 */
export function requireUser<User>(
  ctx: { auth?: AuthSession<User> | null },
  options?: RequireOptions,
): User {
  return requireSession(ctx, options).user;
}

/**
 * Return the authenticated user's ID from `ctx.auth` or throw.
 *
 * Throws `AuthUnauthorizedError` (a framework-owned 401) by default.
 *
 * @example
 * ```ts
 * const userId = requireUserId(ctx);
 * ```
 */
export function requireUserId(
  ctx: { auth?: AuthSession<{ id: string }> | null },
  options?: RequireOptions,
): string {
  return requireUser(ctx, options).id;
}

/**
 * Return the tenant scope from `ctx.tenant` or throw.
 *
 * Throws `TenantRequiredError` (a framework-owned 403) by default. Pass
 * `options.error` to throw an app-owned error instead.
 *
 * @example
 * ```ts
 * const tenant = requireTenant(ctx);
 * ```
 */
export function requireTenant(
  ctx: { tenant?: ActivityTenant | null },
  options?: RequireOptions,
): ActivityTenant {
  if (!ctx.tenant) {
    throwRequired(options, () => new TenantRequiredError());
  }

  return ctx.tenant;
}

/**
 * Return the tenant ID from `ctx.tenant` or throw.
 *
 * Throws `TenantRequiredError` (a framework-owned 403) by default.
 *
 * @example
 * ```ts
 * const tenantId = requireTenantId(ctx);
 * ```
 */
export function requireTenantId(
  ctx: { tenant?: ActivityTenant | null },
  options?: RequireOptions,
): string {
  return requireTenant(ctx, options).id;
}

/**
 * App-facing authentication port.
 *
 * Implement this with a provider adapter such as Better Auth, a custom session
 * lookup, or a test fake. The port identifies the current user; it does not
 * decide whether that user may perform a business action. Keep authorization in
 * policies or use cases.
 */
export interface AuthPort<
  User = unknown,
  Session = unknown,
  RequestLike extends AuthRequestLike = AuthRequestLike,
> {
  /**
   * Return the current session, or `null` when the request is unauthenticated.
   */
  getSession(req: RequestLike): Promise<AuthSession<User, Session> | null>;
  /**
   * Return the current user, or `null` when unauthenticated.
   */
  getUser(req: RequestLike): Promise<User | null>;
  /**
   * Return the current user or throw `AuthUnauthorizedError`.
   */
  requireUser(req: RequestLike): Promise<User>;
}

type MaybePromise<T> = T | Promise<T>;

/**
 * Request-aware factory for a static auth session.
 */
export type StaticAuthSessionFactory<
  User,
  Session,
  RequestLike extends AuthRequestLike,
> = (req: RequestLike) => MaybePromise<AuthSession<User, Session> | null>;

/**
 * Create an auth port from a fixed session or request-aware session factory.
 *
 * This is useful for tests, examples, and simple apps. Production apps usually
 * use a provider-backed auth port that verifies cookies, tokens, or sessions.
 *
 * @example
 * ```ts
 * const auth = createStaticAuth({
 *   user: { id: "user_1", name: "Ada" },
 * });
 * ```
 *
 * @param session - Fixed session, `null`, or a function that resolves a session
 * from the request.
 * @returns An `AuthPort` implementation backed by the provided session source.
 */
export function createStaticAuth<
  User,
  Session = unknown,
  RequestLike extends AuthRequestLike = AuthRequestLike,
>(
  session:
    | AuthSession<User, Session>
    | null
    | StaticAuthSessionFactory<User, Session, RequestLike>,
): AuthPort<User, Session, RequestLike> {
  async function resolveSession(req: RequestLike) {
    return typeof session === "function" ? session(req) : session;
  }

  return {
    async getSession(req) {
      return resolveSession(req);
    },
    async getUser(req) {
      return (await resolveSession(req))?.user ?? null;
    },
    async requireUser(req) {
      const user = (await resolveSession(req))?.user ?? null;
      if (!user) {
        throw new AuthUnauthorizedError();
      }

      return user;
    },
  };
}

/**
 * Create an auth port that always treats requests as unauthenticated.
 *
 * Use this in tests or examples where auth is intentionally absent. It is not a
 * security boundary; it simply returns `null` from `getSession`/`getUser` and
 * throws from `requireUser`.
 *
 * @returns An `AuthPort` with no active session.
 */
export function createAnonymousAuth<
  User = unknown,
  Session = unknown,
  RequestLike extends AuthRequestLike = AuthRequestLike,
>(): AuthPort<User, Session, RequestLike> {
  return createStaticAuth<User, Session, RequestLike>(null);
}
