/**
 * CNAS Layout-B parser — judet-grouped provider lists.
 *
 * Handles the 8 PDFs that the row-numbered parser (parseProviderText in
 * scrape-cnas.ts) returns 0 rows for. Shape (from HANDOFF-cnas-layout-b.md):
 *
 *     BIHOR
 *                  SC Resident Laboratory SRL   Oradea,Str.…    email   phone   DA
 *     CLUJ
 *                  Institutul Oncologic „Prof…  Cluj-Napoca…    email   phone   DA DA DA
 *                  Centrul Medical Unirea S.R.L Punct de lucru…  email   phone   DA DA DA
 *     BUCUREȘTI    Onco Team Diagnostic SRL     București,sec1…  email   phone   DA
 *
 * Key signals (from pdftotext -layout output):
 *   - Judet header: line starts at col 0-10, content is ALL CAPS, ≤16 chars
 *     (BIHOR, CLUJ, IAȘI, BUCUREŞTI, CONSTANȚA, ARGEŞ, BRAŞOV, MARAMUREŞ, CASMB,
 *     ILFOV, AOPSNAJ, SUCEAVA, VÂLCEA, PRAHOVA, TIMIŞ, MUREŞ, DOLJ, GALAŢI,
 *     BACĂU, BRĂILA, ALBA, SIBIU).
 *   - Provider row: line content STARTS at col 15-25 (the "Denumire furnizor"
 *     column). The same line may begin with a judet header (col 0-10) AND
 *     then have the provider name (col 15+). In that case both apply.
 *   - Continuation: line content starts at col 35+ (the address column).
 *   - Inside the provider row, columns are separated by 3+ spaces. Order:
 *     name | address | email | phone | flag1 [flag2...]
 *
 * Two helpers also handle PNS lists where columns are simpler:
 *   - parseProviderTextJudetGrouped(text, hints): main multi-judet parser
 *   - parseProviderTextSingleCAS(text, hints): for files like CAS-GORJ-PNS
 *     which list providers under one fixed CAS header
 *
 * Both return ProviderRow[] compatible with the existing insertProviders
 * function. nr_crt is assigned as 1..N sequential per document.
 */

export interface ProviderRow {
  nr_crt: number | null;
  cod_furnizor: string | null;
  name: string;
  reprezentant: string | null;
  sediu: string | null;
  localitate: string | null;
  telefon: string | null;
  fax: string | null;
  email: string | null;
  specialitate: string | null;
  judet?: string | null;            // overrides document-level judet when set
  raw_text: string;
}

// Romanian judet vocabulary (variants normalized).
// All-caps single tokens; accept diacritic variants.
const JUDET_VOCAB = new Set<string>([
  'ALBA', 'ARAD', 'ARGES', 'ARGEȘ', 'ARGEŞ',
  'BACAU', 'BACĂU', 'BIHOR', 'BISTRITA', 'BISTRIȚA',
  'BOTOSANI', 'BOTOȘANI', 'BRAILA', 'BRĂILA',
  'BRASOV', 'BRAȘOV', 'BRAŞOV',
  'BUCURESTI', 'BUCUREȘTI', 'BUCUREŞTI',
  'BUZAU', 'BUZĂU',
  'CALARASI', 'CĂLĂRAȘI', 'CARASSEVERIN', 'CARAS-SEVERIN', 'CARAȘ', 'CARAȘ-SEVERIN',
  'CASMB',  // CAS Municipiul Bucuresti
  'AOPSNAJ', // Casa OPSNAJ (Apărare/Ordine Publică/Siguranță Națională/Autoritatea Judecătorească)
  'CLUJ', 'CONSTANTA', 'CONSTANȚA', 'CONSTANŢA',
  'COVASNA', 'DAMBOVITA', 'DÂMBOVIȚA', 'DOLJ',
  'GALATI', 'GALAȚI', 'GALAŢI',
  'GIURGIU', 'GORJ', 'HARGHITA', 'HUNEDOARA',
  'IALOMITA', 'IALOMIȚA', 'IASI', 'IAȘI', 'IAŞI',
  'ILFOV',
  'MARAMURES', 'MARAMUREȘ', 'MARAMUREŞ',
  'MEHEDINTI', 'MEHEDINȚI',
  'MURES', 'MUREȘ', 'MUREŞ',
  'NEAMT', 'NEAMȚ',
  'OLT', 'PRAHOVA',
  'SALAJ', 'SĂLAJ', 'SATUMARE', 'SATU-MARE', 'SATU MARE',
  'SIBIU', 'SUCEAVA',
  'TELEORMAN', 'TIMIS', 'TIMIȘ', 'TIMIŞ',
  'TULCEA', 'VALCEA', 'VÂLCEA', 'VASLUI', 'VRANCEA',
]);

