/**
 * TipTap JSON → @pdfme Template Serializer
 *
 * Converts a TipTap document JSON into a @pdfme Template suitable for PDF generation.
 * Supports a constrained formatting subset; unsupported nodes are rejected at validation time.
 *
 * Supported nodes:
 *   paragraph, heading (h1-h3), bulletList, orderedList, listItem,
 *   table, tableRow, tableCell, tableHeader, image, hardBreak,
 *   text (with marks: bold, italic, underline)
 *
 * Unsupported (rejected at save time):
 *   blockquote, codeBlock, horizontalRule, taskList
 */

import type { Template } from '@pdfme/common';

// ─── Types ────────────────────────────────────────────────────────────────────

export interface TipTapMark {
  type: string;
  attrs?: Record<string, unknown>;
}

export interface TipTapNode {
  type: string;
  content?: TipTapNode[];
  text?: string;
  marks?: TipTapMark[];
  attrs?: Record<string, unknown>;
}

// @pdfme schema field shape (matches pdfme text plugin schema)
// We use an index signature to satisfy @pdfme/common's Schema type requirement
interface SchemaField {
  name: string;
  type: 'text';
  position: { x: number; y: number };
  width: number;
  height: number;
  fontSize: number;
  fontName?: string;
  fontColor?: string;
  alignment?: 'left' | 'center' | 'right';
  lineHeight?: number;
  [key: string]: unknown;
}

// ─── Constants ────────────────────────────────────────────────────────────────

const PAGE_WIDTH_MM = 170;         // A4 content width (210 - 20mm margins each side)
const PAGE_BREAK_THRESHOLD = 250;  // y position (mm from page top) to start new logical page
const MARGIN_X_MM = 20;            // Left margin
const MARGIN_TOP_MM = 20;          // Top margin

const UNSUPPORTED_NODES = new Set([
  'blockquote',
  'codeBlock',
  'horizontalRule',
  'taskList',
  'taskItem',
]);

// Line heights per node type (mm)
const PARAGRAPH_HEIGHT = 6;
const H1_HEIGHT = 12;
const H2_HEIGHT = 9;
const H3_HEIGHT = 7;
const LIST_ITEM_HEIGHT = 6;
const TABLE_ROW_HEIGHT = 8;

// Font sizes
const PARAGRAPH_FONT_SIZE = 10;
const H1_FONT_SIZE = 20;
const H2_FONT_SIZE = 16;
const H3_FONT_SIZE = 14;
const LIST_FONT_SIZE = 10;
const TABLE_FONT_SIZE = 9;

// ─── Template Variables ───────────────────────────────────────────────────────

export const TEMPLATE_VARIABLES: Array<{ key: string; label: string; example: string }> = [
  { key: 'client.name', label: 'Client Full Name', example: 'John Smith' },
  { key: 'client.company', label: 'Company Name', example: 'Smith Holdings' },
  { key: 'client.email', label: 'Client Email', example: 'john@smithholdings.com' },
  { key: 'client.phone', label: 'Client Phone', example: '+61 400 000 000' },
  { key: 'interest.stage', label: 'Pipeline Stage', example: 'Signed EOI/NDA' },
  { key: 'interest.berthNumber', label: 'Berth Number (from interest)', example: 'A-23' },
  { key: 'berth.mooring_number', label: 'Berth Number', example: 'A-23' },
  { key: 'berth.price', label: 'Berth Price', example: '$45,000' },
  { key: 'berth.tenure_type', label: 'Tenure Type', example: 'Freehold' },
  { key: 'port.name', label: 'Port Name', example: 'Port Nimara' },
  { key: 'port.currency', label: 'Port Currency', example: 'AUD' },
  { key: 'date.today', label: "Today's Date", example: '2026-03-15' },
  { key: 'date.year', label: 'Current Year', example: '2026' },
];

// ─── Validation ───────────────────────────────────────────────────────────────

/**
 * Recursively walks a TipTap node tree and collects any unsupported node types.
 * Returns an array of unsupported type names found, or empty array if valid.
 */
