import fs from 'node:fs';
import crypto from 'node:crypto';

type PhpMyAdminExportItem =
  | { type: 'header'; version: string; comment?: string }
  | { type: 'database'; name: string }
  | {
      type: 'table';
      name: string;
      database: string;
      data: Record<string, any>[];
    };

type DocChunk = {
  id: string; // unique chunk id
  table: string; // e.g. "attraction"
  rowId: string; // e.g. attraction_id
  chunkIndex: number;
  text: string; // embedded text
  meta: Record<string, any>; // thumbnail, title, slug, venue, coords...
  vector: number[];
};

const OLLAMA_URL = process.env.OLLAMA_URL ?? 'http://localhost:11434';
const EMBED_MODEL = 'nomic-embed-text';

// Only embed public content tables (IMPORTANT)
const TABLE_ALLOWLIST = new Set([
  'attraction',
  'art',
  'best_of_africa',
  'event',
  // 'about_us',
  // 'ai_chat_info',
]);

// Optional runtime selection: TABLES=attraction,art
const TABLES_ENV = process.env.TABLES?.trim();
const TABLES_SELECTED = TABLES_ENV
  ? new Set(
      TABLES_ENV.split(',')
        .map((s) => s.trim())
        .filter(Boolean),
    )
  : null;

function shouldIndexTable(name: string): boolean {
  if (!TABLE_ALLOWLIST.has(name)) return false;
  if (TABLES_SELECTED && !TABLES_SELECTED.has(name)) return false;
  return true;
}

function stripHtml(input: string): string {
  return input
    .replace(/<\s*br\s*\/?\s*>/gi, '\n')
    .replace(/<\/p\s*>/gi, '\n')
    .replace(/<\/div\s*>/gi, '\n')
    .replace(/<[^>]*>/g, '')
    .replace(/&nbsp;/g, ' ')
    .replace(/&amp;/g, '&')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/\n{3,}/g, '\n\n')
    .trim();
}

function cleanValue(v: any): string {
  if (v == null) return '';
  const s = String(v);
  if (!s) return '';
  if (s.toLowerCase() === 'none' || s.toLowerCase() === 'null') return '';
  if (s.includes('<') && s.includes('>')) return stripHtml(s);
  return s.trim();
}

function inferRowId(table: string, row: Record<string, any>): string {
  const preferred = `${table}_id`;
  if (row[preferred] != null) return String(row[preferred]);

  const anyIdKey = Object.keys(row).find((k) =>
    k.toLowerCase().endsWith('_id'),
  );
  if (anyIdKey && row[anyIdKey] != null) return String(row[anyIdKey]);

  return crypto.createHash('sha1').update(JSON.stringify(row)).digest('hex');
}

function inferTitle(row: Record<string, any>): string {
  return (
    cleanValue(row.title) || cleanValue(row.name) || cleanValue(row.slug) || ''
  );
}

function chunkText(text: string, maxChars = 2200): string[] {
  if (text.length <= maxChars) return [text];

  const parts = text
    .split(/\n{2,}/g)
    .map((p) => p.trim())
    .filter(Boolean);

  const chunks: string[] = [];
  let buf = '';

  for (const p of parts) {
    if ((buf + '\n\n' + p).length > maxChars) {
      if (buf) chunks.push(buf);
      buf = p;
    } else {
      buf = buf ? `${buf}\n\n${p}` : p;
    }
  }
  if (buf) chunks.push(buf);

  if (chunks.length === 1 && chunks[0].length > maxChars) {
    const hard: string[] = [];
    for (let i = 0; i < text.length; i += maxChars)
      hard.push(text.slice(i, i + maxChars));
    return hard;
  }
  return chunks;
}

function buildEmbeddingText(
  table: string,
  row: Record<string, any>,
): { baseText: string; meta: Record<string, any> } {
  const rowId = inferRowId(table, row);
  const title = inferTitle(row);

  const description =
    cleanValue(row.description) ||
    cleanValue(row.mission) ||
    cleanValue(row.vision) ||
    cleanValue(row.goal) ||
    cleanValue(row.objectives) ||
    cleanValue(row.description_text) ||
    '';

  const thumbnail = cleanValue(row.thumbnail);
  const slug = cleanValue(row.slug);
  const venue = cleanValue(row.venue);
  const formatted_address = cleanValue(row.formatted_address);
  const open_days = cleanValue(row.open_days);
  const opened_at = cleanValue(row.opened_at);
  const closed_at = cleanValue(row.closed_at);
  const fee = cleanValue(row.fee);
  const hashtag = cleanValue(row.hashtag);
  const latitude = cleanValue(row.latitude);
  const longitude = cleanValue(row.longitude);

  const facebook = cleanValue(row.facebook);
  const instagram = cleanValue(row.instagram);
  const youtube = cleanValue(row.youtube);
  const twitter = cleanValue(row.twitter);
  const email_address = cleanValue(row.email_address);
  const phone_number = cleanValue(row.phone_number);
  const whatsapp = cleanValue(row.whatsapp);

  const lines: string[] = [];
  lines.push(`TYPE: ${table}`);
  if (title) lines.push(`TITLE: ${title}`);
  lines.push(`ID: ${rowId}`);
  if (slug) lines.push(`SLUG: ${slug}`);
  if (venue) lines.push(`VENUE: ${venue}`);
  if (formatted_address) lines.push(`ADDRESS: ${formatted_address}`);
  if (open_days || opened_at || closed_at)
    lines.push(`HOURS: ${open_days} ${opened_at}-${closed_at}`.trim());
  if (fee) lines.push(`FEE: ${fee}`);
  if (hashtag) lines.push(`TAGS: ${hashtag}`);
  if (latitude || longitude) lines.push(`COORDS: ${latitude}, ${longitude}`);
  if (description) lines.push(`DESCRIPTION:\n${description}`);

  const socials = [facebook, instagram, youtube, twitter].filter(Boolean);
  if (socials.length) lines.push(`SOCIALS: ${socials.join(' | ')}`);

  const contacts = [
    phone_number && `phone:${phone_number}`,
    whatsapp && `whatsapp:${whatsapp}`,
    email_address && `email:${email_address}`,
  ].filter(Boolean);

  if (contacts.length) lines.push(`CONTACT: ${contacts.join(' | ')}`);

  const meta: Record<string, any> = {
    table,
    rowId,
    title,
    slug,
    thumbnail,
    venue,
    formatted_address,
    latitude,
    longitude,
    fee,
    open_days,
    opened_at,
    closed_at,
    hashtag,
    facebook,
    instagram,
    youtube,
    twitter,
    phone_number,
    whatsapp,
    email_address,
  };

  return { baseText: lines.join('\n'), meta };
}

