import { CSSProperties, Fragment } from "react";
import type { JSONContent } from "@tiptap/core";
import {
  Text,
  Html,
  Head,
  Body,
  Font,
  Container,
  Link,
  Heading,
  Hr,
  Button,
  Img,
  Preview,
  Row,
  Column,
  render,
  Code,
} from "jsx-email";
import { AllowedAlignments } from "./types";

interface NodeOptions {
  parent?: JSONContent;
  prev?: JSONContent;
  next?: JSONContent;
}

export interface ThemeOptions {
  colors?: {
    heading?: string;
    paragraph?: string;
    horizontal?: string;
    footer?: string;
    blockquoteBorder?: string;
    codeBackground?: string;
    codeText?: string;
    link?: string;
  };
  fontSize?: {
    paragraph?: string;
    footer?: {
      size?: string;
      lineHeight?: string;
    };
  };
}

export interface RenderConfig {
  /**
   * The preview text is the snippet of text that is pulled into the inbox
   * preview of an email client, usually right after the subject line.
   *
   * Default: `undefined`
   */
  preview?: string;
  /**
   * The theme object allows you to customize the colors and font sizes of the
   * rendered email.
   *
   * Default:
   * ```js
   * {
   *   colors: {
   *     heading: 'rgb(17, 24, 39)',
   *     paragraph: 'rgb(55, 65, 81)',
   *     horizontal: 'rgb(234, 234, 234)',
   *     footer: 'rgb(100, 116, 139)',
   *   },
   *   fontSize: {
   *     paragraph: '15px',
   *     footer: {
   *       size: '14px',
   *       lineHeight: '24px',
   *     },
   *   },
   * }
   * ```
   *
   */
  theme?: ThemeOptions;
}

const DEFAULT_THEME: ThemeOptions = {
  colors: {
    heading: "rgb(17, 24, 39)",
    paragraph: "rgb(55, 65, 81)",
    horizontal: "rgb(234, 234, 234)",
    footer: "rgb(100, 116, 139)",
    blockquoteBorder: "rgb(209, 213, 219)",
    codeBackground: "rgb(239, 239, 239)",
    codeText: "rgb(17, 24, 39)",
    link: "rgb(59, 130, 246)",
  },
  fontSize: {
    paragraph: "15px",
    footer: {
      size: ".8rem",
    },
  },
};

const CODE_FONT_FAMILY =
  'SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';

const allowedHeadings = ["h1", "h2", "h3"] as const;
type AllowedHeadings = (typeof allowedHeadings)[number];

const headings: Record<AllowedHeadings, CSSProperties> = {
  h1: {
    fontSize: "36px",
    lineHeight: "40px",
    fontWeight: 800,
  },
  h2: {
    fontSize: "30px",
    lineHeight: "36px",
    fontWeight: 700,
  },
  h3: {
    fontSize: "24px",
    lineHeight: "38px",
    fontWeight: 600,
  },
};

const allowedSpacers = ["sm", "md", "lg", "xl"] as const;
export type AllowedSpacers = (typeof allowedSpacers)[number];

const spacers: Record<AllowedSpacers, string> = {
  sm: "8px",
  md: "16px",
  lg: "32px",
  xl: "64px",
};

export interface MarkType {
  [key: string]: any;
  type: string;
  attrs?: Record<string, any> | undefined;
}

const antialiased: CSSProperties = {
  WebkitFontSmoothing: "antialiased",
  MozOsxFontSmoothing: "grayscale",
};

export function generateKey() {
  return Math.random().toString(36).substring(2, 8);
}

export type VariableFormatter = (options: {
  variable: string;
  fallback?: string;
}) => string;

const allowedLogoSizes = ["sm", "md", "lg"] as const;
type AllowedLogoSizes = (typeof allowedLogoSizes)[number];

const logoSizes: Record<AllowedLogoSizes, string> = {
  sm: "40px",
  md: "48px",
  lg: "64px",
};

type EmailRendererOption = {
  shouldReplaceVariableValues?: boolean;
  variableValues?: Record<string, string | null>;
  linkValues?: Record<string, string | null>;
};

export class EmailRenderer {
  private config: RenderConfig = {
    theme: DEFAULT_THEME,
  };
  private shouldReplaceVariableValues = false;
  private variableValues: Record<string, string | null> = {};
  private linkValues: Record<string, string | null> = {};

  constructor(
    private readonly email: JSONContent = { type: "doc", content: [] },
    options: EmailRendererOption = {}
  ) {
    this.shouldReplaceVariableValues =
      options.shouldReplaceVariableValues || false;
    this.variableValues = options.variableValues || {};
    this.linkValues = options.linkValues || {};
  }

  private variableFormatter: VariableFormatter = ({ variable, fallback }) => {
    return fallback
      ? `{{${variable},fallback=${fallback}}}`
      : `{{${variable}}}`;
  };