export function validateTipTapDocument(doc: TipTapNode): string[] {
  const found = new Set<string>();

  function walk(node: TipTapNode): void {
    if (UNSUPPORTED_NODES.has(node.type)) {
      found.add(node.type);
    }
    if (node.content) {
      for (const child of node.content) {
        walk(child);
      }
    }
  }

  walk(doc);
  return Array.from(found);
}

// ─── Text extraction helpers ──────────────────────────────────────────────────

/**
 * Extracts plain text from a TipTap node and its children.
 * Preserves hardBreak as newline.
 */
function extractText(node: TipTapNode): string {
  if (node.type === 'text') {
    return node.text ?? '';
  }
  if (node.type === 'hardBreak') {
    return '\n';
  }
  if (!node.content || node.content.length === 0) {
    return '';
  }
  return node.content.map(extractText).join('');
}

/**
 * For a paragraph's inline content, returns text and a flag indicating bold.
 * (pdfme text schema doesn't support inline mixed formatting; we honour the
 * dominant mark on the paragraph level for simplicity.)
 */
function extractParagraphContent(node: TipTapNode): { text: string; bold: boolean } {
  let text = '';
  let hasBold = false;

  if (node.content) {
    for (const child of node.content) {
      if (child.type === 'text') {
        text += child.text ?? '';
        if ((child.marks ?? []).some((m) => m.type === 'bold')) hasBold = true;
      } else if (child.type === 'hardBreak') {
        text += '\n';
      } else {
        text += extractText(child);
      }
    }
  }

  return { text, bold: hasBold };
}

// ─── Field name generation ────────────────────────────────────────────────────

let fieldCounter = 0;

function nextFieldName(prefix: string): string {
  return `${prefix}_${fieldCounter++}`;
}

// ─── Serializer State ─────────────────────────────────────────────────────────

interface SerializerState {
  fields: SchemaField[];
  y: number;
  pageIndex: number;
}

function ensurePageSpace(state: SerializerState, needed: number): void {
  if (state.y + needed > PAGE_BREAK_THRESHOLD) {
    state.pageIndex++;
    state.y = MARGIN_TOP_MM;
  }
}

// ─── Node Processors ──────────────────────────────────────────────────────────

function processParagraph(node: TipTapNode, state: SerializerState): void {
  const { text, bold } = extractParagraphContent(node);
  const lineCount = Math.max(1, (text.match(/\n/g) ?? []).length + 1);
  const height = PARAGRAPH_HEIGHT * lineCount;

  ensurePageSpace(state, height);

  const field: SchemaField = {
    name: nextFieldName('para'),
    type: 'text',
    position: { x: MARGIN_X_MM, y: state.y },
    width: PAGE_WIDTH_MM,
    height,
    fontSize: PARAGRAPH_FONT_SIZE,
    fontName: bold ? 'Helvetica-Bold' : 'Helvetica',
  };

  state.fields.push(field);
  state.y += height + 1;
}

function processHeading(node: TipTapNode, state: SerializerState): void {
  const level = (node.attrs?.level as number) ?? 1;

  let height: number;
  let fontSize: number;
  if (level === 1) {
    height = H1_HEIGHT;
    fontSize = H1_FONT_SIZE;
  } else if (level === 2) {
    height = H2_HEIGHT;
    fontSize = H2_FONT_SIZE;
  } else {
    height = H3_HEIGHT;
    fontSize = H3_FONT_SIZE;
  }

  ensurePageSpace(state, height);

  const field: SchemaField = {
    name: nextFieldName(`h${level}`),
    type: 'text',
    position: { x: MARGIN_X_MM, y: state.y },
    width: PAGE_WIDTH_MM,
    height,
    fontSize,
    fontName: 'Helvetica-Bold',
  };

  state.fields.push(field);
  state.y += height + 2;
}

function processBulletList(node: TipTapNode, state: SerializerState): void {
  if (!node.content) return;

  for (const item of node.content) {
    if (item.type !== 'listItem') continue;
    const height = LIST_ITEM_HEIGHT;

    ensurePageSpace(state, height);

    state.fields.push({
      name: nextFieldName('bullet'),
      type: 'text',
      position: { x: MARGIN_X_MM + 5, y: state.y },
      width: PAGE_WIDTH_MM - 5,
      height,
      fontSize: LIST_FONT_SIZE,
      fontName: 'Helvetica',
    });

    state.y += height + 0.5;
  }
  state.y += 2;
}