const JUDET_NORMALIZE: Record<string, string> = {
  'ARGEȘ': 'ARGES', 'ARGEŞ': 'ARGES',
  'BACĂU': 'BACAU',
  'BISTRIȚA': 'BISTRITA',
  'BOTOȘANI': 'BOTOSANI',
  'BRĂILA': 'BRAILA',
  'BRAȘOV': 'BRASOV', 'BRAŞOV': 'BRASOV',
  'BUCUREȘTI': 'BUCURESTI', 'BUCUREŞTI': 'BUCURESTI',
  'BUZĂU': 'BUZAU',
  'CĂLĂRAȘI': 'CALARASI',
  'CARAS-SEVERIN': 'CARAS-SEVERIN', 'CARAȘ': 'CARAS-SEVERIN', 'CARAȘ-SEVERIN': 'CARAS-SEVERIN',
  'CARASSEVERIN': 'CARAS-SEVERIN',
  'CONSTANȚA': 'CONSTANTA', 'CONSTANŢA': 'CONSTANTA',
  'DÂMBOVIȚA': 'DAMBOVITA',
  'GALAȚI': 'GALATI', 'GALAŢI': 'GALATI',
  'IALOMIȚA': 'IALOMITA',
  'IAȘI': 'IASI', 'IAŞI': 'IASI',
  'MARAMUREȘ': 'MARAMURES', 'MARAMUREŞ': 'MARAMURES',
  'MEHEDINȚI': 'MEHEDINTI',
  'MUREȘ': 'MURES', 'MUREŞ': 'MURES',
  'NEAMȚ': 'NEAMT',
  'SĂLAJ': 'SALAJ',
  'SATU-MARE': 'SATU MARE', 'SATUMARE': 'SATU MARE',
  'TIMIȘ': 'TIMIS', 'TIMIŞ': 'TIMIS',
  'VÂLCEA': 'VALCEA',
};

function normalizeJudet(s: string): string {
  const trimmed = s.trim();
  return JUDET_NORMALIZE[trimmed] || trimmed;
}

const EMAIL_RE = /[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g;
// CNAS PDFs commonly format phones as: 0264-598361 / 0265/212111 / 0736 609 761 /
// 021/3192732 / tel.fix: 0264-598361. We accept 9-11 digit numbers starting with 0.
const PHONE_RE = /\b0\d{1,3}[\s./\-]?\d{2,4}[\s./\-]?\d{3,5}(?:[\s./\-]?\d{1,4})?\b/g;
const COL_SPLIT_RE = /\s{3,}/;

// A line is a JUDET header if leading whitespace is small (≤12 spaces) AND the
// rest is a known judet name (case- and diacritic-insensitive). It may also
// have additional content (e.g. provider row) AFTER the judet on the same line.
interface JudetMatch { judet: string; restStart: number; restText: string; }

function matchJudetAtLineStart(line: string): JudetMatch | null {
  // Allow up to 12 leading spaces, then a token that looks like a judet
  const m = line.match(/^(\s{0,12})([A-ZĂÂÎȘŞȚŢ][A-ZĂÂÎȘŞȚŢ \-]{2,15}?)(\s{2,}|$)/);
  if (!m) return null;
  const candidate = m[2].replace(/\s+/g, ' ').trim();
  // Only accept if in vocab
  if (!JUDET_VOCAB.has(candidate)) return null;
  const restStart = (m[1] + m[2] + m[3]).length;
  const restText = line.slice(restStart);
  return { judet: normalizeJudet(candidate), restStart, restText };
}

// Provider row start: line has non-whitespace content STARTING at col 15-30.
// (covers indents like "                    SC Resident..." at col 20, or
//  "                Personal Genetics SRL" at col 16, etc.)
// Reject lines that are clearly continuations (start at col 35+) or judet-only.
function isProviderRowStart(line: string, judet: JudetMatch | null): { startCol: number; content: string } | null {
  if (!line.trim()) return null;
  // If the line had a judet header, the rest after the judet may be a provider row
  if (judet) {
    if (judet.restText.trim().length === 0) return null;
    // The judet line had content after it; treat rest as a name-start line
    return { startCol: judet.restStart, content: judet.restText };
  }
  const m = line.match(/^(\s+)(\S.*)$/);
  if (!m) return null;
  const startCol = m[1].length;
  // Allow 13-30 as "name column". <13 = page header / judet without vocab match.
  // >30 = continuation.
  if (startCol < 13 || startCol > 33) return null;
  return { startCol, content: m[2] };
}

// Heuristic for header/footer/junk lines we want to skip entirely.
function isJunkLine(line: string): boolean {
  const t = line.trim();
  if (t.length === 0) return false; // blank — let caller decide
  // Common header patterns in these PDFs:
  if (/^(Subprogram(ul)?|CANCER|RABDOMIOSARCOM|RETINOBLASTOM|SARCOM|TUMORI|LEUCEMII|NEOPLASME|MASTOCITOZA|Sindroame|CASA NATIONALA|CASA DE ASIGURARI|CASA JUDETEANA|CASJ|Operator de date|Str\.Prahova|E-mail:|Fax:|TelVerde:|www\.cas)/i.test(t)) return true;
  if (/^(Casa de asigurari|Casa de asigur[aă]ri|de sanatate|de s[aă]n[aă]tate|Denumire furnizor|Denumire program|Unitatea sanitar|Adres[aă]\s*Email\s*Telefon|Tipul de servicii|Date de contact|panelul de teste|CREDITE DE ANGAJAMENT|Valoare)\s*$/i.test(t)) return true;
  if (/^(Adresă|Adresa|Email|Telefon|Adresã)/.test(t) && t.length < 60) return true;
  if (/^panelul de teste nr\.?\s*\d*\s*$/i.test(t)) return true;
  if (/^(Imunofenotipare|Citogenetic|Biologie moleculară|Biologie molecular|secven|calitativ|cancer|panel diagnostic|negativ mutaţii|JAK2|markeri\)|funcţie de|indicaţie\))/i.test(t)) return true;
  if (/^(panelul de teste|Tipul de servicii|de testare contractate|nervos primar|Sarcom Ewing|Neuroblastom|Rabdomiosarcom|Retinoblastom|secventiere)/i.test(t)) return true;
  if (/^[a-z]+\s*\)/.test(t) && t.length < 40) return true; // "indicaţie)" "qPCR (în"
  if (/^(Nr\.?\s*crt|Pagina|Page \d|FURNIZORI[-_ ])/.test(t)) return true;
  if (/^Lista furnizori in contract/i.test(t)) return true;
  if (/^Situatia furnizorilor/i.test(t)) return true;
  if (/^sesiunea de contractare/i.test(t)) return true;
  if (/^(Nr\.|nr\.)\s*\d+\.?\s*\/\s*\d+/.test(t)) return true; // "Nr. 29.984 / 27.11.2024"
  if (/^COMISIE CONTRACTARE/i.test(t)) return true;
  if (/^CAS\s+(SB|MB|BIHOR|GORJ|CLUJ|IASI|BV|TM|TIMIS|MUREŞ|CONSTANTA|CT|SIBIU|BRASOV|ARAD|BACAU|MM)\s*$/i.test(t)) return true;
  return false;
}

