import PptxGenJS from 'pptxgenjs';
import type { LayoutName } from './state.js';
import type { ResolvedTheme } from './themes.js';
import { resolveAndGuard } from '../core.js';

// 16:9 LAYOUT_WIDE = 13.33 x 7.5 inch
const SAFE = { x: 0.5, y: 0.5, w: 12.33, h: 6.5 };
const TITLE_BAR = { y: 0.4, h: 0.9 };

export interface SlideMeta { index: number; total: number; }
export interface RenderExtra { workspacePath: string; warnings?: string[]; }

const LAYOUTS_WITHOUT_PAGINATION: LayoutName[] = ['title', 'section', 'closing'];

type Slide = PptxGenJS.Slide;

const BULLET_CODE = '25CF';

interface BulletItem {
  text: string;
  options: { bullet: { code: string }; indentLevel?: number; bold?: boolean };
}

// Extract a display string from one bullet. The documented contract is a plain
// string, but models often pass objects (sub-bullets, emphasis). We pull a
// text-like field instead of coercing the object — `String({...})` would yield
// the literal "[object Object]" in the slide. Returns '' when no text-like
// field exists so the caller can decide on a fallback.
function bulletText(b: unknown): string {
  if (typeof b === 'string') return b;
  if (typeof b === 'number' || typeof b === 'boolean') return String(b);
  if (b && typeof b === 'object' && !Array.isArray(b)) {
    const o = b as Record<string, unknown>;
    for (const k of ['text', 'label', 'title', 'content']) {
      if (typeof o[k] === 'string' && o[k]) return o[k] as string;
    }
    return '';
  }
  return b == null ? '' : String(b);
}

// A nested bullet array on an object bullet, if present.
function subBullets(b: unknown): unknown[] | null {
  if (b && typeof b === 'object' && !Array.isArray(b)) {
    const o = b as Record<string, unknown>;
    const sub = o['bullets'] ?? o['children'] ?? o['items'] ?? o['sub'];
    if (Array.isArray(sub) && sub.length > 0) return sub;
  }
  return null;
}

// Build pptxgenjs bullet items from a (possibly mixed) bullets array. Accepts
// strings, `{ text, bold?, bullets? }` objects, and nested sub-bullets (rendered
// at a deeper indentLevel). Never emits "[object Object]": an object with no
// text-like field falls back to a single JSON line so content is not silently
// lost.
export function toBulletItems(bullets: unknown[], level = 0): BulletItem[] {
  const items: BulletItem[] = [];
  for (const b of bullets) {
    const sub = subBullets(b);
    let text = bulletText(b);
    if (!text && !sub && b && typeof b === 'object') {
      try { text = JSON.stringify(b); } catch { text = ''; }
    }
    if (text) {
      const options: BulletItem['options'] = { bullet: { code: BULLET_CODE } };
      if (level > 0) options.indentLevel = level;
      if (b && typeof b === 'object' && (b as Record<string, unknown>)['bold'] === true) {
        options.bold = true;
      }
      items.push({ text, options });
    }
    if (sub) items.push(...toBulletItems(sub, level + 1));
  }
  return items;
}

export function renderSlide(
  slide: Slide,
  layout: LayoutName,
  content: Record<string, unknown>,
  theme: ResolvedTheme,
  meta: SlideMeta,
  extra?: RenderExtra,
): void {
  slide.background = { color: theme.background };

  switch (layout) {
    case 'title':       renderTitle(slide, content, theme); break;
    case 'section':     renderSection(slide, content, theme); break;
    case 'bullets':     renderBullets(slide, content, theme); break;
    case 'closing':     renderClosing(slide, content, theme); break;
    case 'two-column':  renderTwoColumn(slide, content, theme); break;
    case 'image-right': renderImageSide(slide, content, theme, 'right', extra); break;
    case 'image-left':  renderImageSide(slide, content, theme, 'left',  extra); break;
    case 'image-full':  renderImageFull(slide, content, theme, extra); break;
    case 'table':       renderTable(slide, content, theme); break;
    case 'chart':       renderChart(slide, content, theme); break;
    case 'quote':       renderQuote(slide, content, theme); break;
    case 'custom':      renderCustom(slide, content, theme, extra); break;
  }

  if (!LAYOUTS_WITHOUT_PAGINATION.includes(layout)) {
    drawPageNumber(slide, theme, meta);
  }
}

