import { isMainThread } from 'node:worker_threads';
import { AsyncLocalStorage } from 'node:async_hooks';
import {
  logger,
  configCapability,
  SystemCapabilities,
  readConfigCapability,
} from '@librechat/data-schemas';
import type { SystemCapability, ConfigSection } from '@librechat/data-schemas';
import type { NextFunction, Response } from 'express';
import type { Types, ClientSession } from 'mongoose';
import type { ResolvedPrincipal } from '~/types/principal';
import type { ServerRequest } from '~/types/http';

interface CapabilityDeps {
  getUserPrincipals: (
    params: { userId: string | Types.ObjectId; role?: string | null },
    session?: ClientSession,
  ) => Promise<ResolvedPrincipal[]>;
  hasCapabilityForPrincipals: (params: {
    principals: ResolvedPrincipal[];
    capability: SystemCapability;
    tenantId?: string;
  }) => Promise<boolean>;
}

export interface CapabilityUser {
  id: string;
  role: string;
  tenantId?: string;
}

interface CapabilityStore {
  principals: Map<string, ResolvedPrincipal[]>;
  results: Map<string, boolean>;
}

export type HasCapabilityFn = (
  user: CapabilityUser,
  capability: SystemCapability,
) => Promise<boolean>;

export type RequireCapabilityFn = (
  capability: SystemCapability,
) => (req: ServerRequest, res: Response, next: NextFunction) => Promise<void>;

export type HasConfigCapabilityFn = (
  user: CapabilityUser,
  section: ConfigSection | null,
  verb?: 'manage' | 'read',
) => Promise<boolean>;

/**
 * Per-request store for caching resolved principals and capability check results.
 * When running inside an Express request (via `capabilityContextMiddleware`),
 * duplicate `hasCapability` calls within the same request are served from
 * the in-memory Map instead of hitting the database again.
 * Outside a request context (background jobs, tests), the store is undefined
 * and every check falls through to the database — correct behavior.
 */
export const capabilityStore = new AsyncLocalStorage<CapabilityStore>();

export function capabilityContextMiddleware(
  _req: ServerRequest,
  _res: Response,
  next: NextFunction,
): void {
  if (!isMainThread) {
    logger.error(
      '[capabilityContextMiddleware] Mounted in a worker thread — ' +
        'ALS context will not propagate to the main thread or other workers. ' +
        'This middleware should only run in the main Express process.',
    );
  }
  capabilityStore.run({ principals: new Map(), results: new Map() }, next);
}

/**
 * Reads principals from the per-request ALS cache without side effects.
 * Returns `undefined` when called outside a request context or before
 * `requireCapability` has populated the cache for this user.
 */
export function getCachedPrincipals(user: CapabilityUser): ResolvedPrincipal[] | undefined {
  const store = capabilityStore.getStore();
  if (!store) {
    return undefined;
  }
  const key = `${user.id}:${user.role}:${user.tenantId ?? ''}`;
  return store.principals.get(key);
}

/**
 * Factory that creates `hasCapability` and `requireCapability` with injected
 * database methods. Follows the same dependency-injection pattern as
 * `generateCheckAccess`.
 */
export function generateCapabilityCheck(deps: CapabilityDeps): {
  hasCapability: HasCapabilityFn;
  requireCapability: RequireCapabilityFn;
  hasConfigCapability: HasConfigCapabilityFn;
} {
  const { getUserPrincipals, hasCapabilityForPrincipals } = deps;

  let workerWarned = false;

  async function hasCapability(
    user: CapabilityUser,
    capability: SystemCapability,
  ): Promise<boolean> {
    if (!isMainThread && !workerWarned) {
      workerWarned = true;
      logger.warn(
        '[hasCapability] Called from a worker thread — ALS context is unavailable. ' +
          'Capability checks will hit the database on every call (no per-request caching). ' +
          'If this is intentional, no action needed.',
      );
    }

    const store = capabilityStore.getStore();

    const resultKey = `${user.id}:${user.tenantId ?? ''}:${capability}`;
    const cached = store?.results.get(resultKey);
    if (cached !== undefined) {
      return cached;
    }

    const principalKey = `${user.id}:${user.role}:${user.tenantId ?? ''}`;
    let principals: ResolvedPrincipal[];
    const cachedPrincipals = store?.principals.get(principalKey);
    if (cachedPrincipals) {
      principals = cachedPrincipals;
    } else {
      principals = await getUserPrincipals({ userId: user.id, role: user.role });
      store?.principals.set(principalKey, principals);
    }

    const result = await hasCapabilityForPrincipals({
      principals,
      capability,
      tenantId: user.tenantId,
    });
    store?.results.set(resultKey, result);
    return result;
  }

  /**
   * Checks if a user can manage or read a specific config section.
   * First checks the broad capability (manage:configs / read:configs),
   * then falls back to the section-specific capability (manage:configs:<section>).
   */
  async function hasConfigCapability(
    user: CapabilityUser,
    section: ConfigSection | null,
    verb: 'manage' | 'read' = 'manage',
  ): Promise<boolean> {
    const broadCap =
      verb === 'manage' ? SystemCapabilities.MANAGE_CONFIGS : SystemCapabilities.READ_CONFIGS;
    if (section == null) {
      return hasCapability(user, broadCap);
    }
    if (await hasCapability(user, broadCap)) {
      return true;
    }
    const sectionCap =
      verb === 'manage' ? configCapability(section) : readConfigCapability(section);
    return hasCapability(user, sectionCap);
  }

  function requireCapability(capability: SystemCapability) {
    return async (req: ServerRequest, res: Response, next: NextFunction) => {
      try {
        if (!req.user) {
          res.status(401).json({ message: 'Authentication required' });
          return;
        }

        const id = req.user.id ?? req.user._id?.toString();
        if (!id) {
          res.status(401).json({ message: 'Authentication required' });
          return;
        }

        const user: CapabilityUser = {
          id,
          role: req.user.role ?? '',
          tenantId: (req.user as CapabilityUser).tenantId,
        };

        if (await hasCapability(user, capability)) {
          next();
          return;
        }

        logger.warn(`[requireCapability] Forbidden: user ${id} missing capability '${capability}'`);
        res.status(403).json({ message: 'Forbidden' });
      } catch (err) {
        logger.error(`[requireCapability] Error checking capability: ${capability}`, err);
        res.status(500).json({ message: 'Internal Server Error' });
      }
    };
  }

  return { hasCapability, requireCapability, hasConfigCapability };
}
