import type {
  RegexEngineOptions,
  RegexFlavourId,
  RegexOptionValue,
} from "../model/flavour";

export interface RegexFlavourVersionDefinition {
  readonly value: string;
  readonly label: string;
  readonly syntaxVersion: string;
  readonly legacyValues?: readonly string[];
}

export interface RegexFlagDefinition {
  readonly value: string;
  readonly label: string;
}

export type RegexOptionDefinition =
  | {
      readonly name: string;
      readonly label: string;
      readonly kind: "boolean";
      readonly defaultValue: boolean;
    }
  | {
      readonly name: string;
      readonly label: string;
      readonly kind: "integer";
      readonly defaultValue: number;
      readonly minimum: number;
      readonly maximum: number;
    }
  | {
      readonly name: string;
      readonly label: string;
      readonly kind: "string";
      readonly defaultValue: string;
      readonly maximumLength: number;
      readonly values?: readonly string[];
    };

export interface RegexFlavourDefinition {
  readonly id: RegexFlavourId;
  readonly label: string;
  readonly versions: readonly RegexFlavourVersionDefinition[];
  readonly defaultVersion: string;
  readonly flags: readonly RegexFlagDefinition[];
  readonly defaultFlags: readonly string[];
  readonly mutuallyExclusiveFlags?: readonly (readonly string[])[];
  readonly options: readonly RegexOptionDefinition[];
}

function duplicate(values: readonly string[]): string | undefined {
  const seen = new Set<string>();
  return values.find((value) => {
    if (seen.has(value)) return true;
    seen.add(value);
    return false;
  });
}

function validateDefinition(definition: RegexFlavourDefinition): void {
  if (definition.versions.length === 0) {
    throw new Error(`${definition.label} must define at least one version.`);
  }
  if (
    !definition.versions.some(
      (version) => version.value === definition.defaultVersion,
    )
  ) {
    throw new Error(
      `${definition.label} default version is not present in its version list.`,
    );
  }
  const duplicateVersion = duplicate(
    definition.versions.map((version) => version.value),
  );
  if (duplicateVersion) {
    throw new Error(
      `${definition.label} defines duplicate version ${JSON.stringify(duplicateVersion)}.`,
    );
  }
  const flagValues = definition.flags.map((flag) => flag.value);
  const duplicateFlag = duplicate(flagValues);
  if (duplicateFlag) {
    throw new Error(
      `${definition.label} defines duplicate flag ${JSON.stringify(duplicateFlag)}.`,
    );
  }
  if (definition.defaultFlags.some((flag) => !flagValues.includes(flag))) {
    throw new Error(`${definition.label} has an unsupported default flag.`);
  }
  const duplicateOption = duplicate(
    definition.options.map((option) => option.name),
  );
  if (duplicateOption) {
    throw new Error(
      `${definition.label} defines duplicate option ${JSON.stringify(duplicateOption)}.`,
    );
  }
}

export class RegexFlavourRegistry {
  readonly definitions: readonly RegexFlavourDefinition[];
  private readonly byId: ReadonlyMap<RegexFlavourId, RegexFlavourDefinition>;

  constructor(definitions: readonly RegexFlavourDefinition[]) {
    const duplicateId = duplicate(
      definitions.map((definition) => definition.id),
    );
    if (duplicateId) {
      throw new Error(
        `Regex flavour ${JSON.stringify(duplicateId)} is registered more than once.`,
      );
    }
    definitions.forEach(validateDefinition);
    this.definitions = [...definitions];
    this.byId = new Map(
      definitions.map((definition) => [definition.id, definition]),
    );
  }

  get(id: RegexFlavourId): RegexFlavourDefinition | undefined {
    return this.byId.get(id);
  }

  require(id: RegexFlavourId): RegexFlavourDefinition {
    const definition = this.get(id);
    if (definition) return definition;
    throw new Error(this.unavailableMessage(id));
  }

  parse(value: unknown, label: string): RegexFlavourDefinition {
    if (typeof value !== "string" || value.length > 32) {
      throw new Error(`${label} must be a supported flavour identifier.`);
    }
    const definition = this.definitions.find(
      (candidate) => candidate.id === value,
    );
    if (definition) return definition;
    throw new Error(this.unavailableMessage(value));
  }

