import fs from 'fs';
import { PDFDocument, PDFName, PDFDict, PDFArray, PDFNumber } from 'pdf-lib';

/**
 * Accessibility analysis + plain-English suggestion knowledge base.
 *
 * Each check produces a stable `code`; the SUGGESTIONS map explains every code
 * to a non-technical user: what it means, why it matters, how it gets fixed,
 * and a concrete before → after example (shown in the AI Suggestions popup).
 */

export type CheckStatus = 'passed' | 'failed';

export interface CheckResult {
  code: string;
  status: CheckStatus;
  /** Short, document-specific finding (e.g. "No PageLabels entries were found"). */
  detail: string;
}

export interface Suggestion {
  title: string;
  /** Positive phrasing shown when the check passes. */
  titleOk: string;
  /** What this error actually means, in plain language. */
  plain: string;
  /** Why an end user should care. */
  why: string;
  /** How the fix is applied. */
  fix: string;
  /** Concrete example of the change. */
  example: { before: string; after: string };
  /** True when our pipeline repairs this automatically during AI Remediation. */
  autoFix: boolean;
}

export const SUGGESTIONS: Record<string, Suggestion> = {
  PAGELABELS_MISSING: {
    title: 'Page labels are missing',
    titleOk: 'Page labels are defined',
    plain:
      'The PDF has no "PageLabels" entry — the internal table that tells PDF viewers what each page should be called (1, 2, 3… or i, ii, iii for a preface). Longer documents are expected to define it.',
    why:
      'Without page labels, screen readers and viewers can only say "page 5 of 40" based on position. If the printed page shows "iv" or "A-2", a person using assistive technology hears a number that does not match the visible page, which makes citations and navigation confusing.',
    fix:
      'We add a PageLabels entry to the document catalog that numbers every page in standard decimal style (1, 2, 3…), so what assistive technology announces matches what viewers display.',
    example: {
      before: '/Catalog\n  /Pages 40 pages\n  (no /PageLabels entry)',
      after: '/Catalog\n  /Pages 40 pages\n  /PageLabels << /Nums [ 0 << /S /D >> ] >>\n  → every page announced as "1", "2", "3", …',
    },
    autoFix: true,
  },
  TAGS_MISSING: {
    title: 'Document has no structure tags',
    titleOk: 'Document is fully tagged',
    plain:
      'The PDF is just "painted" text and images — it has no hidden structure tree that says "this is a heading", "this is a paragraph", "this is a table".',
    why:
      'Screen readers depend entirely on tags. An untagged PDF may be read in the wrong order, as one continuous blob, or not at all.',
    fix:
      'AI Remediation analyses the layout and builds the full tag tree (headings, paragraphs, lists, tables, figures) into the file.',
    example: {
      before: '(no /StructTreeRoot)\nScreen reader: "…reads text in drawing order, tables collapse into nonsense…"',
      after: '/StructTreeRoot → H1 → P → Table → TR → TD\nScreen reader: "Heading level 1: Annual Report. Paragraph. Table with 3 columns…"',
    },
    autoFix: true,
  },
  MARKED_FALSE: {
    title: 'File is not marked as "Tagged PDF"',
    titleOk: 'File is marked as Tagged PDF',
    plain:
      'The flag that officially declares "this PDF contains accessibility tags" (/Marked true) is missing, so software treats the file as untagged even if some tags exist.',
    why: 'Assistive technology checks this flag first — without it, tags may be ignored completely.',
    fix: 'We set /Marked true in the document\'s MarkInfo dictionary during remediation.',
    example: {
      before: '/MarkInfo << >>   (or missing)',
      after: '/MarkInfo << /Marked true >>',
    },
    autoFix: true,
  },
  LANG_MISSING: {
    title: 'Document language is not set',
    titleOk: 'Document language is set',
    plain: 'The PDF does not say which language its text is written in.',
    why:
      'Screen readers pick a speech engine based on this value. Without it, English text may be read with the wrong pronunciation rules — or the reader\'s default language, making the document unintelligible.',
    fix:
      'We set the document language (e.g. en-US) at all three levels: the document, its content objects, and every structure tag.',
    example: {
      before: '/Catalog (no /Lang)',
      after: '/Catalog /Lang (en-US)\n+ 117 tags carrying lang="en-US"',
    },
    autoFix: true,
  },
  TITLE_MISSING: {
    title: 'Document title is missing',
    titleOk: 'Document title is set',
    plain: 'The PDF metadata has no title, so software falls back to showing the file name.',
    why:
      'A screen reader user hears the title first when opening the file. "final_v3 (2).pdf" says nothing; "Annual Accessibility Report 2026" orients them immediately.',
    fix:
      'AI Remediation derives a title (from the Title tag, the first H1 heading, existing metadata, or the file name — in that order) and writes it into the document information and XMP metadata.',
    example: {
      before: 'Title: (empty) → shows "final_v3 (2).pdf"',
      after: 'Title: "Annual Accessibility Report 2026"',
    },
    autoFix: true,
  },
  DISPLAY_TITLE_OFF: {
    title: 'Viewer shows file name instead of title',
    titleOk: 'Viewer displays the document title',
    plain:
      'Even when a title exists, the PDF must also set "DisplayDocTitle" so viewers show the title in the window bar instead of the file name.',
    why: 'PDF/UA requires it, and it is the first thing announced when the document opens.',
    fix: 'We set the DisplayDocTitle viewer preference to true.',
    example: {
      before: '/ViewerPreferences (missing)\nWindow bar: "report_final_v3.pdf"',
      after: '/ViewerPreferences << /DisplayDocTitle true >>\nWindow bar: "Annual Accessibility Report 2026"',
    },
    autoFix: true,
  },
  PDFUA_ID_MISSING: {
    title: 'PDF/UA identifier is missing',
    titleOk: 'PDF/UA identifier is present',
    plain:
      'The XMP metadata does not carry the "pdfuaid" flag that declares the file claims PDF/UA (ISO 14289) conformance.',
    why:
      'Checkers and procurement tools look for this flag to recognise the file as an accessible PDF. Without it the document fails automated compliance scans even when its content is fine.',
    fix: 'AI Remediation writes the PDF/UA identifier (part 1) into the XMP metadata.',
    example: {
      before: '<x:xmpmeta …> (no pdfuaid namespace)',
      after: '<pdfuaid:part>1</pdfuaid:part> in XMP metadata',
    },
    autoFix: true,
  },
  FIGURE_ALT_MISSING: {
    title: 'Images may be missing alternate text',
    titleOk: 'Images carry alternate text',
    plain:
      'The document contains Figure tags without an /Alt entry — the short text description a screen reader speaks instead of the image.',
    why: 'Without alt text, a blind reader only hears "figure" and misses whatever the image conveys.',
    fix:
      'AI Remediation adds alternate descriptions to figures during tagging. Decorative images are marked as artifacts so they are skipped instead of announced.',
    example: {
      before: '<Figure> (no /Alt)\nScreen reader: "Figure."',
      after: '<Figure /Alt "Bar chart: revenue rising from $2M to $5M between 2023 and 2026">',
    },
    autoFix: true,
  },
  // ---- Itemized per-content checks (from the extracted tag structure) ----
  FIGURE_NO_ALT: {
    title: 'Image without alternate text',
    titleOk: 'All images carry alternate text',
    plain:
      'This specific image (Figure tag) has no alternate description, so a screen reader can only announce "figure".',
    why: 'Every informative image needs a short description; without it, its content is lost to blind readers.',
    fix: 'AI Remediation writes an /Alt description onto this Figure tag (or marks it as decorative).',
    example: {
      before: '<Figure> (no /Alt)\nScreen reader: "Figure."',
      after: '<Figure /Alt "Company logo">\nScreen reader: "Figure: Company logo."',
    },
    autoFix: true,
  },
  NO_H1: {
    title: 'Document has no main heading (H1)',
    titleOk: 'Document starts with a main heading (H1)',
    plain: 'The tag tree contains headings, but none of them is an H1 — the top-level heading that names the document.',
    why: 'Screen reader users jump between headings to navigate; without an H1 the outline has no starting point.',
    fix: 'AI Remediation promotes the first style-detected top heading to H1 and aligns the rest beneath it.',
    example: {
      before: 'H2 "Introduction" → H2 "Chapter 1" (no H1 anywhere)',
      after: 'H1 "Annual Report" → H2 "Introduction" → H2 "Chapter 1"',
    },
    autoFix: true,
  },
  HEADING_SKIP: {
    title: 'Heading levels skip a step',
    titleOk: 'Heading levels follow a logical sequence',
    plain: 'A heading jumps more than one level below its predecessor (for example H1 straight to H3).',
    why: 'PDF/UA requires headings to descend one level at a time; skipped levels break outline navigation.',
    fix: 'AI Remediation renumbers headings so each level follows the previous one without gaps.',
    example: {
      before: 'H1 "Report" → H3 "Details"   (H2 skipped)',
      after: 'H1 "Report" → H2 "Details"',
    },
    autoFix: true,
  },
  TABLE_NO_HEADERS: {
    title: 'Table without header cells',
    titleOk: 'Tables define header cells',
    plain: 'This table has no TH (header) cells, so cells cannot be related to what they mean.',
    why: 'A screen reader reads "3,500" without headers; with them it reads "Revenue, Q4: 3,500".',
    fix: 'AI Remediation marks the first row/column as TH header cells with the correct scope.',
    example: {
      before: '<Table><TR><TD>Revenue</TD><TD>3,500</TD>',
      after: '<Table><TR><TH scope="col">Revenue</TH><TD>3,500</TD>',
    },
    autoFix: true,
  },
  EMPTY_HEADING: {
    title: 'Empty heading tag',
    titleOk: 'No empty headings',
    plain: 'A heading tag exists but contains no text — usually left over from an earlier tagging pass.',
    why: 'Screen readers announce "heading level N" with nothing after it, which is pure noise for the listener.',
    fix: 'AI Remediation removes empty headings or fills the level by renumbering real ones.',
    example: {
      before: 'H2 "" (empty) → H3 "Pricing"',
      after: 'H2 "Pricing"',
    },
    autoFix: true,
  },
};

