import type { Attr, PageAst, ViewNode } from "./parser.ts";

export class NativeCompileError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "NativeCompileError";
  }
}

const tagMap: Record<string, string> = {
  div: "View",
  main: "View",
  section: "View",
  article: "View",
  nav: "View",
  header: "View",
  footer: "View",
  aside: "View",
  form: "View",
  ul: "View",
  ol: "View",
  li: "View",
  p: "Text",
  span: "Text",
  strong: "Text",
  em: "Text",
  small: "Text",
  label: "Text",
  h1: "Text",
  h2: "Text",
  h3: "Text",
  h4: "Text",
  h5: "Text",
  h6: "Text",
  button: "Pressable",
  a: "Pressable",
  input: "TextInput",
  textarea: "TextInput",
  img: "Image",
  view: "View",
  text: "Text",
  pressable: "Pressable",
  textinput: "TextInput",
  image: "Image",
  scrollview: "ScrollView",
  safeareaview: "SafeAreaView",
  flatlist: "FlatList",
  activityindicator: "ActivityIndicator",
};

const attrMap: Record<string, string> = {
  class: "style",
  className: "style",
  src: "source",
  alt: "accessibilityLabel",
  placeholder: "placeholder",
  disabled: "disabled",
  value: "value",
  href: "__href",
  "aria-label": "accessibilityLabel",
};

function expression(value: string): string | null {
  const exact = /^\{([\s\S]+)\}$/.exec(value.trim());
  return exact?.[1]?.trim() ?? null;
}

function textJsx(value: string): string {
  const pieces: string[] = [];
  let last = 0;
  for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
    if (match.index! > last) pieces.push(value.slice(last, match.index));
    const expr = match[1]!.trim();
    pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
    last = match.index! + match[0].length;
  }
  pieces.push(value.slice(last));
  return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;"));
}

function eventBody(value: string, states: Set<string>): string {
  let body = expression(value) ?? value;
  for (const state of states) {
    const cap = state[0]!.toUpperCase() + state.slice(1);
    body = body
      .replace(new RegExp(`\\b${state}\\+\\+`, "g"), `set${cap}(value => value + 1)`)
      .replace(new RegExp(`\\b${state}--`, "g"), `set${cap}(value => value - 1)`)
      .replace(new RegExp(`\\b${state}\\s*=\\s*([^;]+)`, "g"), `set${cap}($1)`);
  }
  return `() => { ${body} }`;
}

function renderAttrs(attrs: Attr[], states: Set<string>): string {
  return attrs
    .map((attr) => {
      if (attr.event) {
        if (attr.name.startsWith("browser-")) return "";
        const eventName = attr.name.startsWith("mobile-") ? attr.name.slice(7) : attr.name;
        const event =
          eventName === "click" || eventName === "press"
            ? "onPress"
            : eventName === "input" || eventName === "change"
              ? "onChangeText"
              : `on${eventName[0]!.toUpperCase()}${eventName.slice(1)}`;
        return ` ${event}={${eventBody(attr.value, states)}}`;
      }
      if (attr.name === "data-native-browser" || attr.name.startsWith("data-native-on-browser-"))
        return "";
      if (
        attr.name === "data-native-options" ||
        attr.name === "data-native-only" ||
        attr.name === "data-native-requires" ||
        attr.name === "data-native-unsupported"
      )
        return "";
      if (attr.name === "data-native-mobile") {
        throw new NativeCompileError(
          `Declarative native capability "${attr.value}" currently targets browser/Capacitor pages. In Expo output, call the installed Expo package from an @mobile-event handler.`,
        );
      }
      const name = attrMap[attr.name] ?? attr.name;
      if (name === "__href") return ` onPress={() => router.push(${JSON.stringify(attr.value)})}`;
      if (name === "source") {
        const expr = expression(attr.value);
        return ` source={${expr ? `{ uri: ${expr} }` : `{ uri: ${JSON.stringify(attr.value)} }`}}`;
      }
      if (name === "style" && attr.name !== "style") {
        return ` style={[${attr.value
          .split(/\s+/)
          .filter(Boolean)
          .map((value) => `styles[${JSON.stringify(value)}]`)
          .join(", ")} ]}`;
      }
      if (name === "style") {
        const inlineExpression = expression(attr.value);
        if (inlineExpression) return ` style={${inlineExpression}}`;
        throw new NativeCompileError(
          'Inline CSS strings are not portable to native; use class="name" and a page style block',
        );
      }
      if (attr.boolean) return ` ${name}`;
      const expr = expression(attr.value);
      return expr ? ` ${name}={${expr}}` : ` ${name}=${JSON.stringify(attr.value)}`;
    })
    .join("");
}