  private unavailableMessage(value: string): string {
    const available = this.definitions.map((definition) => definition.label);
    const support =
      available.length === 1
        ? available[0]
        : available.length === 0
          ? "no flavours"
          : `${available.slice(0, -1).join(", ")} and ${available.at(-1)}`;
    return `Regex flavour ${JSON.stringify(value)} is unavailable; this build only supports ${support}.`;
  }
}

function parseFlagArray(value: unknown, label: string): readonly string[] {
  if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
  return value.map((flag) => {
    if (typeof flag !== "string" || flag.length === 0 || flag.length > 32) {
      throw new Error(`${label} must contain non-empty flag identifiers.`);
    }
    return flag;
  });
}

export function parseRegexFlags(
  value: unknown,
  definition: RegexFlavourDefinition,
  label: string,
): readonly string[] {
  const flags = parseFlagArray(value, label);
  const supported = new Set(definition.flags.map((flag) => flag.value));
  if (
    new Set(flags).size !== flags.length ||
    flags.some((flag) => !supported.has(flag))
  ) {
    throw new Error(`${label} contain duplicates or unsupported values.`);
  }
  for (const group of definition.mutuallyExclusiveFlags ?? []) {
    const selected = group.filter((flag) => flags.includes(flag));
    if (selected.length > 1) {
      throw new Error(
        `${definition.label} flags ${selected.join(" and ")} cannot be combined.`,
      );
    }
  }
  return flags;
}

function optionObject(value: unknown, label: string): Record<string, unknown> {
  if (value === undefined) return {};
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error(`${label} must be a JSON object.`);
  }
  return value as Record<string, unknown>;
}

function parseOptionValue(
  value: unknown,
  definition: RegexOptionDefinition,
  label: string,
): RegexOptionValue {
  switch (definition.kind) {
    case "boolean":
      if (typeof value !== "boolean") {
        throw new Error(`${label} must be boolean.`);
      }
      return value;
    case "integer":
      if (
        typeof value !== "number" ||
        !Number.isSafeInteger(value) ||
        value < definition.minimum ||
        value > definition.maximum
      ) {
        throw new Error(
          `${label} must be an integer from ${definition.minimum} to ${definition.maximum}.`,
        );
      }
      return value;
    case "string":
      if (
        typeof value !== "string" ||
        value.length > definition.maximumLength ||
        (definition.values !== undefined && !definition.values.includes(value))
      ) {
        throw new Error(`${label} contains an unsupported text value.`);
      }
      return value;
  }
}

export function parseRegexOptions(
  value: unknown,
  definition: RegexFlavourDefinition,
  label: string,
): RegexEngineOptions {
  const input = optionObject(value, label);
  const definitions = new Map(
    definition.options.map((option) => [option.name, option]),
  );
  const unsupported = Object.keys(input).filter(
    (name) => !definitions.has(name),
  );
  if (unsupported.length > 0) {
    if (definition.options.length === 0) {
      throw new Error(
        `${label} contains unsupported entries; this build does not implement ${definition.label} engine options.`,
      );
    }
    throw new Error(
      `${label} contains unsupported entries: ${unsupported.join(", ")}.`,
    );
  }
  const result: Record<string, RegexOptionValue> = {
    ...defaultRegexOptions(definition),
  };
  for (const [name, optionValue] of Object.entries(input)) {
    const option = definitions.get(name);
    if (!option) continue;
    result[name] = parseOptionValue(
      optionValue,
      option,
      `${label} ${JSON.stringify(name)}`,
    );
  }
  return result;
}

export function defaultRegexOptions(
  definition: RegexFlavourDefinition,
): RegexEngineOptions {
  return Object.fromEntries(
    definition.options.map((option) => [option.name, option.defaultValue]),
  );
}

