/**
 * officePres — PowerPoint (.pptx) builder for officeTool.
 *
 * Built on pptxgenjs. Supports the slide surface an agent needs to
 * deliver a polished deck without hand-crafting XML:
 *   - Layouts: 16:9 (default), 16:10, 4:3, wide, plus custom dimensions
 *   - Theme: background colour, primary + accent colour, default font
 *   - Slide masters (header band + footer footer / page number)
 *   - Per-slide background colour or image
 *   - Slide types: title / content / two-content / section-header /
 *     table / chart / blank — but we accept any layout and treat the
 *     layout as a hint for default placements
 *   - Content items: text, bullets, images, tables, shapes, charts
 *   - Speaker notes
 *   - Slide transitions and slide numbers
 *
 * Reader is a stub (PPTX read is rare and pptx-parser support is
 * inconsistent across runtimes). info returns slide count + titles
 * via JSZip-based extraction.
 */

import fs from 'fs/promises';
import path from 'path';
import { createRequire } from 'module';

// Themed-presentation system. Opt-in via `theme: '<preset-name>'` on
// the spec — when present we route to the new builder for stunning
// curated decks; otherwise we keep the legacy pptxgenjs-direct path
// below for backwards compatibility.
import { isThemedSpec, buildThemedPres, listSystem } from './pres/index.js';
import { readPres } from './pres/edit/read.js';
import { fillPres } from './pres/edit/fill.js';

// pptxgenjs ships dual builds (ESM + CJS). The ESM build does
// `import JSZip from 'jszip'` which jszip (CJS-only) does not satisfy
// cleanly under jest's experimental-vm-modules. Loading via createRequire
// resolves the CJS build, which is well-behaved in both Node and Jest.
const localRequire = createRequire(import.meta.url);

let pptxgen = null;
let JSZip = null;

function loadPptx() {
  if (pptxgen) return pptxgen;
  const mod = localRequire('pptxgenjs');
  pptxgen = mod.default || mod;
  return pptxgen;
}

function loadZip() {
  if (JSZip) return JSZip;
  // jszip is a transitive dep of pptxgenjs, available at runtime.
  JSZip = localRequire('jszip');
  return JSZip;
}

