/**
 * ANI declarații de avere și interese — listing scraper (Stage 1).
 *
 * Walks the OLD portal (https://old-declaratii.integritate.eu/search.html)
 * and stores listing rows + PDF source URLs into ani.declaratii (no PDF
 * download yet — that's Stage 2 / cron/download-ani-pdfs.sh).
 *
 * Old portal is JSF/IceFaces. We:
 *   1. GET /search.html to obtain JSESSIONID + javax.faces.ViewState.
 *   2. POST /search.html with form data (search by name OR by institutie).
 *   3. Parse the resulting <table> for rows + DownloadServlet URLs.
 *   4. (TODO Stage 1.b) Paginate through results — JSF AJAX, needs ViewState
 *      cycling. For tonight's skeleton we accept first-page results only and
 *      iterate by date-range slicing in a follow-up loop.
 *
 * New portal (e-DAI, declaratii.integritate.eu) — separate scraper, will live
 * in src/scrape-ani-edai.ts. Needs Cloudflare Turnstile bypass (Playwright).
 *
 * Idempotent. Safe to re-run — INSERT … ON CONFLICT (source_portal, source_id)
 * DO UPDATE SET fetched_at + raw_*.
 *
 * Run:
 *   npx tsx src/scrape-ani-declarations.ts --query=Iohannis [--field=numePrenume]
 *   npx tsx src/scrape-ani-declarations.ts --judet=CLUJ --limit=100
 *
 * Designed for the cron model in cron/enrich-anaf.sh — passes DATABASE_URL
 * via env, runs detached docker, does not require host node install.
 */
import { Pool } from 'pg';

// ── Config ──────────────────────────────────────────────────────────────
const PORTAL = 'https://old-declaratii.integritate.eu';
const USER_AGENT =
  'gov-agreg/1.0 vreaudigital.ro (contact: m.tarau@beletage.ro)';

// CLI args (rough — same style as scrape-beneficiar-privat.ts)
const args = process.argv.slice(2);
const argQuery = args.find((a) => a.startsWith('--query='))?.split('=')[1] ?? '';
const argField =
  (args.find((a) => a.startsWith('--field='))?.split('=')[1] as
    | 'numePrenume'
    | 'institutia'
    | undefined) ?? 'numePrenume';
const argLimit = parseInt(
  args.find((a) => a.startsWith('--limit='))?.split('=')[1] ?? '0',
);
const argSleepMs = parseInt(
  args.find((a) => a.startsWith('--sleep-ms='))?.split('=')[1] ?? '700',
);
const argDryRun = args.includes('--dry-run');

if (!argQuery) {
  console.error(
    'Usage: scrape-ani-declarations.ts --query=<text> [--field=numePrenume|institutia] [--limit=N] [--sleep-ms=700] [--dry-run]',
  );
  process.exit(2);
}

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 4,
});

// ── HTTP session helpers ────────────────────────────────────────────────
interface JsfSession {
  jsessionid: string;
  viewState: string;
  cookies: string;
}

async function fetchWithRetry(
  url: string,
  init: RequestInit,
  retries = 3,
): Promise<Response | null> {
  for (let i = 0; i <= retries; i++) {
    try {
      const res = await fetch(url, {
        ...init,
        headers: {
          'User-Agent': USER_AGENT,
          Accept: 'text/html,application/xhtml+xml',
          ...(init.headers ?? {}),
        },
        signal: AbortSignal.timeout(30_000),
      });
      if (res.status >= 500 && i < retries) {
        await sleep(2000 * (i + 1));
        continue;
      }
      return res;
    } catch (e) {
      if (i < retries) {
        await sleep(2000 * (i + 1));
        continue;
      }
      console.error('fetch failed:', url, (e as Error).message);
      return null;
    }
  }
  return null;
}

function sleep(ms: number) {
  return new Promise((r) => setTimeout(r, ms));
}