function renderNode(node: ViewNode, states: Set<string>, key?: string): string {
  if (node.type === "text") return textJsx(node.value);
  if (node.type === "each") {
    const params = node.index ? `${node.item}, ${node.index}` : `${node.item}, __index`;
    const body = node.body
      .map((child, index) =>
        renderNode(child, states, index === 0 ? (node.index ?? "__index") : undefined),
      )
      .join("");
    const empty = node.empty.map((child) => renderNode(child, states)).join("");
    return `{(${node.list})?.length ? (${node.list}).map((${params}) => <>${body}</>) : <>${empty}</>}`;
  }
  if (node.type === "if") {
    const result = node.branches.reduceRight(
      (fallback, branch) =>
        branch.cond === null
          ? `<>${branch.body.map((child) => renderNode(child, states)).join("")}</>`
          : `(${branch.cond}) ? <>${branch.body.map((child) => renderNode(child, states)).join("")}</> : ${fallback}`,
      "null",
    );
    return `{${result}}`;
  }
  const nativeOnly = node.attrs.find(
    (attr) => !attr.event && attr.name === "data-native-only",
  )?.value;
  if (nativeOnly === "browser" || nativeOnly === "web") return "";
  const nativeTag =
    tagMap[node.tag.toLowerCase()] ?? (/^[A-Z]/.test(node.tag) ? node.tag : undefined);
  if (!nativeTag)
    throw new NativeCompileError(`HTML element <${node.tag}> has no native equivalent`);
  const attrs = renderAttrs(node.attrs, states) + (key ? ` key={${key}}` : "");
  if (nativeTag === "TextInput" || nativeTag === "Image" || nativeTag === "ActivityIndicator")
    return `<${nativeTag}${attrs} />`;
  const children = node.children
    .map((child) => {
      if (child.type !== "text") return renderNode(child, states);
      if (!child.value.trim()) return "";
      const text = textJsx(child.value);
      return nativeTag === "Text" ? text : `<Text>${text}</Text>`;
    })
    .join("");
  return `<${nativeTag}${attrs}>${children}</${nativeTag}>`;
}

function nativeStyles(blocks: string[]): string {
  const entries: string[] = [];
  for (const block of blocks) {
    for (const match of block.matchAll(/\.([A-Za-z_][\w-]*)\s*\{([^}]*)\}/g)) {
      const props: string[] = [];
      for (const declaration of match[2]!.split(";")) {
        const colon = declaration.indexOf(":");
        if (colon < 0) continue;
        const name = declaration
          .slice(0, colon)
          .trim()
          .replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
        let value: string | number = declaration.slice(colon + 1).trim();
        if (/^-?\d+(?:\.\d+)?px$/.test(value)) value = Number(value.slice(0, -2));
        props.push(
          `${JSON.stringify(name)}: ${typeof value === "number" ? value : JSON.stringify(value)}`,
        );
      }
      entries.push(`${JSON.stringify(match[1])}: { ${props.join(", ")} }`);
    }
  }
  return `const styles = StyleSheet.create({ ${entries.join(",\n")} });`;
}

/** Compile a parsed `.wrn` page to an Expo Router React Native screen. */
export function generateNative(ast: PageAst): string {
  if (ast.kind !== "page")
    throw new NativeCompileError("Native route compilation currently accepts page files only");
  if (ast.dataApis.length)
    throw new NativeCompileError(
      "Data API blocks are not yet portable to native screens; fetch through the generated native backend helper",
    );
  const states = new Set(ast.states.map((state) => state.name));
  const hooks = ast.states
    .map((state) => {
      const cap = state.name[0]!.toUpperCase() + state.name.slice(1);
      return `  const [${state.name}, set${cap}] = useState${state.valueType ? `<${state.valueType}>` : ""}(${state.expr});`;
    })
    .join("\n");
  const body = ast.view.map((node) => renderNode(node, states)).join("");
  const typeSource = ast.types
    .map((block) => block.trim())
    .filter(Boolean)
    .join("\n\n");
  return `// generated from .wrn for Expo/React Native\nimport React, { useState } from "react";\nimport { ActivityIndicator, FlatList, Image, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View } from "react-native";\nimport { useRouter } from "expo-router";\n${ast.imports.join("\n")}\n\n${typeSource}\n\nexport default function ${ast.name}() {\n  const router = useRouter();\n${hooks}\n  return <>${body}</>;\n}\n\n${nativeStyles(ast.styles)}\n`;
}
