import type { AST } from "@eslint-community/regexpp";
import type {
  CaptureDefinition,
  NormalizedRegexNode,
  NormalizedRegexToken,
  RegexNodeKind,
  SourceRange,
} from "../../../model/syntax";

const PROVIDER = "regexpp-community";
const PROVIDER_VERSION = "4.12.2";

interface NormalizationState {
  nextNodeId: number;
  nextCaptureNumber: number;
  readonly captures: CaptureDefinition[];
}

interface NodeContext {
  readonly activeCaptureNumber?: number;
  readonly repeated: boolean;
  readonly activeFlags: readonly string[];
}

const FLAG_ORDER = ["d", "g", "i", "m", "s", "u", "v", "y"] as const;

function flagsWithModifiers(
  flags: readonly string[],
  modifiers: AST.Modifiers,
): readonly string[] {
  const active = new Set(flags);
  const update = (modifierFlags: AST.ModifierFlags, enabled: boolean) => {
    if (modifierFlags.ignoreCase) {
      if (enabled) active.add("i");
      else active.delete("i");
    }
    if (modifierFlags.multiline) {
      if (enabled) active.add("m");
      else active.delete("m");
    }
    if (modifierFlags.dotAll) {
      if (enabled) active.add("s");
      else active.delete("s");
    }
  };
  update(modifiers.add, true);
  if (modifiers.remove) update(modifiers.remove, false);
  return FLAG_ORDER.filter((flag) => active.has(flag));
}

function sourceRange(node: AST.Node): SourceRange {
  return { startUtf16: node.start, endUtf16: node.end };
}

function nodeKind(node: AST.Node): RegexNodeKind {
  switch (node.type) {
    case "Pattern":
      return node.alternatives.length > 1 ? "disjunction" : "pattern";
    case "Alternative":
      return node.elements.length > 1 ? "sequence" : "alternative";
    case "Group":
      return node.modifiers ? "inline-flags" : "noncapture-group";
    case "CapturingGroup":
      return node.name ? "named-capture-group" : "capture-group";
    case "Quantifier":
      return "quantifier";
    case "Assertion":
      if (node.kind === "start" || node.kind === "end") return "anchor";
      if (node.kind === "word") return "word-boundary";
      if (node.kind === "lookahead") {
        return node.negate ? "negative-lookahead" : "lookahead";
      }
      return "negate" in node && node.negate
        ? "negative-lookbehind"
        : "lookbehind";
    case "Character":
      return node.raw.startsWith("\\") ? "escaped-literal" : "literal";
    case "CharacterSet":
      if (node.kind === "any") return "dot";
      return node.kind === "property" ? "unicode-property" : "character-class";
    case "CharacterClass":
    case "ExpressionCharacterClass":
    case "ClassIntersection":
    case "ClassSubtraction":
    case "ClassStringDisjunction":
    case "StringAlternative":
      return "character-class";
    case "CharacterClassRange":
      return "character-class-range";
    case "Backreference":
      return "backreference";
    case "Modifiers":
    case "ModifierFlags":
      return "inline-flags";
    case "RegExpLiteral":
    case "Flags":
      return "pattern";
  }
}

function childrenOf(node: AST.Node): readonly AST.Node[] {
  switch (node.type) {
    case "RegExpLiteral":
      return [node.pattern, node.flags];
    case "Pattern":
    case "CapturingGroup":
    case "Group":
      return node.alternatives;
    case "Alternative":
      return node.elements;
    case "Quantifier":
      return [node.element];
    case "Assertion":
      return "alternatives" in node ? node.alternatives : [];
    case "CharacterClass":
      return node.elements;
    case "ExpressionCharacterClass":
      return [node.expression];
    case "CharacterClassRange":
      return [node.min, node.max];
    case "ClassIntersection":
    case "ClassSubtraction":
      return [node.left, node.right];
    case "ClassStringDisjunction":
      return node.alternatives;
    case "StringAlternative":
      return node.elements;
    case "Modifiers":
      return node.remove ? [node.add, node.remove] : [node.add];
    case "Backreference":
    case "Character":
    case "CharacterSet":
    case "Flags":
    case "ModifierFlags":
      return [];
  }
}

