import { DEFAULT_REGEX_LIMITS } from "../../../execution/request-limits";
import type { RegexDiagnostic } from "../../../model/diagnostics";
import type { RegexFlavourId } from "../../../model/flavour";
import type {
  CaptureDefinition,
  RegexSyntaxRequest,
  RegexSyntaxResult,
  ReplacementSyntaxResult,
  ReplacementToken,
  SyntaxCoverage,
} from "../../../model/syntax";
import type {
  RegexSyntaxProvider,
  ReplacementSyntaxRequest,
} from "../../SyntaxProvider";
import {
  lexicalCoverage,
  parseLexicalPattern,
  type LexicalCaptureDialect,
} from "../lexical-captures";

export type CommunityFlavourId =
  "php" | "perl" | "ruby" | "cpp" | "go" | "rust" | "dotnet" | "scala";

interface ReplacementReference {
  readonly kind: ReplacementToken["kind"];
  readonly number?: number;
  readonly name?: string;
  readonly explanation: string;
  readonly compatibilityError?: string;
}

interface CommunityProviderConfiguration {
  readonly dialect: LexicalCaptureDialect;
  readonly replacementPattern: RegExp;
  readonly replacementReference: (raw: string) => ReplacementReference;
  readonly replacementCoverage: string;
}

function namedCapture(
  pattern: string,
  opener: number,
  expressions: readonly RegExp[],
): { readonly name: string; readonly end: number } | undefined {
  const source = pattern.slice(opener);
  for (const expression of expressions) {
    const match = expression.exec(source);
    const name = match?.slice(1).find((value) => value !== undefined);
    if (match && name) {
      return { name, end: opener + match[0].length };
    }
  }
  return undefined;
}

