import { compile, generateBrowserModule } from "@wrnexus/compiler";
import type { ViewNode } from "@wrnexus/syntax";

const escapeHtml = (value: string) =>
  value.replace(
    /[&<>"']/g,
    (character) =>
      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]!,
  );

export interface PlaygroundCompilation {
  source: string;
  generated: string;
  client: string;
  html: string;
  interactiveHtml: string;
  diagnostics: Array<{ code: string; message: string; severity: string }>;
  version: string;
}
export interface PlaygroundVersionAdapter {
  version: string;
  compile(source: string): Promise<Omit<PlaygroundCompilation, "source" | "version">>;
}
const DEFAULT_SOURCE = `component Counter {
  state count = 0
  functions { client function increment(): void { count++ } }
  view { <button @click="increment">Count: {count}</button> }
}`;

function preview(nodes: ViewNode[]): string {
  return nodes
    .map((node) => {
      if (node.type === "text")
        return escapeHtml(node.value).replace(
          /\{([^{}]+)\}/g,
          '<span data-expression="$1">{$1}</span>',
        );
      if (node.type === "each")
        return `<template data-each="${escapeHtml(node.list)}">${preview(node.body)}</template>`;
      if (node.type === "if")
        return `<template data-if="${escapeHtml(node.branches[0]?.cond ?? "else")}">${preview(node.branches[0]?.body ?? [])}</template>`;
      const tag = /^[a-z][a-z0-9-]*$/.test(node.tag) ? node.tag : "div";
      const attrs = node.attrs
        .filter((attribute) => !/^(?:srcdoc|on\w+)$/i.test(attribute.name))
        .map((attribute) =>
          attribute.event
            ? ` data-play-event-${attribute.name}="${escapeHtml(attribute.value)}"`
            : attribute.boolean
              ? ` ${attribute.name}`
              : ` ${attribute.name}="${escapeHtml(attribute.value)}"`,
        )
        .join("");
      return `<${tag}${attrs}>${preview(node.children)}</${tag}>`;
    })
    .join("");
}

