import type { APIRoute } from 'astro';
import { query } from '../../../lib/db.js';

export const prerender = false;

/**
 * CPV autocomplete endpoint.
 *
 * GET /api/cpv/search?q=...&limit=20
 *
 * Returns top matches against `seap.cpv_codes`. Sorted by usage count
 * (number of announcements that have a `cpv_code` starting with the
 * given 8-digit code prefix). Counts are loaded once and cached
 * in-memory for ~1h — the dataset rarely changes.
 *
 * Result shape:
 *   [{ code, label, level, division, emoji, count }, ...]
 */

interface CpvRow {
  code: string;
  name_ro: string;
  level: number;
  division_code: string;
  emoji: string | null;
}

interface CpvHit {
  code: string;
  label: string;
  level: number;
  division: string;
  emoji: string | null;
  count: number;
}

const CACHE_TTL_MS = 60 * 60 * 1000; // 1h

// Per-code announcement count. Key = 8-digit code (no checksum).
let countCache: { map: Map<string, number>; loadedAt: number } | null = null;
let countLoadingPromise: Promise<Map<string, number>> | null = null;

async function loadCounts(): Promise<Map<string, number>> {
  // Use the announcement-level aggregate as the source of truth (one row per
  // 8-digit prefix). We cumulate to parents so that selecting a level-1
  // division shows the total under it.
  const direct = await query<{ code: string; n: number }>(
    `SELECT substr(cpv_code, 1, 8) AS code, COUNT(*)::int AS n
       FROM seap.announcements
      WHERE cpv_code IS NOT NULL
      GROUP BY substr(cpv_code, 1, 8)`,
  );
  const map = new Map<string, number>();
  for (const row of direct.rows) {
    map.set(row.code, Number(row.n));
  }
  // Roll up to parent prefixes so the count for `45000000` reflects every
  // announcement whose cpv_code begins with `45`, not just exact `45000000`.
  const rollup = new Map<string, number>();
  for (const [code, n] of map.entries()) {
    const prefixes = parentPrefixesOf(code);
    for (const p of prefixes) {
      rollup.set(p, (rollup.get(p) || 0) + n);
    }
  }
  // Also keep the exact-match count where it differs (rollup will already
  // include it; rollup wins).
  return rollup;
}

/**
 * For an 8-digit CPV code, yield the code itself and all of its
 * truncated parent prefixes that are valid in the CPV scheme
 * (8-digit codes are padded with zeros; meaningful parents are
 * obtained by replacing trailing digits with zeros).
 *
 * e.g. 45112340 → [45112340, 45112300, 45112000, 45110000, 45100000, 45000000]
 */
function parentPrefixesOf(code: string): string[] {
  const out = [code];
  // Truncate at positions 2,3,4,5,6,7 (right side filled with zeros)
  for (let keep = 7; keep >= 2; keep--) {
    const parent = code.substring(0, keep).padEnd(8, '0');
    if (parent !== out[out.length - 1]) {
      out.push(parent);
    }
  }
  return out;
}

async function getCounts(): Promise<Map<string, number>> {
  const now = Date.now();
  if (countCache && now - countCache.loadedAt < CACHE_TTL_MS) {
    return countCache.map;
  }
  // Coalesce concurrent loads
  if (countLoadingPromise) return countLoadingPromise;
  countLoadingPromise = loadCounts()
    .then((map) => {
      countCache = { map, loadedAt: Date.now() };
      return map;
    })
    .finally(() => {
      countLoadingPromise = null;
    });
  return countLoadingPromise;
}

// Per-query result cache (q + limit). Lightweight LRU-ish bounded map.
const queryCache = new Map<string, { hits: CpvHit[]; at: number }>();
const QUERY_CACHE_MAX = 200;

function cacheGet(key: string): CpvHit[] | null {
  const e = queryCache.get(key);
  if (!e) return null;
  if (Date.now() - e.at > CACHE_TTL_MS) {
    queryCache.delete(key);
    return null;
  }
  return e.hits;
}

function cacheSet(key: string, hits: CpvHit[]): void {
  if (queryCache.size >= QUERY_CACHE_MAX) {
    const firstKey = queryCache.keys().next().value;
    if (firstKey !== undefined) queryCache.delete(firstKey);
  }
  queryCache.set(key, { hits, at: Date.now() });
}

function isDigitPrefix(s: string): boolean {
  return /^\d{1,8}$/.test(s);
}

export const GET: APIRoute = async ({ url }) => {
  const qRaw = (url.searchParams.get('q') || '').trim();
  const limitRaw = Number(url.searchParams.get('limit') || '20');
  const limit = Math.max(1, Math.min(50, Number.isFinite(limitRaw) ? limitRaw : 20));

  if (qRaw.length < 2) {
    return json([]);
  }

  const cacheKey = `${qRaw.toLowerCase()}::${limit}`;
  const cached = cacheGet(cacheKey);
  if (cached) return json(cached);

  const counts = await getCounts();

  // Decide on search strategy:
  //  - If the query is digits-only and 1–8 chars long, do a code-prefix
  //    lookup (cheap PK/btree scan).
  //  - Otherwise do an ILIKE on name_ro (covered by idx_cpv_name_trgm).
  let rows: CpvRow[];
  if (isDigitPrefix(qRaw)) {
    const r = await query<CpvRow>(
      `SELECT code, name_ro, level, division_code, emoji
         FROM seap.cpv_codes
        WHERE code LIKE $1
        ORDER BY level ASC, code ASC
        LIMIT $2`,
      [`${qRaw}%`, Math.min(200, limit * 5)],
    );
    rows = r.rows;
  } else {
    // Match both with-diacritics ("construcţii") and without ("constructii")
    // by unaccent-ing both sides of the comparison. The trigram GIN index
    // still gets used because unaccent() is IMMUTABLE.
    const r = await query<CpvRow>(
      `SELECT code, name_ro, level, division_code, emoji
         FROM seap.cpv_codes
        WHERE unaccent(name_ro) ILIKE unaccent($1)
        LIMIT $2`,
      [`%${qRaw}%`, Math.min(200, limit * 5)],
    );
    rows = r.rows;
  }

  // Decorate with counts and rank by count (most-used first).
  const hits: CpvHit[] = rows
    .map((r) => ({
      code: r.code,
      label: r.name_ro,
      level: r.level,
      division: r.division_code,
      emoji: r.emoji,
      count: counts.get(r.code) || 0,
    }))
    .sort((a, b) => {
      // Codes with usage first, then by count desc, then by level asc,
      // then alphabetical on label.
      if (b.count !== a.count) return b.count - a.count;
      if (a.level !== b.level) return a.level - b.level;
      return a.label.localeCompare(b.label, 'ro');
    })
    .slice(0, limit);

  cacheSet(cacheKey, hits);
  return json(hits);
};

function json(body: unknown): Response {
  return new Response(JSON.stringify(body), {
    headers: {
      'Content-Type': 'application/json; charset=utf-8',
      // Allow shared-cache for 5 minutes; the response is non-personal.
      'Cache-Control': 'public, max-age=60, s-maxage=300',
    },
  });
}
