import type { PageAst, ViewNode } from "@wrnexus/syntax";

export type RouteExecutionKind =
  | "static"
  | "static-interactive"
  | "request-ssr"
  | "authenticated-ssr"
  | "streaming-ssr"
  | "dynamic";

export interface RuntimeRequirements {
  kind: RouteExecutionKind;
  canPrerender: boolean;
  needsClientRuntime: boolean;
  needsServerRuntime: boolean;
  hydrationStrategy: string | null;
  reasons: string[];
  optimization: OptimizationReport;
  cachePolicy: Record<string, string>;
  requiredPermission: string | null;
}

export interface OptimizationReport {
  staticNodes: number;
  reactiveRegions: number;
  eliminatedBranches: number;
  unusedState: string[];
  unusedHandlers: string[];
  constantProps: string[];
  unusedLocalCssClasses: string[];
  batchableStateUpdates: number;
  memoizableComponents: string[];
  preloadDependencies: string[];
  serverOnlyModules: string[];
}

function identifiers(value: string): Set<string> {
  return new Set(value.match(/[A-Za-z_$][\w$]*/g) ?? []);
}

function literalBoolean(expression: string | null): boolean | undefined {
  if (expression === null) return true;
  const value = expression.trim();
  if (value === "true") return true;
  if (
    value === "false" ||
    value === "null" ||
    value === "undefined" ||
    value === "0" ||
    value === "''" ||
    value === '""'
  )
    return false;
  if (/^-?(?:[1-9]\d*|0?\.\d+)$/.test(value) || /^(['"]).+\1$/.test(value)) return true;
  return undefined;
}

function optimizeNodes(nodes: ViewNode[], report: { eliminated: number }): ViewNode[] {
  const output: ViewNode[] = [];
  for (const node of nodes) {
    if (node.type === "element")
      output.push({
        ...node,
        attrs: node.attrs.map((attribute) => ({ ...attribute })),
        children: optimizeNodes(node.children, report),
      });
    else if (node.type === "each")
      output.push({
        ...node,
        body: optimizeNodes(node.body, report),
        empty: optimizeNodes(node.empty, report),
      });
    else if (node.type === "if") {
      let selected: ViewNode[] | undefined;
      let dynamic = false;
      for (const branch of node.branches) {
        const value = literalBoolean(branch.cond);
        if (value === undefined) {
          dynamic = true;
          break;
        }
        report.eliminated++;
        if (value) {
          selected = branch.body;
          break;
        }
      }
      if (dynamic)
        output.push({
          ...node,
          branches: node.branches.map((branch) => ({
            ...branch,
            body: optimizeNodes(branch.body, report),
          })),
        });
      else if (selected) output.push(...optimizeNodes(selected, report));
    } else output.push({ ...node });
  }
  return output;
}

/** Safe compile-time folding for literal conditional branches. */
export function optimizeAst(ast: PageAst): { ast: PageAst; eliminatedBranches: number } {
  const report = { eliminated: 0 };
  return {
    ast: { ...ast, view: optimizeNodes(ast.view, report) },
    eliminatedBranches: report.eliminated,
  };
}

export function analyzeOptimizations(ast: PageAst): OptimizationReport {
  const used = new Set<string>();
  let staticNodes = 0;
  let reactiveRegions = 0;
  const componentNames = new Set<string>();
  const staticClasses = new Set<string>();
  const visit = (nodes: ViewNode[]) => {
    for (const node of nodes) {
      if (node.type === "text") {
        const refs = identifiers(node.value);
        refs.forEach((name) => used.add(name));
        if (node.value.includes("{")) reactiveRegions++;
        else staticNodes++;
      } else if (node.type === "element") {
        if (/^[A-Z]/.test(node.tag)) componentNames.add(node.tag);
        let reactive = false;
        for (const attribute of node.attrs) {
          identifiers(attribute.value).forEach((name) => used.add(name));
          reactive ||= attribute.event || attribute.value.includes("{");
          if (attribute.name === "class" && !attribute.value.includes("{"))
            for (const name of attribute.value.split(/\s+/)) if (name) staticClasses.add(name);
        }
        if (reactive) reactiveRegions++;
        else staticNodes++;
        visit(node.children);
      } else if (node.type === "each") {
        identifiers(`${node.list} ${node.key ?? ""}`).forEach((name) => used.add(name));
        reactiveRegions++;
        visit(node.body);
        visit(node.empty);
      } else {
        for (const branch of node.branches) {
          identifiers(branch.cond ?? "").forEach((name) => used.add(name));
          visit(branch.body);
        }
        reactiveRegions++;
      }
    }
  };
  visit(ast.view);
  const handlerReferences = new Set(used);
  const executable = [
    ...ast.runtimeFunctions.map((fn) => fn.body),
    ...ast.functions,
    ...ast.effects.map((effect) => effect.body),
    ...ast.watches.map((watch) => watch.body),
    ...ast.actions.map((action) => action.body),
  ].join("\n");
  identifiers(executable).forEach((name) => used.add(name));
  const localCss = new Set(
    ast.styles.flatMap((style) =>
      [...style.matchAll(/\.([_a-zA-Z][\w-]*)/g)].map((match) => match[1]!),
    ),
  );
  const optimized = optimizeAst(ast);
  const assignmentCounts = ast.runtimeFunctions.map(
    (fn) =>
      ast.states.filter((state) =>
        new RegExp(`\\b${state.name}\\s*(?:[+*/-]?=|\\+\\+|--)`).test(fn.body),
      ).length,
  );
  return {
    staticNodes,
    reactiveRegions,
    eliminatedBranches: optimized.eliminatedBranches,
    unusedState: ast.states.filter((state) => !used.has(state.name)).map((state) => state.name),
    unusedHandlers: ast.runtimeFunctions
      .filter((fn) => fn.runtime !== "server" && !handlerReferences.has(fn.name))
      .map((fn) => fn.name),
    constantProps: ast.props
      .filter((prop) =>
        /^(?:-?\d+(?:\.\d+)?|true|false|null|(['"]).*\1)$/.test(prop.default.trim()),
      )
      .map((prop) => prop.name),
    unusedLocalCssClasses: [...localCss].filter((name) => !staticClasses.has(name)).sort(),
    batchableStateUpdates: assignmentCounts
      .filter((count) => count > 1)
      .reduce((sum, count) => sum + count - 1, 0),
    memoizableComponents: [...componentNames].sort(),
    preloadDependencies: ast.structuredImports
      .filter((entry) => !entry.typeOnly && !entry.source.startsWith("node:"))
      .map((entry) => entry.source),
    serverOnlyModules: ast.structuredImports
      .filter((entry) => entry.source.startsWith("node:") || ast.runtime === "server")
      .map((entry) => entry.source),
  };
}

function hasEvent(nodes: ViewNode[]): boolean {
  for (const node of nodes) {
    if (node.type === "element") {
      if (node.attrs.some((attribute) => attribute.event)) return true;
      if (hasEvent(node.children)) return true;
    } else if (node.type === "each") {
      if (hasEvent(node.body) || hasEvent(node.empty)) return true;
    } else if (node.type === "if") {
      if (node.branches.some((branch) => hasEvent(branch.body))) return true;
    }
  }
  return false;
}

export function analyzeRuntimeRequirements(ast: PageAst): RuntimeRequirements {
  const reasons: string[] = [];
  const clientFunctions = ast.runtimeFunctions.some((fn) => fn.runtime !== "server");
  const clientState = ast.states.some((state) => state.runtime !== "server");
  const interactive =
    clientFunctions ||
    clientState ||
    ast.effects.length > 0 ||
    ast.watches.length > 0 ||
    hasEvent(ast.view);
  if (interactive) reasons.push("client interactivity");

  const requestData =
    ast.loads.length > 0 ||
    ast.actions.length > 0 ||
    ast.dataApis.length > 0 ||
    ast.apis.length > 0 ||
    ast.realtimes.length > 0 ||
    ast.runtimeFunctions.some((fn) => fn.runtime === "server") ||
    ast.states.some((state) => state.runtime === "server");
  if (requestData) reasons.push("server/request data");

  const authenticated = /^(?:required|true)$/i.test(ast.security.auth ?? "");
  if (authenticated) reasons.push("authentication required");

  const streaming = /^(?:true|required)$/i.test(ast.security.streaming ?? "");
  if (streaming) reasons.push("streaming enabled");

  let kind: RouteExecutionKind;
  if (streaming) kind = "streaming-ssr";
  else if (authenticated) kind = "authenticated-ssr";
  else if (requestData && interactive) kind = "dynamic";
  else if (requestData) kind = "request-ssr";
  else if (interactive) kind = "static-interactive";
  else kind = "static";

  if (ast.renderMode === "static") {
    kind = "static";
    reasons.push("explicit static rendering");
  } else if (ast.renderMode === "server") {
    kind = requestData ? "request-ssr" : "static";
    reasons.push("explicit server rendering");
  } else if (ast.renderMode === "client") {
    kind = "static-interactive";
    reasons.push("explicit client rendering");
  } else if (ast.renderMode === "partial-static") {
    kind = "streaming-ssr";
    reasons.push("partial-static shell with streamed dynamic regions");
  }

  const clientDisabled = ast.renderMode === "static" || ast.renderMode === "server";
  const serverDisabled = ast.renderMode === "client";

  return {
    kind,
    canPrerender: kind === "static" || kind === "static-interactive",
    needsClientRuntime:
      !clientDisabled &&
      (interactive || ast.renderMode === "client") &&
      ast.hydrate !== "none" &&
      ast.runtime !== "server",
    needsServerRuntime:
      !serverDisabled &&
      (requestData ||
        authenticated ||
        streaming ||
        ast.renderMode === "server" ||
        ["server", "edge", "worker", "service-worker"].includes(ast.runtime ?? "")),
    hydrationStrategy: clientDisabled ? null : interactive ? (ast.hydrate ?? "load") : null,
    reasons,
    optimization: analyzeOptimizations(ast),
    cachePolicy: { ...(ast.cache ?? {}) },
    requiredPermission: ast.security.permission ?? null,
  };
}