async function ollamaEmbed(
  texts: string[],
  timeoutMs = 120_000,
): Promise<number[][]> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const resp = await fetch(`${OLLAMA_URL}/api/embed`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        model: EMBED_MODEL,
        input: texts,
        truncate: true,
      }),
      signal: controller.signal,
    });

    if (!resp.ok) {
      const msg = await resp.text();
      throw new Error(`Ollama embed error ${resp.status}: ${msg}`);
    }

    const json = await resp.json();
    return json.embeddings as number[][];
  } finally {
    clearTimeout(timer);
  }
}

async function main() {
  const inputPath = process.argv[2] ?? 'doseeafrica.json';
  const outPath = process.argv[3] ?? 'index.json';

  const RESET = process.env.RESET === '1';
  const APPEND = process.env.APPEND === '1';

  if (RESET && fs.existsSync(outPath)) {
    fs.unlinkSync(outPath);
    console.log(`RESET=1 -> deleted existing ${outPath}`);
  }

  console.log('Starting ingest...', {
    inputPath,
    outPath,
    ollama: OLLAMA_URL,
    model: EMBED_MODEL,
    allowlist: Array.from(TABLE_ALLOWLIST),
    selectedTables: TABLES_SELECTED
      ? Array.from(TABLES_SELECTED)
      : '(all allowlisted)',
    append: APPEND,
  });

  console.time('read+parse');
  const raw = fs.readFileSync(inputPath, 'utf-8');
  console.log('Input size (MB):', (raw.length / 1024 / 1024).toFixed(1));
  const items = JSON.parse(raw) as PhpMyAdminExportItem[];
  console.timeEnd('read+parse');

  const tableCounts: Record<string, number> = {};
  for (const it of items) {
    if (it.type !== 'table') continue;
    tableCounts[it.name] = (tableCounts[it.name] ?? 0) + (it.data?.length ?? 0);
  }
  console.log('Tables found (rows):', tableCounts);

  const rowsToIndex: { table: string; row: Record<string, any> }[] = [];
  for (const it of items) {
    if (it.type !== 'table') continue;
    if (!shouldIndexTable(it.name)) continue;
    for (const row of it.data ?? []) rowsToIndex.push({ table: it.name, row });
  }
  console.log('rowsToIndex:', rowsToIndex.length);

  const pendingChunks: Omit<DocChunk, 'vector'>[] = [];
  for (const { table, row } of rowsToIndex) {
    const rowId = inferRowId(table, row);
    const { baseText, meta } = buildEmbeddingText(table, row);

    const chunks = chunkText(baseText, 2200);
    chunks.forEach((text, chunkIndex) => {
      pendingChunks.push({
        id: `${table}:${rowId}:${chunkIndex}`,
        table,
        rowId,
        chunkIndex,
        text,
        meta,
      });
    });
  }
  console.log('pendingChunks:', pendingChunks.length);

  if (pendingChunks.length === 0) {
    console.log(
      'No chunks to embed. Check TABLE_ALLOWLIST/TABLES vs your export table names.',
    );
    return;
  }

  const BATCH = Number(process.env.BATCH ?? 64);
  const out: DocChunk[] = [];

  console.time('embedding');
  for (let i = 0; i < pendingChunks.length; i += BATCH) {
    const batch = pendingChunks.slice(i, i + BATCH);

    console.log(
      `Embedding batch ${Math.floor(i / BATCH) + 1}/${Math.ceil(
        pendingChunks.length / BATCH,
      )} (size=${batch.length})`,
    );

    const vectors = await ollamaEmbed(batch.map((b) => b.text));
    vectors.forEach((vector, j) => out.push({ ...batch[j], vector }));

    console.log(
      `Embedded ${Math.min(i + BATCH, pendingChunks.length)}/${
        pendingChunks.length
      }`,
    );
  }
  console.timeEnd('embedding');

  // APPEND mode: merge with existing index.json and dedupe by id
  if (APPEND && fs.existsSync(outPath)) {
    const existing = JSON.parse(
      fs.readFileSync(outPath, 'utf-8'),
    ) as DocChunk[];
    const map = new Map<string, DocChunk>();

    for (const ch of existing) map.set(ch.id, ch);
    for (const ch of out) map.set(ch.id, ch);

    const merged = Array.from(map.values());
    fs.writeFileSync(outPath, JSON.stringify(merged));
    console.log(
      `Saved ${merged.length} chunks to ${outPath} (added ${
        merged.length - existing.length
      })`,
    );
  } else {
    fs.writeFileSync(outPath, JSON.stringify(out));
    console.log(`Saved ${out.length} chunks to ${outPath}`);
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
