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

interface NamedCapture {
  readonly name: string;
  readonly end: number;
}

export interface LexicalCaptureDialect {
  readonly flavour: RegexFlavourId;
  readonly providerId: string;
  readonly providerVersion: string;
  readonly engineLabel: string;
  readonly extendedFlag: string;
  readonly quotedLiterals: boolean;
  readonly captureDiscovery?: "parenthesis" | "unavailable";
  readonly namedCapture: (
    pattern: string,
    opener: number,
  ) => NamedCapture | undefined;
  readonly unsupportedConstructs: readonly string[];
}

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

function scanCaptureOpeners(
  pattern: string,
  dialect: LexicalCaptureDialect,
  extended: boolean,
): readonly CaptureOpener[] {
  if (dialect.captureDiscovery === "unavailable") return [];
  const openers: CaptureOpener[] = [];
  let escaped = false;
  let characterClass = false;
  let quoted = 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 (dialect.quotedLiterals && pattern[index + 1] === "Q") {
        quoted = true;
        index += 1;
      } else {
        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 commentEscaped = false;
      while (commentEnd < pattern.length) {
        const character = pattern[commentEnd];
        if (!commentEscaped && character === ")") break;
        if (!commentEscaped && character === "\\") {
          commentEscaped = true;
        } else {
          commentEscaped = false;
        }
        commentEnd += 1;
      }
      index = commentEnd;
      continue;
    }
    if (pattern[index + 1] !== "?") {
      openers.push({ start: index, end: index + 1 });
      continue;
    }
    const named = dialect.namedCapture(pattern, index);
    if (named) {
      openers.push({
        start: index,
        end: named.end,
        name: named.name,
      });
    }
  }
  return openers;
}

function construct(
  pattern: string,
  opener: CaptureOpener,
  number: number,
  dialect: LexicalCaptureDialect,
): {
  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: `${dialect.flavour}-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: dialect.flavour,
        status: "partial",
        notes: [
          `The lexical provider identifies this opener; ${dialect.engineLabel} compilation is authoritative.`,
        ],
      },
      provenance: {
        provider: dialect.providerId,
        providerVersion: dialect.providerVersion,
        source: "parsed",
      },
    },
    token: {
      id: `${dialect.flavour}-token-${number}`,
      kind,
      range,
      raw,
      captureNumber: number,
    },
    capture: {
      number,
      ...(opener.name ? { name: opener.name } : {}),
      range,
      repeated: false,
    },
  };
}

function coverage(dialect: LexicalCaptureDialect): SyntaxCoverage {
  return {
    status: "partial",
    summary: `Application-owned lexical capture structure; ${dialect.engineLabel} is authoritative for syntax acceptance and matching.`,
    unsupportedConstructs: dialect.unsupportedConstructs,
  };
}

function rootNode(
  pattern: string,
  children: readonly NormalizedRegexNode[],
  dialect: LexicalCaptureDialect,
): NormalizedRegexNode {
  const providerCoverage = coverage(dialect);
  return {
    id: `${dialect.flavour}-pattern-root`,
    kind: "pattern",
    range: { startUtf16: 0, endUtf16: pattern.length },
    raw: pattern,
    explanation: `${dialect.engineLabel} pattern; the runtime compiler decides final acceptance`,
    children,
    properties: {
      zeroWidth: false,
      nullable: "unknown",
      consumesInput: "conditional",
    },
    support: {
      flavour: dialect.flavour,
      status: "partial",
      notes: [providerCoverage.summary],
    },
    provenance: {
      provider: dialect.providerId,
      providerVersion: dialect.providerVersion,
      source: "parsed",
    },
  };
}

export function parseLexicalPattern(
  request: RegexSyntaxRequest,
  dialect: LexicalCaptureDialect,
): RegexSyntaxResult {
  if (request.flavour !== dialect.flavour) {
    throw new Error(`Unsupported syntax flavour: ${request.flavour}`);
  }
  const providerCoverage = coverage(dialect);
  if (request.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
    const diagnostics: RegexDiagnostic[] = [
      {
        id: `${dialect.flavour}-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: dialect.flavour,
        provenance: "derived",
      },
    ];
    return {
      accepted: false,
      root: rootNode(request.pattern, [], dialect),
      tokens: [],
      captures: [],
      diagnostics,
      provider: {
        id: dialect.providerId,
        version: dialect.providerVersion,
      },
      coverage: providerCoverage,
    };
  }
  const constructs = scanCaptureOpeners(
    request.pattern,
    dialect,
    request.flags.includes(dialect.extendedFlag),
  ).map((opener, index) =>
    construct(request.pattern, opener, index + 1, dialect),
  );
  return {
    accepted: true,
    root: rootNode(
      request.pattern,
      constructs.map((entry) => entry.node),
      dialect,
    ),
    tokens: constructs.map((entry) => entry.token),
    captures: constructs.map((entry) => entry.capture),
    diagnostics: [],
    provider: {
      id: dialect.providerId,
      version: dialect.providerVersion,
    },
    coverage: providerCoverage,
  };
}

export function lexicalCoverage(
  flavour: RegexFlavourId,
  dialect: LexicalCaptureDialect,
): SyntaxCoverage {
  return flavour === dialect.flavour
    ? coverage(dialect)
    : {
        status: "unavailable",
        summary: `${dialect.engineLabel} lexical provider does not parse ${flavour}.`,
        unsupportedConstructs: ["all"],
      };
}