function explanationFor(
  node: AST.Node,
  kind: RegexNodeKind,
  captureNumber?: number,
): string {
  switch (node.type) {
    case "Pattern":
      return node.alternatives.length > 1
        ? `Choose one of ${node.alternatives.length} alternatives`
        : "Regular-expression pattern";
    case "Alternative":
      return node.elements.length > 1
        ? `Match ${node.elements.length} items in sequence`
        : "Pattern alternative";
    case "CapturingGroup":
      return node.name
        ? `Capture as group ${captureNumber ?? "?"} named “${node.name}”`
        : `Capture as group ${captureNumber ?? "?"}`;
    case "Group":
      return node.modifiers
        ? "Apply inline flags to this non-capturing group"
        : "Group constructs without capturing";
    case "Quantifier": {
      const maximum = Number.isFinite(node.max)
        ? String(node.max)
        : "without limit";
      return `Repeat ${node.min} to ${maximum} times, ${
        node.greedy ? "greedily" : "lazily"
      }`;
    }
    case "Assertion":
      if (node.kind === "start") return "Assert the start of input or line";
      if (node.kind === "end") return "Assert the end of input or line";
      if (node.kind === "word") {
        return node.negate
          ? "Assert that this is not a word boundary"
          : "Assert a word boundary";
      }
      if (node.kind === "lookahead") {
        return node.negate
          ? "Assert that the following pattern does not match"
          : "Assert that the following pattern matches";
      }
      return "negate" in node && node.negate
        ? "Assert that the preceding pattern does not match"
        : "Assert that the preceding pattern matches";
    case "Character":
      return node.raw.startsWith("\\")
        ? `Match escaped character ${node.raw}`
        : `Match literal ${JSON.stringify(node.raw)}`;
    case "CharacterSet":
      if (node.kind === "any") return "Match any allowed character";
      if (node.kind === "property") {
        const property = node.value ? `${node.key}=${node.value}` : node.key;
        return `${node.negate ? "Exclude" : "Match"} Unicode property ${property}`;
      }
      return `Match ${node.negate ? "anything except " : ""}${node.kind} characters`;
    case "CharacterClass":
      return `${node.negate ? "Exclude" : "Match"} one character from this class`;
    case "ExpressionCharacterClass":
      return `${node.negate ? "Negated " : ""}Unicode set expression`;
    case "CharacterClassRange":
      return `Match a character in range ${node.raw}`;
    case "ClassIntersection":
      return "Match the intersection of two Unicode character sets";
    case "ClassSubtraction":
      return "Match the left Unicode set excluding the right set";
    case "ClassStringDisjunction":
      return "Match one string from this Unicode string set";
    case "StringAlternative":
      return "Unicode string-set alternative";
    case "Backreference":
      return `Match the text retained by capture ${String(node.ref)}`;
    case "Modifiers":
    case "ModifierFlags":
      return "Inline regular-expression flags";
    case "RegExpLiteral":
      return "Regular-expression literal";
    case "Flags":
      return "Regular-expression flags";
    default:
      return kind.replaceAll("-", " ");
  }
}

interface LengthProperties {
  readonly zeroWidth: boolean;
  readonly nullable: boolean | "unknown";
  readonly minimumLength?: number;
  readonly maximumLength?: number | null;
  readonly consumesInput: boolean | "conditional";
}

const oneCharacterProperties: LengthProperties = {
  zeroWidth: false,
  nullable: false,
  minimumLength: 1,
  maximumLength: 1,
  consumesInput: true,
};