function literalState(ast: ReturnType<typeof compile>["ast"]): Record<string, unknown> {
  const values: Record<string, unknown> = {};
  for (const state of ast.states) {
    const value = state.expr.trim();
    if (/^-?\d+(?:\.\d+)?$/.test(value)) values[state.name] = Number(value);
    else if (value === "true" || value === "false") values[state.name] = value === "true";
    else if (value === "null") values[state.name] = null;
    else if (/^(["']).*\1$/.test(value)) values[state.name] = value.slice(1, -1);
  }
  return values;
}

function safeOperations(ast: ReturnType<typeof compile>["ast"]): Record<string, unknown> {
  const output: Record<string, unknown> = {};
  const states = new Set(ast.states.map((state) => state.name));
  for (const fn of ast.runtimeFunctions.filter((entry) => entry.runtime !== "server")) {
    const body = fn.body.trim().replace(/;$/, "");
    const unary = /^([A-Za-z_$][\w$]*)(\+\+|--)$/.exec(body);
    const assignment =
      /^([A-Za-z_$][\w$]*)\s*(\+=|-=|=)\s*(-?\d+(?:\.\d+)?|true|false|(["']).*\4)$/.exec(body);
    const match = unary ?? assignment;
    if (!match || !states.has(match[1]!)) continue;
    const raw = assignment?.[3];
    const value =
      raw === undefined
        ? undefined
        : /^-?\d/.test(raw)
          ? Number(raw)
          : raw === "true" || raw === "false"
            ? raw === "true"
            : raw.slice(1, -1);
    output[fn.name] = {
      state: match[1]!,
      operation: match[2]!,
      ...(value !== undefined ? { value } : {}),
    };
  }
  return output;
}

function interactiveDocument(fragment: string, ast: ReturnType<typeof compile>["ast"]): string {
  const data = JSON.stringify({
    state: literalState(ast),
    operations: safeOperations(ast),
  }).replace(/</g, "\\u003c");
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><style>body{font:16px system-ui;padding:1rem}</style></head><body>${fragment}<script>const model=${data};function draw(){document.querySelectorAll('[data-expression]').forEach(n=>{const k=n.dataset.expression.trim();if(Object.prototype.hasOwnProperty.call(model.state,k))n.textContent=String(model.state[k])})}document.addEventListener('click',e=>{const n=e.target.closest('[data-play-event-click]');if(!n)return;const op=model.operations[n.dataset.playEventClick];if(!op)return;if(op.operation==='++')model.state[op.state]++;else if(op.operation==='--')model.state[op.state]--;else if(op.operation==='+=')model.state[op.state]+=op.value;else if(op.operation==='-=')model.state[op.state]-=op.value;else model.state[op.state]=op.value;draw()});draw()</script></body></html>`;
}

export function compilePlayground(source: string, version = "0.8.0"): PlaygroundCompilation {
  if (new TextEncoder().encode(source).byteLength > 128 * 1024)
    throw new RangeError("WRN-PLAYGROUND-SOURCE-LIMIT");
  const result = compile(source, "playground.wrn");
  const html = preview(result.ast.view);
  return {
    source,
    generated: result.code,
    client: generateBrowserModule(result.ast),
    html,
    interactiveHtml: interactiveDocument(html, result.ast),
    diagnostics: result.richDiagnostics.map(({ code, message, severity }) => ({
      code,
      message,
      severity,
    })),
    version,
  };
}

export function encodePlaygroundShare(source: string): string {
  const bytes = new TextEncoder().encode(source);
  if (bytes.byteLength > 64 * 1024) throw new RangeError("WRN-PLAYGROUND-SHARE-LIMIT");
  let binary = "";
  for (const byte of bytes) binary += String.fromCharCode(byte);
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
export function decodePlaygroundShare(value: string): string {
  if (!/^[A-Za-z0-9_-]{1,100000}$/.test(value))
    throw new Error("WRN-PLAYGROUND-SHARE: invalid payload");
  const binary = atob(value.replace(/-/g, "+").replace(/_/g, "/"));
  const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
  if (bytes.byteLength > 64 * 1024) throw new RangeError("WRN-PLAYGROUND-SHARE-LIMIT");
  return new TextDecoder().decode(bytes);
}
export async function comparePlaygroundVersions(
  source: string,
  adapters: PlaygroundVersionAdapter[],
) {
  return {
    current: compilePlayground(source),
    comparisons: await Promise.all(
      adapters.map(async (adapter) => ({
        version: adapter.version,
        ...(await adapter.compile(source)),
      })),
    ),
  };
}

const CLIENT = String.raw`
const source=document.querySelector('#source'),status=document.querySelector('#status'),examples=document.querySelector('#examples');
async function run(versions=false){status.textContent=versions?'Comparing':'Compiling';const response=await fetch('/api/compile',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({source:source.value,versions})});const value=await response.json(),current=value.current||value;status.textContent=response.ok?(versions?'Compared':'Compiled'):'Failed';for(const name of ['html','generated','client','diagnostics'])document.querySelector('#'+name).textContent=typeof current[name]==='string'?current[name]:JSON.stringify(current[name]||[],null,2);document.querySelector('#versions').textContent=JSON.stringify(value.comparisons||[],null,2);document.querySelector('#preview').srcdoc=current.interactiveHtml||current.html||'';if(value.share)history.replaceState(null,'','?code='+value.share)}
async function loadExamples(){const response=await fetch('/api/examples'),value=await response.json();for(const [name,code] of Object.entries(value)){const option=document.createElement('option');option.value=name;option.textContent=name;examples.append(option);examples.dataset[name]=code}if(examples.options.length)examples.dispatchEvent(new Event('change'))}
document.querySelector('#run').addEventListener('click',()=>run());
document.querySelector('#compare').addEventListener('click',()=>run(true));
examples.addEventListener('change',()=>{const value=examples.dataset[examples.value];if(value)source.value=value});
document.querySelector('#share').addEventListener('click',()=>navigator.clipboard&&navigator.clipboard.writeText(location.href));
loadExamples().catch(()=>{status.textContent='Examples unavailable'});
`;

function page(source: string) {
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>WRNexus Playground</title><style>body{font:14px system-ui;margin:0;padding:1rem;background:#111827;color:#f9fafb}main{display:grid;grid-template-columns:1fr 1fr;gap:1rem}textarea,pre,iframe{box-sizing:border-box;width:100%;min-height:18rem;background:#fff;color:#111;padding:.75rem;overflow:auto}button,select{padding:.6rem 1rem;margin:.25rem}section{min-width:0}@media(max-width:800px){main{grid-template-columns:1fr}}</style></head><body><h1>WRNexus Playground</h1><p>Write WRN, inspect SSR HTML/client/generated output, test components, share the URL, or attach it to a report.</p><label for="examples">Example</label><select id="examples" aria-label="Playground example"></select><button id="run">Compile</button><button id="compare">Compare versions</button><button id="share">Copy share URL</button> <span id="status" role="status"></span><main><section><h2>WRN source</h2><textarea id="source">${escapeHtml(source)}</textarea><h2>Sandboxed preview</h2><iframe id="preview" title="WRNexus component preview" sandbox="allow-scripts"></iframe></section><section><h2>SSR HTML</h2><pre id="html"></pre><h2>Generated JavaScript</h2><pre id="generated"></pre><h2>Client output</h2><pre id="client"></pre><h2>Diagnostics</h2><pre id="diagnostics"></pre><h2>Version comparisons</h2><pre id="versions"></pre></section></main><script src="/playground.js" defer></script></body></html>`;
}

export function createPlaygroundHandler(
  options: { versions?: PlaygroundVersionAdapter[]; examples?: Record<string, string> } = {},
) {
  return async (request: Request): Promise<Response> => {
    const url = new URL(request.url);
    if (url.pathname === "/playground.js")
      return new Response(CLIENT, {
        headers: {
          "content-type": "text/javascript; charset=utf-8",
          "cache-control": "public, max-age=3600",
          "x-content-type-options": "nosniff",
        },
      });
    if (url.pathname === "/api/examples")
      return Response.json(options.examples ?? { counter: DEFAULT_SOURCE });
    if (url.pathname === "/api/compile" && request.method === "POST") {
      try {
        const text = await request.text();
        if (new TextEncoder().encode(text).byteLength > 140 * 1024)
          return Response.json({ error: "Payload too large" }, { status: 413 });
        const body = JSON.parse(text) as { source?: unknown; versions?: boolean };
        if (typeof body.source !== "string")
          return Response.json({ error: "source is required" }, { status: 400 });
        const result =
          body.versions && options.versions?.length
            ? await comparePlaygroundVersions(body.source, options.versions)
            : compilePlayground(body.source);
        return Response.json(
          { ...result, share: encodePlaygroundShare(body.source) },
          { headers: { "cache-control": "no-store" } },
        );
      } catch (error) {
        return Response.json(
          { error: error instanceof Error ? error.message : "Compilation failed" },
          { status: 400 },
        );
      }
    }
    if (url.pathname !== "/") return new Response("Not Found", { status: 404 });
    let source = DEFAULT_SOURCE;
    const encoded = url.searchParams.get("code");
    if (encoded) {
      try {
        source = decodePlaygroundShare(encoded);
      } catch {
        source = DEFAULT_SOURCE;
      }
    }
    return new Response(page(source), {
      headers: {
        "content-type": "text/html; charset=utf-8",
        "content-security-policy":
          "default-src 'none'; script-src 'self'; style-src 'unsafe-inline'; frame-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'",
        "x-frame-options": "SAMEORIGIN",
        "x-content-type-options": "nosniff",
        "referrer-policy": "no-referrer",
      },
    });
  };
}