function notImplemented(layout: LayoutName): never {
  throw new Error(`renderSlide: layout "${layout}" not implemented yet`);
}

function drawPageNumber(slide: Slide, theme: ResolvedTheme, meta: SlideMeta): void {
  slide.addText(`${meta.index + 1} / ${meta.total}`, {
    x: 12.0, y: 7.0, w: 1.0, h: 0.3,
    fontSize: 12, color: stripHash(theme.muted),
    fontFace: theme.body_font, align: 'right',
  });
}

function drawTitleBar(slide: Slide, title: string, theme: ResolvedTheme): void {
  slide.addText(title, {
    x: SAFE.x, y: TITLE_BAR.y, w: SAFE.w, h: TITLE_BAR.h,
    fontSize: theme.heading_size, bold: true,
    color: stripHash(theme.text), fontFace: theme.heading_font,
    valign: 'middle',
  });
  slide.addShape('rect' as any, {
    x: SAFE.x, y: TITLE_BAR.y + TITLE_BAR.h, w: 1.5, h: 0.04,
    fill: { color: stripHash(theme.primary) },
    line: { type: 'none' },
  });
}

function renderTitle(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme): void {
  const title = String(c['title'] ?? '');
  const subtitle = typeof c['subtitle'] === 'string' ? c['subtitle'] : '';
  const author = typeof c['author'] === 'string' ? c['author'] : '';
  const date = typeof c['date'] === 'string' ? c['date'] : '';

  slide.addText(title, {
    x: SAFE.x, y: 2.5, w: SAFE.w, h: 1.5,
    fontSize: theme.title_size, bold: true,
    color: stripHash(theme.text), fontFace: theme.heading_font,
    valign: 'middle',
  });
  if (subtitle) {
    slide.addText(subtitle, {
      x: SAFE.x, y: 4.1, w: SAFE.w, h: 0.7,
      fontSize: theme.heading_size, color: stripHash(theme.muted),
      fontFace: theme.body_font, valign: 'top',
    });
  }
  if (author || date) {
    slide.addText(`${author}${author && date ? '  ' : ''}${date}`, {
      x: SAFE.x, y: 6.6, w: SAFE.w, h: 0.4,
      fontSize: theme.body_size, color: stripHash(theme.muted),
      fontFace: theme.body_font, align: 'right',
    });
  }
}

function renderSection(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme): void {
  slide.addShape('rect' as any, {
    x: 0, y: 0, w: 13.33, h: 0.4,
    fill: { color: stripHash(theme.primary) },
    line: { type: 'none' },
  });
  const number = typeof c['number'] === 'string' ? c['number'] : '';
  const title = String(c['title'] ?? '');
  if (number) {
    slide.addText(number, {
      x: 0.6, y: 2.0, w: 4.0, h: 3.5,
      fontSize: 140, bold: true,
      color: stripHash(theme.primary),
      fontFace: theme.heading_font, valign: 'middle',
    });
  }
  slide.addText(title, {
    x: 4.8, y: 2.5, w: 8.0, h: 2.5,
    fontSize: theme.title_size, bold: true,
    color: stripHash(theme.text),
    fontFace: theme.heading_font, valign: 'middle',
  });
}

function renderBullets(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme): void {
  drawTitleBar(slide, String(c['title'] ?? ''), theme);
  const bullets = (Array.isArray(c['bullets']) ? c['bullets'] : []) as unknown[];
  const items = toBulletItems(bullets);
  slide.addText(items as any, {
    x: SAFE.x, y: 1.8, w: SAFE.w, h: 4.7,
    fontSize: theme.body_size, color: stripHash(theme.text),
    fontFace: theme.body_font, valign: 'top', paraSpaceAfter: 8,
  });
  const footnote = typeof c['footnote'] === 'string' ? c['footnote'] : '';
  if (footnote) {
    slide.addText(footnote, {
      x: SAFE.x, y: 6.6, w: SAFE.w, h: 0.4,
      fontSize: 12, italic: true,
      color: stripHash(theme.muted), fontFace: theme.body_font,
    });
  }
}