const ANGLE_NAME = /^\(\?<([A-Za-z_][A-Za-z0-9_]*)>/u;
const PYTHON_NAME = /^\(\?P<([A-Za-z_][A-Za-z0-9_]*)>/u;
const QUOTED_NAME = /^\(\?'([A-Za-z_][A-Za-z0-9_]*)'/u;
const JAVA_NAME = /^\(\?<([A-Za-z][A-Za-z0-9]*)>/u;

function numberedReference(
  raw: string,
  expression: RegExp,
): ReplacementReference | undefined {
  const number = expression.exec(raw)?.[1];
  if (number === undefined) return undefined;
  const captureNumber = Number.parseInt(number, 10);
  return {
    kind: captureNumber === 0 ? "full-match" : "numbered-capture",
    ...(captureNumber === 0 ? {} : { number: captureNumber }),
    explanation:
      captureNumber === 0
        ? "Insert the complete match"
        : `Insert numbered capture ${captureNumber}`,
  };
}

function namedReference(
  raw: string,
  expressions: readonly RegExp[],
): ReplacementReference | undefined {
  for (const expression of expressions) {
    const name = expression.exec(raw)?.[1];
    if (name) {
      return {
        kind: "named-capture",
        name,
        explanation: `Insert named capture “${name}”`,
      };
    }
  }
  return undefined;
}

function dollarReference(raw: string): ReplacementReference {
  if (raw === "$$") {
    return {
      kind: "escaped-dollar",
      explanation: "Insert one literal dollar sign",
    };
  }
  if (raw === "$&" || raw === "$0") {
    return { kind: "full-match", explanation: "Insert the complete match" };
  }
  if (raw === "$`") {
    return {
      kind: "prefix",
      explanation: "Insert the subject text before this match",
    };
  }
  if (raw === "$'") {
    return {
      kind: "suffix",
      explanation: "Insert the subject text after this match",
    };
  }
  if (raw === "$_") {
    return {
      kind: "subject",
      explanation: "Insert the complete input subject",
    };
  }
  if (raw === "$+") {
    return {
      kind: "highest-capture",
      explanation: "Insert the highest-numbered participating capture",
    };
  }
  return (
    numberedReference(raw, /^\$(\d+)$/u) ??
    namedReference(raw, [
      /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/u,
      /^\$([A-Za-z_][A-Za-z0-9_]*)$/u,
    ]) ?? {
      kind: "engine-special",
      explanation:
        "The selected runtime interprets this replacement reference; its output is authoritative",
    }
  );
}

const CONFIGURATIONS: Readonly<
  Record<CommunityFlavourId, CommunityProviderConfiguration>
> = {
  php: {
    dialect: {
      flavour: "php",
      providerId: "regex-tools-php-lexical",
      providerVersion: "community-lexical-1",
      engineLabel: "PHP 8.5.8 preg / PCRE2 10.44",
      extendedFlag: "x",
      quotedLiterals: true,
      namedCapture: (pattern, opener) =>
        namedCapture(pattern, opener, [PYTHON_NAME, ANGLE_NAME, QUOTED_NAME]),
      unsupportedConstructs: [
        "PHP pattern delimiters and delimiter escaping",
        "complete PCRE2 structural nesting and branch-reset numbering",
        "inline option scope, assertions, recursion and backreferences as structural nodes",
        "complete preg replacement-template semantics",
      ],
    },
    replacementPattern: /\\[0-9]+|\$\{[0-9]+\}|\$[0-9]+/gu,
    replacementReference: (raw) =>
      numberedReference(raw, /^(?:\\|\$\{?)(\d+)\}?$/u) ?? {
        kind: "engine-special",
        explanation:
          "PHP preg_replace interprets this replacement reference; runtime output is authoritative",
      },
    replacementCoverage:
      "Numbered preg_replace references are identified lexically; PHP remains authoritative.",
  },
  perl: {
    dialect: {
      flavour: "perl",
      providerId: "regex-tools-perl-lexical",
      providerVersion: "community-lexical-1",
      engineLabel: "legacy Perl 5.28.1 via WebPerl 0.09-beta",
      extendedFlag: "x",
      quotedLiterals: true,
      namedCapture: (pattern, opener) =>
        namedCapture(pattern, opener, [ANGLE_NAME, QUOTED_NAME, PYTHON_NAME]),
      unsupportedConstructs: [
        "complete Perl regex structural nesting",
        "embedded code, dynamic patterns and locale-sensitive behavior",
        "the two-character /aa modifier (not exposed by this worker protocol)",
        "branch-reset numbering, recursion and inline option scope",
        "complete interpolating substitution semantics",
      ],
    },
    replacementPattern: /\$\$|\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[0-9]+/gu,
    replacementReference: (raw) =>
      raw === "$$"
        ? {
            kind: "escaped-dollar",
            explanation: "Insert one literal dollar sign",
          }
        : (numberedReference(raw, /^\$(\d+)$/u) ??
          namedReference(raw, [/^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/u]) ?? {
            kind: "engine-special",
            explanation:
              "The fixed Perl bridge interprets this data-only replacement reference",
          }),
    replacementCoverage:
      "The fixed bridge subset ($$, greedy $n and ${name}) is identified; Perl replacement expressions are never evaluated.",
  },
  ruby: {
    dialect: {
      flavour: "ruby",
      providerId: "regex-tools-ruby-lexical",
      providerVersion: "community-lexical-1",
      engineLabel: "CRuby 4.0.0 Regexp",
      extendedFlag: "x",
      quotedLiterals: false,
      namedCapture: (pattern, opener) =>
        namedCapture(pattern, opener, [ANGLE_NAME, QUOTED_NAME]),
      unsupportedConstructs: [
        "complete Onigmo structural nesting and capture-level rules",
        "subexpression calls, absent operators and encoding-sensitive behavior",
        "inline option scope and assertions as structural nodes",
        "complete String#gsub replacement-string semantics",
      ],
    },
    replacementPattern: /\\k<[A-Za-z_][A-Za-z0-9_]*>|\\[0-9]+|\\[&`'+]/gu,
    replacementReference: (raw) => {
      if (raw === "\\&") {
        return { kind: "full-match", explanation: "Insert the complete match" };
      }
      if (raw === "\\`") {
        return {
          kind: "prefix",
          explanation: "Insert the subject text before this match",
        };
      }
      if (raw === "\\'") {
        return {
          kind: "suffix",
          explanation: "Insert the subject text after this match",
        };
      }
      if (raw === "\\+") {
        return {
          kind: "highest-capture",
          explanation: "Insert the highest-numbered participating capture",
        };
      }
      return (
        numberedReference(raw, /^\\(\d+)$/u) ??
        namedReference(raw, [/^\\k<([A-Za-z_][A-Za-z0-9_]*)>$/u]) ?? {
          kind: "engine-special",
          explanation:
            "CRuby interprets this replacement reference; runtime output is authoritative",
        }
      );
    },
    replacementCoverage:
      "Common CRuby String#gsub references are identified lexically; CRuby remains authoritative.",
  },
  cpp: {
    dialect: {
      flavour: "cpp",
      providerId: "regex-tools-cpp-lexical",
      providerVersion: "community-lexical-1",
      engineLabel: "libc++ std::regex via Emscripten 6.0.4",
      extendedFlag: "__unavailable__",
      quotedLiterals: false,
      namedCapture: () => undefined,
      unsupportedConstructs: [
        "non-ECMAScript grammar capture discovery",
        "complete structural nesting and quantifier ownership",
        "locale and collating-element interpretation",
        "std::regex implementation-specific diagnostics",
      ],
    },
    replacementPattern: /\$\$|\$[&`']|\$[0-9]+/gu,
    replacementReference: dollarReference,
    replacementCoverage:
      "Common ECMAScript-format std::regex_replace references are identified lexically.",
  },
  go: {
    dialect: {
      flavour: "go",
      providerId: "regex-tools-go-lexical",
      providerVersion: "community-lexical-1",
      engineLabel: "Go 1.26.5 standard-library regexp",
      extendedFlag: "__unavailable__",
      quotedLiterals: false,
      namedCapture: (pattern, opener) =>
        namedCapture(pattern, opener, [PYTHON_NAME, ANGLE_NAME]),
      unsupportedConstructs: [
        "complete RE2 structural nesting and repetition validation",
        "inline flag scope and character classes as structural nodes",
        "authoritative capture-name validation",
        "complete Expand replacement-name disambiguation",
      ],
    },
    replacementPattern:
      /\$\$|\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*|\$[0-9]+/gu,
    replacementReference: dollarReference,
    replacementCoverage:
      "Common regexp.Expand references are identified lexically; Go remains authoritative.",
  },
  rust: {
    dialect: {
      flavour: "rust",
      providerId: "regex-tools-rust-lexical",
      providerVersion: "community-lexical-1",
      engineLabel: "Rust regex 1.13.1",
      extendedFlag: "x",
      quotedLiterals: false,
      namedCapture: (pattern, opener) =>
        namedCapture(pattern, opener, [ANGLE_NAME, PYTHON_NAME]),
      unsupportedConstructs: [
        "complete regex crate structural nesting and Unicode-set semantics",
        "inline flag scope and character classes as structural nodes",
        "authoritative capture-name validation",
        "complete replacement-name disambiguation",
      ],
    },
    replacementPattern:
      /\$\$|\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[A-Za-z_][A-Za-z0-9_]*|\$[0-9]+/gu,
    replacementReference: dollarReference,
    replacementCoverage:
      "Common Captures::expand references are identified lexically; the Rust regex crate remains authoritative.",
  },
  dotnet: {
    dialect: {
      flavour: "dotnet",
      providerId: "regex-tools-dotnet-lexical",
      providerVersion: "community-lexical-1",
      engineLabel: ".NET 10 System.Text.RegularExpressions",
      extendedFlag: "x",
      quotedLiterals: false,
      namedCapture: (pattern, opener) =>
        namedCapture(pattern, opener, [ANGLE_NAME, QUOTED_NAME]),
      unsupportedConstructs: [
        "complete .NET structural nesting and capture-history ownership",
        "balancing-group semantics and capture subtraction",
        "inline option scope, conditionals and right-to-left behavior",
        "complete substitution-language validation",
      ],
    },
    replacementPattern:
      /\$\$|\$[&`'_+]|\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$[0-9]+/gu,
    replacementReference: dollarReference,
    replacementCoverage:
      "Common .NET substitutions are identified lexically; System.Text.RegularExpressions remains authoritative.",
  },
  scala: {
    dialect: {
      flavour: "scala",
      providerId: "regex-tools-scala-java-lexical",
      providerVersion: "community-lexical-1",
      engineLabel: "Scala/JVM regex syntax via TeaVM java.util.regex 0.15.0",
      extendedFlag: "x",
      quotedLiterals: true,
      namedCapture: (pattern, opener) =>
        namedCapture(pattern, opener, [JAVA_NAME]),
      unsupportedConstructs: [
        "Scala Regex wrapper APIs and extractors",
        "complete Java Pattern structural nesting",
        "inline flag scope, intersections and properties as structural nodes",
        "OpenJDK-only behavior absent from TeaVM 0.15.0",
      ],
    },
    replacementPattern: /\\.|\$\{[^}]*\}|\$[0-9]/gu,
    replacementReference: (raw) => {
      const named = namedReference(raw, [/^\$\{([A-Za-z][A-Za-z0-9]*)\}$/u]);
      if (named) {
        return {
          ...named,
          compatibilityError:
            "The shared TeaVM 0.15.0 Matcher runtime does not implement Java/Scala ${name} replacement references.",
        };
      }
      return (
        numberedReference(raw, /^\$([0-9])$/u) ?? {
          kind: "engine-special",
          explanation:
            "TeaVM Matcher interprets this quoted replacement character; runtime output is authoritative",
        }
      );
    },
    replacementCoverage:
      "TeaVM's single-digit Java Matcher replacement subset is identified; this is a Scala/JVM syntax compatibility profile.",
  },
};

function literalToken(
  flavour: CommunityFlavourId,
  id: number,
  raw: string,
  start: number,
): ReplacementToken {
  return {
    id: `${flavour}-replacement-${id}`,
    kind: "literal",
    raw,
    range: { startUtf16: start, endUtf16: start + raw.length },
    explanation: `Insert literal text ${JSON.stringify(raw.slice(0, 160))}${
      raw.length > 160 ? "…" : ""
    }`,
  };
}

function referenceToken(
  flavour: CommunityFlavourId,
  id: number,
  raw: string,
  start: number,
  reference: ReplacementReference,
): ReplacementToken {
  return {
    id: `${flavour}-replacement-${id}`,
    kind: reference.kind,
    raw,
    range: { startUtf16: start, endUtf16: start + raw.length },
    explanation: reference.explanation,
    ...(reference.number === undefined
      ? {}
      : { captureNumber: reference.number }),
    ...(reference.name === undefined ? {} : { captureName: reference.name }),
  };
}

function parseReplacement(
  flavour: CommunityFlavourId,
  replacement: string,
  captures: readonly CaptureDefinition[],
  configuration: CommunityProviderConfiguration,
): ReplacementSyntaxResult {
  if (
    replacement.length > DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
  ) {
    return {
      accepted: false,
      tokens: [],
      diagnostics: [
        {
          id: `${flavour}-replacement-template-input-limit`,
          source: "syntax-provider",
          severity: "error",
          code: "replacement-template-limit",
          message: `Replacement template exceeds the ${DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16.toLocaleString()} UTF-16 unit limit; it was not parsed or executed.`,
          flavour,
          provenance: "derived",
        },
      ],
      totalDiagnostics: 1,
      diagnosticsTruncated: false,
    };
  }
  const numbers = new Set(captures.map((capture) => capture.number));
  const names = new Set(
    captures.flatMap((capture) => (capture.name ? [capture.name] : [])),
  );
  const tokens: ReplacementToken[] = [];
  const diagnostics: RegexDiagnostic[] = [];
  let cursor = 0;
  for (const match of replacement.matchAll(configuration.replacementPattern)) {
    const start = match.index;
    const raw = match[0];
    if (start > cursor) {
      tokens.push(
        literalToken(
          flavour,
          tokens.length,
          replacement.slice(cursor, start),
          cursor,
        ),
      );
    }
    const reference = configuration.replacementReference(raw);
    const token = referenceToken(flavour, tokens.length, raw, start, reference);
    tokens.push(token);
    if (reference.compatibilityError) {
      diagnostics.push({
        id: `${flavour}-replacement-compatibility-${start}`,
        source: "syntax-provider",
        severity: "error",
        code: "engine-compatibility",
        message: reference.compatibilityError,
        range: token.range,
        flavour,
        provenance: "reported",
      });
    } else if (
      reference.number !== undefined &&
      reference.number > 0 &&
      !numbers.has(reference.number)
    ) {
      diagnostics.push({
        id: `${flavour}-replacement-missing-number-${start}`,
        source: "syntax-provider",
        severity: "warning",
        code: "replacement-unknown-capture",
        message: `Replacement references capture ${reference.number}, which the partial lexical provider did not find. The selected runtime remains authoritative.`,
        range: token.range,
        flavour,
        provenance: "derived",
      });
    } else if (
      reference.name !== undefined &&
      names.size > 0 &&
      !names.has(reference.name)
    ) {
      diagnostics.push({
        id: `${flavour}-replacement-missing-name-${start}`,
        source: "syntax-provider",
        severity: "warning",
        code: "replacement-unknown-capture",
        message: `Replacement references unknown named capture “${reference.name}”.`,
        range: token.range,
        flavour,
        provenance: "derived",
      });
    }
    cursor = start + raw.length;
  }
  if (cursor < replacement.length) {
    tokens.push(
      literalToken(flavour, tokens.length, replacement.slice(cursor), cursor),
    );
  }
  diagnostics.push({
    id: `${flavour}-replacement-partial-coverage`,
    source: "syntax-provider",
    severity: "information",
    code: "replacement-preview-partial",
    message: configuration.replacementCoverage,
    flavour,
    provenance: "reported",
  });
  return {
    accepted: !diagnostics.some(
      (diagnostic) => diagnostic.severity === "error",
    ),
    tokens,
    diagnostics,
    totalDiagnostics: diagnostics.length,
    diagnosticsTruncated: false,
  };
}

export class CommunitySyntaxProvider implements RegexSyntaxProvider {
  readonly id: string;
  readonly version: string;
  readonly supportedFlavours: readonly CommunityFlavourId[];
  readonly #flavour: CommunityFlavourId;
  readonly #configuration: CommunityProviderConfiguration;

  constructor(flavour: CommunityFlavourId) {
    this.#flavour = flavour;
    this.#configuration = CONFIGURATIONS[flavour];
    this.id = this.#configuration.dialect.providerId;
    this.version = this.#configuration.dialect.providerVersion;
    this.supportedFlavours = [flavour];
  }

  async parsePattern(request: RegexSyntaxRequest): Promise<RegexSyntaxResult> {
    const grammar = request.options.grammar;
    const captureDiscovery =
      this.#flavour === "cpp" &&
      (grammar !== "ECMAScript" || request.flags.includes("n"))
        ? "unavailable"
        : "parenthesis";
    return parseLexicalPattern(request, {
      ...this.#configuration.dialect,
      captureDiscovery,
    });
  }

  async parseReplacement(
    request: ReplacementSyntaxRequest,
  ): Promise<ReplacementSyntaxResult> {
    if (request.flavour !== this.#flavour) {
      throw new Error(`Unsupported replacement flavour: ${request.flavour}`);
    }
    return parseReplacement(
      this.#flavour,
      request.replacement,
      request.captureMetadata,
      this.#configuration,
    );
  }

  getCoverage(flavour: RegexFlavourId): SyntaxCoverage {
    return lexicalCoverage(flavour, this.#configuration.dialect);
  }
}

export const COMMUNITY_SYNTAX_PROVIDERS: Readonly<
  Record<CommunityFlavourId, RegexSyntaxProvider>
> = {
  php: new CommunitySyntaxProvider("php"),
  perl: new CommunitySyntaxProvider("perl"),
  ruby: new CommunitySyntaxProvider("ruby"),
  cpp: new CommunitySyntaxProvider("cpp"),
  go: new CommunitySyntaxProvider("go"),
  rust: new CommunitySyntaxProvider("rust"),
  dotnet: new CommunitySyntaxProvider("dotnet"),
  scala: new CommunitySyntaxProvider("scala"),
};