function choiceProperties(
  children: readonly NormalizedRegexNode[],
  emptyChoice: LengthProperties,
): LengthProperties {
  if (children.length === 0) return emptyChoice;
  const minimums = children.map((child) => child.properties.minimumLength);
  const maximums = children.map((child) => child.properties.maximumLength);
  const minimumLength = minimums.some((value) => value === undefined)
    ? undefined
    : Math.min(...(minimums as number[]));
  const maximumLength = maximums.some((value) => value === undefined)
    ? undefined
    : maximums.some((value) => value === null)
      ? null
      : Math.max(...(maximums as number[]));
  const nullable = children.some((child) => child.properties.nullable === true)
    ? true
    : children.every((child) => child.properties.nullable === false)
      ? false
      : "unknown";
  return {
    zeroWidth: maximumLength === 0,
    nullable,
    ...(minimumLength === undefined ? {} : { minimumLength }),
    ...(maximumLength === undefined ? {} : { maximumLength }),
    consumesInput:
      maximumLength === 0
        ? false
        : minimumLength !== undefined && minimumLength > 0
          ? true
          : "conditional",
  };
}

function sumMaximum(
  children: readonly NormalizedRegexNode[],
): number | null | undefined {
  let total = 0;
  for (const child of children) {
    if (child.properties.maximumLength === undefined) return undefined;
    if (child.properties.maximumLength === null) return null;
    total += child.properties.maximumLength;
  }
  return total;
}

function propertiesFor(
  node: AST.Node,
  children: readonly NormalizedRegexNode[],
): LengthProperties {
  if (node.type === "Assertion") {
    return {
      zeroWidth: true,
      nullable: true,
      minimumLength: 0,
      maximumLength: 0,
      consumesInput: false,
    };
  }
  if (node.type === "Character" || node.type === "CharacterClassRange") {
    return oneCharacterProperties;
  }
  if (node.type === "CharacterSet") {
    return "strings" in node && node.strings
      ? {
          zeroWidth: false,
          nullable: false,
          consumesInput: true,
        }
      : oneCharacterProperties;
  }
  if (node.type === "CharacterClass") {
    return choiceProperties(children, oneCharacterProperties);
  }
  if (node.type === "ClassStringDisjunction") {
    return choiceProperties(children, {
      zeroWidth: true,
      nullable: true,
      minimumLength: 0,
      maximumLength: 0,
      consumesInput: false,
    });
  }
  if (node.type === "ExpressionCharacterClass") {
    return (
      children[0]?.properties ?? {
        zeroWidth: false,
        nullable: "unknown",
        consumesInput: "conditional",
      }
    );
  }
  if (node.type === "Backreference") {
    return {
      zeroWidth: false,
      nullable: "unknown",
      consumesInput: "conditional",
    };
  }
  if (node.type === "Quantifier") {
    const child = children[0];
    const childMinimum = child?.properties.minimumLength;
    const childMaximum = child?.properties.maximumLength;
    const minimumLength =
      childMinimum === undefined ? undefined : childMinimum * node.min;
    const maximumLength =
      childMaximum === undefined
        ? undefined
        : !Number.isFinite(node.max) || childMaximum === null
          ? null
          : childMaximum * node.max;
    return {
      zeroWidth: maximumLength === 0,
      nullable:
        node.min === 0
          ? true
          : child?.properties.nullable === undefined
            ? "unknown"
            : child.properties.nullable,
      ...(minimumLength === undefined ? {} : { minimumLength }),
      ...(maximumLength === undefined ? {} : { maximumLength }),
      consumesInput:
        maximumLength === 0
          ? false
          : minimumLength !== undefined && minimumLength > 0
            ? true
            : "conditional",
    };
  }
  if (
    node.type === "Pattern" ||
    node.type === "CapturingGroup" ||
    node.type === "Group"
  ) {
    return choiceProperties(children, {
      zeroWidth: true,
      nullable: true,
      minimumLength: 0,
      maximumLength: 0,
      consumesInput: false,
    });
  }
  if (node.type === "Alternative" || node.type === "StringAlternative") {
    const minimumLength = children.every(
      (child) => child.properties.minimumLength !== undefined,
    )
      ? children.reduce(
          (total, child) => total + (child.properties.minimumLength ?? 0),
          0,
        )
      : undefined;
    const maximumLength = sumMaximum(children);
    const nullable = children.every(
      (child) => child.properties.nullable === true,
    )
      ? true
      : children.some((child) => child.properties.nullable === "unknown")
        ? "unknown"
        : false;
    return {
      zeroWidth: maximumLength === 0,
      nullable,
      ...(minimumLength === undefined ? {} : { minimumLength }),
      ...(maximumLength === undefined ? {} : { maximumLength }),
      consumesInput:
        maximumLength === 0
          ? false
          : minimumLength !== undefined && minimumLength > 0
            ? true
            : "conditional",
    };
  }
  return {
    zeroWidth: false,
    nullable: "unknown",
    consumesInput: "conditional",
  };
}