// ---------------------------------------------------------------------------
// Itemized structure analysis — findings scale with the document's content.
// ---------------------------------------------------------------------------
interface StructNode {
  kid_type?: string;
  type?: string;
  alt?: string;
  kids?: StructNode[];
  [k: string]: unknown;
}

/** Walk the PDFix struct tree and produce per-item findings. */
export function analyzeStructure(tree: Record<string, unknown>): CheckResult[] {
  const results: CheckResult[] = [];
  const headings: { level: number; index: number; empty: boolean }[] = [];
  const figures: { index: number; hasAlt: boolean }[] = [];
  const tables: { index: number; hasTH: boolean }[] = [];
  let elementCount = 0;

  const hasTextDescendant = (n: StructNode): boolean => {
    if (!n.kids?.length) return false;
    return n.kids.some((k) => k.kid_type !== 'element' || hasTextDescendant(k));
  };

  const walk = (n: StructNode) => {
    if (n.kid_type === 'element' && typeof n.type === 'string') {
      elementCount += 1;
      const t = n.type;
      const h = /^H([1-6])$/.exec(t);
      if (h) headings.push({ level: Number(h[1]), index: headings.length + 1, empty: !hasTextDescendant(n) });
      if (t === 'Figure') figures.push({ index: figures.length + 1, hasAlt: typeof n.alt === 'string' && n.alt.replace(/\0/g, '').trim() !== '' });
      if (t === 'Table') {
        let hasTH = false;
        const findTH = (x: StructNode) => {
          if (x.type === 'TH') hasTH = true;
          x.kids?.forEach(findTH);
        };
        n.kids?.forEach(findTH);
        tables.push({ index: tables.length + 1, hasTH });
      }
    }
    n.kids?.forEach(walk);
  };
  walk(tree as StructNode);

  if (elementCount === 0) return results; // nothing tagged — doc-level TAGS_MISSING covers it

  // Figures: one finding per missing alt; a single passed line when all are fine.
  const missingAlt = figures.filter((f) => !f.hasAlt);
  for (const f of missingAlt) {
    results.push({ code: 'FIGURE_NO_ALT', status: 'failed', detail: `Figure ${f.index} of ${figures.length} has no alternate text.` });
  }
  if (figures.length > 0 && missingAlt.length === 0) {
    results.push({ code: 'FIGURE_NO_ALT', status: 'passed', detail: `All ${figures.length} figure${figures.length === 1 ? '' : 's'} carry alternate text.` });
  }

  // Headings: H1 presence, empty headings, skipped levels.
  if (headings.length > 0) {
    const hasH1 = headings.some((h) => h.level === 1);
    results.push({
      code: 'NO_H1',
      status: hasH1 ? 'passed' : 'failed',
      detail: hasH1 ? 'The document opens its outline with an H1 heading.' : `${headings.length} headings found, but none is an H1.`,
    });
    for (const h of headings.filter((x) => x.empty)) {
      results.push({ code: 'EMPTY_HEADING', status: 'failed', detail: `Heading ${h.index} (H${h.level}) contains no text.` });
    }
    let prev = 0;
    let skips = 0;
    for (const h of headings) {
      if (prev > 0 && h.level > prev + 1) {
        skips += 1;
        results.push({
          code: 'HEADING_SKIP', status: 'failed',
          detail: `Heading ${h.index} jumps from H${prev} to H${h.level} (H${prev + 1} skipped).`,
        });
      }
      prev = h.level;
    }
    if (skips === 0) {
      results.push({ code: 'HEADING_SKIP', status: 'passed', detail: `All ${headings.length} headings descend one level at a time.` });
    }
  }

  // Tables: one finding per table without headers; one passed line otherwise.
  const badTables = tables.filter((t) => !t.hasTH);
  for (const t of badTables) {
    results.push({ code: 'TABLE_NO_HEADERS', status: 'failed', detail: `Table ${t.index} of ${tables.length} defines no TH header cells.` });
  }
  if (tables.length > 0 && badTables.length === 0) {
    results.push({ code: 'TABLE_NO_HEADERS', status: 'passed', detail: `All ${tables.length} table${tables.length === 1 ? '' : 's'} define header cells.` });
  }

  return results;
}

