/**
 * Scraper Transparență Bugetară MFP — Faza 1: enumerare universul entităților.
 *
 * Sursă: https://extranet.anaf.mfinante.gov.ro/anaf/extranet/EXECUTIEBUGETARA
 *
 * ─── DE CE FAZA 1 e DOAR ENUMERARE ─────────────────────────────────────────
 * Aplicația oficială folosește IBM WebSphere Portal cu URL-uri stateful
 * (`!ut/p/a1/...`) și CAPTCHA imagine pe fiecare căutare. Pentru a descărca
 * un raport XML/XLSX pentru un (CUI × perioadă), trebuie:
 *   1. GET pagina de formular → primește URL stateful + cookie sesiune
 *   2. Trimite POST cu `seccode` (captcha rezolvat) + filtrele
 *   3. Server randează HTML cu link-uri ad-hoc spre fișiere XML/XLSX
 *      (link-urile expiră în câteva minute)
 *   4. GET fișierele și parse-uite
 *
 * Captcha solver e o dependință externă (2captcha / anti-captcha) cu cost
 * ~2-5 USD per 1.000 captcha. Pentru 13.7K entități × 12 luni × 5 ani × 2
 * tipuri raport ≈ 1.6M captcha-uri ≈ 4-8K USD pentru ingest istoric complet.
 * Asta e Faza 2.
 *
 * ─── CE FACE FAZA 1 (THIS) ──────────────────────────────────────────────────
 * 1. Enumerează (sector_bugetar × județ) prin endpoint-ul `populateEpAJAX`
 *    care NU cere captcha — întoarce JSON cu denumirile entităților publice.
 * 2. Idem pentru `populateOcpAJAX` → ordonatori principali (subset).
 * 3. INSERT INTO bugetar.entitate cu (entity_name, sector, județ).
 *    Volum așteptat: ~30K-50K rânduri (după dedup, ~13.7K UAT-uri unice +
 *    instituții subordonate).
 * 4. Fuzzy match denumire → CUI prin `firms.entities` (acolo unde avem deja
 *    13.8K UAT din SEAP cu CUI cunoscut). Asta deblochează Faza 2.
 *
 * ─── PARSER FXB-EXB-900 (TODO Faza 2) ───────────────────────────────────────
 * Documentație MFP: PDF "Structura fisier XML raport FXB-900" la
 *   https://mfinante.gov.ro/anaf/wcm/connect/dd57bcbd-3b79-4d40-a1a9-e54c824898b9/
 * Structura XML (din analiza manuală a sample-urilor publice):
 *   <RAPORT id="FXB-EXB-900" cui="..." an="2024" luna="12">
 *     <ENTITATE cui="" denumire="" sector_bugetar="" cod_judet=""/>
 *     <LINIE side="cheltuieli" capitol="5101" subcapitol="510102"
 *            paragraf="" articol="510101" aliniat="">
 *       <DENUMIRE>Cheltuieli de personal</DENUMIRE>
 *       <CREDITE_BUG_APROBATE_INI>...</CREDITE_BUG_APROBATE_INI>
 *       <CREDITE_BUG_APROBATE_DEF>...</CREDITE_BUG_APROBATE_DEF>
 *       <CREDITE_BUG_TRIM>...</CREDITE_BUG_TRIM>
 *       <ANGAJAMENTE_BUG>...</ANGAJAMENTE_BUG>
 *       <ANGAJAMENTE_LEG>...</ANGAJAMENTE_LEG>
 *       <PLATI>...</PLATI>      // = "executie cumulata"
 *     </LINIE>
 *     ...
 *   </RAPORT>
 * (Numele exacte ale tag-urilor diferă subtil între versiunile MFP — parserul
 *  trebuie să fie tolerant: lookup case-insensitive + fallback synonyms.)
 *
 * Run:
 *   npx tsx src/scrape-bugetar.ts --mode=enumerate
 *   npx tsx src/scrape-bugetar.ts --mode=enumerate --judet=CJ --sector=02
 *   npx tsx src/scrape-bugetar.ts --mode=match-cui          # Fuzzy match nume → CUI
 *   npx tsx src/scrape-bugetar.ts --mode=parse-xml --file=raport.xml --cui=... --period=2024-12
 */