function normalizeNode(
  node: AST.Node,
  state: NormalizationState,
  context: NodeContext,
): NormalizedRegexNode {
  const id = `syntax-${state.nextNodeId++}`;
  const activeFlags =
    node.type === "Group" && node.modifiers
      ? flagsWithModifiers(context.activeFlags, node.modifiers)
      : context.activeFlags;
  let captureNumber: number | undefined;
  let captureName: string | undefined;
  if (node.type === "CapturingGroup") {
    captureNumber = state.nextCaptureNumber++;
    captureName = node.name ?? undefined;
    state.captures.push({
      number: captureNumber,
      ...(captureName ? { name: captureName } : {}),
      range: sourceRange(node),
      repeated: context.repeated || node.parent.type === "Quantifier",
      ...(context.activeCaptureNumber === undefined
        ? {}
        : { parentCaptureNumber: context.activeCaptureNumber }),
    });
  }

  const childContext: NodeContext = {
    ...(captureNumber === undefined
      ? context.activeCaptureNumber === undefined
        ? {}
        : { activeCaptureNumber: context.activeCaptureNumber }
      : { activeCaptureNumber: captureNumber }),
    repeated: context.repeated || node.type === "Quantifier",
    activeFlags,
  };
  const children = childrenOf(node).map((child) =>
    normalizeNode(child, state, childContext),
  );
  const kind = nodeKind(node);

  let assertion: NormalizedRegexNode["assertion"];
  if (node.type === "Assertion") {
    if (node.kind === "start" || node.kind === "end") {
      assertion = { kind: node.kind };
    } else if (node.kind === "word") {
      assertion = {
        kind: node.negate ? "non-word-boundary" : "word-boundary",
      };
    } else if (node.kind === "lookahead") {
      assertion = { kind: node.negate ? "negative-lookahead" : "lookahead" };
    } else {
      assertion = {
        kind:
          "negate" in node && node.negate
            ? "negative-lookbehind"
            : "lookbehind",
      };
    }
  }

  return {
    id,
    kind,
    range: sourceRange(node),
    raw: node.raw,
    explanation: explanationFor(node, kind, captureNumber),
    children,
    activeFlags,
    ...(captureNumber === undefined
      ? {}
      : {
          capture: {
            number: captureNumber,
            ...(captureName ? { name: captureName } : {}),
          },
        }),
    ...(node.type === "Quantifier"
      ? {
          quantifier: {
            minimum: node.min,
            maximum: Number.isFinite(node.max) ? node.max : null,
            mode: node.greedy ? ("greedy" as const) : ("lazy" as const),
          },
        }
      : {}),
    ...(assertion ? { assertion } : {}),
    properties: propertiesFor(node, children),
    support: {
      flavour: "ecmascript",
      status: "supported",
      notes: [],
    },
    provenance: {
      provider: PROVIDER,
      providerVersion: PROVIDER_VERSION,
      source: "parsed",
    },
  };
}