async function startSession(): Promise<JsfSession | null> {
  const res = await fetchWithRetry(`${PORTAL}/search.html`, { method: 'GET' });
  if (!res || !res.ok) return null;
  const html = await res.text();
  const setCookie = res.headers.get('set-cookie') ?? '';
  const jsidMatch = setCookie.match(/JSESSIONID=([A-F0-9]+)/i);
  const viewStateMatch = html.match(
    /javax\.faces\.ViewState:0"\s+value="([^"]+)"/,
  );
  if (!jsidMatch || !viewStateMatch) {
    console.error('failed to extract JSESSIONID + ViewState');
    return null;
  }
  return {
    jsessionid: jsidMatch[1],
    viewState: viewStateMatch[1],
    cookies: `JSESSIONID=${jsidMatch[1]}`,
  };
}

async function searchPortal(
  session: JsfSession,
  query: string,
  field: 'numePrenume' | 'institutia',
): Promise<string | null> {
  const body = new URLSearchParams({
    form: 'form',
    'form:searchKey_input': query,
    'form:searchField_input': field,
    'form:submitButtonSS': 'cauta',
    'javax.faces.ViewState': session.viewState,
  });
  const res = await fetchWithRetry(`${PORTAL}/search.html`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Cookie: session.cookies,
    },
    body: body.toString(),
  });
  if (!res || !res.ok) return null;
  return await res.text();
}

// ── HTML row parsing ────────────────────────────────────────────────────
interface ListingRow {
  source_id: string;          // e.g. NTNTARTLNE_14636741
  source_url: string;         // full DownloadServlet URL
  raw_official_name: string;
  raw_institution: string | null;
  raw_function: string | null;
  raw_localitate: string | null;
  raw_judet: string | null;
  data_completare: string | null; // ISO date
  declaration_type: 'avere' | 'interese' | 'avere+interese';
}

/**
 * Crude HTML row extractor — IceFaces emits a deeply-nested table, but each
 * data row matches a stable pattern:
 *   <tr ...>
 *     <td ...><div ...>NAME</div></td>
 *     <td ...><div ...>INSTITUTIE</div></td>
 *     <td ...><div ...>FUNCTIE</div></td>
 *     <td ...><div ...>LOCALITATE</div></td>
 *     <td ...><div ...>JUDET</div></td>
 *     <td ...><div ...>DATA</div></td>
 *     <td ...><div ...>TIP</div></td>
 *     <td ...> ...DownloadServlet?fileName=X.pdf&uniqueIdentifier=Y... </td>
 *     <td ...> share buttons </td>
 *   </tr>
 *
 * We split on `</tr>` and pull cells with a regex.  Fragile to layout changes —
 * if ANI re-themes the portal we'll need to rewrite this.  Keep raw HTML hash
 * around for diff detection (TODO).
 */
