/*
  Extrait d'une instance Dolibarr le MANIFESTE D'UN EXERCICE : la description
  complète et ordonnée des faits qui le composent.

  POURQUOI. Un exercice ouvert n'a d'engagement envers personne — aucun compte
  déposé, aucune liasse, aucun FEC remis. Tant qu'il en va ainsi, une erreur de
  MODÉLISATION (mauvais compte, auxiliaire manquant, libellé illisible) se
  corrige mieux en rejouant depuis les pièces qu'en la contournant : la piste
  d'audit fiable demande que l'écriture reflète sa pièce, et rejouer resserre ce
  lien là où une rustine le distend.

  Ce fichier ne fait que LIRE. Il produit le manifeste ; le rejeu est un autre
  outil, et il ne s'exécute que sur une cible vierge.

  CE QUI EST REJOUABLE, ET CE QUI NE L'EST PAS. La ligne de partage n'est pas
  technique, elle est juridique :

    rejouable  : factures fournisseur enregistrées, lignes bancaires, paiements
                 divers, charges sociales, tiers — tout ce qui est interne ;
    FIGÉ       : toute pièce déjà remise à un tiers (facture client ENVOYÉE,
                 dépôt au greffe, déclaration transmise) et tout exercice clos.

  Une facture partie chez le client existe chez lui : aucun rejeu ne l'efface.
  Le manifeste les extrait quand même — il décrit l'exercice — mais les marque
  `delivered: true`, et le rejeu devra les reproduire à l'identique plutôt que
  les recalculer.

  LE RÉFÉRENT DE VÉRIFICATION. Les mouvements bancaires sont la seule partie de
  l'exercice qu'on ne peut pas réécrire : Qonto et Wise sont hors de Dolibarr.
  Le manifeste les embarque donc intégralement, et le test d'acceptation d'un
  rejeu est qu'ils soient retrouvés au centime et à la date.

  Usage :
    DENO_TLS_CA_STORE=system \
    DOLIBARR_URL=https://erp.arcodange.lab DOLAPIKEY=$(cat test/....key) \
      deno run -A test/extractExercise.ts --year 2026 --out manifest.json

  `DENO_TLS_CA_STORE=system` n'est pas décoratif : les hôtes du lab portent un
  certificat signé par une autorité interne, présente dans le trousseau système
  mais absente du magasin que Deno embarque. Sans lui, `fetch` échoue sur
  « invalid peer certificate: UnknownIssuer » — alors que curl et Playwright,
  qui lisent le trousseau système, passent sans rien dire.
*/
import "load_dotenv";

const argv = Deno.args;
const pick = (f: string, d = "") => (argv.includes(f) ? argv[argv.indexOf(f) + 1] : d);

const base = pick("--url", Deno.env.get("DOLIBARR_URL") ?? "https://erp.arcodange.lab");
const key = Deno.env.get("DOLAPIKEY") ?? "";
const year = Number(pick("--year", "2026"));
const out = pick("--out", `manifest-${year}.json`);

/**
 * Pièces volontairement écartées du rejeu, avec leur motif.
 *
 * Une exclusion n'est légitime que si la pièce N'A JAMAIS ÉTÉ REMISE À UN TIERS.
 * Le motif est consigné dans le manifeste : un auditeur doit pouvoir lire
 * pourquoi une référence manque, sans avoir à le deviner ni à nous croire.
 */
const EXCLUSIONS: Record<string, string> = {
  "FAC001-CL00001":
    "Référence malformée (CL00001 au lieu de CL0001001) — défaut de configuration du masque. "
    + "JAMAIS REMISE AU CLIENT : vérifié le 2026-08-14, aucune trace d'envoi ; KissMetrics a été "
    + "facturé via Wise en février 2026, et le document qu'il détient référence FAC001-CL0001001. "
    + "La séquence FAC reste continue (FAC001 à FAC008), la réémission portant elle aussi FAC001.",
  "AVC001-CL0001001":
    "Avoir émis le 24/02/2026 pour annuler FAC001-CL00001. Sans objet dès lors que la facture "
    + "annulée n'a jamais quitté la société : il ne documentait qu'un faux pas interne, non un "
    + "événement commercial. La séquence AVC devient vide.",
};

if (!key) {
  console.error("DOLAPIKEY requis (lecture seule).");
  Deno.exit(2);
}

async function api<T>(path: string): Promise<T | null> {
  const r = await fetch(`${base}/api/index.php${path}`, { headers: { DOLAPIKEY: key } });
  if (!r.ok) return null;
  try {
    return await r.json() as T;
  } catch {
    return null;
  }
}

/** Dolibarr rend des epochs ; on les fige en ISO dans le fuseau du serveur. */
const iso = (ts: unknown): string | null => {
  const n = Number(ts);
  if (!n) return null;
  return new Date(n * 1000).toISOString().slice(0, 10);
};

const inYear = (ts: unknown) => {
  const d = iso(ts);
  return !!d && d.startsWith(String(year));
};

type Any = Record<string, unknown>;

console.log(`source   : ${base}`);
console.log(`exercice : ${year}\n`);

// --- tiers ------------------------------------------------------------------
const tiers = (await api<Any[]>("/thirdparties?limit=500") ?? []).map((t) => ({
  id: t.id,
  name: t.name,
  code_client: t.code_client,
  code_fournisseur: t.code_fournisseur,
  client: t.client,
  fournisseur: t.fournisseur,
  country_id: t.country_id,
  tva_intra: t.tva_intra,
  code_compta: t.code_compta,
  code_compta_fournisseur: t.code_compta_fournisseur,
}));
console.log(`tiers                 : ${tiers.length}`);