export function resolveRegexFlavourVersion(
  value: unknown,
  definition: RegexFlavourDefinition,
  label: string,
): {
  readonly definition: RegexFlavourVersionDefinition;
  readonly value: string;
} {
  const candidate =
    value === undefined
      ? definition.defaultVersion
      : typeof value === "string" && value.length <= 128
        ? value
        : (() => {
            throw new Error(`${label} must be a supported version identifier.`);
          })();
  const version = definition.versions.find(
    (entry) =>
      entry.value === candidate || entry.legacyValues?.includes(candidate),
  );
  if (!version) {
    throw new Error(
      `${label} ${JSON.stringify(candidate)} is unsupported for ${definition.label}.`,
    );
  }
  return { definition: version, value: candidate };
}

const ECMASCRIPT_VERSION = "ECMAScript 2025 syntax / current browser runtime";

export const ECMASCRIPT_FLAVOUR: RegexFlavourDefinition = {
  id: "ecmascript",
  label: "JavaScript (ECMAScript)",
  versions: [
    {
      value: ECMASCRIPT_VERSION,
      label: "2025 syntax · current browser",
      syntaxVersion: "2025",
      legacyValues: ["ECMAScript 2025", "2025"],
    },
  ],
  defaultVersion: ECMASCRIPT_VERSION,
  flags: [
    { value: "g", label: "Global" },
    { value: "i", label: "Ignore case" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Dot all" },
    { value: "u", label: "Unicode" },
    { value: "v", label: "Unicode sets" },
    { value: "y", label: "Sticky" },
    { value: "d", label: "Expose indices" },
  ],
  defaultFlags: ["g", "u"],
  mutuallyExclusiveFlags: [["u", "v"]],
  options: [],
};

export const PCRE2_FLAVOUR: RegexFlavourDefinition = {
  id: "pcre2",
  label: "PCRE2",
  versions: [
    {
      value: "PCRE2 10.47 8-bit WebAssembly",
      label: "10.47 · 8-bit WebAssembly",
      syntaxVersion: "10.47",
      legacyValues: ["PCRE2 10.47", "10.47"],
    },
  ],
  defaultVersion: "PCRE2 10.47 8-bit WebAssembly",
  flags: [
    { value: "g", label: "Global" },
    { value: "i", label: "Caseless" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Dot all" },
    { value: "x", label: "Extended" },
    { value: "U", label: "Ungreedy" },
    { value: "J", label: "Duplicate names" },
  ],
  defaultFlags: ["g"],
  options: [
    {
      name: "matchLimit",
      label: "Match steps",
      kind: "integer",
      defaultValue: 1_000_000,
      minimum: 1,
      maximum: 100_000_000,
    },
    {
      name: "depthLimit",
      label: "Depth",
      kind: "integer",
      defaultValue: 1_000,
      minimum: 1,
      maximum: 100_000,
    },
    {
      name: "heapLimitKib",
      label: "Heap KiB",
      kind: "integer",
      defaultValue: 32_768,
      minimum: 1,
      maximum: 131_072,
    },
  ],
};

export const PYTHON_FLAVOUR: RegexFlavourDefinition = {
  id: "python",
  label: "Python re",
  versions: [
    {
      value: "CPython 3.14 via Pyodide 314.0.3",
      label: "CPython 3.14 · Pyodide 314.0.3",
      syntaxVersion: "Python 3.14 re",
      legacyValues: ["Python 3.14", "Pyodide 314.0.3"],
    },
  ],
  defaultVersion: "CPython 3.14 via Pyodide 314.0.3",
  flags: [
    { value: "g", label: "Global" },
    { value: "a", label: "ASCII character classes" },
    { value: "i", label: "Ignore case" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Dot all" },
    { value: "x", label: "Verbose" },
  ],
  defaultFlags: ["g"],
  options: [],
};

export const JAVA_FLAVOUR: RegexFlavourDefinition = {
  id: "java",
  label: "Java",
  versions: [
    {
      value: "TeaVM java.util.regex 0.15.0",
      label: "java.util.regex · TeaVM 0.15.0",
      syntaxVersion: "Java Pattern / TeaVM 0.15.0",
      legacyValues: ["Java Pattern", "TeaVM 0.15.0"],
    },
  ],
  defaultVersion: "TeaVM java.util.regex 0.15.0",
  flags: [
    { value: "g", label: "Global" },
    { value: "i", label: "Case insensitive" },
    { value: "d", label: "Unix lines" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Dot all" },
    { value: "u", label: "Unicode case" },
    { value: "x", label: "Comments" },
    { value: "U", label: "Unicode character classes" },
  ],
  defaultFlags: ["g"],
  options: [],
};

export const PHP_FLAVOUR: RegexFlavourDefinition = {
  id: "php",
  label: "PHP (preg / PCRE2)",
  versions: [
    {
      value: "PHP 8.5.8 preg / PCRE2 10.44",
      label: "PHP 8.5.8 preg · PCRE2 10.44",
      syntaxVersion: "PHP 8.5.8 preg / PCRE2 10.44",
    },
  ],
  defaultVersion: "PHP 8.5.8 preg / PCRE2 10.44",
  flags: [
    { value: "g", label: "Find all (application iteration)" },
    { value: "i", label: "Caseless" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Dot all" },
    { value: "x", label: "Extended" },
    { value: "u", label: "UTF-8 mode" },
    { value: "U", label: "Ungreedy" },
    { value: "A", label: "Anchored" },
    { value: "D", label: "Dollar end only" },
    { value: "J", label: "Duplicate names" },
    { value: "n", label: "No automatic captures" },
    { value: "r", label: "Restricted caseless" },
  ],
  defaultFlags: ["g", "u"],
  options: [],
};

export const PERL_FLAVOUR: RegexFlavourDefinition = {
  id: "perl",
  label: "Perl (legacy WebPerl)",
  versions: [
    {
      value: "Perl 5.28.1 via WebPerl 0.09-beta",
      label: "Perl 5.28.1 · WebPerl 0.09-beta · legacy",
      syntaxVersion: "Perl 5.28.1 regex",
    },
  ],
  defaultVersion: "Perl 5.28.1 via WebPerl 0.09-beta",
  flags: [
    { value: "g", label: "Global" },
    { value: "i", label: "Ignore case" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Single line" },
    { value: "x", label: "Extended" },
    { value: "n", label: "No capture" },
    { value: "a", label: "ASCII restrictions" },
    { value: "d", label: "Default character-set rules" },
    { value: "l", label: "Locale rules" },
    { value: "u", label: "Unicode rules" },
  ],
  defaultFlags: ["g", "u"],
  mutuallyExclusiveFlags: [["a", "d", "l", "u"]],
  options: [],
};

export const RUBY_FLAVOUR: RegexFlavourDefinition = {
  id: "ruby",
  label: "Ruby",
  versions: [
    {
      value: "CRuby 4.0.0 WebAssembly",
      label: "CRuby 4.0.0 · WebAssembly",
      syntaxVersion: "Ruby 4.0 Regexp",
    },
  ],
  defaultVersion: "CRuby 4.0.0 WebAssembly",
  flags: [
    { value: "g", label: "Find all (application iteration)" },
    { value: "i", label: "Ignore case" },
    { value: "m", label: "Multiline / dot all" },
    { value: "x", label: "Extended" },
  ],
  defaultFlags: ["g"],
  options: [],
};

export const CPP_FLAVOUR: RegexFlavourDefinition = {
  id: "cpp",
  label: "C++ std::regex",
  versions: [
    {
      value: "C++ std::regex · Emscripten 6.0.4 libc++",
      label: "C++ std::regex · Emscripten 6.0.4 libc++",
      syntaxVersion: "C++ ECMAScript regex grammar",
    },
  ],
  defaultVersion: "C++ std::regex · Emscripten 6.0.4 libc++",
  flags: [
    { value: "g", label: "Find all (application iteration)" },
    { value: "i", label: "Ignore case" },
    { value: "n", label: "Suppress submatches (nosubs)" },
    { value: "o", label: "Optimize matching" },
    { value: "c", label: "Locale-sensitive collation" },
  ],
  defaultFlags: ["g"],
  options: [
    {
      name: "grammar",
      label: "Grammar",
      kind: "string",
      defaultValue: "ECMAScript",
      maximumLength: 10,
      values: ["ECMAScript", "basic", "extended", "awk", "grep", "egrep"],
    },
  ],
};

export const GO_FLAVOUR: RegexFlavourDefinition = {
  id: "go",
  label: "Go regexp",
  versions: [
    {
      value: "Go 1.26.5 standard-library regexp",
      label: "Go 1.26.5 · standard-library regexp",
      syntaxVersion: "Go 1.26 regexp/syntax",
    },
  ],
  defaultVersion: "Go 1.26.5 standard-library regexp",
  flags: [
    { value: "g", label: "Find all (application iteration)" },
    { value: "i", label: "Case insensitive" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Dot all" },
    { value: "U", label: "Ungreedy" },
  ],
  defaultFlags: ["g"],
  options: [],
};

export const RUST_FLAVOUR: RegexFlavourDefinition = {
  id: "rust",
  label: "Rust regex",
  versions: [
    {
      value: "Rust regex 1.13.1 WebAssembly",
      label: "regex 1.13.1 · WebAssembly",
      syntaxVersion: "Rust regex 1.13.1",
    },
  ],
  defaultVersion: "Rust regex 1.13.1 WebAssembly",
  flags: [
    { value: "g", label: "Find all (application iteration)" },
    { value: "i", label: "Case insensitive" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Dot all" },
    { value: "U", label: "Swap greediness" },
    { value: "u", label: "Unicode" },
    { value: "x", label: "Verbose" },
    { value: "R", label: "CRLF mode" },
  ],
  defaultFlags: ["g", "u"],
  options: [],
};

export const DOTNET_FLAVOUR: RegexFlavourDefinition = {
  id: "dotnet",
  label: ".NET Regex",
  versions: [
    {
      value: ".NET 10 System.Text.RegularExpressions",
      label: ".NET 10 · System.Text.RegularExpressions",
      syntaxVersion: ".NET 10 regex",
    },
  ],
  defaultVersion: ".NET 10 System.Text.RegularExpressions",
  flags: [
    { value: "g", label: "Find all (application iteration)" },
    { value: "i", label: "Ignore case" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Single line" },
    { value: "n", label: "Explicit capture" },
    { value: "x", label: "Ignore pattern whitespace" },
    { value: "r", label: "Right to left" },
    { value: "c", label: "Culture invariant" },
    { value: "b", label: "Non-backtracking" },
  ],
  defaultFlags: ["g"],
  options: [],
};

export const SCALA_FLAVOUR: RegexFlavourDefinition = {
  id: "scala",
  label: "Scala Regex (Java compatibility)",
  versions: [
    {
      value: "Scala/JVM regex syntax via TeaVM java.util.regex 0.15.0",
      label: "Scala/JVM syntax · TeaVM java.util.regex 0.15.0",
      syntaxVersion: "Scala Regex / Java Pattern compatibility",
    },
  ],
  defaultVersion: "Scala/JVM regex syntax via TeaVM java.util.regex 0.15.0",
  flags: [
    { value: "g", label: "Find all (application iteration)" },
    { value: "i", label: "Case insensitive" },
    { value: "d", label: "Unix lines" },
    { value: "m", label: "Multiline" },
    { value: "s", label: "Dot all" },
    { value: "u", label: "Unicode case" },
    { value: "x", label: "Comments" },
    { value: "U", label: "Unicode character-class compatibility" },
  ],
  defaultFlags: ["g"],
  options: [],
};

export const AVAILABLE_REGEX_FLAVOURS = new RegexFlavourRegistry([
  ECMASCRIPT_FLAVOUR,
  PCRE2_FLAVOUR,
  PHP_FLAVOUR,
  PERL_FLAVOUR,
  PYTHON_FLAVOUR,
  RUBY_FLAVOUR,
  JAVA_FLAVOUR,
  CPP_FLAVOUR,
  GO_FLAVOUR,
  DOTNET_FLAVOUR,
  RUST_FLAVOUR,
  SCALA_FLAVOUR,
]);
