// TagAlly Engine — Phase 0: PDF/UA-Validierung über die veraPDF-CLI.
// Nutzung direkt:  node validate.js /Pfad/zur/Datei.pdf [ua1|ua2]
// Oder als Modul über server.js.

const { execFile } = require("child_process");
const fs = require("fs");
const path = require("path");
const os = require("os");
const { translator, available } = require("./i18n.js");

const { loadConfig } = require("./config.js");

const VERSION = "1.0.0";

// Sucht die veraPDF-CLI: config.json → Umgebungsvariable → übliche
// Installationsorte (je Plattform) → PATH. Unter Windows heißt die CLI
// verapdf.bat, und die PATH-Suche läuft über `where` statt `command -v`.
const IS_WIN = process.platform === "win32";

function findVerapdf() {
  const binName = IS_WIN ? "verapdf.bat" : "verapdf";
  const candidates = [
    loadConfig().verapdfPath,
    process.env.VERAPDF_PATH,
    path.join(os.homedir(), "verapdf", binName),
    ...(IS_WIN
      ? [
          path.join(process.env.ProgramFiles || "C:\\Program Files", "veraPDF", "verapdf.bat"),
          path.join(process.env.LOCALAPPDATA || "", "veraPDF", "verapdf.bat")
        ]
      : [
          "/Applications/veraPDF/verapdf",
          "/usr/local/bin/verapdf",
          "/opt/homebrew/bin/verapdf"
        ])
  ].filter(Boolean);
  for (const c of candidates) {
    if (fs.existsSync(c)) return c;
  }
  // Zuletzt der PATH — `where` liefert unter Windows den vollen Pfad zur .bat,
  // den wir brauchen, weil execFile eine .bat nicht direkt starten kann.
  try {
    const cmd = IS_WIN ? "where verapdf" : "command -v verapdf";
    const out = String(require("child_process").execSync(cmd, { stdio: "pipe" })).trim();
    const first = out.split(/\r?\n/)[0];
    if (first) return IS_WIN ? first : "verapdf";
  } catch (e) { /* nicht im PATH */ }
  return null;
}

