/**
 * Per-language partial GIN provisioning.
 *
 * Creates a partial GIN index on deal_translations.search for a given locale.
 * Called after upsertLanguage when searchConfig changes or a new language is added.
 *
 * DROP + CREATE is idempotent — DROP IF EXISTS prevents errors on first run.
 * Safe from SQL injection: code and searchConfig are validated via KNOWN_REGCONFIGS
 * allowlist and SAFE_CODE regex before this function is called.
 *
 * Note: sql.raw is required because index names and regconfig values are not
 * bindable in PostgreSQL. The SAFE_CODE + SAFE_REGCONFIG guards below are the
 * only injection prevention needed — character classes are tight.
 */

import { sql } from 'drizzle-orm';
import type { DrizzleDb } from '@/server/db/client.js';
import { KNOWN_REGCONFIGS } from '@/server/schemas/languages.js';

const SAFE_CODE = /^[a-z]{2,3}(-[A-Z]{2})?$/;
const SAFE_INDEX_PART = /^[a-z0-9_]{1,40}$/;

type KnownRegconfig = (typeof KNOWN_REGCONFIGS)[number];

/**
 * Provision (or re-provision) a partial GIN index for a language locale.
 *
 * @param db          - DrizzleDb (supports .execute for raw DDL)
 * @param code        - Language BCP-47 code, e.g. "en", "ar", "pt-BR"
 * @param searchConfig - One of KNOWN_REGCONFIGS
 */
export async function provisionLanguageGinIndex(
  db: DrizzleDb,
  code: string,
  searchConfig: KnownRegconfig,
): Promise<void> {
  if (!SAFE_CODE.test(code)) throw new Error(`provisionLanguageGinIndex: invalid code "${code}"`);
  if (!(KNOWN_REGCONFIGS as readonly string[]).includes(searchConfig)) {
    throw new Error(`provisionLanguageGinIndex: invalid searchConfig "${searchConfig}"`);
  }

  // Normalise code for use in index name: "pt-BR" → "pt_br"
  const codePart = code.toLowerCase().replace('-', '_');
  if (!SAFE_INDEX_PART.test(codePart)) {
    throw new Error(`provisionLanguageGinIndex: unsafe codePart "${codePart}"`);
  }

  const indexName = `deal_translations_search_${codePart}_idx`;

  // DROP first (idempotent) then CREATE with the current searchConfig.
  // This handles both the "new language" and "searchConfig changed" cases.
  await db.execute(sql.raw(`DROP INDEX IF EXISTS ${indexName}`));

  // `search` is a text column holding a serialized tsvector (populated by
  // recomputeSearchTsvector). Cast to tsvector so the GIN index uses tsvector_ops:
  // a plain gin(search) over text has no default operator class (SQLSTATE 42704).
  await db.execute(
    sql.raw(
      `CREATE INDEX ${indexName} ON deal_translations USING gin((search::tsvector)) WHERE locale = '${code}'`,
    ),
  );
}