  public render(options: EmailRendererOption = {}) {
    this.shouldReplaceVariableValues =
      options.shouldReplaceVariableValues || false;
    this.variableValues = options.variableValues || {};
    this.linkValues = options.linkValues || {};
    const markup = this.markup();
    return render(markup);
  }

  markup() {
    const nodes = this.email.content || [];

    const jsxNodes = nodes.map((node, index) => {
      const nodeOptions: NodeOptions = {
        prev: nodes[index - 1],
        next: nodes[index + 1],
        parent: node,
      };

      const component = this.renderNode(node, nodeOptions);
      if (!component) {
        return null;
      }

      return <Fragment key={generateKey()}>{component}</Fragment>;
    });

    const markup = (
      <Html>
        <Head>
          <Font
            fallbackFontFamily="sans-serif"
            fontFamily="Inter"
            fontStyle="normal"
            fontWeight={400}
            webFont={{
              url: "https://rsms.me/inter/font-files/Inter-Regular.woff2?v=3.19",
              format: "woff2",
            }}
          />
          <style
            dangerouslySetInnerHTML={{
              __html: `blockquote,h1,h2,h3,img,li,ol,p,ul{margin-top:0;margin-bottom:0} pre{padding:16px;border-radius:6px}`,
            }}
          />
          <meta content="width=device-width" name="viewport" />
          <meta content="IE=edge" httpEquiv="X-UA-Compatible" />
          <meta name="x-apple-disable-message-reformatting" />
          <meta
            // http://www.html-5.com/metatags/format-detection-meta-tag.html
            // It will prevent iOS from automatically detecting possible phone numbers in a block of text
            content="telephone=no,address=no,email=no,date=no,url=no"
            name="format-detection"
          />
          <meta content="light" name="color-scheme" />
          <meta content="light" name="supported-color-schemes" />
        </Head>
        <Body>
          <Container
            style={{
              maxWidth: "600px",
              minWidth: "300px",
              width: "100%",
              marginLeft: "auto",
              marginRight: "auto",
              padding: "0.5rem",
            }}
          >
            {jsxNodes}
          </Container>
        </Body>
      </Html>
    );

    return markup;
  }

  // `renderMark` will call the method of the corresponding mark type
  private renderMark(node: JSONContent): JSX.Element {
    // It will wrap the text with the corresponding mark type
    const text = node.text || <>&nbsp;</>;
    const marks = node.marks || [];

    return marks.reduce(
      (acc, mark) => {
        const type = mark.type;
        if (type in this) {
          // @ts-expect-error - `this` is not assignable to type 'never'
          return this[type]?.(mark, acc) as JSX.Element;
        }

        throw new Error(`Mark type "${type}" is not supported.`);
      },
      <>{text}</>
    );
  }

  private getMappedContent(
    node: JSONContent,
    options?: NodeOptions
  ): JSX.Element[] {
    return node.content
      ?.map((childNode) => {
        const component = this.renderNode(childNode, options);
        if (!component) {
          return null;
        }

        return <Fragment key={generateKey()}>{component}</Fragment>;
      })
      .filter((n) => n !== null) as JSX.Element[];
  }

  private renderNode(
    node: JSONContent,
    options: NodeOptions = {}
  ): JSX.Element | null {
    const type = node.type || "";

    if (type in this) {
      // @ts-expect-error - `this` is not assignable to type 'never'
      return this[type]?.(node, options) as JSX.Element;
    }

    throw new Error(`Node type "${type}" is not supported.`);
  }

  private unsubscribeFooter(
    node: JSONContent,
    options?: NodeOptions
  ): JSX.Element {
    return (
      <Container
        style={{
          fontSize: this.config.theme?.fontSize?.footer?.size,
          lineHeight: this.config.theme?.fontSize?.footer?.lineHeight,
          maxWidth: "100%",
          color: this.config.theme?.colors?.footer,
        }}
      >
        {this.getMappedContent(node)}
      </Container>
    );
  }

  private paragraph(node: JSONContent, options?: NodeOptions): JSX.Element {
    const { attrs } = node;
    const alignment = attrs?.textAlign || "left";

    const { parent, next } = options || {};
    const isParentListItem = parent?.type === "listItem";
    const isNextSpacer = next?.type === "spacer";

    return (
      <Text
        style={{
          textAlign: alignment,
          marginBottom: isParentListItem || isNextSpacer ? "0px" : "20px",
          marginTop: "0px",
          fontSize: this.config.theme?.fontSize?.paragraph,
          color: this.config.theme?.colors?.paragraph,
          ...antialiased,
        }}
      >
        {node.content ? this.getMappedContent(node) : <>&nbsp;</>}
      </Text>
    );
  }