function collectTokens(
  root: NormalizedRegexNode,
): readonly NormalizedRegexToken[] {
  const tokens: NormalizedRegexToken[] = [];
  const visit = (node: NormalizedRegexNode) => {
    if (
      node.kind !== "pattern" &&
      node.kind !== "disjunction" &&
      node.kind !== "sequence" &&
      node.kind !== "alternative" &&
      node.range.endUtf16 > node.range.startUtf16
    ) {
      tokens.push({
        id: `token-${node.id}`,
        kind: node.kind,
        range: node.range,
        raw: node.raw,
        ...(node.capture ? { captureNumber: node.capture.number } : {}),
      });
    }
    node.children.forEach(visit);
  };
  visit(root);
  return tokens.sort(
    (left, right) =>
      left.range.startUtf16 - right.range.startUtf16 ||
      right.range.endUtf16 - left.range.endUtf16,
  );
}

export interface NormalizedPattern {
  readonly root: NormalizedRegexNode;
  readonly tokens: readonly NormalizedRegexToken[];
  readonly captures: readonly CaptureDefinition[];
}

export function normalizePattern(
  pattern: AST.Pattern,
  flags: readonly string[] = [],
): NormalizedPattern {
  const state: NormalizationState = {
    nextNodeId: 1,
    nextCaptureNumber: 1,
    captures: [],
  };
  const root = normalizeNode(pattern, state, {
    repeated: false,
    activeFlags: FLAG_ORDER.filter((flag) => flags.includes(flag)),
  });
  return {
    root,
    tokens: collectTokens(root),
    captures: state.captures,
  };
}

export function withRecoveryNode(
  normalized: NormalizedPattern,
  completePattern: string,
  errorRange: SourceRange,
  message: string,
): NormalizedPattern {
  const recovery: NormalizedRegexNode = {
    id: "syntax-recovery",
    kind: "error-recovery",
    range: errorRange,
    raw: completePattern.slice(errorRange.startUtf16, errorRange.endUtf16),
    explanation: message,
    children: [],
    properties: {
      zeroWidth: false,
      nullable: "unknown",
      consumesInput: "conditional",
    },
    support: {
      flavour: "ecmascript",
      status: "partial",
      notes: [
        "regexpp does not expose tolerant recovery; this application error node marks the reported range.",
      ],
    },
    provenance: {
      provider: PROVIDER,
      providerVersion: PROVIDER_VERSION,
      source: "derived",
    },
  };
  const root: NormalizedRegexNode = {
    ...normalized.root,
    kind: "pattern",
    range: { startUtf16: 0, endUtf16: completePattern.length },
    raw: completePattern,
    explanation: "Malformed pattern with an application-owned error node",
    children: [...normalized.root.children, recovery],
    support: {
      flavour: "ecmascript",
      status: "partial",
      notes: [
        "Execution is disabled until the engine accepts the pattern.",
        "No partial provider AST is claimed.",
      ],
    },
    provenance: {
      provider: PROVIDER,
      providerVersion: PROVIDER_VERSION,
      source: "derived",
    },
  };
  return {
    root,
    tokens: [
      ...normalized.tokens,
      {
        id: "token-syntax-recovery",
        kind: "error-recovery",
        range: errorRange,
        raw: recovery.raw,
      },
    ],
    captures: normalized.captures,
  };
}

export function emptyRecoveredPattern(
  completePattern: string,
  errorRange: SourceRange,
  message: string,
  flags: readonly string[] = [],
): NormalizedPattern {
  const root: NormalizedRegexNode = {
    id: "syntax-root",
    kind: "pattern",
    range: { startUtf16: 0, endUtf16: completePattern.length },
    raw: completePattern,
    explanation: "Malformed regular-expression pattern",
    children: [],
    activeFlags: FLAG_ORDER.filter((flag) => flags.includes(flag)),
    properties: {
      zeroWidth: false,
      nullable: "unknown",
      consumesInput: "conditional",
    },
    support: {
      flavour: "ecmascript",
      status: "partial",
      notes: ["The provider does not expose tolerant syntax recovery."],
    },
    provenance: {
      provider: PROVIDER,
      providerVersion: PROVIDER_VERSION,
      source: "derived",
    },
  };
  return withRecoveryNode(
    { root, tokens: [], captures: [] },
    completePattern,
    errorRange,
    message,
  );
}