export function parseProviderTextJudetGrouped(text: string, hints: { tip: string }): ProviderRow[] {
  const lines = text.split('\n').map((l) => l.replace(/\r/g, ''));
  // STRATEGY (v2): use email-containing lines as the "spine" of each provider
  // record. Walk forward from each spine, gathering surrounding lines that
  // share the same provider until we hit either: another email line, a junk/
  // page-header line, or a "new provider name" candidate at the name column
  // that's clearly a fresh entry (judged by left-aligned start AND followed
  // shortly by another email line).
  //
  // For each spine line we look BACKWARD to collect continuation lines from
  // the previous provider's end up to (but not including) the previous spine.
  // We split by 50/50 to the closer spine.

  // Pre-scan: classify each non-blank, non-junk line.
  type LineInfo = {
    idx: number;
    raw: string;
    col: number;            // leading whitespace count
    isJunk: boolean;
    judet: JudetMatch | null;
    hasEmail: boolean;
    hasPhone: boolean;
  };
  const info: LineInfo[] = [];
  for (let i = 0; i < lines.length; i++) {
    const ln = lines[i];
    if (!ln.trim()) continue;
    const junk = isJunkLine(ln);
    const m = ln.match(/^(\s*)/);
    const col = m ? m[1].length : 0;
    const judet = matchJudetAtLineStart(ln);
    const hasEmail = EMAIL_RE.test(ln);
    // Re-init lastIndex because EMAIL_RE has /g flag
    EMAIL_RE.lastIndex = 0;
    const hasPhone = /\b0\d{1,3}[\s./\-]?\d{2,4}[\s./\-]?\d{3,5}\b/.test(ln);
    info.push({ idx: i, raw: ln, col, isJunk: junk, judet, hasEmail, hasPhone });
  }

  // Collect judet events (line index + judet)
  const judetEvents: Array<{ line: number; judet: string }> = [];
  for (const li of info) {
    if (li.judet && !li.isJunk) judetEvents.push({ line: li.idx, judet: li.judet.judet });
  }

  // Spine lines = ones with an email pattern. Each becomes one provider.
  const spineIdxs = info
    .map((li, k) => (li.hasEmail && !li.isJunk ? k : -1))
    .filter((k) => k >= 0);

  interface RawProvider { spineIdx: number; lineNos: number[]; texts: string[]; }
  const providers: RawProvider[] = [];

  for (let s = 0; s < spineIdxs.length; s++) {
    const spineK = spineIdxs[s];
    const spineLi = info[spineK];
    const prevSpineK = s > 0 ? spineIdxs[s - 1] : -1;

    // BACKWARD-only attachment: between this spine and the previous spine,
    // all non-junk continuation lines belong to THIS spine (since the spine
    // line completes the row with email/phone). This avoids double-counting
    // lines between two spines.
    const blockLines: number[] = [];
    for (let k = spineK - 1; k > prevSpineK; k--) {
      const li = info[k];
      if (li.isJunk) break;
      // Skip judet-only lines (those that ARE judet header and have no content after)
      if (li.judet && li.judet.restText.trim().length === 0) continue;
      // Lines starting at col < 8 are usually judet labels (with or without
      // adjacent content). If the line has a judet match with non-empty rest,
      // include it (the rest is provider name/address). Otherwise break.
      if (li.col < 8) {
        if (li.judet && li.judet.restText.trim().length > 0) {
          blockLines.unshift(k);
          continue;
        }
        break;
      }
      blockLines.unshift(k);
    }
    blockLines.push(spineK);
    providers.push({
      spineIdx: spineLi.idx,
      lineNos: blockLines.map((k) => info[k].idx),
      texts: blockLines.map((k) => info[k].raw),
    });
  }

  // Split logic: when between two spines we have multiple "name-col-start"
  // lines (i.e., continuation lines that have content at col 13-25), one of
  // them might be a separate provider with no email (rare). For v1 we accept
  // the simpler model: each email = one provider.

  // Second pass: assign judet to each provider by nearest judet label (line dist).
  // Maintain a "last seen judet" baseline for fallbacks.
  const result: ProviderRow[] = [];
  let nrCrt = 0;
  let lastJudet: string | null = null;
  let judetIdx = 0;

  for (let p = 0; p < providers.length; p++) {
    const prov = providers[p];
    const startLine = prov.lineNos[0];
    const endLine = prov.lineNos[prov.lineNos.length - 1];
    // Find the closest judet within [startLine - 6, endLine + 6].
    let best: { judet: string; dist: number } | null = null;
    for (const je of judetEvents) {
      if (je.line < startLine - 6) continue;
      if (je.line > endLine + 6) break;
      const dist = Math.min(Math.abs(je.line - startLine), Math.abs(je.line - endLine));
      if (!best || dist < best.dist) best = { judet: je.judet, dist };
    }
    // Update lastJudet baseline based on all judet labels up to startLine.
    while (judetIdx < judetEvents.length && judetEvents[judetIdx].line <= startLine) {
      lastJudet = judetEvents[judetIdx].judet;
      judetIdx++;
    }
    const judet = best?.judet || lastJudet;

    nrCrt++;
    result.push(extractFields(nrCrt, prov.texts, judet, hints));
  }

  return result;
}

