/*
  Rejeu d'un exercice à partir de son manifeste — pour l'instant EN LECTURE SEULE.

  CE QU'IL FAIT AUJOURD'HUI. Il calcule l'état que produirait le rejeu, corrections
  comprises, et le confronte au RÉFÉRENT : les mouvements bancaires. Qonto et Wise
  sont hors de Dolibarr, on ne peut pas les réécrire ; si le rejeu les reproduit au
  centime et à la date, il décrit le même exercice. C'est le seul test qui vaille.

  CE QU'IL NE FAIT PAS ENCORE, et pourquoi. Un rejeu en écriture suppose une
  instance VIERGE. `ops/sandbox/sandbox-lifecycle.sh` ne sait que `refresh-from-prod`
  — un re-seed iso-prod, qui réimporte précisément les défauts qu'on veut corriger.
  Écrire le rejeu sans cette capacité produirait des doublons, pas une reconstruction.
  La capacité manquante est donc un préalable, pas un détail d'implémentation.

  LES CORRECTIONS QUE LE REJEU APPLIQUERAIT. Elles sont déclarées ici, pas éparpillées
  dans le code : chacune nomme le défaut, sa cause et son remède. C'est ce que lira
  un comptable repreneur pour comprendre pourquoi le livre reconstruit diffère de
  l'ancien.

  Usage :
    DENO_TLS_CA_STORE=system DOLAPIKEY=… \
      deno run -A test/replayExercise.ts --manifest …/manifest.json [--url …]
*/
import "load_dotenv";

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

const manifestPath = pick("--manifest", "manifest-2026.json");
const base = pick("--url", Deno.env.get("DOLIBARR_URL") ?? "https://erp.arcodange.lab");
const key = Deno.env.get("DOLAPIKEY") ?? "";

type Any = Record<string, unknown>;
type Line = { date: string; amount: number; label: string; generic_label: boolean };
type Account = { id: string; label: string; balance: number; lines: Line[] };

const manifest = JSON.parse(await Deno.readTextFile(manifestPath)) as {
  meta: Any; exclusions: Any[]; accounts: Account[];
  invoices: Any[]; supplierInvoices: Any[]; tiers: Any[];
};

/**
 * Corrections que le rejeu appliquerait. Déclaratives : le défaut, sa cause, son
 * remède. Une correction sans cause écrite est une réécriture silencieuse.
 */
const CORRECTIONS = [
  {
    id: "libelles-generiques",
    defaut: "20 des 37 mouvements bancaires portent « (SupplierInvoicePayment) » ou "
      + "« (CustomerInvoicePayment) », libellés générés par Dolibarr",
    cause: "aucun libellé n'est fourni à l'enregistrement du règlement ; Dolibarr retombe "
      + "sur une clé de traduction",
    remede: "libeller chaque règlement d'après sa pièce — référence du fournisseur ou du client, "
      + "objet, période",
    portee: "grand livre : ces libellés le traversent tels quels et y sont illisibles pour un "
      + "repreneur comme pour un inspecteur",
  },
  {
    id: "auxiliaire-indemnites",
    defaut: "les 7 paiements divers d'indemnité d'occupation ne se journalisent pas",
    cause: "compte auxiliaire vide sur la ligne de charge ; le journal de banque exige une "
      + "contrepartie identifiée (« Tiers inconnu et code comptable auxiliaire non défini »)",
    remede: "renseigner subledger_account = 455110 — vérifié en sandbox, l'écriture journalise",
    portee: "sans cela le compte courant reste hors du grand livre : 1 483,23 EUR absents",
  },
  {
    id: "tva-ovh-non-deductible",
    defaut: "FAF2025001 enregistrée 6,08 HT + 1,22 TVA, la TVA figurant en déductible "
      + "(CA3 ligne 20, période 2025-10)",
    cause: "facture émise au nom personnel du fondateur, la société n'étant pas immatriculée ; "
      + "une facture adressée à un particulier n'ouvre aucun droit à déduction",
    remede: "enregistrer 7,30 EUR TTC au taux 0 — la charge est reprise pour son montant TTC "
      + "(décision n° 3, reprise L. 210-6)",
    portee: "TVA déductible de l'exercice : 353,76 → 352,54 EUR. Aucune déclaration déposée "
      + "(CA12 annuelle, exercice clos le 31/12) — la correction ne coûte rien",
  },
] as const;

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; }
}