function renderClosing(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme): void {
  const message = typeof c['message'] === 'string' && c['message']
    ? c['message'] : 'Thank you';
  const contact = typeof c['contact'] === 'string' ? c['contact'] : '';
  slide.addText(message, {
    x: SAFE.x, y: 3.0, w: SAFE.w, h: 1.5,
    fontSize: theme.title_size + 8, bold: true,
    color: stripHash(theme.primary),
    fontFace: theme.heading_font, align: 'center', valign: 'middle',
  });
  if (contact) {
    slide.addText(contact, {
      x: SAFE.x, y: 4.7, w: SAFE.w, h: 0.6,
      fontSize: theme.body_size, color: stripHash(theme.muted),
      fontFace: theme.body_font, align: 'center',
    });
  }
}

export function stripHash(c: string): string {
  return c.startsWith('#') ? c.slice(1) : c;
}

function renderTwoColumn(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme): void {
  drawTitleBar(slide, String(c['title'] ?? ''), theme);
  const cols: Array<{ obj: Record<string, unknown>; x: number }> = [
    { obj: (c['left']  as Record<string, unknown>) ?? {}, x: SAFE.x },
    { obj: (c['right'] as Record<string, unknown>) ?? {}, x: SAFE.x + SAFE.w / 2 + 0.2 },
  ];
  const colW = SAFE.w / 2 - 0.2;
  for (const { obj, x } of cols) {
    const heading = typeof obj['heading'] === 'string' ? obj['heading'] : '';
    const bullets = Array.isArray(obj['bullets']) ? obj['bullets'] as unknown[] : [];
    const text    = typeof obj['text']    === 'string' ? obj['text']    : '';
    if (heading) {
      slide.addText(heading, {
        x, y: 1.8, w: colW, h: 0.6,
        fontSize: theme.heading_size - 4, bold: true,
        color: stripHash(theme.primary), fontFace: theme.heading_font,
      });
    }
    if (bullets.length > 0) {
      const items = toBulletItems(bullets);
      slide.addText(items as any, {
        x, y: 2.5, w: colW, h: 4.0,
        fontSize: theme.body_size, color: stripHash(theme.text),
        fontFace: theme.body_font, valign: 'top', paraSpaceAfter: 8,
      });
    } else if (text) {
      slide.addText(text, {
        x, y: 2.5, w: colW, h: 4.0,
        fontSize: theme.body_size, color: stripHash(theme.text),
        fontFace: theme.body_font, valign: 'top',
      });
    }
  }
}

function renderImageSide(
  slide: Slide,
  c: Record<string, unknown>,
  theme: ResolvedTheme,
  side: 'left' | 'right',
  extra?: RenderExtra,
): void {
  drawTitleBar(slide, String(c['title'] ?? ''), theme);
  const imgRel = (c['image'] as Record<string, unknown>)?.['path'] as string;
  const textX  = side === 'right' ? SAFE.x          : SAFE.x + 5.5;
  const imgX   = side === 'right' ? SAFE.x + 7.5    : SAFE.x;
  const textW  = 7.0;
  const imgW   = 5.0;
  const blockY = 1.8;
  const blockH = 4.7;

  const body    = c['body'];
  const bullets = Array.isArray(body) ? body as unknown[] : null;
  if (bullets) {
    const items = toBulletItems(bullets);
    slide.addText(items as any, {
      x: textX, y: blockY, w: textW, h: blockH,
      fontSize: theme.body_size, color: stripHash(theme.text),
      fontFace: theme.body_font, valign: 'top', paraSpaceAfter: 8,
    });
  } else if (typeof body === 'string' && body) {
    slide.addText(body, {
      x: textX, y: blockY, w: textW, h: blockH,
      fontSize: theme.body_size, color: stripHash(theme.text),
      fontFace: theme.body_font, valign: 'top',
    });
  }

  addImageGuarded(slide, imgRel, { x: imgX, y: blockY, w: imgW, h: blockH }, extra);
}