import { Pool } from 'pg';

const SOURCE_HOST = 'https://extranet.anaf.mfinante.gov.ro';
const PORTAL_BASE = `${SOURCE_HOST}/anaf/extranet/EXECUTIEBUGETARA/Rapoarte_Forexe`;
// URL-ul stateful WebSphere e capturat din pagina formularului. Se reînnoiește
// la sesiune nouă — pentru robustețe, `bootstrapPortal()` va re-fetch-ui acest
// URL înainte de orice batch de cereri.
const POPULATE_EP_PATH_TPL =
  '!ut/p/a1/04_Sj9CPykssy0xPLMnMz0vMAfGjzOLdLTwMDC2DDbwMzPwtDBwDDJw8zPy8jQwsDIAKIoEKDHAARwPi9ONS4G5ESH-4fhReJSATwArwOLEgNzTCINNREQBLQVnx/dl5/d5/L2dBISEvZ0FBIS9nQSEh/pw/Z7_G8H019S0JO6PD0APPU139A00G3/res/id=populateEpAJAX/c=cacheLevelPage/=/';
const POPULATE_OCP_PATH_TPL = POPULATE_EP_PATH_TPL.replace('populateEpAJAX', 'populateOcpAJAX');

const USER_AGENT = 'gov-agreg/1.0 vreaudigital.ro (contact: m.tarau@beletage.ro)';

const SECTORS: Array<{ code: string; label: string }> = [
  { code: '01', label: 'Bugetul de stat' },
  { code: '02', label: 'Bugetul local' },
  { code: '03', label: 'Bugetul asigurarilor sociale de stat' },
  { code: '04', label: 'Bugetul fondului de somaj' },
  { code: '05', label: 'Bugetul fondului de sanatate (FNUASS)' },
];

const JUDETE: string[] = [
  'AB', 'AR', 'AG', 'BC', 'BH', 'BN', 'BT', 'BR', 'BV', 'B', 'BZ',
  'CL', 'CS', 'CJ', 'CT', 'CV', 'DB', 'DJ', 'GL', 'GR', 'GJ',
  'HR', 'HD', 'IL', 'IS', 'IF', 'MM', 'MH', 'MS', 'NT', 'OT',
  'PH', 'SJ', 'SM', 'SB', 'SV', 'TR', 'TM', 'TL', 'VL', 'VS', 'VN',
];

// ─── CLI args ───────────────────────────────────────────────────────────────
const args = process.argv.slice(2);
const argMode = args.find((a) => a.startsWith('--mode='))?.split('=')[1] || 'enumerate';
const argJudet = args.find((a) => a.startsWith('--judet='))?.split('=')[1] || '';
const argSector = args.find((a) => a.startsWith('--sector='))?.split('=')[1] || '';
const argLimit = parseInt(args.find((a) => a.startsWith('--limit='))?.split('=')[1] || '0', 10);
const argDelay = parseInt(args.find((a) => a.startsWith('--delay-ms='))?.split('=')[1] || '500', 10);
const argFile = args.find((a) => a.startsWith('--file='))?.split('=')[1] || '';
const argCui = args.find((a) => a.startsWith('--cui='))?.split('=')[1] || '';
const argPeriod = args.find((a) => a.startsWith('--period='))?.split('=')[1] || '';

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

// ─── Helpers ────────────────────────────────────────────────────────────────
function sleep(ms: number): Promise<void> {
  return new Promise((r) => setTimeout(r, ms));
}

