/**
 * Recursive-descent parser for `.wrn`, producing a small AST.
 *
 * Grammar (subset of the vision, but real):
 *
 *   page <Name> {
 *     state <ident> = <expr>            // zero or more
 *     view { <html> }                   // plain HTML (see parseHtmlView)
 *     seo { title = "Home" description = "..." }
 *     ssr { api <name> <METHOD> <path> { <render js> } functions { <raw js> } }
 *     client { api <name> <METHOD> <path> { <render js> } functions { <raw js> } }
 *     style { <raw css> }               // zero or more, inlined with the page
 *     functions { <raw js> }            // zero or more, shared helpers
 *     api <METHOD> <path> { <raw js> }  // zero or more
 *     realtime <name> { on <evt>(<args>) { <raw js> } * }  // zero or more
 *   }
 *
 * The `view` block is written as ordinary HTML — nothing new to learn. Text may
 * contain `{expr}` interpolation, attributes may be hyphenated (`data-*`), and
 * `@event="..."` declares a client event binding. See `parseHtmlView`.
 */

import { Lexer, LexError, type Token } from "./tokenizer.ts";

export interface StateDecl {
  name: string;
  /** Raw JS initializer expression, e.g. `0` or `'x'`. */
  expr: string;
}

export interface Attr {
  name: string;
  value: string;
  /** True for `@event` bindings (vs. plain HTML attributes). */
  event: boolean;
  /** True for a valueless boolean attribute, e.g. `<button disabled>`. */
  boolean?: boolean;
}

/**
 * HTML void elements: they have no children and no closing tag.
 * @see https://html.spec.whatwg.org/multipage/syntax.html#void-elements
 */
export const VOID_ELEMENTS = new Set([
  "area",
  "base",
  "br",
  "col",
  "embed",
  "hr",
  "img",
  "input",
  "link",
  "meta",
  "param",
  "source",
  "track",
  "wbr",
]);

export type ViewNode =
  | { type: "text"; value: string }
  | { type: "element"; tag: string; attrs: Attr[]; children: ViewNode[] }
  /**
   * A server-side loop: `{#each <list> as <item>[, <index>]} …body… {:empty} …empty… {/each}`.
   * `list` is a JS expression (evaluated on the server, may reference an `ssr` data
   * binding). The `body` is rendered once per item with `{item.field}` interpolation;
   * `empty` renders when the list is empty. See codegen `compileEach`.
   */
  | {
      type: "each";
      list: string;
      item: string;
      index?: string;
      body: ViewNode[];
      empty: ViewNode[];
    }
  /**
   * A server-side conditional: `{#if <expr>} … {:else if <expr>} … {:else} … {/if}`.
   * Rendered branches are chosen on the server. Each branch's `cond` is a JS expression
   * (`null` for the final `{:else}`); the first truthy branch renders. See `compileIfExpr`.
   */
  | { type: "if"; branches: { cond: string | null; body: ViewNode[] }[] };

export interface ApiBlock {
  method: string;
  path: string;
  body: string;
}

export type SeoBlock = Record<string, string>;

export type DataMode = "ssr" | "client";

export interface DataApiBlock {
  mode: DataMode;
  name: string;
  method: string;
  path: string;
  body: string;
}

export interface ModeFunctionsBlock {
  mode: DataMode;
  body: string;
}

export type LifecycleHookName = "mount" | "update" | "unmount";

export interface LifecycleBlock {
  mount?: string;
  update?: string;
  unmount?: string;
}

export interface WatchBlock {
  state: string;
  body: string;
}

export interface RealtimeHandler {
  event: string;
  args: string[];
  body: string;
}

export interface RealtimeBlock {
  name: string;
  handlers: RealtimeHandler[];
}

export interface PropDecl {
  name: string;
  /** Raw JS default expression, e.g. `0` or `'Count'`. Its type drives coercion. */
  default: string;
}

export interface PageAst {
  type: "page";
  /**
   * `page` is a route, `component` is a reusable fragment,
   * and `layout` is a reusable page wrapper.
   */
  kind: "page" | "component" | "layout";
  name: string;
  /** Name of the page layout (`app/layouts/<layout>.wrn`), if the page sets one. */
  layout?: string;
  /** Declared component props (empty for pages). */
  props: PropDecl[];
  states: StateDecl[];
  seo: SeoBlock;
  view: ViewNode[];
  styles: string[];
  functions: string[];
  dataApis: DataApiBlock[];
  modeFunctions: ModeFunctionsBlock[];
  lifecycle: LifecycleBlock;
  watches: WatchBlock[];
  apis: ApiBlock[];
  realtimes: RealtimeBlock[];
}

