import { escapeHtml } from "./security.ts";

export type Renderable = Html | string | number | boolean | null | undefined | Renderable[];

export type Props = Record<string, unknown> & {
  children?: Renderable;
  dangerouslySetInnerHTML?: { __html?: unknown };
};

export type Component<P extends Props = Props> = (props: P) => Renderable;
export type ElementType = string | Component | typeof Fragment;

export class Html {
  constructor(public readonly html: string) {}

  toString(): string {
    return this.html;
  }
}

export const Fragment = Symbol.for("wrnexus.fragment");

const VOID_ELEMENTS = new Set([
  "area",
  "base",
  "br",
  "col",
  "embed",
  "hr",
  "img",
  "input",
  "link",
  "meta",
  "param",
  "source",
  "track",
  "wbr",
]);
const SAFE_TAG_NAME = /^[A-Za-z][A-Za-z0-9._:-]*$/;
const SAFE_ATTR_NAME = /^[A-Za-z_:][A-Za-z0-9:._-]*$/;

function isHtml(value: unknown): value is Html {
  return value instanceof Html;
}

function raw(value: string): Html {
  return new Html(value);
}

export function mustache(expr: string): Html;
export function mustache(strings: TemplateStringsArray, ...values: unknown[]): Html;
export function mustache(input: string | TemplateStringsArray, ...values: unknown[]): Html {
  const expr =
    typeof input === "string"
      ? input
      : input.reduce((out, part, index) => {
          const value = index < values.length ? String(values[index]) : "";
          return out + part + value;
        }, "");

  return raw(`{{${expr.trim()}}}`);
}

function renderChild(value: Renderable): string {
  if (value === null || value === undefined || typeof value === "boolean") return "";
  if (Array.isArray(value)) return value.map(renderChild).join("");
  if (isHtml(value)) return value.html;
  return escapeHtml(String(value));
}

function renderComponentResult(value: Renderable): string {
  if (value === null || value === undefined || typeof value === "boolean") return "";
  if (Array.isArray(value)) return value.map(renderComponentResult).join("");
  if (isHtml(value)) return value.html;

  // WrNexus page/component strings are HTML by convention.
  if (typeof value === "string") return value;
  return escapeHtml(String(value));
}

function attrName(name: string): string {
  if (name === "className") return "class";
  if (name === "htmlFor") return "for";
  return name;
}

function styleToString(value: Record<string, unknown>): string {
  return Object.entries(value)
    .filter(([, v]) => v !== null && v !== undefined && v !== false)
    .map(([k, v]) => `${k.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`)}: ${String(v)}`)
    .join("; ");
}

function renderAttrs(props: Props): string {
  const attrs: string[] = [];

  for (const [key, value] of Object.entries(props)) {
    if (
      key === "children" ||
      key === "key" ||
      key === "ref" ||
      key === "dangerouslySetInnerHTML" ||
      value === null ||
      value === undefined ||
      value === false
    ) {
      continue;
    }

    if (typeof value === "function") continue;

    const name = attrName(key);
    if (!SAFE_ATTR_NAME.test(name)) continue;
    if (value === true) {
      attrs.push(name);
      continue;
    }

    const rendered =
      key === "style" && typeof value === "object" && !Array.isArray(value)
        ? styleToString(value as Record<string, unknown>)
        : String(value);

    attrs.push(`${name}="${escapeHtml(rendered)}"`);
  }

  return attrs.length ? ` ${attrs.join(" ")}` : "";
}

export function jsx(type: ElementType, props: Props | null): Html {
  const safeProps = props ?? {};

  if (type === Fragment) {
    return raw(renderChild(safeProps.children));
  }

  if (typeof type === "function") {
    return raw(renderComponentResult(type(safeProps)));
  }

  if (!SAFE_TAG_NAME.test(type)) throw new TypeError(`Invalid JSX tag name: ${type}`);

  const attrs = renderAttrs(safeProps);
  if (VOID_ELEMENTS.has(type)) {
    return raw(`<${type}${attrs}>`);
  }

  const children =
    safeProps.dangerouslySetInnerHTML && "__html" in safeProps.dangerouslySetInnerHTML
      ? String(safeProps.dangerouslySetInnerHTML.__html ?? "")
      : renderChild(safeProps.children);

  return raw(`<${type}${attrs}>${children}</${type}>`);
}

export const jsxs = jsx;

// TypeScript's automatic JSX runtime looks for this exported namespace.
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace JSX {
  export type Element = Html;
  export type ElementType = string | Component;

  export interface ElementChildrenAttribute {
    children: unknown;
  }

  export interface IntrinsicAttributes {
    key?: string | number;
  }

  export interface IntrinsicElements {
    [tagName: string]: Props;
  }
}