function parseListingRows(html: string): ListingRow[] {
  const rows: ListingRow[] = [];
  // pull ONLY the result rows — IceFaces emits id="form:resultsTable_row_N"
  // and class="ui-datatable-even/odd"
  const rowRegex =
    /<tr[^>]*id="form:resultsTable_row_\d+"[^>]*>([\s\S]*?)<\/tr>/g;
  let m: RegExpExecArray | null;
  while ((m = rowRegex.exec(html)) !== null) {
    const tr = m[1];
    // each td
    const tdRegex = /<td[^>]*ui-col-(\d+)[^>]*>([\s\S]*?)<\/td>/g;
    const cells: Record<number, string> = {};
    let tm: RegExpExecArray | null;
    while ((tm = tdRegex.exec(tr)) !== null) {
      const col = parseInt(tm[1]);
      // strip nested HTML, keep plain text
      const text = tm[2]
        .replace(/<[^>]+>/g, ' ')
        .replace(/&amp;/g, '&')
        .replace(/&nbsp;/g, ' ')
        .replace(/&quot;/g, '"')
        .replace(/&#039;/g, "'")
        .replace(/\s+/g, ' ')
        .trim();
      cells[col] = text;
    }
    // download URL is in cell 7 (Vezi declaratie)
    const dlMatch = tr.match(
      /DownloadServlet\?fileName=([^&"]+)&(?:amp;)?uniqueIdentifier=([A-Z0-9_]+)/,
    );
    if (!dlMatch) continue;
    const fileName = dlMatch[1];
    const uniqueIdentifier = dlMatch[2];
    const sourceUrl = `${PORTAL}/DownloadServlet?fileName=${encodeURIComponent(
      fileName,
    )}&uniqueIdentifier=${uniqueIdentifier}`;

    const tipText = cells[6] ?? '';
    let declarationType: ListingRow['declaration_type'] = 'avere';
    if (/interes/i.test(tipText) && /avere/i.test(tipText))
      declarationType = 'avere+interese';
    else if (/interes/i.test(tipText)) declarationType = 'interese';

    // parse date — Romanian format dd-MM-yyyy or dd.MM.yyyy
    let dataCompletare: string | null = null;
    const dateMatch = (cells[5] ?? '').match(
      /(\d{1,2})[-./](\d{1,2})[-./](\d{4})/,
    );
    if (dateMatch) {
      const [, d, mo, y] = dateMatch;
      dataCompletare = `${y}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`;
    }

    rows.push({
      source_id: uniqueIdentifier,
      source_url: sourceUrl,
      raw_official_name: cells[0] ?? '',
      raw_institution: cells[1] || null,
      raw_function: cells[2] || null,
      raw_localitate: cells[3] || null,
      raw_judet: cells[4] || null,
      data_completare: dataCompletare,
      declaration_type: declarationType,
    });
  }
  return rows;
}

// ── DB helpers ──────────────────────────────────────────────────────────
async function upsertListings(rows: ListingRow[]): Promise<{
  inserted: number;
  updated: number;
}> {
  if (rows.length === 0) return { inserted: 0, updated: 0 };
  let inserted = 0;
  let updated = 0;
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    for (const r of rows) {
      // year is required NOT NULL — derive from data_completare or default
      // to current year if not parseable (rare)
      const year = r.data_completare
        ? parseInt(r.data_completare.slice(0, 4))
        : new Date().getFullYear();
      const result = await client.query(
        `INSERT INTO ani.declaratii
           (raw_official_name, raw_institution, raw_function, raw_localitate,
            raw_judet, year, declaration_type, data_completare,
            source_portal, source_url, source_id)
         VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'old',$9,$10)
         ON CONFLICT (source_portal, source_id) WHERE source_id IS NOT NULL
         DO UPDATE SET
           raw_official_name = EXCLUDED.raw_official_name,
           raw_institution   = EXCLUDED.raw_institution,
           raw_function      = EXCLUDED.raw_function,
           raw_localitate    = EXCLUDED.raw_localitate,
           raw_judet         = EXCLUDED.raw_judet,
           data_completare   = EXCLUDED.data_completare,
           source_url        = EXCLUDED.source_url
         RETURNING (xmax = 0) AS inserted`,
        [
          r.raw_official_name,
          r.raw_institution,
          r.raw_function,
          r.raw_localitate,
          r.raw_judet,
          year,
          r.declaration_type,
          r.data_completare,
          r.source_url,
          r.source_id,
        ],
      );
      if (result.rows[0]?.inserted) inserted++;
      else updated++;
    }
    await client.query('COMMIT');
  } catch (e) {
    await client.query('ROLLBACK');
    throw e;
  } finally {
    client.release();
  }
  return { inserted, updated };
}

// ── Main ────────────────────────────────────────────────────────────────
async function main() {
  console.log(
    `[ani-listing] starting | query="${argQuery}" field=${argField} limit=${argLimit} dryRun=${argDryRun}`,
  );
  const session = await startSession();
  if (!session) {
    console.error('[ani-listing] failed to start JSF session');
    process.exit(1);
  }
  console.log(
    `[ani-listing] session OK | JSESSIONID=${session.jsessionid.slice(0, 8)}…`,
  );

  await sleep(argSleepMs);

  const html = await searchPortal(session, argQuery, argField);
  if (!html) {
    console.error('[ani-listing] search failed');
    process.exit(1);
  }
  console.log(`[ani-listing] search response: ${html.length} bytes`);

  const rows = parseListingRows(html);
  console.log(`[ani-listing] parsed ${rows.length} rows on first page`);
  if (argLimit > 0 && rows.length > argLimit) rows.length = argLimit;

  if (argDryRun) {
    for (const r of rows.slice(0, 10)) {
      console.log(
        `  ${r.source_id} | ${r.raw_official_name} | ${r.raw_institution ?? '-'} | ${r.data_completare ?? '?'} | ${r.declaration_type}`,
      );
    }
    console.log(`[ani-listing] (dry-run, no DB writes)`);
    await pool.end();
    return;
  }

  const { inserted, updated } = await upsertListings(rows);
  console.log(`[ani-listing] DB: ${inserted} inserted, ${updated} updated`);

  // TODO Stage 1.b: pagination via JSF AJAX. First-page-only acceptable
  // for tonight's skeleton; production loop iterates date ranges (lună de
  // lună) until all results retrieved. See ANI-PLAN.md §4 Day 1.

  await pool.end();
}

main().catch((e) => {
  console.error('[ani-listing] fatal:', e);
  pool.end();
  process.exit(1);
});