function extractFields(nrCrt: number, rawLines: string[], judet: string | null, hints: { tip: string }): ProviderRow {
  // Strip judet prefix from any line where it appears as a leading word.
  // (We've already used the judet info for assignment; in the data we don't
  // want "BIHOR" leaking into name.)
  const cleanedLines = rawLines.map((ln) => {
    const jm = matchJudetAtLineStart(ln);
    if (jm) {
      // Keep the rest part, padded with spaces to preserve column alignment
      return ' '.repeat(jm.restStart) + jm.restText;
    }
    return ln;
  });

  // Determine column boundaries from the UNION of all lines in the block.
  // For each line, find positions where a non-whitespace cluster STARTS (i.e.,
  // a non-space char preceded by start-of-line or 3+ spaces). Collect all
  // distinct positions across all lines, then cluster nearby positions
  // (within 3 chars of each other) into a single column boundary.
  const allColStarts: number[] = [];
  for (const ln of cleanedLines) {
    let i = 0;
    while (i < ln.length) {
      if (ln[i] !== ' ') {
        allColStarts.push(i);
        // advance until 3+ spaces
        while (i < ln.length) {
          if (ln[i] === ' ') {
            let j = i;
            while (j < ln.length && ln[j] === ' ') j++;
            if (j - i >= 3) { i = j; break; }
            i = j;
          } else {
            i++;
          }
        }
      } else {
        i++;
      }
    }
  }
  allColStarts.sort((a, b) => a - b);
  // Cluster: if two starts are within 4 chars, treat as same column.
  const colBoundaries: number[] = [];
  for (const s of allColStarts) {
    if (colBoundaries.length === 0 || s - colBoundaries[colBoundaries.length - 1] > 4) {
      colBoundaries.push(s);
    } else {
      // Use the leftmost of the cluster (already there)
    }
  }
  // Now we have N column starts. For each row, slice based on these starts
  // and collect text. We also accept a small tolerance (-2..+2) when a
  // continuation line's content starts slightly off-column.
  const N = colBoundaries.length;
  const cellsByCol: string[][] = Array.from({ length: N }, () => []);
  // Helper: given a line and the position boundaries, return the cell content
  // for each column.
  const slicesFor = (ln: string): string[] => {
    if (!ln.trim()) return [];
    const out: string[] = Array(N).fill('');
    for (let c = 0; c < N; c++) {
      const start = colBoundaries[c];
      const end = c + 1 < N ? colBoundaries[c + 1] : ln.length;
      // Apply tolerance: include 2 chars to the left of start (catches small
      // misalignments) and use `end - 1` for trailing space buffer.
      const left = Math.max(0, start - 2);
      const right = Math.min(ln.length, end);
      if (left < right) out[c] = ln.slice(left, right);
    }
    return out;
  };

  // For each line, compute slices and append non-empty text to each column.
  for (const ln of cleanedLines) {
    if (!ln.trim()) continue;
    const sl = slicesFor(ln);
    for (let c = 0; c < N; c++) {
      const txt = sl[c].trim();
      if (txt) cellsByCol[c].push(txt);
    }
  }
  const cols = cellsByCol.map((arr) => arr.join(' ').replace(/\s+/g, ' ').trim());

  // Heuristic: identify columns. cols[0] is usually name, but in some PDFs the
  // ordering is name | sediu | email | telefon | flag1 | flag2 ...
  // For oncology/genetica, the consistent shape is:
  //   col 0: name
  //   col 1: sediu/adresa
  //   col 2: email
  //   col 3: telefon
  //   col 4+: DA/NU flags
  // For radioterapie:
  //   col 0: name | col 1: tipuri servicii (e.g. "IMRT, SBRT")
  // For PNS-13.11.2024:
  //   col 0: name | col 1: program | col 2: valoare

  // Find email and phone using regex on the full joined text — order may vary.
  const joined = rawLines.join(' ');
  const emailMatches = joined.match(EMAIL_RE) || [];
  const email = emailMatches[0] || null;

  // Find phones. Filter out garbage that the regex catches (e.g. "0.7" prices,
  // dates, panel numbers). A phone has 9-11 digits total and starts with 0.
  const phoneCandidates = (joined.match(PHONE_RE) || [])
    .map((p) => p.replace(/[\s.\-/]/g, ''))
    .filter((p) => /^0\d{8,10}$/.test(p))
    .map((p) => formatPhone(p));
  // Dedup
  const phones = Array.from(new Set(phoneCandidates));

  let telefon = phones[0] || null;
  const fax = phones[1] || null;

  // The 'name' is cols[0]. But sometimes it's not present because the first
  // non-empty line of the provider block starts deep in the page (e.g.
  // "BUCUREŞTI    Onco Team Diagnostic SRL ..."). In our preprocessing this
  // already had the judet stripped, so cols[0] should be the name.
  let name = cols[0] || '';
  // Strip trailing fragments like "DA" "NU" if name was a single-column line
  name = name.replace(/\s+(DA|NU)(\s+(DA|NU))*\s*$/i, '').trim();
  // Some pdfs have name = "SC Resident Laboratory" with " SRL" pulled to next col;
  // join cols[0] and any cols[i] that look like SRL/SA/PFA suffix
  if (cols[1] && /^(SRL|S\.R\.L\.|SA|S\.A\.|PFA|PFI|CMI|S\.C\.|SCM|S\.C\.M\.)$/i.test(cols[1])) {
    name = (name + ' ' + cols[1]).trim();
    cols.splice(1, 1);
  }

  // sediu: next col after name that isn't email/phone/DA/NU
  let sediu: string | null = null;
  for (let i = 1; i < cols.length; i++) {
    const c = cols[i];
    if (!c) continue;
    if (/@/.test(c)) continue;
    if (/^0\d/.test(c) && PHONE_RE.test(c)) continue;
    if (/^(DA|NU)(\s+(DA|NU))*\s*$/i.test(c)) continue;
    sediu = c;
    break;
  }
  // If we didn't find sediu OR sediu IS the address pattern, strip email/phone from it
  if (sediu) {
    sediu = sediu.replace(EMAIL_RE, '').replace(/\s+/g, ' ').trim();
    sediu = sediu.replace(/\s+(DA|NU)(\s+(DA|NU))*\s*$/i, '').trim();
  }

  // localitate: first word/phrase before the comma in sediu, capped at 100 chars
  let localitate: string | null = null;
  if (sediu) {
    const idx = sediu.indexOf(',');
    if (idx > 0 && idx < 50) {
      localitate = sediu.slice(0, idx).trim();
    }
  }

  // DA/NU flags → collect into specialitate (text-encoded for v1)
  const flags: string[] = [];
  for (const c of cols) {
    const m = c.match(/^((?:DA|NU)(?:\s+(?:DA|NU))*)$/);
    if (m) {
      flags.push(...m[1].split(/\s+/));
    }
  }
  // Also pull flags out of last col when mixed with other content
  if (flags.length === 0 && cols.length > 0) {
    const last = cols[cols.length - 1];
    const fm = last.match(/((?:\b(?:DA|NU)\b\s*){1,})$/);
    if (fm) flags.push(...fm[1].trim().split(/\s+/));
  }

  // Specialitate: for radioterapie, col 1 may be "IMRT, SBRT" instead of address.
  let specialitate: string | null = null;
  if (hints.tip === 'radioterapie') {
    // The address col is missing — col 1 is service type
    specialitate = cols[1] || null;
    sediu = null;
    localitate = null;
  } else if (hints.tip === 'pns' && cols[1]) {
    // PNS: col 1 = program/subprogram
    specialitate = cols[1].slice(0, 500);
    sediu = null;
    localitate = null;
  } else if (flags.length > 0) {
    specialitate = `flags:${flags.join(',')}`;
  }

  return {
    nr_crt: nrCrt,
    cod_furnizor: null,
    name: cleanName(name).slice(0, 500),
    reprezentant: null,
    sediu: sediu ? sediu.slice(0, 500) : null,
    localitate: localitate ? localitate.slice(0, 200) : null,
    telefon,
    fax,
    email: email ? email.slice(0, 200) : null,
    specialitate,
    judet,
    raw_text: rawLines.join('\n').slice(0, 2000),
  };
}