export class ParseError extends Error {}

function parseSeoBlock(body: string): SeoBlock {
  const out: SeoBlock = {};
  const pair =
    /([A-Za-z][A-Za-z0-9_-]*)\s*=\s*(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^\n;]+))/g;
  for (const match of body.matchAll(pair)) {
    const key = match[1]!;
    const rawValue = match[2] ?? match[3] ?? match[4] ?? "";
    out[key] = unescapeSeoValue(rawValue.trim());
  }
  return out;
}

function unescapeSeoValue(value: string): string {
  return value.replace(/\\(["'\\nrt])/g, (_match, ch: string) => {
    if (ch === "n") return "\n";
    if (ch === "r") return "\r";
    if (ch === "t") return "\t";
    return ch;
  });
}

export function parse(source: string): PageAst {
  const lx = new Lexer(source);

  const expect = (type: Token["type"]): Token => {
    const t = lx.next();
    if (t.type !== type) {
      throw new ParseError(`Expected ${type} but got '${t.value || t.type}' at offset ${t.pos}`);
    }
    return t;
  };
  const expectKeyword = (kw: string): void => {
    const t = lx.next();
    if (t.type !== "ident" || t.value !== kw) {
      throw new ParseError(`Expected '${kw}' but got '${t.value || t.type}' at offset ${t.pos}`);
    }
  };

  try {
    // A file may contain a page, reusable component, or reusable layout.
    const opener = lx.next();

    if (opener.type !== "ident" || !["page", "component", "layout"].includes(opener.value)) {
      throw new ParseError(
        `Expected 'page', 'component', or 'layout' but got '${
          opener.value || opener.type
        }' at offset ${opener.pos}`,
      );
    }

    const kind = opener.value as "page" | "component" | "layout";
    const name = expect("ident").value;
    expect("lbrace");

    let layout: string | undefined;
    const props: PropDecl[] = [];
    const states: StateDecl[] = [];
    const seo: SeoBlock = {};
    const view: ViewNode[] = [];
    const styles: string[] = [];
    const functions: string[] = [];
    const dataApis: DataApiBlock[] = [];
    const modeFunctions: ModeFunctionsBlock[] = [];
    const lifecycle: LifecycleBlock = {};
    const watches: WatchBlock[] = [];
    const apis: ApiBlock[] = [];
    const realtimes: RealtimeBlock[] = [];

    while (lx.peek().type !== "rbrace") {
      const kw = lx.peek();
      if (kw.type === "eof") throw new ParseError(`Unexpected end of input inside ${kind}`);
      if (kw.type !== "ident") {
        throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`);
      }
      switch (kw.value) {
        case "layout": {
          // layout = "public" — selects app/layouts/<name>.wrn for this page.
          lx.next();
          expect("eq");
          layout = expect("string").value;
          break;
        }
        case "props": {
          // props { name = <default> ... } — one declaration per line.
          lx.next();
          expect("lbrace");
          while (lx.peek().type !== "rbrace") {
            const t = lx.peek();
            if (t.type === "eof") throw new ParseError("Unexpected end of input inside props");
            if (t.type !== "ident") {
              throw new ParseError(`Expected a prop name at offset ${t.pos}`);
            }
            const pName = expect("ident").value;
            expect("eq");
            props.push({ name: pName, default: lx.readToLineEnd() });
          }
          expect("rbrace");
          break;
        }
        case "state": {
          lx.next();
          const sName = expect("ident").value;
          expect("eq");
          states.push({ name: sName, expr: lx.readToLineEnd() });
          break;
        }
        case "view": {
          lx.next();
          expect("lbrace");
          // The view body is plain HTML. Parse it straight off the source
          // (the token lexer isn't used for markup), then resume after the
          // block's closing `}`.
          const { nodes, endPos } = parseHtmlView(lx.src, lx.pos);
          view.push(...nodes);
          lx.pos = endPos;
          expect("rbrace");
          break;
        }
        case "seo": {
          lx.next();
          Object.assign(seo, parseSeoBlock(lx.readBalancedBraces()));
          break;
        }
        case "api": {
          lx.next();
          const method = expect("ident").value.toUpperCase();
          const path = lx.readPath();
          const body = lx.readBalancedBraces();
          apis.push({ method, path, body });
          break;
        }
        case "ssr":
        case "client": {
          const mode: DataMode = kw.value === "ssr" ? "ssr" : "client";
          lx.next();
          expect("lbrace");
          while (lx.peek().type !== "rbrace") {
            const member = lx.peek();
            if (member.type === "eof") {
              throw new ParseError(`Unexpected end of input inside ${mode} block`);
            }
            if (member.type !== "ident") {
              throw new ParseError(`Expected a ${mode} member keyword at offset ${member.pos}`);
            }
            switch (member.value) {
              case "api": {
                lx.next();
                const name = expect("ident").value;
                const method = expect("ident").value.toUpperCase();
                const path = lx.readPath();
                const body = lx.readBalancedBraces();
                dataApis.push({ mode, name, method, path, body });
                break;
              }
              case "functions": {
                lx.next();
                modeFunctions.push({ mode, body: lx.readBalancedBraces() });
                break;
              }
              default:
                throw new ParseError(
                  `Unknown ${mode} member '${member.value}' at offset ${member.pos}`,
                );
            }
          }
          expect("rbrace");
          break;
        }
        case "realtime": {
          lx.next();
          const rName = expect("ident").value;
          expect("lbrace");
          const handlers: RealtimeHandler[] = [];
          while (lx.peek().type !== "rbrace") {
            expectKeyword("on");
            const event = expect("ident").value;
            expect("lparen");
            const args: string[] = [];
            while (lx.peek().type !== "rparen") {
              args.push(expect("ident").value);
              if (lx.peek().type === "comma") lx.next();
            }
            expect("rparen");
            handlers.push({ event, args, body: lx.readBalancedBraces() });
          }
          expect("rbrace");
          realtimes.push({ name: rName, handlers });
          break;
        }
        case "style": {
          lx.next();
          styles.push(lx.readBalancedBraces());
          break;
        }
        case "lifecycle": {
          lx.next();
          expect("lbrace");

          while (lx.peek().type !== "rbrace") {
            const hook = lx.peek();

            if (hook.type === "eof") {
              throw new ParseError("Unexpected end of input inside lifecycle block");
            }

            if (hook.type !== "ident") {
              throw new ParseError(`Expected a lifecycle hook at offset ${hook.pos}`);
            }

            if (hook.value !== "mount" && hook.value !== "update" && hook.value !== "unmount") {
              throw new ParseError(`Unknown lifecycle hook '${hook.value}' at offset ${hook.pos}`);
            }

            const hookName = hook.value as LifecycleHookName;

            lx.next();

            if (lifecycle[hookName] !== undefined) {
              throw new ParseError(`Duplicate lifecycle hook '${hookName}' at offset ${hook.pos}`);
            }

            lifecycle[hookName] = lx.readBalancedBraces();
          }

          expect("rbrace");
          break;
        }
        case "watch": {
          lx.next();

          const stateName = expect("ident").value;
          const body = lx.readBalancedBraces();

          watches.push({
            state: stateName,
            body,
          });

          break;
        }
        case "functions": {
          lx.next();
          functions.push(lx.readBalancedBraces());
          break;
        }
        default:
          throw new ParseError(`Unknown page member '${kw.value}' at offset ${kw.pos}`);
      }
    }
    expect("rbrace");

    const declaredStates = new Set(states.map((state) => state.name));

    for (const watcher of watches) {
      if (!declaredStates.has(watcher.state)) {
        throw new ParseError(`Cannot watch undeclared state '${watcher.state}'`);
      }
    }

    return {
      type: "page",
      kind,
      name,
      layout,
      props,
      states,
      seo,
      view,
      styles,
      functions,
      dataApis,
      modeFunctions,
      lifecycle,
      watches,
      apis,
      realtimes,
    };
  } catch (err) {
    if (err instanceof LexError) throw new ParseError(err.message);
    throw err;
  }
}

/**
 * Parse the body of a `view { ... }` block as plain HTML.
 *
 * `src` is the whole `.wrn` source; `pos` points just past the view block's
 * opening `{`. Returns the parsed nodes plus the index of the block's closing
 * `}` (left for the caller to consume). It is intentionally lenient — you write
 * markup the way you already know:
 *
 *   - `<tag attr="v" @event="expr">children</tag>` — elements with attributes
 *   - `<tag/>` and HTML void elements (`<br>`, `<img>`, …) — no closing tag
 *   - text may contain `{expr}` interpolation, kept verbatim for the runtime
 *   - `@event="..."` becomes a client event binding; hyphenated names are fine
 *   - `<!-- comments -->` are dropped
 *
 * `{` and `}` in text are reserved for interpolation; a lone `<` that isn't a
 * tag is treated as literal text.
 */
export function parseHtmlView(src: string, pos: number): { nodes: ViewNode[]; endPos: number } {
  let i = pos;

  const isNameStart = (c: string): boolean => /[A-Za-z_]/.test(c);

  const isTagNamePart = (c: string): boolean => /[A-Za-z0-9_$:.-]/.test(c);

  const isAttributeNamePart = (c: string, next: string | undefined): boolean => {
    if (c === "/") {
      return next !== ">";
    }

    return /[A-Za-z0-9_$:.[\]%-]/.test(c);
  };
  const isWs = (c: string): boolean => c === " " || c === "\t" || c === "\n" || c === "\r";

  const fail = (msg: string): never => {
    throw new ParseError(`${msg} at offset ${i}`);
  };
  const skipWs = (): void => {
    while (i < src.length && isWs(src[i]!)) i++;
  };

  /** Read a `{...}` interpolation (brace-balanced), braces included. */
  const readInterpolation = (): string => {
    const start = i;
    let depth = 0;
    for (; i < src.length; i++) {
      if (src[i] === "{") depth++;
      else if (src[i] === "}" && --depth === 0) {
        i++;
        return src.slice(start, i);
      }
    }
    return fail("Unterminated `{` interpolation in view");
  };

  const readQuoted = (): string => {
    const quote = src[i];
    if (quote !== '"' && quote !== "'") return fail("Expected a quoted attribute value");
    i++;
    const start = i;
    while (i < src.length && src[i] !== quote) i++;
    if (i >= src.length) return fail("Unterminated attribute value");
    const value = src.slice(start, i);
    i++; // closing quote
    return value;
  };

  const readTagName = (): string => {
    if (i >= src.length || !isNameStart(src[i]!)) {
      return fail("Expected a tag name");
    }

    const start = i++;

    while (i < src.length && isTagNamePart(src[i]!)) {
      i++;
    }

    return src.slice(start, i);
  };

  const readAttributeName = (): string => {
    const first = src[i];

    if (i >= src.length || (!isNameStart(first!) && first !== ":" && first !== "$")) {
      return fail("Expected an attribute name");
    }

    const start = i++;

    while (i < src.length && isAttributeNamePart(src[i]!, src[i + 1])) {
      i++;
    }

    return src.slice(start, i);
  };

  const parseTag = (): ViewNode => {
    i++; // consume '<'
    const tag = readTagName();
    const attrs: Attr[] = [];

    for (;;) {
      skipWs();
      const c = src[i];
      if (c === undefined) return fail(`Unterminated <${tag}> tag`);
      if (c === ">") {
        i++;
        break;
      }
      if (c === "/" && src[i + 1] === ">") {
        i += 2;
        return { type: "element", tag, attrs, children: [] };
      }
      if (c === "@") {
        i++;
        const name = readAttributeName();
        skipWs();
        if (src[i] !== "=") return fail(`Expected '=' after @${name}`);
        i++;
        skipWs();
        attrs.push({ name, value: readQuoted(), event: true });
        continue;
      }
      const name = readAttributeName();
      skipWs();
      if (src[i] === "=") {
        i++;
        skipWs();
        attrs.push({ name, value: readQuoted(), event: false });
      } else {
        attrs.push({ name, value: "", event: false, boolean: true });
      }
    }

    if (VOID_ELEMENTS.has(tag.toLowerCase())) {
      return { type: "element", tag, attrs, children: [] };
    }

    const children = parseNodeList("element");
    // parseNodeList stops at the parent's closing tag `</`.
    if (src[i] !== "<" || src[i + 1] !== "/") return fail(`Expected </${tag}>`);
    i += 2;
    skipWs();
    const close = readTagName();
    if (close !== tag) return fail(`Mismatched </${close}>, expected </${tag}>`);
    skipWs();
    if (src[i] !== ">") return fail(`Expected '>' to close </${tag}>`);
    i++;
    return { type: "element", tag, attrs, children };
  };

  const EACH_HEADER =
    /^\{#each\s+([\s\S]+?)\s+as\s+([A-Za-z_$][\w$]*)\s*(?:,\s*([A-Za-z_$][\w$]*)\s*)?\}$/;

  /** Parse `{#each <list> as <item>[, <index>]} …body… {:empty} …empty… {/each}`. */
  function parseEach(): ViewNode {
    const header = readInterpolation(); // reads the full `{#each …}`
    const m = EACH_HEADER.exec(header);
    if (!m) return fail(`Invalid {#each …} header: ${header}`);
    const list = m[1]!.trim();
    const item = m[2]!;
    const index = m[3];
    const body = parseNodeList("each"); // stops at {:empty} or {/each}
    let empty: ViewNode[] = [];
    if (src.startsWith("{:empty}", i)) {
      i += "{:empty}".length;
      empty = parseNodeList("each"); // stops at {/each}
    }
    if (!src.startsWith("{/each}", i)) return fail("Expected `{/each}` to close `{#each}`");
    i += "{/each}".length;
    return { type: "each", list, item, index, body, empty };
  }

  /** Parse `{#if <expr>} … {:else if <expr>} … {:else} … {/if}`. */
  function parseIf(): ViewNode {
    const header = readInterpolation(); // reads the full `{#if …}`
    const m = /^\{#if\s+([\s\S]+?)\s*\}$/.exec(header);
    if (!m) return fail(`Invalid {#if …} header: ${header}`);
    const branches: { cond: string | null; body: ViewNode[] }[] = [
      { cond: m[1]!.trim(), body: parseNodeList("if") },
    ];
    for (;;) {
      if (src.startsWith("{:else if", i)) {
        const h = readInterpolation();
        const mm = /^\{:else if\s+([\s\S]+?)\s*\}$/.exec(h);
        if (!mm) return fail(`Invalid {:else if …}: ${h}`);
        branches.push({ cond: mm[1]!.trim(), body: parseNodeList("if") });
        continue;
      }
      if (src.startsWith("{:else}", i)) {
        i += "{:else}".length;
        branches.push({ cond: null, body: parseNodeList("if") });
        continue;
      }
      break;
    }
    if (!src.startsWith("{/if}", i)) return fail("Expected `{/if}` to close `{#if}`");
    i += "{/if}".length;
    return { type: "if", branches };
  }

  /**
   * Parse a run of nodes. `mode` sets the terminator:
   *   - "root":    stops at the view block's closing `}`
   *   - "element": stops at the parent element's closing tag (`</`)
   *   - "each":    stops (without consuming) at `{:empty}` or `{/each}`
   *   - "if":      stops (without consuming) at `{:else …}` or `{/if}`
   * `{#each …}` and `{#if …}` start nested blocks in any mode.
   */
  function parseNodeList(mode: "root" | "element" | "each" | "if"): ViewNode[] {
    const nodes: ViewNode[] = [];
    let text = "";
    const flush = (): void => {
      if (text.length > 0) {
        nodes.push({ type: "text", value: text });
        text = "";
      }
    };

    for (;;) {
      if (i >= src.length) {
        return mode === "root"
          ? fail("Unexpected end of view (missing `}`)")
          : fail("Unclosed block");
      }
      const c = src[i]!;

      if (c === "<") {
        const next = src[i + 1];
        if (next === "/") {
          flush();
          break; // parent's closing tag
        }
        if (src.startsWith("<!--", i)) {
          const end = src.indexOf("-->", i + 4);
          i = end === -1 ? src.length : end + 3;
          continue;
        }
        if (next !== undefined && (isNameStart(next) || next === "!")) {
          flush();
          nodes.push(parseTag());
          continue;
        }
        // A lone `<` that doesn't start a tag: treat as literal text.
        text += c;
        i++;
        continue;
      }

      if (c === "{") {
        if (src.startsWith("{#each", i)) {
          flush();
          nodes.push(parseEach());
          continue;
        }
        if (src.startsWith("{#if", i)) {
          flush();
          nodes.push(parseIf());
          continue;
        }
        if (mode === "each" && (src.startsWith("{:empty}", i) || src.startsWith("{/each}", i))) {
          flush();
          break; // loop-section terminator; left for parseEach
        }
        if (mode === "if" && (src.startsWith("{:else", i) || src.startsWith("{/if}", i))) {
          flush();
          break; // conditional-section terminator; left for parseIf
        }
        text += readInterpolation();
        continue;
      }

      if (c === "}" && mode === "root") {
        flush();
        break; // view terminator; leave `}` for the caller
      }

      text += c;
      i++;
    }

    return nodes;
  }

  const nodes = parseNodeList("root");
  return { nodes, endPos: i };
}
