import type { Blocks, BlocksV1 } from "@slackwsh/contracts";

export interface RichTextMark {
  type: "bold" | "italic" | "underline" | "strike" | "code" | "link";
  attrs?: { href?: string };
}

export interface RichTextNode {
  type:
    | "doc"
    | "paragraph"
    | "heading"
    | "text"
    | "hardBreak"
    | "bulletList"
    | "orderedList"
    | "listItem"
    | "blockquote"
    | "codeBlock";
  text?: string;
  attrs?: { level?: number; start?: number };
  marks?: RichTextMark[];
  content?: RichTextNode[];
}

const BLOCK_TYPES = new Set(["paragraph", "heading", "bulletList", "orderedList", "listItem", "blockquote", "codeBlock"]);
const CONTAINER_TYPES = new Set(["doc", ...BLOCK_TYPES]);
const MARK_TYPES = new Set<RichTextMark["type"]>(["bold", "italic", "underline", "strike", "code", "link"]);
const MAX_NODES = 2_000;
const MAX_DEPTH = 12;
const MAX_TEXT_LENGTH = 40_000;

export function plainTextToBlocksV1(text: string): BlocksV1 {
  return {
    v: 1,
    doc: {
      type: "doc",
      content: text.split("\n").map((line) => ({
        type: "paragraph",
        content: line ? [{ type: "text", text: line }] : [],
      })),
    },
  };
}

/** Accept only the ProseMirror subset used by the composer. */
export function sanitizeMessageBlocks(value: unknown, fallbackText = ""): BlocksV1 & Record<string, unknown> {
  if (!value || typeof value !== "object") return plainTextToBlocksV1(fallbackText);
  const record = value as Record<string, unknown>;
  if (record.v !== 1 || !record.doc || typeof record.doc !== "object") return plainTextToBlocksV1(fallbackText);

  const budget = { nodes: 0, chars: 0 };
  const doc = sanitizeNode(record.doc, 0, budget);
  if (!doc || doc.type !== "doc") return plainTextToBlocksV1(fallbackText);

  const result: BlocksV1 & Record<string, unknown> = { v: 1, doc: doc as unknown as Record<string, unknown> };
  if (Array.isArray(record.attachments)) result.attachments = record.attachments.slice(0, 10);
  const rawDoc = record.doc as Record<string, unknown>;
  if (Array.isArray(rawDoc.attachments)) {
    (result.doc as Record<string, unknown>).attachments = rawDoc.attachments.slice(0, 10);
  }
  return result;
}

export function blocksToPlainText(blocks: Blocks | null | undefined): string {
  if (!blocks || blocks.v !== 1) return "";
  return nodeText(blocks.doc).replace(/\n{3,}/g, "\n\n").replace(/\n$/, "");
}

function sanitizeNode(value: unknown, depth: number, budget: { nodes: number; chars: number }): RichTextNode | null {
  if (!value || typeof value !== "object" || depth > MAX_DEPTH || ++budget.nodes > MAX_NODES) return null;
  const node = value as Record<string, unknown>;
  const type = typeof node.type === "string" ? node.type : "";
  if (type === "text") {
    if (typeof node.text !== "string" || budget.chars >= MAX_TEXT_LENGTH) return null;
    const text = node.text.slice(0, MAX_TEXT_LENGTH - budget.chars);
    budget.chars += text.length;
    if (!text) return null;
    const marks = Array.isArray(node.marks)
      ? node.marks.map(sanitizeMark).filter((mark): mark is RichTextMark => Boolean(mark)).slice(0, 8)
      : undefined;
    return { type: "text", text, ...(marks?.length ? { marks } : {}) };
  }
  if (type === "hardBreak") return { type: "hardBreak" };
  if (!CONTAINER_TYPES.has(type)) return null;

  const content = Array.isArray(node.content)
    ? node.content.map((child) => sanitizeNode(child, depth + 1, budget)).filter((child): child is RichTextNode => Boolean(child))
    : [];
  const safe: RichTextNode = { type: type as RichTextNode["type"], content };
  if (type === "heading") {
    const level = Number((node.attrs as Record<string, unknown> | undefined)?.level);
    safe.attrs = { level: Number.isInteger(level) ? Math.min(3, Math.max(1, level)) : 1 };
  } else if (type === "orderedList") {
    const start = Number((node.attrs as Record<string, unknown> | undefined)?.start);
    safe.attrs = { start: Number.isInteger(start) ? Math.min(10_000, Math.max(1, start)) : 1 };
  }
  return safe;
}

function sanitizeMark(value: unknown): RichTextMark | null {
  if (!value || typeof value !== "object") return null;
  const mark = value as Record<string, unknown>;
  if (typeof mark.type !== "string" || !MARK_TYPES.has(mark.type as RichTextMark["type"])) return null;
  if (mark.type !== "link") return { type: mark.type as Exclude<RichTextMark["type"], "link"> };
  const href = (mark.attrs as Record<string, unknown> | undefined)?.href;
  return typeof href === "string" && safeLink(href) ? { type: "link", attrs: { href } } : null;
}

function safeLink(href: string): boolean {
  if (href.startsWith("/") || href.startsWith("#")) return true;
  try {
    return ["http:", "https:", "mailto:"].includes(new URL(href).protocol);
  } catch {
    return false;
  }
}

function nodeText(value: unknown): string {
  if (!value || typeof value !== "object") return "";
  const node = value as Record<string, unknown>;
  if (node.type === "text" && typeof node.text === "string") return node.text;
  if (node.type === "hardBreak") return "\n";
  const children = Array.isArray(node.content) ? node.content.map(nodeText) : [];
  if (node.type === "doc" || node.type === "bulletList" || node.type === "orderedList") return children.join("\n");
  return children.join("");
}
