import { parse, ParseError, type PageAst, type ViewNode } from "./parser.ts";
import {
  WRN_DIAGNOSTIC_CODES,
  WRN_HYDRATION_STRATEGIES,
  WRN_RUNTIME_TARGETS,
  type WrnHydrationStrategy,
  type WrnRuntimeTarget,
} from "./spec.ts";

export type WrnDiagnosticSeverity = "error" | "warning" | "info";

export interface WrnSourcePosition {
  offset: number;
  line: number;
  column: number;
}

export interface WrnDiagnostic {
  code: string;
  severity: WrnDiagnosticSeverity;
  message: string;
  hint?: string;
  file?: string;
  position?: WrnSourcePosition;
  expected?: string;
  received?: string;
  related?: Array<{ file?: string; message: string; position?: WrnSourcePosition }>;
}

export interface DiagnoseOptions {
  file?: string;
  accessibility?: boolean;
}

function stripAsciiControlAndSpace(value: string): string {
  let result = "";
  for (const character of value) {
    if (character.charCodeAt(0) > 0x20) result += character;
  }
  return result;
}

function maskJavaScriptTrivia(source: string): string {
  let result = "";
  let index = 0;
  let quote: "'" | '"' | "`" | null = null;
  let lineComment = false;
  let blockComment = false;

  while (index < source.length) {
    const char = source[index]!;
    const next = source[index + 1];

    if (lineComment) {
      if (char === "\n") {
        lineComment = false;
        result += "\n";
      } else result += " ";
      index++;
      continue;
    }

    if (blockComment) {
      if (char === "*" && next === "/") {
        result += "  ";
        index += 2;
        blockComment = false;
      } else {
        result += char === "\n" ? "\n" : " ";
        index++;
      }
      continue;
    }

    if (quote) {
      if (char === "\\") {
        result += "  ";
        index += Math.min(2, source.length - index);
      } else if (char === quote) {
        result += " ";
        index++;
        quote = null;
      } else {
        result += char === "\n" ? "\n" : " ";
        index++;
      }
      continue;
    }

    if (char === "/" && next === "/") {
      result += "  ";
      index += 2;
      lineComment = true;
      continue;
    }
    if (char === "/" && next === "*") {
      result += "  ";
      index += 2;
      blockComment = true;
      continue;
    }
    if (char === "'" || char === '"' || char === "`") {
      quote = char;
      result += " ";
      index++;
      continue;
    }

    result += char;
    index++;
  }

  return result;
}

export function containsReadonlyPropMutation(
  body: string,
  propName: string,
  parameterNames: Set<string>,
): boolean {
  const code = maskJavaScriptTrivia(body);
  const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const operator = String.raw`(?:\+\+|--|(?:\*\*|&&|\|\||\?\?|[+\-*/%&|^])?=(?!=|>))`;

  if (new RegExp(String.raw`\bprops\.${escaped}\s*${operator}`).test(code)) return true;
  if (parameterNames.has(propName)) return false;
  if (new RegExp(String.raw`\b(?:const|let|var)\s+${escaped}\b`).test(code)) return false;

  return new RegExp(String.raw`(?:^|[^\w$.])${escaped}\s*${operator}`, "m").test(code);
}

