import { DataSource } from 'typeorm';

/**
 * Cross-corp scoping helper for /api/* endpoints.
 *
 * Pattern:
 *   const corpId = await resolveCorpId(this.dataSource, query.corp);
 *   const corpClause = corpSessionFilter(NEXT_PARAM_IDX, corpId);
 *   const sql = `... WHERE sf."createdAt" BETWEEN $1 AND $2${corpClause}`;
 *   const params = corpId === null ? [start, end] : [start, end, corpId];
 *
 * When `query.corp` is undefined → no filter, behaviour unchanged (firehose).
 * When `query.corp` resolves to a valid corp → AND sf.sessionId IN (... corp_id=$N)
 * is appended. Subquery hits idx_raw_events_corp_id_ts (corp_id leading).
 */


/**
 * Thrown by resolveCorpId when a non-empty slug doesn't match any corp.
 * Framework-agnostic — mapped to HTTP 400 by UnknownCorpSlugFilter.
 */
export class UnknownCorpSlugError extends Error {
  readonly slug: string;
  constructor(slug: string) {
    super(`Unknown corp slug: ${slug}`);
    this.name = 'UnknownCorpSlugError';
    this.slug = slug;
  }
}

const slugCache = new Map<string, number>();

/**
 * Resolve a corp slug to its surrogate id. Process-memoized.
 * Returns null when slug is undefined/empty (no filter requested).
 * Throws when slug is provided but doesn't match any corp.
 */
export async function resolveCorpId(
  ds: DataSource,
  slug: string | undefined,
): Promise<number | null> {
  if (slug === undefined || slug.length === 0) return null;

  const cached = slugCache.get(slug);
  if (cached !== undefined) return cached;

  let rows: Array<{ id: number }>;
  try {
    rows = await ds.query<Array<{ id: number }>>(
      `SELECT id FROM corps WHERE slug = $1 LIMIT 1`,
      [slug],
    );
  } catch (cause) {
    throw new Error(
      `corp-filter: failed to resolve slug '${slug}': ${cause instanceof Error ? cause.message : String(cause)}`,
      { cause },
    );
  }

  if (rows.length === 0) {
    throw new UnknownCorpSlugError(slug);
  }
  const id = Number(rows[0].id);
  slugCache.set(slug, id);
  return id;
}

/**
 * SQL fragment to append to a `WHERE` clause that filters by sessionId →
 * raw_events.corp_id. Returns an empty string when corpId is null, so the
 * caller can unconditionally interpolate it.
 *
 * The fragment uses the next parameter index passed in (`paramIdx`). The
 * caller is responsible for appending `corpId` to its parameter array iff
 * the fragment is non-empty (i.e. iff corpId !== null).
 *
 * Assumes the session_fingerprints table is aliased as `sf` in the caller's
 * SQL (which is the universal convention across the existing services).
 */
export function corpSessionFilter(paramIdx: number, corpId: number | null): string {
  if (corpId === null) return '';
  return ` AND sf."sessionId" IN (
    SELECT DISTINCT "sessionId" FROM raw_events WHERE corp_id = $${paramIdx}
  )`;
}

/**
 * Variant for queries that read raw_events directly (no session_fingerprints
 * involvement). Aliased `re` or unaliased — caller passes the alias.
 *
 * Returns ` AND <alias>.corp_id = $N` or empty.
 */
export function corpRawEventsFilter(
  paramIdx: number,
  corpId: number | null,
  alias: string = 'raw_events',
): string {
  if (corpId === null) return '';
  return ` AND ${alias}.corp_id = $${paramIdx}`;
}