function renderImageFull(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme, extra?: RenderExtra): void {
  const imgRel = (c['image'] as Record<string, unknown>)?.['path'] as string;
  const caption = typeof c['caption'] === 'string' ? c['caption'] : '';
  const hasCaption = caption.length > 0;
  addImageGuarded(slide, imgRel, {
    x: 0, y: 0, w: 13.33, h: hasCaption ? 6.7 : 7.5,
  }, extra);
  if (hasCaption) {
    slide.addShape('rect' as any, {
      x: 0, y: 6.7, w: 13.33, h: 0.8,
      fill: { color: stripHash(theme.background), transparency: 20 },
      line: { type: 'none' },
    });
    slide.addText(caption, {
      x: SAFE.x, y: 6.8, w: SAFE.w, h: 0.6,
      fontSize: theme.body_size, color: stripHash(theme.text),
      fontFace: theme.body_font, align: 'center', valign: 'middle',
    });
  }
}

function addImageGuarded(
  slide: Slide,
  rel: string | undefined,
  box: { x: number; y: number; w: number; h: number },
  extra?: RenderExtra,
): void {
  if (!rel || !extra) return;
  try {
    const abs = resolveAndGuard(extra.workspacePath, rel);
    const fs = require('fs') as typeof import('fs');
    if (!fs.existsSync(abs)) {
      extra.warnings?.push(`image not found: ${rel}`);
      return;
    }
    slide.addImage({ path: abs, sizing: { type: 'contain', w: box.w, h: box.h }, ...box });
  } catch (e) {
    extra.warnings?.push(`image error (${rel}): ${(e as Error).message}`);
  }
}

function renderTable(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme): void {
  drawTitleBar(slide, String(c['title'] ?? ''), theme);
  const headers = (c['headers'] as unknown[]).map(String);
  const rows = (c['rows'] as unknown[][]).map((r) => r.map(String));
  const ratios = Array.isArray(c['col_widths'])
    ? (c['col_widths'] as number[])
    : headers.map(() => 1 / headers.length);
  const totalW = SAFE.w;
  const colW = ratios.map((r) => r * totalW);

  const headerRow = headers.map((h) => ({
    text: h,
    options: {
      bold: true, color: 'FFFFFF',
      fill: { color: stripHash(theme.primary) },
      align: 'left' as const, valign: 'middle' as const,
      fontFace: theme.heading_font, fontSize: theme.body_size,
    },
  }));
  const dataRows = rows.map((row, ri) =>
    row.map((cell) => ({
      text: cell,
      options: {
        color: stripHash(theme.text),
        fill: { color: ri % 2 === 0 ? 'F5F5F5' : 'FFFFFF' },
        align: 'left' as const, valign: 'middle' as const,
        fontFace: theme.body_font, fontSize: theme.body_size - 2,
      },
    })),
  );

  slide.addTable([headerRow as any, ...dataRows.map((r) => r as any)], {
    x: SAFE.x, y: 1.8, w: totalW,
    colW, rowH: 0.5,
    border: { type: 'solid', pt: 0.5, color: stripHash(theme.muted) },
  });
}

const CHART_MAP: Record<string, string> = {
  bar:      'bar',
  line:     'line',
  pie:      'pie',
  doughnut: 'doughnut',
  area:     'area',
  scatter:  'scatter',
};

function renderChart(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme): void {
  drawTitleBar(slide, String(c['title'] ?? ''), theme);
  const chart_type = CHART_MAP[String(c['chart_type'])];
  const data = c['data'] as { categories: string[]; series: Array<{ name: string; values: number[] }> };

  const chartData = data.series.map((s) => ({
    name: s.name,
    labels: data.categories,
    values: s.values,
  }));

  slide.addChart(chart_type as any, chartData as any, {
    x: SAFE.x, y: 1.8, w: SAFE.w, h: 4.8,
    showLegend: true, legendPos: 'b',
    chartColors: [stripHash(theme.primary), stripHash(theme.accent), '999999', '4ECDC4', 'FFAA5C'],
    showTitle: false,
    catAxisLabelFontFace: theme.body_font,
    valAxisLabelFontFace: theme.body_font,
    catAxisLabelFontSize: 12,
    valAxisLabelFontSize: 12,
  });
}