function processOrderedList(node: TipTapNode, state: SerializerState): void {
  if (!node.content) return;

  // Use a counter that increments to build ordered list prefixes
  let listIndex = (node.attrs?.start as number) ?? 1;

  for (const item of node.content) {
    if (item.type !== 'listItem') continue;
    const height = LIST_ITEM_HEIGHT;

    ensurePageSpace(state, height);

    // listIndex is used via the field name prefix to distinguish items
    state.fields.push({
      name: nextFieldName(`ol_${listIndex}`),
      type: 'text',
      position: { x: MARGIN_X_MM + 5, y: state.y },
      width: PAGE_WIDTH_MM - 5,
      height,
      fontSize: LIST_FONT_SIZE,
      fontName: 'Helvetica',
    });

    state.y += height + 0.5;
    listIndex++;
  }
  state.y += 2;
}

function processTable(node: TipTapNode, state: SerializerState): void {
  if (!node.content) return;

  const rows = node.content.filter((r) => r.type === 'tableRow');
  if (rows.length === 0) return;

  const firstRow = rows[0];
  const colCount = firstRow?.content?.length ?? 1;
  const colWidth = PAGE_WIDTH_MM / colCount;

  for (const row of rows) {
    if (!row.content) continue;
    const rowHeight = TABLE_ROW_HEIGHT;

    ensurePageSpace(state, rowHeight);

    row.content.forEach((cell, colIdx) => {
      const isHeader = cell.type === 'tableHeader';

      state.fields.push({
        name: nextFieldName(isHeader ? 'th' : 'td'),
        type: 'text',
        position: {
          x: MARGIN_X_MM + colIdx * colWidth,
          y: state.y,
        },
        width: colWidth - 0.5,
        height: rowHeight,
        fontSize: TABLE_FONT_SIZE,
        fontName: isHeader ? 'Helvetica-Bold' : 'Helvetica',
      });
    });

    state.y += rowHeight + 0.5;
  }
  state.y += 3;
}

// ─── Top-level Node Dispatch ──────────────────────────────────────────────────

function processNode(node: TipTapNode, state: SerializerState): void {
  switch (node.type) {
    case 'paragraph':
      processParagraph(node, state);
      break;
    case 'heading':
      processHeading(node, state);
      break;
    case 'bulletList':
      processBulletList(node, state);
      break;
    case 'orderedList':
      processOrderedList(node, state);
      break;
    case 'table':
      processTable(node, state);
      break;
    case 'image':
      state.y += 20;
      break;
    case 'hardBreak':
      state.y += 3;
      break;
    default:
      break;
  }
}

// ─── Main Serializer ──────────────────────────────────────────────────────────

/**
 * Converts a TipTap JSON document to a @pdfme Template.
 * Variables like {{client.name}} are left as-is in text content.
 * Call buildContentInputsFromDoc to get inputs with actual values.
 */
export function tipTapToPdfmeTemplate(doc: TipTapNode): Template {
  fieldCounter = 0;

  const state: SerializerState = {
    fields: [],
    y: MARGIN_TOP_MM,
    pageIndex: 0,
  };

  const children = doc.type === 'doc' ? (doc.content ?? []) : [doc];
  for (const node of children) {
    processNode(node, state);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const basePdf = 'BLANK_PDF' as any;
  // pdfme's Schema type has a string index signature we satisfy via the
  // [key: string]: unknown on SchemaField
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const schemas = [state.fields] as any;

  return { basePdf, schemas } as Template;
}

// ─── Input Builder ────────────────────────────────────────────────────────────

/**
 * Given a @pdfme Template and a flat key→value data record,
 * builds empty input records keyed by field name.
 * Use buildContentInputsFromDoc for content-populated inputs.
 */
export function buildTemplateInputs(
  template: Template,
  data: Record<string, string>,
): Record<string, string>[] {
  return template.schemas.map((pageSchema) => {
    const record: Record<string, string> = {};
    for (const field of pageSchema as SchemaField[]) {
      record[field.name] = data[field.name] ?? '';
    }
    return record;
  });
}

/**
 * Replaces all {{variable.key}} tokens in a string with values from the data map.
 * Unmatched tokens are left as-is.
 */
export function substituteVariables(
  text: string,
  data: Record<string, string>,
): string {
  return text.replace(/\{\{([^}]+)\}\}/g, (_match, key: string) => {
    return data[key.trim()] ?? _match;
  });
}