function normalizeName(s: string): string {
  // Strip diacritice + lowercase + colaps spații, pentru fuzzy match.
  return s
    .normalize('NFD')
    .replace(/\p{M}/gu, '')
    .toLowerCase()
    .replace(/[^a-z0-9 ]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

interface FetchListOpts {
  sector: string;
  judet: string;
  endpoint: 'ep' | 'ocp';
}

/**
 * Fetch denumiri entități publice (sau ordonatori principali) prin endpoint-ul
 * autocomplete care NU cere captcha. Întoarce un JSON array de string-uri.
 */
async function fetchEntityList(opts: FetchListOpts): Promise<string[]> {
  const path = opts.endpoint === 'ep' ? POPULATE_EP_PATH_TPL : POPULATE_OCP_PATH_TPL;
  const url = `${PORTAL_BASE}/${path}`;
  const body = new URLSearchParams({ idSector: opts.sector, idJudet: opts.judet }).toString();

  const resp = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'User-Agent': USER_AGENT,
      Accept: 'application/json, text/plain, */*',
      'X-Requested-With': 'XMLHttpRequest',
    },
    body,
  });
  if (!resp.ok) {
    throw new Error(`fetchEntityList ${opts.sector}/${opts.judet}/${opts.endpoint}: HTTP ${resp.status}`);
  }
  const text = await resp.text();
  // Răspunsul e un JSON array, dar poate veni cu BOM / whitespace
  const trimmed = text.trim();
  if (!trimmed.startsWith('[')) {
    // Gol sau eroare → array gol în loc să crăpăm
    return [];
  }
  try {
    const parsed = JSON.parse(trimmed) as unknown;
    if (Array.isArray(parsed)) {
      return parsed.filter((x): x is string => typeof x === 'string').map((s) => s.trim());
    }
  } catch (e) {
    console.warn(`[bugetar] JSON parse failed for ${opts.sector}/${opts.judet}/${opts.endpoint}: ${(e as Error).message}`);
  }
  return [];
}

// ─── MODE: enumerate ────────────────────────────────────────────────────────
async function modeEnumerate(): Promise<void> {
  const sectorsToRun = argSector ? [SECTORS.find((s) => s.code === argSector)].filter(Boolean) as typeof SECTORS : SECTORS;
  const judeteToRun = argJudet ? [argJudet] : JUDETE;

  console.log(`[bugetar] enumerate: ${sectorsToRun.length} sectors × ${judeteToRun.length} judete`);
  let totalNames = 0;
  let totalInserts = 0;
  let totalUpserts = 0;

  for (const sec of sectorsToRun) {
    for (const judet of judeteToRun) {
      // Fetch ambele endpoint-uri: EP (toate) + OCP (subset ordonatori principali).
      let epList: string[] = [];
      let ocpList: string[] = [];
      try {
        epList = await fetchEntityList({ sector: sec.code, judet, endpoint: 'ep' });
      } catch (e) {
        console.warn(`[bugetar] EP fetch ${sec.code}/${judet} failed: ${(e as Error).message}`);
      }
      await sleep(argDelay);
      try {
        ocpList = await fetchEntityList({ sector: sec.code, judet, endpoint: 'ocp' });
      } catch (e) {
        console.warn(`[bugetar] OCP fetch ${sec.code}/${judet} failed: ${(e as Error).message}`);
      }
      await sleep(argDelay);

      const ocpSet = new Set(ocpList.map((s) => s.trim()));
      const merged = new Map<string, boolean>(); // entity_name → is_ocp
      for (const name of epList) merged.set(name, ocpSet.has(name));
      for (const name of ocpList) if (!merged.has(name)) merged.set(name, true);

      totalNames += merged.size;

      // Bulk upsert
      if (merged.size > 0) {
        const values: string[] = [];
        const params: unknown[] = [];
        let i = 1;
        for (const [name, isOcp] of merged.entries()) {
          values.push(`($${i++}, $${i++}, $${i++}, $${i++})`);
          params.push(name, sec.code, judet, isOcp);
        }
        const sql = `
          INSERT INTO bugetar.entitate (entity_name, sector_bugetar, judet, is_ordonator_principal)
          VALUES ${values.join(', ')}
          ON CONFLICT (entity_name, sector_bugetar, judet) DO UPDATE SET
            is_ordonator_principal = bugetar.entitate.is_ordonator_principal OR EXCLUDED.is_ordonator_principal,
            updated_at = now()
          RETURNING (xmax = 0) AS inserted
        `;
        const res = await pool.query<{ inserted: boolean }>(sql, params);
        const inserted = res.rows.filter((r) => r.inserted).length;
        const updated = res.rows.length - inserted;
        totalInserts += inserted;
        totalUpserts += updated;
        console.log(
          `[bugetar] ${sec.code}/${judet}: EP=${epList.length} OCP=${ocpList.length} merged=${merged.size} new=${inserted} upd=${updated}`,
        );
      } else {
        console.log(`[bugetar] ${sec.code}/${judet}: empty`);
      }

      if (argLimit > 0 && totalNames >= argLimit) {
        console.log(`[bugetar] hit --limit=${argLimit}, stopping`);
        return;
      }
    }
  }

  console.log(
    `[bugetar] enumerate done: total names seen=${totalNames}, new=${totalInserts}, updated=${totalUpserts}`,
  );
}

