import { DEFAULT_REGEX_LIMITS } from "../../../execution/request-limits";
import type { RegexFlavourId } from "../../../model/flavour";
import type { RegexDiagnostic } from "../../../model/diagnostics";
import type {
  CaptureDefinition,
  NormalizedRegexNode,
  NormalizedRegexToken,
  RegexNodeKind,
  RegexSyntaxRequest,
  RegexSyntaxResult,
  ReplacementSyntaxResult,
  SyntaxCoverage,
} from "../../../model/syntax";
import type {
  RegexSyntaxProvider,
  ReplacementSyntaxRequest,
} from "../../SyntaxProvider";
import { parsePcre2Replacement } from "./replacement";

const PROVIDER_ID = "regex-tools-pcre2-lexical";
const PROVIDER_VERSION = "0.2.0";
const COVERAGE: SyntaxCoverage = {
  status: "partial",
  summary:
    "Application-owned PCRE2 lexical structure and common capture forms; the bundled PCRE2 10.47 compiler is authoritative for syntax acceptance.",
  unsupportedConstructs: [
    "complete structural nesting for branch-reset and conditional groups",
    "compile-time option changes in normalized explanations",
    "callout and control-verb argument structure",
    "exact group numbering after branch-reset groups",
  ],
};

interface CaptureOpener {
  readonly start: number;
  readonly end: number;
  readonly name?: string;
}