const iso = (ts: unknown): string | null => {
  const n = Number(ts);
  return n ? new Date(n * 1000).toISOString().slice(0, 10) : null;
};

console.log(`manifeste : ${manifestPath}`);
console.log(`exercice  : ${manifest.meta.exercice}`);
console.log(`cible     : ${base}\n`);

// --- 1. le référent : la banque ---------------------------------------------
console.log("=== CONTRÔLE BANCAIRE — le rejeu reproduit-il les mouvements ? ===\n");

let totalAttendu = 0, totalTrouve = 0, ecarts = 0;
for (const a of manifest.accounts) {
  const live = (await api<Any[]>(`/bankaccounts/${a.id}/lines?limit=1000`) ?? [])
    .map((l) => ({ date: iso(l.datev ?? l.dateo), amount: Number(l.amount) }))
    .filter((l) => l.date);

  // Appariement par (date, montant) : ce que la banque impose, et rien de plus.
  // Le libellé ne participe PAS à l'appariement — c'est justement ce qu'on corrige.
  const reste = [...live];
  const manquants: Line[] = [];
  for (const m of a.lines) {
    const i = reste.findIndex((l) => l.date === m.date && Math.abs(l.amount - m.amount) < 0.005);
    if (i >= 0) reste.splice(i, 1); else manquants.push(m);
  }
  totalAttendu += a.lines.length;
  totalTrouve += a.lines.length - manquants.length;
  ecarts += manquants.length + reste.length;

  const verdict = manquants.length === 0 && reste.length === 0 ? "conforme" : "ÉCART";
  console.log(`  ${a.label.padEnd(32)} ${String(a.lines.length).padStart(3)} mouvements   ${a.balance.toFixed(2).padStart(10)} EUR   ${verdict}`);
  for (const m of manquants) console.log(`      manquant à la cible : ${m.date}  ${m.amount.toFixed(2)}  ${m.label}`);
  for (const r of reste) console.log(`      en trop à la cible  : ${r.date}  ${r.amount.toFixed(2)}`);
}

console.log(`\n  ${totalTrouve}/${totalAttendu} mouvements appariés à la date et au centime`
  + (ecarts ? `  —  ${ecarts} écart(s)` : "  —  aucun écart"));

// --- 2. ce que le rejeu changerait ------------------------------------------
console.log("\n=== CORRECTIONS QUE LE REJEU APPLIQUERAIT ===\n");
for (const c of CORRECTIONS) {
  console.log(`  ▸ ${c.id}`);
  console.log(`      défaut : ${c.defaut}`);
  console.log(`      cause  : ${c.cause}`);
  console.log(`      remède : ${c.remede}`);
  console.log(`      portée : ${c.portee}\n`);
}

// --- 3. ce que le rejeu écarterait ------------------------------------------
console.log("=== PIÈCES ÉCARTÉES ===\n");
for (const e of manifest.exclusions) {
  console.log(`  ⊘ ${e.ref}  (${Number(e.total_ttc).toFixed(2)} EUR, ${e.date})`);
  console.log(`      ${String(e.motif).replace(/(.{92})/g, "$1\n      ")}\n`);
}

// --- 4. verdict --------------------------------------------------------------
const genericCount = manifest.accounts.flatMap((a) => a.lines).filter((l) => l.generic_label).length;
console.log("=== VERDICT ===\n");
console.log(`  manifeste complet vis-à-vis de la banque : ${ecarts === 0 ? "OUI" : "NON — " + ecarts + " écart(s)"}`);
console.log(`  corrections à appliquer                  : ${CORRECTIONS.length}`);
console.log(`  libellés à réécrire                      : ${genericCount}`);
console.log(`  pièces écartées                          : ${manifest.exclusions.length}`);
console.log(`\n  Le rejeu EN ÉCRITURE reste bloqué : il exige une instance vierge, que`);
console.log(`  sandbox-lifecycle.sh ne sait pas produire (refresh-from-prod uniquement).`);

Deno.exit(ecarts === 0 ? 0 : 1);