// --- factures client --------------------------------------------------------
// `delivered: true` : elles sont parties chez le client. Un rejeu les reproduit
// à l'identique — référence, date, montant — il ne les recalcule pas.
const invoices = [];
const excluded: Any[] = [];
for (const f of await api<Any[]>("/invoices?limit=500") ?? []) {
  if (!inYear(f.date)) continue;
  const motif = EXCLUSIONS[String(f.ref)];
  if (motif) {
    excluded.push({ ref: f.ref, date: iso(f.date), total_ttc: f.total_ttc, motif });
    continue;
  }
  const payments = await api<Any[]>(`/invoices/${f.id}/payments`) ?? [];
  invoices.push({
    ref: f.ref,
    socid: f.socid,
    date: iso(f.date),
    date_lim_reglement: iso(f.date_lim_reglement),
    type: f.type,
    total_ht: f.total_ht,
    total_tva: f.total_tva,
    total_ttc: f.total_ttc,
    multicurrency_code: f.multicurrency_code,
    statut: f.statut,
    paye: f.paye,
    delivered: true,
    lines: (f.lines as Any[] ?? []).map((l) => ({
      desc: l.desc, subprice: l.subprice, qty: l.qty, tva_tx: l.tva_tx, product_type: l.product_type,
    })),
    payments: payments.map((p) => ({ amount: p.amount, type: p.type, date: p.date, ref: p.ref })),
  });
}
console.log(`factures client       : ${invoices.length}  (figées — déjà remises au client)`);
for (const e of excluded) console.log(`   ⊘ exclue : ${e.ref} (${e.total_ttc} EUR) — ${String(e.motif).slice(0, 72)}…`);

// --- factures fournisseur ---------------------------------------------------
const supplierInvoices = [];
for (const f of await api<Any[]>("/supplierinvoices?limit=500") ?? []) {
  if (!inYear(f.date)) continue;
  const payments = await api<Any[]>(`/supplierinvoices/${f.id}/payments`) ?? [];
  supplierInvoices.push({
    ref: f.ref,
    ref_supplier: f.ref_supplier,
    socid: f.socid,
    date: iso(f.date),
    total_ht: f.total_ht,
    total_tva: f.total_tva,
    total_ttc: f.total_ttc,
    statut: f.statut,
    paye: f.paye,
    lines: (f.lines as Any[] ?? []).map((l) => ({
      desc: l.desc, subprice: l.subprice, qty: l.qty, tva_tx: l.tva_tx, product_type: l.product_type,
    })),
    payments: payments.map((p) => ({ amount: p.amount, type: p.type, date: p.date, ref: p.ref })),
  });
}
console.log(`factures fournisseur  : ${supplierInvoices.length}`);

// --- comptes et mouvements bancaires ---------------------------------------
// LE RÉFÉRENT. Tout le reste se vérifie contre ces lignes.
const accounts = [];
let bankLineCount = 0;
for (const a of await api<Any[]>("/bankaccounts") ?? []) {
  const lines = (await api<Any[]>(`/bankaccounts/${a.id}/lines?limit=1000`) ?? [])
    .map((l) => ({
      date: iso(l.datev ?? l.dateo),
      amount: Number(l.amount),
      label: l.label,
      // Les libellés générés par Dolibarr — « SupplierInvoicePayment »,
      // « CustomerInvoicePayment » — traversent le grand livre tels quels et y
      // sont illisibles. Le rejeu doit les remplacer par le libellé de la pièce.
      generic_label: /^\(?(Customer|Supplier)InvoicePayment\)?$/.test(String(l.label ?? "")),
      num_releve: l.num_releve,
    }))
    .filter((l) => l.date)
    .sort((x, y) => String(x.date).localeCompare(String(y.date)));
  bankLineCount += lines.length;
  accounts.push({
    id: a.id,
    label: a.label,
    ref: a.ref,
    account_number: a.account_number,
    accountancy_journal: a.accountancy_journal,
    currency_code: a.currency_code,
    balance: Number(lines.reduce((s, l) => s + l.amount, 0).toFixed(2)),
    lines,
  });
}
console.log(`comptes bancaires     : ${accounts.length}  (${bankLineCount} mouvements — référent de vérification)`);

const generics = accounts.flatMap((a) => a.lines).filter((l) => l.generic_label).length;

const manifest = {
  meta: {
    exercice: year,
    source: base,
    extracted_at: new Date().toISOString(),
    // Un exercice clos ne se rejoue pas : la clôture est la limite dure.
    replayable: true,
    note: "Manifeste d'exercice — description des faits, non des écritures comptables. "
      + "Les factures client sont marquées delivered:true : elles existent chez le client "
      + "et se reproduisent à l'identique, jamais ne se recalculent.",
  },
  exclusions: excluded,
  anomalies: {
    libelles_generiques: generics,
    detail: generics
      ? "lignes bancaires portant un libellé Dolibarr générique — à remplacer au rejeu"
      : "aucune",
  },
  tiers,
  invoices,
  supplierInvoices,
  accounts,
};

await Deno.writeTextFile(out, JSON.stringify(manifest, null, 2));

const totalClient = invoices.reduce((s, f) => s + Number(f.total_ttc ?? 0), 0);
const totalFourn = supplierInvoices.reduce((s, f) => s + Number(f.total_ttc ?? 0), 0);
console.log(`\n--- récapitulatif ---`);
console.log(`  produits facturés   : ${totalClient.toFixed(2)} EUR`);
console.log(`  achats facturés     : ${totalFourn.toFixed(2)} EUR`);
for (const a of accounts) console.log(`  ${String(a.label).padEnd(32)} ${a.balance.toFixed(2)} EUR`);
console.log(`  libellés génériques : ${generics}  ← à corriger au rejeu`);
console.log(`\nmanifeste écrit : ${out}`);