function normHex(v) {
  if (!v || typeof v !== 'string') return null;
  const s = v.replace(/^#/, '').trim().toUpperCase();
  return /^[0-9A-F]{6}$/.test(s) ? s : null;
}

function resolveLayout(pptx, layout) {
  if (!layout) return 'LAYOUT_WIDE';
  if (typeof layout === 'object' && layout.width && layout.height) {
    // Custom: register a layout name and return it.
    pptx.defineLayout({
      name: 'CUSTOM_LAYOUT',
      width: Number(layout.width),
      height: Number(layout.height),
    });
    return 'CUSTOM_LAYOUT';
  }
  const k = String(layout).toUpperCase();
  switch (k) {
    case '16:9':
    case '16X9':
    case 'WIDE':
    case 'LAYOUT_WIDE':
      return 'LAYOUT_WIDE';
    case '16:10':
    case '16X10':
    case 'LAYOUT_16X10':
      return 'LAYOUT_16x10';
    case '4:3':
    case '4X3':
    case 'STANDARD':
    case 'LAYOUT_4X3':
      return 'LAYOUT_4x3';
    default:
      return 'LAYOUT_WIDE';
  }
}

/** Convert a text-style object → pptxgenjs text options. */
function textOpts(style = {}, defaults = {}) {
  const out = {
    fontFace: style.fontFace || style.font || defaults.fontFace,
    fontSize: Number(style.fontSize) || defaults.fontSize || 18,
    bold: !!style.bold,
    italic: !!style.italic,
    underline: style.underline ? { style: 'sng' } : undefined,
    align: style.align || 'left',
    valign: style.valign || 'top',
  };
  const hex = normHex(style.color || defaults.color);
  if (hex) out.color = hex;
  const fill = normHex(style.fill);
  if (fill) out.fill = { color: fill };
  return out;
}

function placeItem(slide, item, defaults) {
  const at = { x: item.x ?? 0.5, y: item.y ?? 0.5, w: item.w ?? 9, h: item.h ?? 1 };
  switch ((item.type || '').toLowerCase()) {
    case 'text':
      slide.addText(item.text || '', { ...at, ...textOpts(item, defaults) });
      break;

    case 'bullets':
    case 'bullet-list':
    case 'list': {
      const items = (item.items || []).map(li => {
        if (typeof li === 'string') return { text: li, options: { bullet: true } };
        return {
          text: li.text || '',
          options: {
            bullet: li.bullet === false ? false : { type: li.ordered ? 'number' : 'bullet' },
            indentLevel: li.level || 0,
          },
        };
      });
      slide.addText(items, {
        ...at,
        ...textOpts(item, defaults),
        paraSpaceAfter: item.spacing ?? 6,
      });
      break;
    }

    case 'image': {
      const src = item.path || item.data;
      if (!src) break;
      slide.addImage({ ...at, [item.data ? 'data' : 'path']: src });
      break;
    }

    case 'table': {
      const rows = [];
      if (Array.isArray(item.headers) && item.headers.length > 0) {
        rows.push(item.headers.map(h => ({
          text: String(h),
          options: {
            bold: true,
            color: normHex(item.headerStyle?.color) || 'FFFFFF',
            fill: { color: normHex(item.headerStyle?.fill) || '1F3864' },
            align: 'left',
          },
        })));
      }
      for (const row of item.rows || []) {
        if (Array.isArray(row)) {
          rows.push(row.map(c => ({
            text: String(c ?? ''),
            options: { color: normHex(item.cellStyle?.color) || '202020' },
          })));
        }
      }
      slide.addTable(rows, {
        ...at,
        colW: item.colW,
        rowH: item.rowH,
        fontSize: item.fontSize || 12,
        border: item.border === false ? undefined : { type: 'solid', pt: 0.5, color: 'CCCCCC' },
      });
      break;
    }

    case 'shape': {
      const shape = (item.shape || 'rect').toLowerCase();
      const map = {
        rect: 'rect', rectangle: 'rect',
        rounded: 'roundRect', 'rounded-rect': 'roundRect',
        oval: 'ellipse', ellipse: 'ellipse',
        line: 'line',
        triangle: 'triangle',
      };
      const shapeId = map[shape] || 'rect';
      slide.addShape(shapeId, {
        ...at,
        fill: normHex(item.fill) ? { color: normHex(item.fill) } : undefined,
        line: normHex(item.border)
          ? { color: normHex(item.border), width: item.borderWidth || 1 }
          : undefined,
      });
      break;
    }

    case 'chart': {
      const chartTypeMap = {
        bar: 'bar', column: 'bar',
        line: 'line',
        pie: 'pie',
        doughnut: 'doughnut',
        area: 'area',
      };
      const type = chartTypeMap[(item.chartType || 'bar').toLowerCase()] || 'bar';
      slide.addChart(type, item.data || [], {
        ...at,
        showTitle: !!item.title,
        title: item.title || '',
      });
      break;
    }

    default:
      // Treat unknown items as plain text so the deck never crashes.
      slide.addText(item.text || '', { ...at, ...textOpts(item, defaults) });
  }
}

/* ------------------------------------------------------------------------ */
/* Public API                                                                */
/* ------------------------------------------------------------------------ */

export async function createPres(spec, outputPath) {
  // New themed path: any spec with a string `theme` (one of the
  // registered presets) routes to the curated archetype system, which
  // produces decks the agent doesn't have to hand-design.
  if (isThemedSpec(spec)) {
    return await buildThemedPres(spec, outputPath);
  }

  const Pptx = loadPptx();
  const pptx = new Pptx();

  pptx.title = spec.title || '';
  pptx.subject = spec.subject || '';
  pptx.author = spec.author || 'Loxia Agent';
  pptx.company = spec.company || '';
  pptx.layout = resolveLayout(pptx, spec.layout);

  const theme = spec.theme || {};
  const defaults = {
    fontFace: theme.fontFamily || 'Calibri',
    fontSize: theme.fontSize || 18,
    color: normHex(theme.textColor) || '202020',
  };
  const themeBg = normHex(theme.background);
  const primary = normHex(theme.primaryColor) || '1F3864';
  const accent = normHex(theme.accentColor) || '2E75B6';

  // Slide master: title bar + footer with page number.
  if (spec.useMaster !== false) {
    pptx.defineSlideMaster({
      title: 'LOXIA_MASTER',
      background: themeBg ? { color: themeBg } : { color: 'FFFFFF' },
      objects: [
        { rect: { x: 0, y: 0, w: '100%', h: 0.35, fill: { color: primary } } },
        {
          text: {
            text: spec.title || '',
            options: { x: 0.4, y: 0.05, w: 8, h: 0.3, fontSize: 12, color: 'FFFFFF', bold: true, fontFace: defaults.fontFace },
          },
        },
        {
          placeholder: {
            options: { name: 'footer', type: 'body', x: 0.4, y: '95%', w: 5, h: 0.3, fontSize: 9, color: '707070' },
            text: spec.company || '',
          },
        },
        { rect: { x: 0, y: '99.4%', w: '100%', h: 0.06, fill: { color: accent } } },
      ],
      slideNumber: { x: '93%', y: '95%', fontSize: 9, color: '707070' },
    });
  }

  const slides = Array.isArray(spec.slides) ? spec.slides : [];
  for (const slideDef of slides) {
    const slide = pptx.addSlide({ masterName: spec.useMaster !== false ? 'LOXIA_MASTER' : undefined });

    // Per-slide background override
    const slideBg = normHex(slideDef.background);
    if (slideBg) slide.background = { color: slideBg };

    // Speaker notes
    if (slideDef.notes) slide.addNotes(String(slideDef.notes));

    // Default placements by layout
    const layout = (slideDef.layout || 'content').toLowerCase();

    if (layout === 'section-header' || layout === 'section') {
      if (slideDef.title) {
        slide.addText(slideDef.title, {
          x: 0.5, y: 2.0, w: 9, h: 1.5,
          fontSize: 40, bold: true, color: primary, fontFace: defaults.fontFace, align: 'left',
        });
      }
      if (slideDef.subtitle) {
        slide.addText(slideDef.subtitle, {
          x: 0.5, y: 3.5, w: 9, h: 0.8,
          fontSize: 20, color: '595959', fontFace: defaults.fontFace, align: 'left',
        });
      }
    } else if (layout === 'title' || layout === 'title-slide') {
      if (slideDef.title) {
        slide.addText(slideDef.title, {
          x: 0.5, y: 1.5, w: 9, h: 1.5,
          fontSize: 44, bold: true, color: primary, fontFace: defaults.fontFace, align: 'center',
        });
      }
      if (slideDef.subtitle) {
        slide.addText(slideDef.subtitle, {
          x: 0.5, y: 3.2, w: 9, h: 0.8,
          fontSize: 20, italic: true, color: '595959', fontFace: defaults.fontFace, align: 'center',
        });
      }
    } else {
      // Default content layout — title at top, body fills the rest.
      if (slideDef.title) {
        slide.addText(slideDef.title, {
          x: 0.5, y: 0.5, w: 9, h: 0.7,
          fontSize: 28, bold: true, color: primary, fontFace: defaults.fontFace, align: 'left',
        });
      }
      if (slideDef.subtitle) {
        slide.addText(slideDef.subtitle, {
          x: 0.5, y: 1.15, w: 9, h: 0.4,
          fontSize: 14, italic: true, color: '595959', fontFace: defaults.fontFace, align: 'left',
        });
      }
    }

    // Place any free-form content items
    for (const item of slideDef.content || []) {
      placeItem(slide, item, defaults);
    }

    // Slide transition (pptxgenjs accepts addNotes only; transitions need
    // raw XML hacks — skipped for now). Slide-number override:
    if (slideDef.slideNumber === false) slide.slideNumber = { x: -1, y: -1 };
  }

  // pptxgenjs writeFile uses its own filename mechanism; we capture buffer instead.
  const buf = await pptx.write({ outputType: 'nodebuffer' });
  await fs.writeFile(outputPath, buf);
  const stat = await fs.stat(outputPath);
  return { path: outputPath, size: stat.size, slideCount: slides.length };
}

/* ----- read / fill: outline + token replacement for existing decks ----- */

/**
 * Read the structural outline + tokens of an existing .pptx so the
 * agent can decide what to fill. Delegates to pres/edit/read.js so the
 * heavy XML walk lives in its own file.
 */
export async function readPresOutline(filePath) {
  return await readPres(filePath);
}

/**
 * Fill `{{token}}` placeholders in an existing .pptx with the provided
 * `values` map. Delegates to pres/edit/fill.js. Lets the agent take a
 * hand-made template deck and stamp customer/data specifics into it
 * without ever touching the design.
 */
export async function fillPresPlaceholders(args) {
  return await fillPres(args);
}

/**
 * Expose the available themes / archetypes / backgrounds for the
 * agent-facing tool docstring + the showcase verifier.
 */
export function listPresentationSystem() {
  return listSystem();
}

/* ----- info (light reader via JSZip) ----- */

export async function presInfo(filePath) {
  const stat = await fs.stat(filePath);
  // Count slides by listing entries in the pptx zip.
  try {
    const Zip = loadZip();
    const buf = await fs.readFile(filePath);
    const zip = await Zip.loadAsync(buf);
    const slidePaths = Object.keys(zip.files).filter(p => /^ppt\/slides\/slide\d+\.xml$/.test(p));
    return {
      fileName: path.basename(filePath),
      fileSize: stat.size,
      modified: stat.mtime.toISOString(),
      slideCount: slidePaths.length,
    };
  } catch {
    return {
      fileName: path.basename(filePath),
      fileSize: stat.size,
      modified: stat.mtime.toISOString(),
      slideCount: null,
    };
  }
}