function captureOpeners(
  pattern: string,
  extended: boolean,
): {
  readonly openers: readonly CaptureOpener[];
  readonly branchReset: boolean;
} {
  const openers: CaptureOpener[] = [];
  let escaped = false;
  let characterClass = false;
  let quoted = false;
  let branchReset = false;
  for (let index = 0; index < pattern.length; index += 1) {
    const current = pattern[index];
    if (quoted) {
      if (current === "\\" && pattern[index + 1] === "E") {
        quoted = false;
        index += 1;
      }
      continue;
    }
    if (escaped) {
      escaped = false;
      continue;
    }
    if (current === "\\") {
      if (pattern[index + 1] === "Q") {
        quoted = true;
        index += 1;
        continue;
      }
      escaped = true;
      continue;
    }
    if (current === "[" && !characterClass) {
      characterClass = true;
      continue;
    }
    if (current === "]" && characterClass) {
      characterClass = false;
      continue;
    }
    if (extended && current === "#" && !characterClass) {
      const lineEnd = pattern.indexOf("\n", index + 1);
      if (lineEnd < 0) break;
      index = lineEnd;
      continue;
    }
    if (characterClass || current !== "(") continue;
    if (pattern.startsWith("(?#", index)) {
      let commentEnd = index + 3;
      let commentEscape = false;
      while (commentEnd < pattern.length) {
        const commentCharacter = pattern[commentEnd];
        if (!commentEscape && commentCharacter === ")") break;
        if (!commentEscape && commentCharacter === "\\") {
          commentEscape = true;
        } else {
          commentEscape = false;
        }
        commentEnd += 1;
      }
      index = commentEnd;
      continue;
    }
    if (pattern[index + 1] !== "?") {
      openers.push({ start: index, end: index + 1 });
      continue;
    }
    if (pattern.startsWith("(?|", index)) {
      branchReset = true;
      continue;
    }
    const angleName = /^\(\?<([A-Za-z_][A-Za-z0-9_]*)>/u.exec(
      pattern.slice(index),
    );
    if (angleName?.[1]) {
      openers.push({
        start: index,
        end: index + angleName[0].length,
        name: angleName[1],
      });
      continue;
    }
    const quoteName = /^\(\?'([A-Za-z_][A-Za-z0-9_]*)'/u.exec(
      pattern.slice(index),
    );
    if (quoteName?.[1]) {
      openers.push({
        start: index,
        end: index + quoteName[0].length,
        name: quoteName[1],
      });
      continue;
    }
    const pythonName = /^\(\?P<([A-Za-z_][A-Za-z0-9_]*)>/u.exec(
      pattern.slice(index),
    );
    if (pythonName?.[1]) {
      openers.push({
        start: index,
        end: index + pythonName[0].length,
        name: pythonName[1],
      });
    }
  }
  return { openers, branchReset };
}

function construct(
  pattern: string,
  opener: CaptureOpener,
  number: number,
): {
  readonly node: NormalizedRegexNode;
  readonly token: NormalizedRegexToken;
  readonly capture: CaptureDefinition;
} {
  const kind: RegexNodeKind = opener.name
    ? "named-capture-group"
    : "capture-group";
  const range = { startUtf16: opener.start, endUtf16: opener.end };
  const raw = pattern.slice(opener.start, opener.end);
  return {
    node: {
      id: `pcre2-capture-${number}`,
      kind,
      range,
      raw,
      explanation: opener.name
        ? `Open capture group ${number} named “${opener.name}”`
        : `Open capture group ${number}`,
      children: [],
      capture: { number, ...(opener.name ? { name: opener.name } : {}) },
      properties: {
        zeroWidth: false,
        nullable: "unknown",
        consumesInput: "conditional",
      },
      support: {
        flavour: "pcre2",
        status: "partial",
        notes: [
          "The lexical provider identifies the opener; PCRE2 compilation is authoritative.",
        ],
      },
      provenance: {
        provider: PROVIDER_ID,
        providerVersion: PROVIDER_VERSION,
        source: "parsed",
      },
    },
    token: {
      id: `pcre2-token-${number}`,
      kind,
      range,
      raw,
      captureNumber: number,
    },
    capture: {
      number,
      ...(opener.name ? { name: opener.name } : {}),
      range,
      repeated: false,
    },
  };
}

function rootNode(
  pattern: string,
  children: readonly NormalizedRegexNode[],
): NormalizedRegexNode {
  return {
    id: "pcre2-pattern-root",
    kind: "pattern",
    range: { startUtf16: 0, endUtf16: pattern.length },
    raw: pattern,
    explanation:
      "PCRE2 pattern; the bundled PCRE2 compiler decides final acceptance",
    children,
    properties: {
      zeroWidth: false,
      nullable: "unknown",
      consumesInput: "conditional",
    },
    support: {
      flavour: "pcre2",
      status: "partial",
      notes: [COVERAGE.summary],
    },
    provenance: {
      provider: PROVIDER_ID,
      providerVersion: PROVIDER_VERSION,
      source: "parsed",
    },
  };
}

export class Pcre2SyntaxProvider implements RegexSyntaxProvider {
  readonly id = PROVIDER_ID;
  readonly version = PROVIDER_VERSION;
  readonly supportedFlavours = ["pcre2"] as const;

  async parsePattern(request: RegexSyntaxRequest): Promise<RegexSyntaxResult> {
    if (request.flavour !== "pcre2") {
      throw new Error(`Unsupported syntax flavour: ${request.flavour}`);
    }
    if (request.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
      return {
        accepted: false,
        root: rootNode(request.pattern, []),
        tokens: [],
        captures: [],
        diagnostics: [
          {
            id: "pcre2-pattern-input-limit",
            source: "syntax-provider",
            severity: "error",
            code: "pattern-limit",
            message: `Pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
            flavour: "pcre2",
            provenance: "derived",
          },
        ],
        provider: { id: this.id, version: this.version },
        coverage: COVERAGE,
      };
    }
    const scanned = captureOpeners(
      request.pattern,
      request.flags.includes("x"),
    );
    const diagnostics: RegexDiagnostic[] = [];
    const constructs = scanned.branchReset
      ? []
      : scanned.openers.map((opener, index) =>
          construct(request.pattern, opener, index + 1),
        );
    if (scanned.branchReset) {
      diagnostics.push({
        id: "pcre2-branch-reset-numbering",
        source: "syntax-provider",
        severity: "information",
        code: "partial-syntax-coverage",
        message:
          "Branch-reset grouping is executed by PCRE2, but capture numbering is omitted from the partial explanation until a complete structural provider is available.",
        flavour: "pcre2",
        provenance: "derived",
      });
    }
    return {
      accepted: true,
      root: rootNode(
        request.pattern,
        constructs.map((entry) => entry.node),
      ),
      tokens: constructs.map((entry) => entry.token),
      captures: constructs.map((entry) => entry.capture),
      diagnostics,
      provider: { id: this.id, version: this.version },
      coverage: COVERAGE,
    };
  }

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

  getCoverage(flavour: RegexFlavourId): SyntaxCoverage {
    return flavour === "pcre2"
      ? COVERAGE
      : {
          status: "unavailable",
          summary: `The PCRE2 lexical provider does not parse ${flavour}.`,
          unsupportedConstructs: ["all"],
        };
  }
}