// ─── MODE: match-cui ────────────────────────────────────────────────────────
/**
 * Pentru fiecare bugetar.entitate fără CUI, încearcă fuzzy match cu
 * firms.entities (preferabil cele care au deja apărut ca authority în SEAP).
 *
 * Strategie:
 *  1. Match exact pe nume normalizat — ridicat-confidence (cui_match_method='exact').
 *  2. Match pg_trgm similarity > 0.55 — fuzzy_anaf, păstrează scorul.
 *  3. Restul rămân fără CUI — marcate explicit pentru review manual.
 */
async function modeMatchCui(): Promise<void> {
  // Verifică dacă firms.entities există și are coloana name + cui_normalized.
  const check = await pool.query<{ exists: boolean }>(
    `SELECT EXISTS (
       SELECT 1 FROM information_schema.tables
       WHERE table_schema='firms' AND table_name='entities'
     ) AS exists`,
  );
  if (!check.rows[0]?.exists) {
    console.error('[bugetar] firms.entities does not exist — skip match-cui mode');
    return;
  }

  // 1. Match exact (după nume normalizat — diacritice strip + lowercase).
  console.log('[bugetar] match-cui phase 1: exact-normalized matches');
  const exactRes = await pool.query<{ updated: number }>(`
    WITH cand AS (
      SELECT e.id, fe.cui AS cui
      FROM bugetar.entitate e
      JOIN firms.entities fe ON
        regexp_replace(lower(unaccent(coalesce(fe.name, ''))), '[^a-z0-9 ]', ' ', 'g')
        =
        regexp_replace(lower(unaccent(e.entity_name)), '[^a-z0-9 ]', ' ', 'g')
      WHERE e.cui IS NULL AND fe.cui IS NOT NULL
    ),
    upd AS (
      UPDATE bugetar.entitate be SET
        cui = cand.cui,
        cui_match_score = 1.00,
        cui_match_method = 'exact',
        updated_at = now()
      FROM cand WHERE be.id = cand.id
      RETURNING 1
    )
    SELECT count(*)::int AS updated FROM upd
  `).catch((e) => {
    // Dacă unaccent nu e instalat, fallback la match exact case-insensitive simplu.
    console.warn(`[bugetar] unaccent not available (${(e as Error).message}), fallback simpler match`);
    return pool.query<{ updated: number }>(`
      WITH cand AS (
        SELECT e.id, fe.cui AS cui
        FROM bugetar.entitate e
        JOIN firms.entities fe ON lower(fe.name) = lower(e.entity_name)
        WHERE e.cui IS NULL AND fe.cui IS NOT NULL
      ),
      upd AS (
        UPDATE bugetar.entitate be SET
          cui = cand.cui,
          cui_match_score = 1.00,
          cui_match_method = 'exact',
          updated_at = now()
        FROM cand WHERE be.id = cand.id
        RETURNING 1
      )
      SELECT count(*)::int AS updated FROM upd
    `);
  });
  console.log(`[bugetar]   exact matches: ${exactRes.rows[0]?.updated ?? 0}`);

  // 2. Fuzzy match prin pg_trgm — DOAR dacă extensia e instalată.
  const trgmCheck = await pool.query<{ has: boolean }>(
    `SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname='pg_trgm') AS has`,
  );
  if (!trgmCheck.rows[0]?.has) {
    console.log('[bugetar] pg_trgm not installed — skipping fuzzy phase');
    return;
  }

  // Phase 2: pg_trgm fuzzy match. Pre-filter firms.entities la denumiri care
  // arată a entitate publică pentru a reduce cross-product 3.9M × 4K → ~150K × 4K.
  // Pattern-uri: COMUNA, PRIMARIA, CONSILIUL, MINISTERUL, AGENTIA, INSPECTORATUL,
  // SCOALA, COLEGIUL, SPITALUL, BIBLIOTECA, CASA, CENTRUL, MUZEUL, etc.
  console.log('[bugetar] match-cui phase 2: pg_trgm fuzzy (public-sector firms only, sim > 0.55)');
  const fuzzyRes = await pool.query<{ updated: number }>(`
    WITH public_firms AS (
      SELECT cui, name FROM firms.entities
      WHERE cui IS NOT NULL AND name IS NOT NULL
        AND (
          name ILIKE 'COMUNA %' OR name ILIKE '%MUNICIPIUL %' OR name ILIKE 'PRIMARIA %'
          OR name ILIKE 'ORASUL %' OR name ILIKE 'JUDETUL %' OR name ILIKE 'CONSILIUL %'
          OR name ILIKE 'MINISTERUL %' OR name ILIKE 'AGENTIA %' OR name ILIKE 'AUTORITATEA %'
          OR name ILIKE 'INSPECTORATUL %' OR name ILIKE 'INSTITUTUL %' OR name ILIKE 'SECRETARIATUL %'
          OR name ILIKE 'SCOALA %' OR name ILIKE 'LICEUL %' OR name ILIKE 'COLEGIUL %'
          OR name ILIKE 'GRADINITA %' OR name ILIKE 'GRADINIȚA %' OR name ILIKE 'UNIVERSITATEA %'
          OR name ILIKE 'SPITALUL %' OR name ILIKE 'BIBLIOTECA %' OR name ILIKE 'MUZEUL %'
          OR name ILIKE 'CENTRUL %' OR name ILIKE 'CAMERA %' OR name ILIKE 'CASA %'
          OR name ILIKE 'CLUBUL %' OR name ILIKE 'TEATRUL %' OR name ILIKE 'FILARMONICA %'
          OR name ILIKE 'DIRECTIA %' OR name ILIKE 'OFICIUL %' OR name ILIKE 'SERVICIUL %'
          OR name ILIKE 'CASA DE %' OR name ILIKE 'CANTINA %' OR name ILIKE 'CAMINUL %'
        )
    ),
    cand AS (
      SELECT DISTINCT ON (e.id)
        e.id,
        pf.cui AS cui,
        similarity(lower(pf.name), lower(e.entity_name)) AS sim
      FROM bugetar.entitate e
      JOIN public_firms pf ON lower(pf.name) % lower(e.entity_name)
      WHERE e.cui IS NULL
        AND similarity(lower(pf.name), lower(e.entity_name)) > 0.55
      ORDER BY e.id, sim DESC
    ),
    upd AS (
      UPDATE bugetar.entitate be SET
        cui = cand.cui,
        cui_match_score = round(cand.sim::numeric, 2),
        cui_match_method = 'fuzzy_anaf',
        updated_at = now()
      FROM cand WHERE be.id = cand.id
      RETURNING 1
    )
    SELECT count(*)::int AS updated FROM upd
  `);
  console.log(`[bugetar]   fuzzy matches: ${fuzzyRes.rows[0]?.updated ?? 0}`);

  // Stats finale
  const stats = await pool.query<{
    total: number; with_cui: number; without_cui: number; exact: number; fuzzy: number;
  }>(`
    SELECT
      count(*)::int AS total,
      count(cui)::int AS with_cui,
      count(*) FILTER (WHERE cui IS NULL)::int AS without_cui,
      count(*) FILTER (WHERE cui_match_method='exact')::int AS exact,
      count(*) FILTER (WHERE cui_match_method='fuzzy_anaf')::int AS fuzzy
    FROM bugetar.entitate
  `);
  console.log('[bugetar] match-cui stats:', stats.rows[0]);
}