  private text(node: JSONContent, _?: NodeOptions): JSX.Element {
    const text = node.text || "&nbsp";
    if (node.marks) {
      return this.renderMark(node);
    }

    return <>{text}</>;
  }

  private bold(_: MarkType, text: JSX.Element): JSX.Element {
    return <strong>{text}</strong>;
  }

  private italic(_: MarkType, text: JSX.Element): JSX.Element {
    return <em>{text}</em>;
  }

  private underline(_: MarkType, text: JSX.Element): JSX.Element {
    return <u>{text}</u>;
  }

  private strike(_: MarkType, text: JSX.Element): JSX.Element {
    return <s style={{ textDecoration: "line-through" }}>{text}</s>;
  }

  private textStyle(mark: MarkType, text: JSX.Element): JSX.Element {
    const { attrs } = mark;
    const { fontSize, fontWeight, color } = attrs || {};

    return <span style={{ fontSize, fontWeight, color }}>{text}</span>;
  }

  private link(mark: MarkType, text: JSX.Element): JSX.Element {
    const { attrs } = mark;
    let href = attrs?.href || "#";
    const target = attrs?.target || "_blank";
    const rel = attrs?.rel || "noopener noreferrer nofollow";

    // If the href value is provided, use it to replace the link
    // Otherwise, use the original link
    if (
      typeof this.linkValues === "object" ||
      typeof this.variableValues === "object"
    ) {
      href = this.linkValues[href] || this.variableValues[href] || href;
    }

    return (
      <Link
        href={href}
        rel={rel}
        style={{
          fontWeight: 500,
          textDecoration: "underline",
          color: this.config.theme?.colors?.link,
        }}
        target={target}
      >
        {text}
      </Link>
    );
  }

  private heading(node: JSONContent, options?: NodeOptions): JSX.Element {
    const { attrs } = node;
    const { next, prev } = options || {};

    const level = `h${Number(attrs?.level) || 1}`;
    const alignment = attrs?.textAlign || "left";
    const isNextSpacer = next?.type === "spacer";
    const isPrevSpacer = prev?.type === "spacer";

    const { fontSize, lineHeight, fontWeight } =
      headings[level as AllowedHeadings];

    return (
      <Heading
        // @ts-expect-error - `this` is not assignable to type 'never'
        as={level}
        style={{
          textAlign: alignment,
          color: this.config.theme?.colors?.heading,
          marginBottom: isNextSpacer ? "0px" : "12px",
          marginTop: isPrevSpacer ? "0px" : "0px",
          fontSize,
          lineHeight,
          fontWeight,
        }}
      >
        {this.getMappedContent(node)}
      </Heading>
    );
  }

  private variable(node: JSONContent, _?: NodeOptions): JSX.Element {
    const { id: variable, fallback } = node.attrs || {};

    let formattedVariable = this.variableFormatter({
      variable,
      fallback,
    });

    // If `shouldReplaceVariableValues` is true, replace the variable values
    // Otherwise, just return the formatted variable
    if (this.shouldReplaceVariableValues) {
      formattedVariable =
        this.variableValues[variable] || fallback || formattedVariable;
    }

    return <>{formattedVariable}</>;
  }

  private horizontalRule(_: JSONContent, __?: NodeOptions): JSX.Element {
    return (
      <Hr
        style={{
          marginTop: "32px",
          marginBottom: "32px",
          borderTopWidth: "2px",
        }}
      />
    );
  }

  private orderedList(node: JSONContent, _?: NodeOptions): JSX.Element {
    return (
      <Container style={{ maxWidth: "100%" }}>
        <ol
          style={{
            marginTop: "0px",
            marginBottom: "20px",
            paddingLeft: "26px",
            listStyleType: "decimal",
          }}
        >
          {this.getMappedContent(node)}
        </ol>
      </Container>
    );
  }

  private bulletList(node: JSONContent, _?: NodeOptions): JSX.Element {
    return (
      <Container
        style={{
          maxWidth: "100%",
        }}
      >
        <ul
          style={{
            marginTop: "0px",
            marginBottom: "20px",
            paddingLeft: "26px",
            listStyleType: "disc",
          }}
        >
          {this.getMappedContent(node)}
        </ul>
      </Container>
    );
  }

  private listItem(node: JSONContent, options?: NodeOptions): JSX.Element {
    return (
      <Container
        style={{
          maxWidth: "100%",
        }}
      >
        <li
          style={{
            marginBottom: "8px",
            paddingLeft: "6px",
            ...antialiased,
          }}
        >
          {this.getMappedContent(node, { ...options, parent: node })}
        </li>
      </Container>
    );
  }