// Externes Werkzeug starten. Windows-Sonderfall: .bat/.cmd sind keine
// Programme, sondern Skripte — sie müssen über cmd.exe laufen; Argumente
// werden dafür selbst in Anführungszeichen gesetzt (Pfade mit Leerzeichen!).
function runTool(bin, args, opts, cb) {
  if (IS_WIN && /\.(bat|cmd)$/i.test(bin)) {
    const q = (s) => '"' + String(s).replace(/"/g, "") + '"';
    const line = [bin, ...args].map(q).join(" ");
    return execFile(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", '"' + line + '"'],
      { ...opts, windowsVerbatimArguments: true }, cb);
  }
  return execFile(bin, args, opts, cb);
}

// Zieht aus dem veraPDF-JSON die Kernaussagen; tolerant gegenüber Schema-Varianten.
function parseReport(json) {
  const job = json && json.report && json.report.jobs && json.report.jobs[0];
  if (!job) throw new Error("Unerwartetes veraPDF-Reportformat (kein jobs[0]).");
  let vr = job.validationResult;
  if (Array.isArray(vr)) vr = vr[0];
  if (!vr) throw new Error("Kein validationResult im Report — wurde eine gültige PDF-Datei übergeben?");
  const details = vr.details || {};
  const failures = (details.ruleSummaries || [])
    .filter((r) => String(r.ruleStatus || r.status).toUpperCase() === "FAILED")
    .map((r) => {
      // Fundstellen nach Seiten aggregieren (aus den Check-Kontexten).
      const pageCounts = {};
      for (const c of r.checks || []) {
        const m = /pages\[(\d+)\]/.exec(c.context || "");
        if (m) {
          const p = Number(m[1]) + 1;
          pageCounts[p] = (pageCounts[p] || 0) + 1;
        }
      }
      const pages = Object.entries(pageCounts)
        .sort((a, b) => b[1] - a[1])
        .slice(0, 12)
        .map(([p, n]) => `${p} (${n}×)`)
        .join(", ");
      return {
        specification: r.specification,
        clause: r.clause,
        testNumber: r.testNumber,
        description: r.description,
        failedChecks: r.failedChecks,
        object: r.object,
        pages
      };
    });
  return {
    compliant: vr.compliant === true,
    profile: vr.profileName || vr.profile || "",
    passedRules: details.passedRules != null ? details.passedRules : null,
    failedRules: details.failedRules != null ? details.failedRules : failures.length,
    passedChecks: details.passedChecks != null ? details.passedChecks : null,
    failedChecks: details.failedChecks != null ? details.failedChecks : null,
    failures
  };
}

function escapeHtml(s) {
  return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
}

// Sichtprüfungs-Werkzeuge: Icons erscheinen im Werkzeug-Kasten UND als Chips
// am Kopf jeder Checklisten-Gruppe — man sieht sofort, welche Ansicht hilft.
// Einfarbige Minimal-Outline-Icons als Inline-SVG (erben die Textfarbe,
// drucken sauber, keine Emoji-Bunttöne).
function icon(paths) {
  return `<svg class="ticon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
}
const TOOLS = {
  sv: { icon: icon('<path d="M5 4v15"/><path d="M5 6h13"/><path d="M9 11h9"/><path d="M13 16h5"/>'), key: "tool.sv" },
  acr: { icon: icon('<path d="M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8z"/><path d="M14 3v5h5"/><path d="M8.5 13h7M8.5 16.5h7"/>'), key: "tool.acr" },
  sr: { icon: icon('<path d="M4 9.5v5h3.5L13 19V5L7.5 9.5H4z"/><path d="M16 9a4.2 4.2 0 0 1 0 6"/><path d="M18.5 6.5a8 8 0 0 1 0 11"/>'), key: "tool.sr" },
  cc: { icon: icon('<circle cx="12" cy="12" r="9"/><path d="M12 3v18A9 9 0 0 0 12 3z" fill="currentColor" stroke="none"/>'), key: "tool.cc" }
};
function toolChips(keys, t) {
  return (keys || []).map((k) => `<span class="tchip">${TOOLS[k].icon}<span>${escapeHtml(t(TOOLS[k].key))}</span></span>`).join("");
}

// Manuelle Matterhorn-Checkliste: die nicht maschinell prüfbaren Bedingungen,
// gruppiert nach Themen (Checkpoint-Referenzen des Matterhorn-Protokolls).
// Texte stehen als Schlüssel in engine/strings/<sprache>.js — die Klausel-
// Referenzen (ref) sind Normbezeichner und bleiben in jeder Sprache gleich.
const MANUAL_CHECKS = [
  { key: "check.structure", ref: "Matterhorn 01 · 09", tools: ["sv", "sr"], items: 3 },
  { key: "check.headings", ref: "Matterhorn 14", tools: ["sv"], items: 1 },
  { key: "check.language", ref: "Matterhorn 11", tools: ["sv", "acr"], items: 2 },
  { key: "check.graphics", ref: "Matterhorn 13", tools: ["sv", "sr"], items: 3 },
  { key: "check.tables", ref: "Matterhorn 15", tools: ["sv"], items: 2 },
  { key: "check.links", ref: "Matterhorn 28", tools: ["sv", "acr"], items: 1 },
  { key: "check.color", ref: "WCAG 1.4.1 · 1.4.3", tools: ["cc"], items: 2 },
  { key: "check.meta", ref: "Matterhorn 06 · 07", tools: ["acr"], items: 2 },
  { key: "check.final", ref: "TagAlly", tools: ["acr"], items: 1 }
];

// Gestaltetes Prüfprotokoll: maschinelles Ergebnis + manuelle Checkliste,
// bildschirm- und drucktauglich (Browser-Druck ⇒ PDF-Nachweis).
function writeHtmlReport(pdfFile, flavour, result, contrast, lang) {
  const t = translator(lang);
  const fileName = path.basename(pdfFile);
  const now = new Date();
  const failRows = result.failures
    .map((f) => `<tr><td class="mono">${escapeHtml(f.clause)}<br><span class="dim">${escapeHtml(t("report.test", { n: f.testNumber }))}</span></td><td>${escapeHtml(f.description)}${f.pages ? `<div class="dim">${escapeHtml(t("report.pages", { list: f.pages }))}</div>` : ""}</td><td class="num">${escapeHtml(f.failedChecks)}</td></tr>`)
    .join("\n");
  // Kontrast-Daten vom Panel-Scan (TagAlly Kontrast-Prüfung): Wenn vorhanden,
  // wird die Checklisten-Gruppe „Farbe & Kontrast" dynamisch — maschinell
  // Erledigtes wird als erledigt ausgewiesen, die Sichtprüfung erscheint NUR,
  // wenn es im Dokument tatsächlich Text über Bildern/Verläufen gibt.
  const hasContrastData = contrast && typeof contrast.checked === "number";
  const conManualPages = hasContrastData && Array.isArray(contrast.manualPages) ? contrast.manualPages : [];
  const contrastCovered = hasContrastData && !contrast.fails && !conManualPages.length;
  const manual = MANUAL_CHECKS.map((g, gi) => {
    let items = [];
    for (let i = 1; i <= g.items; i++) items.push(t(g.key + "." + i));
    let chips = toolChips(g.tools, t);
    let extra = "";
    if (g.ref.indexOf("1.4.3") !== -1 && hasContrastData) {
      items = [items[0]]; // „Farbe nicht alleiniges Mittel" (1.4.1) bleibt immer manuell
      if (contrast.fails > 0) {
        extra += `<div class="mnote warn">${t("contrast.fails", { n: contrast.fails })}</div>`;
      }
      if (conManualPages.length) {
        items = items.concat([t("contrast.manualItem", { pages: conManualPages.join(", ") })]);
      }
      if (contrastCovered) {
        const apr = Number(contrast.approved || 0);
        extra += `<div class="mnote ok">${t("contrast.covered", {
          checked: contrast.checked,
          approved: apr ? t("contrast.coveredApproved", { n: apr }) : ""
        })}</div>`;
        chips = "";
      }
    }
    return `
    <div class="mgroup">
      <div class="mhead"><span>${escapeHtml(t(g.key))}</span><span class="mh-right">${chips}<span class="ref">${escapeHtml(g.ref)}</span></span></div>
      ${items.map((item, ii) => `
      <label class="mitem"><input type="checkbox" id="m${gi}_${ii}"><span>${escapeHtml(item)}</span></label>`).join("")}${extra}
    </div>`;
  }).join("\n");

  const html = `<!DOCTYPE html>
<html lang="${t.htmlLang}"><head><meta charset="utf-8"><title>${escapeHtml(t("report.title", { file: fileName }))}</title>
<style>
  :root { --brand:#14705e; --brand2:#0d4a3e; --ok:#1a7f4e; --okbg:#e7f5ec; --fail:#b2323c; --failbg:#fbeaec; --ink:#1c2430; --dim:#5c6773; --line:#d9dee5; }
  * { box-sizing:border-box; }
  body { font-family:-apple-system,"Segoe UI","Helvetica Neue",Arial,sans-serif; color:var(--ink); margin:0; background:#f2f4f7; }
  .page { max-width:52em; margin:2em auto; background:#fff; padding:3em 3.5em; box-shadow:0 2px 14px rgba(0,0,0,.08); }
  header { display:flex; justify-content:space-between; align-items:baseline; border-bottom:3px solid var(--brand); padding-bottom:1em; margin-bottom:1.6em; }
  .wordmark { font-size:1.5em; font-weight:800; letter-spacing:.01em; color:var(--brand2); }
  .wordmark span { color:var(--brand); }
  .doctype { color:var(--dim); font-size:.95em; }
  h1 { font-size:1.25em; margin:.2em 0 .8em; }
  h2 { font-size:1.02em; margin:2em 0 .6em; color:var(--brand2); text-transform:uppercase; letter-spacing:.06em; }
  .meta { display:grid; grid-template-columns:11em 1fr; gap:.25em .8em; font-size:.92em; margin-bottom:1.6em; }
  .meta dt { color:var(--dim); } .meta dd { margin:0; word-break:break-all; }
  .verdict { border-radius:8px; padding:1em 1.2em; font-size:1.05em; font-weight:700; display:flex; gap:.6em; align-items:center; }
  .verdict.pass { background:var(--okbg); color:var(--ok); border:1px solid var(--ok); }
  .verdict.fail { background:var(--failbg); color:var(--fail); border:1px solid var(--fail); }
  .verdict .sub { font-weight:400; font-size:.85em; display:block; margin-top:.2em; }
  table { border-collapse:collapse; width:100%; font-size:.88em; }
  th,td { border:1px solid var(--line); padding:.45em .6em; text-align:left; vertical-align:top; }
  th { background:#eef1f4; }
  .mono { font-family:ui-monospace,Menlo,monospace; font-size:.92em; white-space:nowrap; }
  .num { text-align:right; }
  .dim { color:var(--dim); font-size:.9em; }
  .mgroup { border:1px solid var(--line); border-radius:8px; margin-bottom:.8em; overflow:hidden; page-break-inside:avoid; }
  .mhead { display:flex; justify-content:space-between; align-items:center; gap:.6em; background:#eef1f4; padding:.5em .8em; font-weight:700; font-size:.92em; }
  .mhead .ref { color:var(--dim); font-weight:400; }
  .mh-right { display:flex; align-items:center; gap:.35em; flex-wrap:wrap; justify-content:flex-end; }
  .tchip { display:inline-flex; align-items:center; gap:.35em; background:#fff; border:1px solid var(--line); border-radius:99px; padding:.14em .6em; font-size:.76em; font-weight:400; color:var(--dim); white-space:nowrap; }
  .ticon { width:1.1em; height:1.1em; flex:none; }
  .tools li .ticon { width:1.05em; height:1.05em; vertical-align:-.18em; margin-right:.15em; }
  .mitem { display:flex; gap:.6em; padding:.45em .8em; border-top:1px solid var(--line); font-size:.92em; align-items:flex-start; cursor:pointer; }
  .mitem input { margin-top:.15em; width:1.05em; height:1.05em; accent-color:var(--brand); }
  .mnote { padding:.45em .8em; border-top:1px solid var(--line); font-size:.92em; }
  .mnote.ok { color:var(--ok); background:var(--okbg); }
  .mnote.warn { color:var(--fail); background:var(--failbg); }
  .signing { display:grid; grid-template-columns:1fr 1fr; gap:1.5em; margin-top:1em; font-size:.92em; }
  .signing .field { border-bottom:1px solid var(--ink); padding:1.6em .2em .3em; }
  .signing .lbl { color:var(--dim); font-size:.85em; margin-top:.3em; }
  textarea { width:100%; min-height:5em; border:1px solid var(--line); border-radius:6px; padding:.5em; font:inherit; font-size:.92em; }
  .hint { background:#fff8e6; border:1px solid #e6d28a; border-radius:6px; padding:.6em .9em; font-size:.88em; color:#6b5a12; margin:1.2em 0; }
  .tools { background:#eef4fb; border:1px solid #b9cfe8; border-radius:6px; padding:.7em 1em; font-size:.88em; color:#274b73; margin:0 0 1.2em; }
  .tools .tools-title { font-weight:700; margin-bottom:.2em; }
  .tools ul { margin:.35em 0 .3em 1.1em; padding:0; }
  .tools li { margin:.3em 0; }
  .tools a { color:#1d5da8; }
  footer { margin-top:2.2em; padding-top:.8em; border-top:1px solid var(--line); color:var(--dim); font-size:.8em; display:flex; justify-content:space-between; }
  @media print {
    body { background:#fff; } .page { box-shadow:none; margin:0; max-width:none; padding:1.2cm 1.6cm; }
    /* Arbeitshilfen bleiben am Bildschirm — der gedruckte Nachweis ist schlank:
       keine Erklärboxen, keine Werkzeug-Pillen, kein lokaler Dateipfad. */
    .hint, .tools, .tchip, .no-print { display:none; } textarea { border:1px solid var(--line); }
  }
</style></head><body><div class="page">
<header>
  <div class="wordmark">TagAlly<span>&nbsp;Pro</span></div>
  <div class="doctype">${escapeHtml(t("report.docType"))}</div>
</header>

<h1>${escapeHtml(fileName)}</h1>
<dl class="meta">
  <dt class="no-print">${escapeHtml(t("report.metaFile"))}</dt><dd class="no-print">${escapeHtml(pdfFile)}</dd>
  <dt>${escapeHtml(t("report.metaProfile"))}</dt><dd>${escapeHtml(t("report.metaProfileValue", { profile: result.profile || ("PDF/UA-" + String(flavour).replace(/\D/g, "")) }))}</dd>
  <dt>${escapeHtml(t("report.metaEngine"))}</dt><dd>${escapeHtml(t("report.metaEngineValue", { version: VERSION }))}</dd>
  <dt>${escapeHtml(t("report.metaDate"))}</dt><dd>${now.toLocaleDateString(t.locale, { day: "2-digit", month: "long", year: "numeric" })}, ${now.toLocaleTimeString(t.locale)}</dd>
</dl>

<h2>${escapeHtml(t("report.part1"))}</h2>
<div class="verdict ${result.compliant ? "pass" : "fail"}">
  <div style="font-size:1.5em">${result.compliant ? "✔" : "✘"}</div>
  <div>${escapeHtml(t(result.compliant ? "report.passed" : "report.failed"))}
    <span class="sub">${escapeHtml(t("report.verdictSub", {
      passed: result.passedRules,
      failed: result.failedRules,
      checks: result.failedChecks != null ? t("report.verdictChecks", { n: result.failedChecks }) : ""
    }))}</span>
  </div>
</div>
${result.failures.length ? `<table style="margin-top:1em"><tr><th>${escapeHtml(t("report.thClause"))}</th><th>${escapeHtml(t("report.thCondition"))}</th><th>${escapeHtml(t("report.thViolations"))}</th></tr>${failRows}</table>` : ""}

<h2>${escapeHtml(t("report.part2"))}</h2>
<p class="dim" style="margin-top:-.3em">${escapeHtml(t("report.part2Intro"))}</p>
<p class="hint">${escapeHtml(t("report.hint"))}</p>
<div class="tools">
  <div class="tools-title">${escapeHtml(t("report.toolsTitle"))}</div>
  <ul>
    <li>${TOOLS.sv.icon} ${t("tool.svText")}</li>
    <li>${TOOLS.acr.icon} ${t("tool.acrText")}</li>
    <li>${TOOLS.sr.icon} ${t("tool.srText")}</li>
    ${contrastCovered ? "" : `<li>${TOOLS.cc.icon} ${t("tool.ccText")}</li>`}
  </ul>
  <div style="opacity:.75">${escapeHtml(t("report.toolsFooter"))}</div>
</div>
${manual}

<h2>${escapeHtml(t("report.remarks"))}</h2>
<textarea placeholder="${escapeHtml(t("report.remarksPlaceholder"))}"></textarea>

<div class="signing">
  <div><div class="field" contenteditable="true"></div><div class="lbl">${escapeHtml(t("report.signedBy"))}</div></div>
  <div><div class="field" contenteditable="true"></div><div class="lbl">${escapeHtml(t("report.signedDate"))}</div></div>
</div>

<footer>
  <span>${escapeHtml(t("report.footer"))}</span>
  <span>${escapeHtml(t("report.page"))}</span>
</footer>
</div></body></html>`;
  const out = pdfFile.replace(/\.pdf$/i, "") + "-pruefprotokoll.html";
  fs.writeFileSync(out, html, "utf8");
  return out;
}

function validate(pdfFile, flavour = "ua1", contrast = null, lang = "de") {
  const t = translator(lang);
  return new Promise((resolve, reject) => {
    if (!fs.existsSync(pdfFile)) return reject(new Error(t("err.fileNotFound", { file: pdfFile })));
    const bin = findVerapdf();
    if (!bin) return reject(new Error(t("err.verapdfMissing")));
    // Exit-Code 1 bedeutet bei veraPDF „Datei nicht konform“ — kein technischer Fehler.
    runTool(bin, ["--flavour", flavour, "--format", "json", "--maxfailedchecks", "500", pdfFile], { maxBuffer: 64 * 1024 * 1024 }, (err, stdout, stderr) => {
      if (err && !stdout) {
        return reject(new Error(t("err.verapdfFailed", { msg: stderr || err.message })));
      }
      let result;
      try {
        result = parseReport(JSON.parse(stdout));
      } catch (e) {
        return reject(new Error(t("err.verapdfParse", { msg: e.message })));
      }
      result.reportHtml = writeHtmlReport(pdfFile, flavour, result, contrast, lang);
      resolve(result);
    });
  });
}

module.exports = { validate, findVerapdf, VERSION, languages: available };

// Direktaufruf als CLI
if (require.main === module) {
  const file = process.argv[2];
  const flavour = process.argv[3] || "ua1";
  if (!file) {
    console.log("Nutzung: node validate.js /Pfad/zur/Datei.pdf [ua1|ua2]");
    process.exit(2);
  }
  validate(file, flavour)
    .then((r) => {
      console.log(r.compliant ? "✔ BESTANDEN" : "✘ NICHT BESTANDEN");
      console.log(`Regeln: ${r.passedRules} bestanden, ${r.failedRules} verletzt`);
      for (const f of r.failures) {
        console.log(`  • [${f.clause}] ${f.description} (${f.failedChecks}×)`);
        if (f.pages) console.log(`    Seiten: ${f.pages}`);
      }
      console.log("Protokoll: " + r.reportHtml);
      process.exit(r.compliant ? 0 : 1);
    })
    .catch((e) => {
      console.error("FEHLER: " + e.message);
      process.exit(2);
    });
}