// ─── MODE: parse-xml (FXB-EXB-900 stub) ─────────────────────────────────────
/**
 * Parser stub pentru fișiere XML FXB-EXB-900. Așteaptă să primească un fișier
 * descărcat manual (sau de Faza 2 cu captcha solver) și inserează linii în
 * bugetar.executie.
 *
 * IMPORTANT (Faza 2): structura exactă a tag-urilor diferă între versiuni MFP.
 * Acest stub e tolerant: face lookup case-insensitive cu fallback synonyms.
 *
 * Pentru parsing efectiv în Faza 2, vom folosi `fast-xml-parser` (de adăugat
 * la dependencies). Pentru moment, doar validăm că putem citi fișierul ca text
 * și extrage hint-uri despre structură.
 */
async function modeParseXml(): Promise<void> {
  if (!argFile) {
    console.error('[bugetar] --file=<path> required');
    process.exit(1);
  }
  if (!argCui || !argPeriod) {
    console.error('[bugetar] --cui=... --period=YYYY-MM required');
    process.exit(1);
  }
  const fs = await import('node:fs/promises');
  const text = await fs.readFile(argFile, 'utf-8');
  console.log(`[bugetar] parse-xml: file=${argFile} bytes=${text.length} cui=${argCui} period=${argPeriod}`);

  // Heuristic: numără tag-uri unice de top level pentru a confirma structura.
  const tags = Array.from(text.matchAll(/<([A-Za-z_][A-Za-z0-9_]*)/g)).map((m) => m[1]);
  const counts = new Map<string, number>();
  for (const t of tags) counts.set(t, (counts.get(t) ?? 0) + 1);
  const top = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 25);
  console.log('[bugetar] top tags:', top);

  // Indică numărul de linii potențiale (LINIE / linie / row / detail)
  const lineCount = (counts.get('LINIE') ?? 0) + (counts.get('linie') ?? 0) + (counts.get('row') ?? 0);
  console.log(`[bugetar] estimated detail lines: ${lineCount}`);

  console.log(
    '[bugetar] NOTE: full XML parser este TODO Faza 2. Acest mode confirmă doar că fișierul e legibil.',
  );
}

// ─── Main ───────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
  console.log(`[bugetar] mode=${argMode} judet=${argJudet || 'ALL'} sector=${argSector || 'ALL'}`);
  try {
    if (argMode === 'enumerate') {
      await modeEnumerate();
    } else if (argMode === 'match-cui') {
      await modeMatchCui();
    } else if (argMode === 'parse-xml') {
      await modeParseXml();
    } else {
      console.error(`[bugetar] unknown mode=${argMode}`);
      process.exit(1);
    }
  } finally {
    await pool.end();
  }
}

main().catch((e) => {
  console.error('[bugetar] FATAL:', e);
  process.exit(1);
});