/** Decode a PDF text string for display (handles UTF-16BE with BOM and hex forms). */
function decodePdfString(raw: string): string {
  let s = raw.replace(/^\(|\)$/g, '');
  if (s.charCodeAt(0) === 0xfe && s.charCodeAt(1) === 0xff) {
    // UTF-16BE: drop the BOM, keep every second byte of ASCII-range text.
    s = s.slice(2).split('').filter((_, i) => i % 2 === 1).join('');
  }
  return s.replace(/\0/g, '').trim();
}

/** Load once, reuse across checks. */
async function loadDoc(filePath: string) {
  const bytes = fs.readFileSync(filePath);
  const doc = await PDFDocument.load(bytes, { ignoreEncryption: true, updateMetadata: false });
  return { doc, raw: bytes };
}

/** Run all checks against a PDF file. */
export async function analyzePdf(filePath: string): Promise<CheckResult[]> {
  const { doc, raw } = await loadDoc(filePath);
  const catalog = doc.catalog;
  const pages = doc.getPageCount();
  const results: CheckResult[] = [];
  const has = (needle: string) => raw.includes(Buffer.from(needle));

  const structTree = has('/StructTreeRoot');
  results.push({
    code: 'TAGS_MISSING',
    status: structTree ? 'passed' : 'failed',
    detail: structTree ? 'Structure tag tree is present.' : 'No structure tags (StructTreeRoot) were found.',
  });

  const marked = /\/Marked\s+true/.test(raw.toString('latin1'));
  results.push({
    code: 'MARKED_FALSE',
    status: marked ? 'passed' : 'failed',
    detail: marked ? 'File is declared as Tagged PDF.' : 'The /Marked true flag is not set.',
  });

  const langMatch = catalog.get(PDFName.of('Lang'));
  results.push({
    code: 'LANG_MISSING',
    status: langMatch ? 'passed' : 'failed',
    detail: langMatch ? `Document language is set (${decodePdfString(String(langMatch))}).` : 'No document language is set.',
  });

  const title = doc.getTitle();
  results.push({
    code: 'TITLE_MISSING',
    status: title ? 'passed' : 'failed',
    detail: title ? `Document title: "${title}".` : 'The document has no title in its metadata.',
  });

  const viewerPrefs = catalog.get(PDFName.of('ViewerPreferences'));
  let displayTitle = false;
  if (viewerPrefs) {
    const dict = catalog.lookupMaybe(PDFName.of('ViewerPreferences'), PDFDict);
    const v = dict?.get(PDFName.of('DisplayDocTitle'));
    displayTitle = String(v) === 'true';
  }
  results.push({
    code: 'DISPLAY_TITLE_OFF',
    status: displayTitle ? 'passed' : 'failed',
    detail: displayTitle
      ? 'Viewers are told to display the document title.'
      : 'DisplayDocTitle is not enabled — viewers show the file name instead of the title.',
  });

  const pageLabels = catalog.get(PDFName.of('PageLabels'));
  const pageLabelsRelevant = pages >= 10;
  results.push({
    code: 'PAGELABELS_MISSING',
    status: pageLabels || !pageLabelsRelevant ? 'passed' : 'failed',
    detail: pageLabels
      ? 'Page labels are defined.'
      : pageLabelsRelevant
        ? `No PageLabels entries were found for this ${pages}-page document.`
        : 'Short document — page labels are optional here.',
  });

  const pdfua = has('pdfuaid');
  results.push({
    code: 'PDFUA_ID_MISSING',
    status: pdfua ? 'passed' : 'failed',
    detail: pdfua ? 'PDF/UA identifier is present in XMP metadata.' : 'The XMP metadata carries no PDF/UA identifier.',
  });

  // Crude but useful: count Figure tags vs Alt entries in the raw file.
  const latin = raw.toString('latin1');
  const figures = (latin.match(/\/Figure\b/g) || []).length;
  const alts = (latin.match(/\/Alt\b/g) || []).length;
  const altOk = figures === 0 || alts > 0;
  results.push({
    code: 'FIGURE_ALT_MISSING',
    status: altOk ? 'passed' : 'failed',
    detail:
      figures === 0
        ? 'No figures detected in the document.'
        : altOk
          ? `Figures carry alternate text (${alts} /Alt entr${alts === 1 ? 'y' : 'ies'}).`
          : `${figures} figure tag${figures === 1 ? '' : 's'} found but no /Alt alternate text.`,
  });

  return results;
}