export function positionAt(source: string, offset: number): WrnSourcePosition {
  const safe = Math.max(0, Math.min(offset, source.length));
  const before = source.slice(0, safe);
  const lines = before.split(/\r?\n/);
  return { offset: safe, line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
}

function offsetFromMessage(message: string): number | undefined {
  const match = /offset\s+(\d+)/i.exec(message);
  return match ? Number(match[1]) : undefined;
}

export function classifyParseError(message: string): string {
  if (/Expected 'page', 'component', or 'layout'/.test(message)) return WRN_DIAGNOSTIC_CODES.root;
  if (/Unknown (?:page|component|layout|ssr|client) member/.test(message)) {
    return WRN_DIAGNOSTIC_CODES.member;
  }
  if (/prop initializer|Expected eq/.test(message)) return WRN_DIAGNOSTIC_CODES.propInitializer;
  if (/State '.+' requires an initializer/.test(message)) {
    return WRN_DIAGNOSTIC_CODES.stateInitializer;
  }
  if (/Cannot watch undeclared state/.test(message)) return WRN_DIAGNOSTIC_CODES.watchUndeclared;
  return WRN_DIAGNOSTIC_CODES.parse;
}

export function diagnosticFromError(
  source: string,
  error: unknown,
  options: DiagnoseOptions = {},
): WrnDiagnostic {
  const message = error instanceof Error ? error.message : String(error);
  const offset =
    error instanceof ParseError && error.offset !== undefined
      ? error.offset
      : offsetFromMessage(message);
  return {
    code: error instanceof ParseError ? error.code : classifyParseError(message),
    severity: "error",
    message,
    file: options.file,
    ...(offset === undefined ? {} : { position: positionAt(source, offset) }),
  };
}

function walk(nodes: ViewNode[], visit: (node: ViewNode) => void): void {
  for (const node of nodes) {
    visit(node);
    if (node.type === "element") walk(node.children, visit);
    else if (node.type === "each") {
      walk(node.body, visit);
      walk(node.empty, visit);
    } else if (node.type === "if") {
      for (const branch of node.branches) walk(branch.body, visit);
    }
  }
}

function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[] {
  const diagnostics: WrnDiagnostic[] = [];
  const seen = new Map<string, string>();
  for (const [kind, declarations] of [
    ["prop", ast.props],
    ["state", ast.states],
    ["computed", ast.computed],
  ] as const) {
    for (const declaration of declarations) {
      const previous = seen.get(declaration.name);
      if (previous) {
        diagnostics.push({
          code: WRN_DIAGNOSTIC_CODES.duplicateSymbol,
          severity: "error",
          message: `Duplicate symbol '${declaration.name}' (${previous} and ${kind}).`,
          hint: "Rename one declaration so every prop, state, and computed value is unique.",
          file: options.file,
        });
      } else {
        seen.set(declaration.name, kind);
      }
    }
  }

  if (ast.hydrate && !isHydrationStrategy(ast.hydrate)) {
    diagnostics.push({
      code: WRN_DIAGNOSTIC_CODES.invalidHydration,
      severity: "error",
      message: `Unknown hydration strategy '${ast.hydrate}'.`,
      hint: "Use load, idle, visible, interaction, none, or media:<query>.",
      file: options.file,
    });
  }
  if (ast.runtime && !isRuntimeTarget(ast.runtime)) {
    diagnostics.push({
      code: WRN_DIAGNOSTIC_CODES.invalidRuntime,
      severity: "error",
      message: `Unknown runtime target '${ast.runtime}'.`,
      hint: "Use server, client, or universal.",
      file: options.file,
    });
  }

  let interactive = ast.states.length > 0 || ast.effects.length > 0 || ast.watches.length > 0;
  const urlAttributes = new Set([
    "href",
    "src",
    "action",
    "formaction",
    "poster",
    "cite",
    "background",
    "xlink:href",
  ]);
  walk(ast.view, (node) => {
    if (node.type !== "element") return;
    if (node.attrs.some((attribute) => attribute.event)) interactive = true;
    const tag = node.tag.toLowerCase();

    for (const attribute of node.attrs) {
      if (attribute.event || attribute.boolean || !urlAttributes.has(attribute.name.toLowerCase()))
        continue;
      if (attribute.value.includes("{")) continue;
      const value = stripAsciiControlAndSpace(attribute.value.trim()).toLowerCase();
      if (
        /^(?:javascript|vbscript|file):/.test(value) ||
        /^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(value)
      ) {
        diagnostics.push({
          code: "WRN-SEC-UNSAFE-URL",
          severity: "error",
          message: `Unsafe URL protocol in ${attribute.name} on <${node.tag}>.`,
          hint: "Use a relative URL, https:, mailto:, tel:, or a framework-validated URL helper.",
          file: options.file,
        });
      }
    }

    if (tag === "a") {
      const target = node.attrs.find((attribute) => attribute.name === "target")?.value;
      const rel = node.attrs.find((attribute) => attribute.name === "rel")?.value ?? "";
      if (target === "_blank" && !/\bnoopener\b/i.test(rel)) {
        diagnostics.push({
          code: "WRN-SEC-BLANK-REL",
          severity: "warning",
          message: "A target=_blank link should include rel=noopener.",
          hint: 'Add rel="noopener noreferrer".',
          file: options.file,
        });
      }
    }

    if (!options.accessibility) return;
    if (tag === "img") {
      if (!node.attrs.some((attribute) => attribute.name === "alt")) {
        diagnostics.push({
          code: WRN_DIAGNOSTIC_CODES.accessibility,
          severity: "warning",
          message: "Image is missing an alt attribute.",
          hint: 'Add alt text, or alt="" for a decorative image.',
          file: options.file,
        });
      }
      const hasWidth = node.attrs.some((attribute) => attribute.name === "width");
      const hasHeight = node.attrs.some((attribute) => attribute.name === "height");
      if (!hasWidth || !hasHeight) {
        diagnostics.push({
          code: "WRN-PERF-IMAGE-DIMENSIONS",
          severity: "warning",
          message: "Image width and height are required to prevent layout shifts.",
          hint: "Declare intrinsic width and height, or use @wrnexus/image.",
          file: options.file,
        });
      }
    }
  });
  if (ast.runtime === "server" && interactive) {
    diagnostics.push({
      code: WRN_DIAGNOSTIC_CODES.serverInteractive,
      severity: "error",
      message:
        "A server-only WRN root cannot contain client state, effects, watches, or event handlers.",
      hint: 'Use runtime = "universal" or remove interactive behavior.',
      file: options.file,
    });
  }

  const outputs = new Set(ast.outputs.map((output) => output.name));
  for (const fn of ast.runtimeFunctions) {
    for (const call of fn.body.matchAll(/\boutput\.([A-Za-z_$][\w$]*)\s*\(/g)) {
      const outputName = call[1]!;
      if (fn.runtime === "server") {
        diagnostics.push({
          code: "WRN-OUTPUT-SERVER-CALL",
          severity: "error",
          message: `Server function '${fn.name}' cannot call output.${outputName}().`,
          hint: "Return a typed value to the browser and call the output from a client function.",
          file: options.file,
        });
      } else if (!outputs.has(outputName)) {
        diagnostics.push({
          code: "WRN-OUTPUT-UNKNOWN",
          severity: "error",
          message: `Unknown output '${outputName}' called from '${fn.name}'.`,
          hint: `Declare ${outputName}(payload) inside outputs { ... }.`,
          file: options.file,
        });
      }
    }
    if (fn.runtime === "client" && /\b(?:process|Bun|Deno|__dirname|require)\b/.test(fn.body)) {
      diagnostics.push({
        code: "WRN-CLIENT-SERVER-API",
        severity: "error",
        message: `Client function '${fn.name}' references a server-only API.`,
        hint: "Move that operation into a server function and call it through server.name(...).",
        file: options.file,
      });
    }
    if (
      fn.runtime === "server" &&
      /\b(?:window|document|localStorage|sessionStorage|navigator)\b/.test(fn.body)
    ) {
      diagnostics.push({
        code: "WRN-SERVER-BROWSER-API",
        severity: "error",
        message: `Server function '${fn.name}' references a browser-only API.`,
        hint: "Move that code into a client function.",
        file: options.file,
      });
    }
    if (
      fn.runtime !== "server" &&
      /\b(?:eval\s*\(|new\s+Function\s*\(|document\.write\s*\(|\.innerHTML\s*=|\.outerHTML\s*=|insertAdjacentHTML\s*\()/.test(
        fn.body,
      )
    ) {
      diagnostics.push({
        code: "WRN-SEC-DOM-SINK",
        severity: "error",
        message: `Client function '${fn.name}' uses an unsafe dynamic-code or HTML sink.`,
        hint: "Use compiled templates, textContent, typed outputs, or a reviewed TrustedHTML sanitizer.",
        file: options.file,
      });
    }
    if (fn.runtime !== "server" && /\b(?:setTimeout|setInterval)\s*\(\s*["'`]/.test(fn.body)) {
      diagnostics.push({
        code: "WRN-SEC-STRING-TIMER",
        severity: "error",
        message: `Client function '${fn.name}' passes a string to a timer.`,
        hint: "Pass a function instead of executable text.",
        file: options.file,
      });
    }
    const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));
    for (const prop of ast.props) {
      if (containsReadonlyPropMutation(fn.body, prop.name, parameterNames)) {
        diagnostics.push({
          code: "WRN-PROP-READONLY",
          severity: "error",
          message: `Function '${fn.name}' attempts to mutate readonly prop '${prop.name}'.`,
          hint: "Copy the prop into state before mutating it.",
          file: options.file,
        });
      }
    }
  }
  for (const state of ast.states) {
    if (
      state.runtime === "shared" &&
      /^(?:new\s+(?:Map|Set|WeakMap|WeakSet)|(?:async\s+)?function\b|.*=>)/.test(state.expr.trim())
    ) {
      diagnostics.push({
        code: "WRN-STATE-NON-SERIALIZABLE",
        severity: "error",
        message: `Shared state '${state.name}' is not safely serializable.`,
        hint: "Use JSON-compatible data or move the value into client/server state.",
        file: options.file,
      });
    }
    if (
      state.runtime !== "server" &&
      /\b(?:process\.env|Bun\.env|Deno\.env|ctx\.env|import\.meta\.env)\b/.test(state.expr)
    ) {
      diagnostics.push({
        code: "WRN-SEC-SERVER-SECRET-SOURCE",
        severity: "error",
        message: `Browser-visible state '${state.name}' reads from a server environment source.`,
        hint: "Move environment-backed values into server state and return only an explicitly safe result.",
        file: options.file,
      });
    }
  }
  if (ast.persist) {
    const stateNames = new Set(
      ast.states.filter((state) => state.runtime !== "server").map((state) => state.name),
    );
    for (const name of ast.persist.include)
      if (!stateNames.has(name))
        diagnostics.push({
          code: "WRN-PERSIST-UNKNOWN-FIELD",
          severity: "error",
          message: `Persist include references unknown or server-only state '${name}'.`,
          hint: "Persist only declared shared/client state fields.",
          file: options.file,
        });
    for (const name of ast.persist.include)
      if (/token|password|secret|otp|api.?key/i.test(name))
        diagnostics.push({
          code: "WRN-PERSIST-SENSITIVE",
          severity: "error",
          message: `Sensitive field '${name}' cannot be persisted.`,
          hint: "Remove secrets, tokens, passwords, OTPs, and API keys from persistence.",
          file: options.file,
        });
  }
  return diagnostics;
}

export function diagnose(source: string, options: DiagnoseOptions = {}): WrnDiagnostic[] {
  try {
    return astDiagnostics(parse(source), options);
  } catch (error) {
    return [diagnosticFromError(source, error, options)];
  }
}

export function assertValidAst(ast: PageAst, options: DiagnoseOptions = {}): void {
  const errors = astDiagnostics(ast, options).filter(
    (diagnostic) => diagnostic.severity === "error",
  );
  if (!errors.length) return;
  const first = errors[0]!;
  throw new ParseError(first.message, first.code);
}

export function isHydrationStrategy(value: string): value is WrnHydrationStrategy {
  return (
    (WRN_HYDRATION_STRATEGIES as readonly string[]).includes(value) ||
    (value.startsWith("media:") && value.length > "media:".length)
  );
}

export function isRuntimeTarget(value: string): value is WrnRuntimeTarget {
  return (WRN_RUNTIME_TARGETS as readonly string[]).includes(value);
}

export function formatDiagnostic(source: string, diagnostic: WrnDiagnostic): string {
  const location = diagnostic.position
    ? `${diagnostic.file ?? "<inline .wrn>"}:${diagnostic.position.line}:${diagnostic.position.column}`
    : (diagnostic.file ?? "<inline .wrn>");
  const lines = [
    `${diagnostic.code} ${diagnostic.severity.toUpperCase()}`,
    "",
    diagnostic.message,
    "",
    location,
  ];
  if (diagnostic.position) {
    const sourceLine = source.split(/\r?\n/)[diagnostic.position.line - 1] ?? "";
    lines.push("", sourceLine, `${" ".repeat(Math.max(0, diagnostic.position.column - 1))}^`);
  }
  if (diagnostic.hint) lines.push("", `Hint: ${diagnostic.hint}`);
  return lines.join("\n");
}