function renderQuote(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme): void {
  slide.addShape('rect' as any, {
    x: 1.0, y: 1.5, w: 0.15, h: 4.5,
    fill: { color: stripHash(theme.accent) },
    line: { type: 'none' },
  });
  slide.addText(`"${String(c['quote'] ?? '')}"`, {
    x: 1.5, y: 1.8, w: 11.0, h: 3.5,
    fontSize: theme.heading_size + 4, italic: true,
    color: stripHash(theme.text), fontFace: theme.heading_font,
    valign: 'middle',
  });
  const attr = typeof c['attribution'] === 'string' ? c['attribution'] : '';
  if (attr) {
    slide.addText(`— ${attr}`, {
      x: 1.5, y: 5.5, w: 11.0, h: 0.6,
      fontSize: theme.body_size, color: stripHash(theme.muted),
      fontFace: theme.body_font,
    });
  }
}

const SHAPE_MAP: Record<string, string> = {
  rect:      'rect',
  roundRect: 'roundRect',
  arrow:     'rightArrow',
  oval:      'ellipse',
  line:      'line',
};

function renderCustom(slide: Slide, c: Record<string, unknown>, theme: ResolvedTheme, extra?: RenderExtra): void {
  const elements = (c['elements'] as Array<Record<string, unknown>>) ?? [];
  for (const el of elements) {
    const x = el['x'] as number, y = el['y'] as number, w = el['w'] as number, h = el['h'] as number;
    const options = (el['options'] as Record<string, unknown>) ?? {};
    switch (el['type']) {
      case 'text': {
        slide.addText(String(el['text'] ?? ''), {
          x, y, w, h,
          fontSize: typeof options['font_size'] === 'number' ? options['font_size'] as number : theme.body_size,
          bold: !!options['bold'],
          color: stripHash(typeof options['color'] === 'string' ? options['color'] as string : theme.text),
          fontFace: theme.body_font,
          align: (options['align'] as any) ?? 'left',
        });
        break;
      }
      case 'image': {
        addImageGuarded(slide, el['path'] as string, { x, y, w, h }, extra);
        break;
      }
      case 'shape': {
        const shape = SHAPE_MAP[String(el['shape'])] ?? 'rect';
        slide.addShape(shape as any, {
          x, y, w, h,
          fill: { color: stripHash(typeof options['fill'] === 'string' ? options['fill'] as string : theme.accent) },
          line: typeof options['line'] === 'string'
            ? { type: 'solid', color: stripHash(options['line'] as string), pt: 1 }
            : { type: 'none' },
        });
        if (typeof options['text'] === 'string') {
          slide.addText(options['text'] as string, {
            x, y, w, h, valign: 'middle', align: 'center',
            color: stripHash(theme.text), fontSize: theme.body_size, fontFace: theme.body_font,
          });
        }
        break;
      }
      case 'table': {
        const headers = (el['headers'] as unknown[]).map(String);
        const rows = (el['rows'] as unknown[][]).map((r) => r.map(String));
        slide.addTable(
          [headers as any, ...rows.map((r) => r as any)],
          { x, y, w, colW: headers.map(() => w / headers.length), fontFace: theme.body_font, fontSize: theme.body_size - 2 },
        );
        break;
      }
      case 'chart': {
        const chart_type = CHART_MAP[String(el['chart_type'])];
        const data = el['data'] as { categories: string[]; series: Array<{ name: string; values: number[] }> };
        const chartData = data.series.map((s) => ({ name: s.name, labels: data.categories, values: s.values }));
        slide.addChart(chart_type as any, chartData as any, {
          x, y, w, h,
          chartColors: [stripHash(theme.primary), stripHash(theme.accent)],
        });
        break;
      }
    }
  }
}