/**
 * Public alias for internal buildContentInputs.
 * Builds pdfme input records by extracting text content from the TipTap doc
 * and mapping it to generated field names (in schema order).
 * Use after calling tipTapToPdfmeTemplate on the same (already-substituted) doc.
 */
export function buildContentInputsFromDoc(
  doc: TipTapNode,
  template: Template,
): Record<string, string>[] {
  return buildContentInputs(doc, template);
}

/**
 * Full pipeline: validate → substitute variables → convert to pdfme template + inputs.
 * Returns { template, inputs, errors }.
 */
export function tiptapDocumentToTemplateWithData(
  doc: TipTapNode,
  data: Record<string, string> = {},
): {
  template: Template | null;
  inputs: Record<string, string>[];
  errors: string[];
} {
  const errors = validateTipTapDocument(doc);
  if (errors.length > 0) {
    return { template: null, inputs: [], errors };
  }

  const substitutedDoc = substituteInDoc(doc, data);
  const template = tipTapToPdfmeTemplate(substitutedDoc);
  const inputs = buildContentInputs(substitutedDoc, template);

  return { template, inputs, errors: [] };
}

// ─── Internals ────────────────────────────────────────────────────────────────

/**
 * Deeply substitutes variables in all text nodes of a TipTap document.
 */
function substituteInDoc(
  node: TipTapNode,
  data: Record<string, string>,
): TipTapNode {
  if (node.type === 'text' && node.text) {
    return { ...node, text: substituteVariables(node.text, data) };
  }
  if (node.content) {
    return {
      ...node,
      content: node.content.map((child) => substituteInDoc(child, data)),
    };
  }
  return node;
}

/**
 * Builds pdfme input records by extracting text content from the TipTap doc
 * and mapping it to generated field names (in schema order).
 */
function buildContentInputs(
  doc: TipTapNode,
  template: Template,
): Record<string, string>[] {
  const textContents = extractAllTextContents(doc);
  const schemas = template.schemas;

  return schemas.map((pageSchema, pageIdx) => {
    const record: Record<string, string> = {};
    const fields = pageSchema as SchemaField[];

    fields.forEach((field, fieldIdx) => {
      const globalIdx =
        schemas
          .slice(0, pageIdx)
          .reduce((acc, ps) => acc + (ps as SchemaField[]).length, 0) + fieldIdx;
      record[field.name] = textContents[globalIdx] ?? '';
    });

    return record;
  });
}

/**
 * Extracts text content for each schema field in document order.
 * Order must mirror the order that processNode() creates fields.
 */
function extractAllTextContents(doc: TipTapNode): string[] {
  const contents: string[] = [];
  const children = doc.type === 'doc' ? (doc.content ?? []) : [doc];

  for (const node of children) {
    extractNodeContent(node, contents);
  }

  return contents;
}

function extractNodeContent(node: TipTapNode, out: string[]): void {
  switch (node.type) {
    case 'paragraph': {
      const { text } = extractParagraphContent(node);
      out.push(text);
      break;
    }
    case 'heading': {
      out.push(extractText(node));
      break;
    }
    case 'bulletList': {
      if (node.content) {
        for (const item of node.content) {
          if (item.type === 'listItem') {
            out.push('• ' + extractText(item).replace(/\n/g, ' '));
          }
        }
      }
      break;
    }
    case 'orderedList': {
      if (node.content) {
        let idx = (node.attrs?.start as number) ?? 1;
        for (const item of node.content) {
          if (item.type === 'listItem') {
            out.push(`${idx}. ` + extractText(item).replace(/\n/g, ' '));
            idx++;
          }
        }
      }
      break;
    }
    case 'table': {
      if (node.content) {
        for (const row of node.content) {
          if (row.type !== 'tableRow' || !row.content) continue;
          for (const cell of row.content) {
            out.push(extractText(cell));
          }
        }
      }
      break;
    }
    case 'image':
      out.push('');
      break;
    case 'hardBreak':
      break;
    default:
      break;
  }
}