  private button(node: JSONContent, options?: NodeOptions): JSX.Element {
    const { attrs } = node;
    const {
      text,
      url,
      buttonColor,
      textColor,
      borderRadius,
      borderColor,
      borderWidth,
      // @TODO: Update the attribute to `textAlign`
      alignment = "left",
    } = attrs || {};

    const { next } = options || {};
    const isNextSpacer = next?.type === "spacer";

    const href = this.linkValues[url] || this.variableValues[url] || url;

    return (
      <Container
        style={{
          textAlign: alignment,
          maxWidth: "100%",
          marginBottom: isNextSpacer ? "0px" : "20px",
        }}
      >
        <Button
          href={href}
          style={{
            color: String(textColor),
            backgroundColor: buttonColor,
            borderColor: borderColor,
            padding: "12px 34px",
            borderWidth,
            borderStyle: "solid",
            textDecoration: "none",
            fontSize: "14px",
            fontWeight: 500,
            borderRadius: `${borderRadius}px`,
          }}
        >
          {text}
        </Button>
      </Container>
    );
  }

  private spacer(node: JSONContent, _?: NodeOptions): JSX.Element {
    const { attrs } = node;
    const { height = "auto" } = attrs || {};

    return (
      <Container
        style={{
          height: spacers[height as AllowedSpacers] || height,
        }}
      />
    );
  }

  private hardBreak(_: JSONContent, __?: NodeOptions): JSX.Element {
    return <br />;
  }

  private logo(node: JSONContent, options?: NodeOptions): JSX.Element {
    const { attrs } = node;
    const {
      src,
      alt,
      title,
      size,
      // @TODO: Update the attribute to `textAlign`
      alignment = "left",
    } = attrs || {};

    const { next } = options || {};
    const isNextSpacer = next?.type === "spacer";

    return (
      <Row
        style={{
          marginTop: "0px",
          marginBottom: isNextSpacer ? "0px" : "32px",
        }}
      >
        <Column align={alignment}>
          <Img
            alt={alt || title || "Logo"}
            src={src}
            style={{
              width: logoSizes[size as AllowedLogoSizes] || size,
              height: logoSizes[size as AllowedLogoSizes] || size,
            }}
            title={title || alt || "Logo"}
          />
        </Column>
      </Row>
    );
  }

  private image(node: JSONContent, options?: NodeOptions): JSX.Element {
    const { attrs } = node;
    const {
      src,
      alt,
      title,
      width = "auto",
      height = "auto",
      alignment = "center",
      externalLink = "",
      borderRadius,
      borderColor,
      borderWidth,
    } = attrs || {};

    const { next } = options || {};
    const isNextSpacer = next?.type === "spacer";

    const mainImage = (
      <Img
        alt={alt || title || "Image"}
        src={src}
        style={{
          height,
          width,
          maxWidth: "100%",
          outline: "none",
          textDecoration: "none",
          borderStyle: "solid",
          borderRadius: `${borderRadius}px`,
          borderColor,
          borderWidth: `${borderWidth}px`,
        }}
        title={title || alt || "Image"}
      />
    );

    return (
      <Row
        style={{
          marginTop: "0px",
          marginBottom: isNextSpacer ? "0px" : "32px",
        }}
      >
        <Column align={alignment}>
          {externalLink ? (
            <a
              href={externalLink}
              rel="noopener noreferrer"
              style={{
                display: "block",
                maxWidth: "100%",
                textDecoration: "none",
              }}
              target="_blank"
            >
              {mainImage}
            </a>
          ) : (
            mainImage
          )}
        </Column>
      </Row>
    );
  }

  private blockquote(node: JSONContent, options?: NodeOptions): JSX.Element {
    const { next, prev } = options || {};
    const isNextSpacer = next?.type === "spacer";
    const isPrevSpacer = prev?.type === "spacer";

    return (
      <blockquote
        style={{
          borderLeftWidth: "4px",
          borderLeftStyle: "solid",
          borderLeftColor: this.config.theme?.colors?.blockquoteBorder,
          paddingLeft: "16px",
          marginLeft: "0px",
          marginRight: "0px",
          marginTop: isPrevSpacer ? "0px" : "20px",
          marginBottom: isNextSpacer ? "0px" : "20px",
        }}
      >
        {this.getMappedContent(node)}
      </blockquote>
    );
  }

  private code(_: MarkType, text: JSX.Element): JSX.Element {
    return (
      <code
        style={{
          backgroundColor: this.config.theme?.colors?.codeBackground,
          color: this.config.theme?.colors?.codeText,
          padding: "2px 4px",
          borderRadius: "6px",
          fontFamily: CODE_FONT_FAMILY,
          fontWeight: 400,
          letterSpacing: 0,
        }}
      >
        {text}
      </code>
    );
  }

  private codeBlock(node: JSONContent, options?: NodeOptions): JSX.Element {
    const { attrs } = node;
    const language = attrs?.language;

    const content = node.content || [];

    return (
      <Code language={language}>
        {content
          .map((n) => {
            return n.text;
          })
          .join("")}
      </Code>
    );
  }
}