/**
 * Post-remediation quick fixes for gaps PDFix leaves behind.
 * Currently: PageLabels (the "No PageLabels entries" error) and DisplayDocTitle.
 * Returns the codes that were repaired.
 */
export async function applyQuickFixes(filePath: string): Promise<string[]> {
  const { doc } = await loadDoc(filePath);
  const catalog = doc.catalog;
  const fixed: string[] = [];

  if (!catalog.get(PDFName.of('PageLabels'))) {
    // /PageLabels << /Nums [ 0 << /S /D >> ] >> — decimal labels for all pages.
    const style = doc.context.obj({ S: PDFName.of('D') });
    const nums = PDFArray.withContext(doc.context);
    nums.push(PDFNumber.of(0));
    nums.push(style);
    catalog.set(PDFName.of('PageLabels'), doc.context.obj({ Nums: nums }));
    fixed.push('PAGELABELS_MISSING');
  }

  let prefs = catalog.lookupMaybe(PDFName.of('ViewerPreferences'), PDFDict);
  const hasDisplay = prefs && String(prefs.get(PDFName.of('DisplayDocTitle'))) === 'true';
  if (!hasDisplay) {
    if (!prefs) {
      prefs = doc.context.obj({}) as PDFDict;
      catalog.set(PDFName.of('ViewerPreferences'), prefs);
    }
    prefs.set(PDFName.of('DisplayDocTitle'), doc.context.obj(true));
    fixed.push('DISPLAY_TITLE_OFF');
  }

  if (fixed.length) {
    fs.writeFileSync(filePath, await doc.save({ useObjectStreams: false }));
  }
  return fixed;
}