function cleanName(s: string): string {
  return s
    .replace(/\s+/g, ' ')
    .replace(/[„"]+/g, '"')
    .replace(/^\s*[„"]\s*/, '"')
    .replace(/\s*[„"]\s*$/, '"')
    .trim();
}

function formatPhone(digits: string): string {
  // 10-digit → 4-3-3 (e.g. 0264 598 361). 11-digit (rare) keep as-is.
  if (digits.length === 10) {
    return `${digits.slice(0, 4)} ${digits.slice(4, 7)} ${digits.slice(7)}`;
  }
  return digits;
}

/**
 * Parser for radioterapie-style PDFs (no email/phone per row).
 * Each provider row has: judet (sometimes) | name | service-type
 *
 * Detection: line has content starting at col 10-30 (name col) AND has a
 * "service-type" token in the right (3D, IMRT, SBRT, brahiterapie, etc.).
 */
const RADIO_SERVICE_RE = /\b(3D|2D|IMRT|SBRT|brahiterapie|cobaltoterapie|ortovoltaj|iradiere)\b/i;

export function parseProviderTextRadio(text: string, hints: { tip: string }): ProviderRow[] {
  const lines = text.split('\n').map((l) => l.replace(/\r/g, ''));
  // Filter junk and identify spines: lines containing service-type tokens
  interface LineInfo { idx: number; raw: string; col: number; isJunk: boolean; judet: JudetMatch | null; isSpine: boolean; }
  const info: LineInfo[] = [];
  // Extra junk for radio PDFs: table header "Tipul de servicii radioterapie acordate"
  const radioJunkRe = /^(Tipul de servicii|acordate|Denumire furnizor)/i;
  for (let i = 0; i < lines.length; i++) {
    const ln = lines[i];
    if (!ln.trim()) continue;
    const junk = isJunkLine(ln) || radioJunkRe.test(ln.trim());
    const m = ln.match(/^(\s*)/);
    const col = m ? m[1].length : 0;
    const judet = matchJudetAtLineStart(ln);
    const isSpine = RADIO_SERVICE_RE.test(ln) && !junk;
    info.push({ idx: i, raw: ln, col, isJunk: junk, judet, isSpine });
  }

  const judetEvents: Array<{ line: number; judet: string }> = [];
  for (const li of info) {
    if (li.judet && !li.isJunk) judetEvents.push({ line: li.idx, judet: li.judet.judet });
  }

  const spineIdxs = info.map((li, k) => (li.isSpine ? k : -1)).filter((k) => k >= 0);

  interface RawProvider { lineNos: number[]; texts: string[]; }
  const providers: RawProvider[] = [];
  for (let s = 0; s < spineIdxs.length; s++) {
    const spineK = spineIdxs[s];
    const prevSpineK = s > 0 ? spineIdxs[s - 1] : -1;
    const blockLines: number[] = [];
    for (let k = spineK - 1; k > prevSpineK; k--) {
      const li = info[k];
      if (li.isJunk) break;
      if (li.judet && li.judet.restText.trim().length === 0) continue;
      if (li.col < 5 && !li.judet) break;
      blockLines.unshift(k);
    }
    blockLines.push(spineK);
    providers.push({ lineNos: blockLines.map((k) => info[k].idx), texts: blockLines.map((k) => info[k].raw) });
  }

  const result: ProviderRow[] = [];
  let nrCrt = 0;
  let lastJudet: string | null = null;
  let judetIdx = 0;
  for (const prov of providers) {
    const startLine = prov.lineNos[0];
    const endLine = prov.lineNos[prov.lineNos.length - 1];
    let best: { judet: string; dist: number } | null = null;
    for (const je of judetEvents) {
      if (je.line < startLine - 6) continue;
      if (je.line > endLine + 6) break;
      const dist = Math.min(Math.abs(je.line - startLine), Math.abs(je.line - endLine));
      if (!best || dist < best.dist) best = { judet: je.judet, dist };
    }
    while (judetIdx < judetEvents.length && judetEvents[judetIdx].line <= startLine) {
      lastJudet = judetEvents[judetIdx].judet;
      judetIdx++;
    }
    const judet = best?.judet || lastJudet;

    // Extract name + service type. Strip judet from any line first.
    const cleaned = prov.texts.map((ln) => {
      const jm = matchJudetAtLineStart(ln);
      return jm ? ' '.repeat(jm.restStart) + jm.restText : ln;
    });
    const joined = cleaned.join(' ').replace(/\s{2,}/g, '  ').trim();
    // Service: extract all service-type tokens (in order)
    const services: string[] = [];
    let mtype: RegExpExecArray | null;
    const re2 = new RegExp(RADIO_SERVICE_RE.source, 'gi');
    while ((mtype = re2.exec(joined))) services.push(mtype[1]);
    // Name: text before the first service token
    const firstService = joined.search(RADIO_SERVICE_RE);
    let name = firstService > 0 ? joined.slice(0, firstService) : joined;
    name = name.replace(/\s+/g, ' ').trim();
    // Strip trailing connecting words
    name = name.replace(/[\s,;.\-]+$/, '');
    if (!name || name.length < 3) continue;
    nrCrt++;
    result.push({
      nr_crt: nrCrt,
      cod_furnizor: null,
      name: name.slice(0, 500),
      reprezentant: null,
      sediu: null,
      localitate: null,
      telefon: null, fax: null, email: null,
      specialitate: services.length > 0 ? services.join(', ').slice(0, 500) : null,
      judet,
      raw_text: prov.texts.join('\n').slice(0, 2000),
    });
  }
  return result;
}

/**
 * Parser variant for single-CAS PNS lists (e.g. CAS-GORJ-Lista-furnizori-PNS,
 * Valori-de-contract-furnizori-PNS-13.11.2024). The table layout has:
 *
 *   col 0-34 : name (multi-line, may wrap)
 *   col 35+  : program/subprogram (multi-line cell, spans several rows)
 *
 * Name cells span MULTIPLE program rows in the visual layout — i.e. one name
 * is associated with several programs/subprograms. We treat each name as one
 * provider and concatenate all programs visible in that name's vertical span.
 */
export function parseProviderTextSingleCAS(text: string, hints: { tip: string; judet?: string | null }): ProviderRow[] {
  const lines = text.split('\n').map((l) => l.replace(/\r/g, ''));
  // Skip past the table header
  let startIdx = 0;
  for (let i = 0; i < lines.length; i++) {
    if (/Unitatea sanitar/i.test(lines[i]) || /CREDITE DE ANGAJAMENT/i.test(lines[i])) {
      startIdx = i + 1; break;
    }
  }

  // Determine the name/program column boundary by sampling: the typical split
  // is around col 36. We'll auto-detect by finding the SMALLEST col position
  // with content among lines that ALSO have content at col 0-10. If no clear
  // detect, default to 36.
  let nameRight = 36;
  // (We'll keep the default for v1.)

  interface NameRow { line: number; namePart: string; progPart: string; }
  const nameRows: NameRow[] = [];
  const orphanProgRows: Array<{ line: number; text: string; assignedTo: number | null }> = [];

  for (let i = startIdx; i < lines.length; i++) {
    const ln = lines[i];
    if (!ln.trim()) continue;
    if (isJunkLine(ln)) continue;
    const m = ln.match(/^(\s*)(\S.*)$/);
    if (!m) continue;
    const col = m[1].length;
    // Determine name part (col 0..nameRight) and program part (col nameRight+)
    const left = ln.slice(0, nameRight).trim();
    const right = ln.slice(nameRight).trim();
    if (col < nameRight - 4) {
      // Has name content at left
      // Filter out lines that look like noise (e.g. "Lista furnizori in contract", page numbers)
      if (left.length < 3) continue;
      if (/^[\d.,\s]+$/.test(left)) continue;
      // Skip explicit header tokens
      if (/^(Unitatea\s+sanitar|Denumire\s+furnizor|Nr\.?\s*crt|CASJ|CAS\s+(?:GORJ|MB|SB|CT|CJ|BV|TM))/i.test(left)) continue;
      nameRows.push({ line: i, namePart: left, progPart: right });
    } else {
      // Purely program content (col >= nameRight)
      orphanProgRows.push({ line: i, text: ln.slice(nameRight).trim(), assignedTo: null });
    }
  }

  // Merge consecutive name rows that are close together (≤2 lines apart) into
  // a single provider when the name part appears to be a wrap (no terminator
  // like "S.R.L." / "S.A." / "Tg Jiu" alone is fine but if name 1 ends with
  // open-ended like "Spitalul Orăşenesc de Urgenţă Tg" then merge with next).
  interface Provider { startLine: number; endLine: number; names: string[]; programs: string[]; }
  const providers: Provider[] = [];
  for (const nr of nameRows) {
    const last = providers[providers.length - 1];
    // Merge if previous name's last word doesn't include a corp suffix and is close
    if (last && nr.line - last.endLine <= 2 && !/(S\.?R\.?L\.?|S\.?A\.?|PFA|CMI|s\.r\.l\.|SRL|S\.C\.|Jiu|Mare|Iaşi|Cluj|Bucuresti|București|Craiova|Constanța|Galați|Iași|Sibiu|Brașov|Pitești|Suceava|Oradea|Timișoara|Tg-Jiu|Cărbuneşti|Bacău|Brăila)\s*$/i.test(last.names[last.names.length - 1])) {
      last.names.push(nr.namePart);
      if (nr.progPart) last.programs.push(nr.progPart);
      last.endLine = nr.line;
    } else {
      providers.push({ startLine: nr.line, endLine: nr.line, names: [nr.namePart], programs: nr.progPart ? [nr.progPart] : [] });
    }
  }

  // Assign orphan program rows to the closest provider's vertical span.
  // Rule: orphan at line k → provider whose [startLine - 10, endLine + 5]
  // window contains k. If none, attach to the NEXT provider in line order
  // (since program cells appear BEFORE their name in the table layout).
  for (const op of orphanProgRows) {
    let best: number = -1;
    let bestDist = Infinity;
    for (let p = 0; p < providers.length; p++) {
      const prov = providers[p];
      // Distance: 0 if inside [startLine-10, endLine+5]; else min(|op-startLine|, |op-endLine|)
      const within = op.line >= prov.startLine - 10 && op.line <= prov.endLine + 5;
      const d = within ? 0 : Math.min(Math.abs(op.line - prov.startLine), Math.abs(op.line - prov.endLine));
      if (d < bestDist) { bestDist = d; best = p; }
    }
    if (best >= 0 && bestDist <= 12) {
      providers[best].programs.push(op.text);
    }
  }

  const result: ProviderRow[] = [];
  let nrCrt = 0;
  for (const prov of providers) {
    const name = prov.names.join(' ').replace(/\s+/g, ' ').trim();
    if (!name || name.length < 3) continue;
    if (/^[\d.,\s]+$/.test(name)) continue;
    // Strip program text from name (some lines have BOTH name and "-medicamente din care:" at the same line)
    let cleanName = name;
    // Cut at em-dash + lowercase word (program continuation)
    const dashCut = cleanName.search(/\s+[–—-]\s*[a-zăâîșțţş]/);
    if (dashCut > 5) cleanName = cleanName.slice(0, dashCut).trim();

    nrCrt++;
    const programs = prov.programs
      .map((p) => p.replace(/\s+/g, ' ').trim())
      .filter((p) => p.length > 1);
    // Try to extract a valoare (money) from programs if hint=pns valori
    let valoare: string | null = null;
    const valoareRe = /([\d]{1,3}(?:\.\d{3})*,\d{2})/;
    const cleanedPrograms: string[] = [];
    for (const p of programs) {
      const vm = p.match(valoareRe);
      if (vm) {
        if (!valoare) valoare = vm[1];
        // keep program text without the value
        cleanedPrograms.push(p.replace(valoareRe, '').replace(/\s+/g, ' ').trim());
      } else {
        cleanedPrograms.push(p);
      }
    }
    const spec = cleanedPrograms.filter(Boolean).join(' | ');
    result.push({
      nr_crt: nrCrt,
      cod_furnizor: null,
      name: cleanName.slice(0, 500),
      reprezentant: null,
      sediu: null,
      localitate: null,
      telefon: null, fax: null, email: null,
      specialitate: spec.length > 0 ? spec.slice(0, 2000) : null,
      judet: hints.judet || null,
      raw_text: (prov.names.join('\n') + (valoare ? `\nvaloare:${valoare}` : '')).slice(0, 2000),
    });
  }
  return result;
}

/**
 * For FURNIZORI-SERVICII-ASISTENTA-MEDICALA-PRIMARA-style PDFs that have a
 * numbered list with columns: Nr.crt | Denumire | Localitate | Adresa | Admis | Respins | Motivul
 *
 * This is closer to Layout A but with row-number "1." (dot), and very few rows.
 */
export function parseProviderTextNumberedDot(text: string, hints: { tip: string; judet?: string | null }): ProviderRow[] {
  const lines = text.split('\n').map((l) => l.replace(/\r/g, ''));
  const rowRe = /^\s*(\d{1,4})\.\s+(\S.*)$/;
  const result: ProviderRow[] = [];
  let nrCrt = 0;
  let current: { nr: number; lines: string[] } | null = null;
  const flush = () => {
    if (!current) return;
    nrCrt++;
    const joined = current.lines.join(' ');
    const parts = joined.split(COL_SPLIT_RE).map((s) => s.trim()).filter(Boolean);
    // parts: [name, localitate?, adresa?, admis?, respins?, motivul?]
    const name = parts[0] || '';
    const localitate = parts[1] || null;
    const adresa = parts[2] || null;
    result.push({
      nr_crt: nrCrt,
      cod_furnizor: null,
      name: name.slice(0, 500),
      reprezentant: null,
      sediu: adresa ? adresa.slice(0, 500) : null,
      localitate: localitate ? localitate.slice(0, 200) : null,
      telefon: null, fax: null, email: null,
      specialitate: null,
      judet: hints.judet || null,
      raw_text: current.lines.join('\n').slice(0, 2000),
    });
    current = null;
  };
  for (const ln of lines) {
    if (!ln.trim()) continue;
    if (isJunkLine(ln)) { flush(); continue; }
    const m = rowRe.exec(ln);
    if (m) {
      flush();
      current = { nr: parseInt(m[1], 10), lines: [m[2]] };
    } else if (current) {
      // Continuation: only add if indented (col 8+)
      const cm = ln.match(/^(\s+)(\S.*)$/);
      if (cm && cm[1].length >= 6) current.lines.push(cm[2]);
    }
  }
  flush();
  return result;
}
