import type { RegexProjectV1, WorkspaceMode } from "./project.types";
import type {
  RegexTestCase,
  RegexTestExpectation,
} from "../regex/tests/test-case.types";
import {
  MAXIMUM_REGEX_TEST_CAPTURE_NAME_UTF16,
  MAXIMUM_REGEX_TEST_EXPECTED_UTF16,
  MAXIMUM_REGEX_TEST_NAME_UTF16,
  MAXIMUM_REGEX_TEST_PATTERN_UTF16,
  MAXIMUM_REGEX_TEST_SUBJECT_UTF16,
  MAXIMUM_REGEX_TESTS,
} from "../regex/tests/test-case.types";
import { parseListTemplate } from "../regex/list/list-template";
import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../regex/execution/request-limits";
import { assertProjectDocumentWithinLimit } from "./project-limits";
import {
  AVAILABLE_REGEX_FLAVOURS,
  parseRegexFlags,
  parseRegexOptions,
  resolveRegexFlavourVersion,
  type RegexFlavourRegistry,
} from "../regex/flavours/flavour-registry";
import { parseGeneratedCaseProvenance } from "../regex/generation/generation-provenance";

const IMPLEMENTED_MODES = new Set<WorkspaceMode>([
  "match",
  "replace",
  "list",
  "corpus",
  "tests",
]);
const MAXIMUM_PATTERN_UTF16 = MAXIMUM_REGEX_TEST_PATTERN_UTF16;
const MAXIMUM_SUBJECT_UTF16 = MAXIMUM_REGEX_TEST_SUBJECT_UTF16;

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

function stringValue(value: unknown, label: string, maximum: number): string {
  if (typeof value !== "string") throw new Error(`${label} must be text.`);
  if (value.length > maximum) {
    throw new Error(`${label} exceeds the ${maximum} UTF-16 unit limit.`);
  }
  return value;
}

function subjectValue(value: unknown, label: string): string {
  const subject = stringValue(value, label, MAXIMUM_SUBJECT_UTF16);
  if (
    utf8ByteLength(subject) > DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
  ) {
    throw new Error(
      `${label} exceeds the ${DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes.toLocaleString()} UTF-8 byte limit.`,
    );
  }
  return subject;
}

function parseTestResourceLimits(
  value: unknown,
  label: string,
): RegexTestCase["resourceLimits"] {
  if (value === undefined) return undefined;
  const limits = objectValue(value, label);
  const unsupported = Object.keys(limits).filter(
    (key) => key !== "manualExecutionTimeoutMs",
  );
  if (unsupported.length > 0) {
    throw new Error(
      `${label} contains unsupported entries: ${unsupported.join(", ")}.`,
    );
  }
  const timeout = limits.manualExecutionTimeoutMs;
  if (timeout === undefined) return {};
  if (
    typeof timeout !== "number" ||
    !Number.isSafeInteger(timeout) ||
    timeout < 1 ||
    timeout > 10_000
  ) {
    throw new Error(
      `${label} manualExecutionTimeoutMs must be an integer from 1 to 10000.`,
    );
  }
  return { manualExecutionTimeoutMs: timeout };
}

export function parseRegexTests(
  value: unknown,
  registry: RegexFlavourRegistry = AVAILABLE_REGEX_FLAVOURS,
): readonly RegexTestCase[] {
  if (!Array.isArray(value)) throw new Error("Project tests must be an array.");
  if (value.length > MAXIMUM_REGEX_TESTS) {
    throw new Error(`Project contains more than ${MAXIMUM_REGEX_TESTS} tests.`);
  }
  const testIds = new Set<string>();
  return value.map((candidate, index) => {
    const test = objectValue(candidate, `Test ${index + 1}`);
    const flavour = registry.parse(test.flavour, `Test ${index + 1} flavour`);
    const flags = parseRegexFlags(
      test.flags,
      flavour,
      `Test ${index + 1} flags`,
    );
    const options = parseRegexOptions(
      test.options,
      flavour,
      `Test ${index + 1} options`,
    );
    const flavourVersion = resolveRegexFlavourVersion(
      test.flavourVersion,
      flavour,
      `Test ${index + 1} flavour version`,
    ).value;
    const resourceLimits = parseTestResourceLimits(
      test.resourceLimits,
      `Test ${index + 1} resource limits`,
    );
    const expectation = objectValue(
      test.expectation,
      `Test ${index + 1} expectation`,
    );
    const kind = stringValue(
      expectation.kind,
      `Test ${index + 1} expectation`,
      32,
    );
    const integer = (
      candidate: unknown,
      label: string,
      maximum = 1_000_000,
    ) => {
      if (
        typeof candidate !== "number" ||
        !Number.isSafeInteger(candidate) ||
        candidate < 0 ||
        candidate > maximum
      ) {
        throw new Error(`${label} must be a bounded non-negative integer.`);
      }
      return candidate;
    };
    let parsedExpectation: RegexTestExpectation;
    switch (kind) {
      case "should-match":
      case "should-not-match":
        parsedExpectation = { kind };
        break;
      case "match-count":
        parsedExpectation = {
          kind,
          count: integer(expectation.count, "Expected match count"),
        };
        break;
      case "full-match":
        parsedExpectation = {
          kind,
          matchIndex: integer(expectation.matchIndex, "Match index"),
          value: stringValue(
            expectation.value,
            "Expected full match",
            MAXIMUM_REGEX_TEST_EXPECTED_UTF16,
          ),
        };
        break;
      case "capture": {
        const status = stringValue(
          expectation.status,
          "Expected capture status",
          32,
        );
        if (
          !["participated", "did-not-participate", "matched-empty"].includes(
            status,
          )
        ) {
          throw new Error("Expected capture status is unsupported.");
        }
        const groupNumber =
          expectation.groupNumber === undefined
            ? undefined
            : integer(expectation.groupNumber, "Capture number", 1_000);
        const groupName =
          expectation.groupName === undefined
            ? undefined
            : stringValue(
                expectation.groupName,
                "Capture name",
                MAXIMUM_REGEX_TEST_CAPTURE_NAME_UTF16,
              );
        if (groupNumber === undefined && groupName === undefined) {
          throw new Error("Capture expectation needs a group number or name.");
        }
        parsedExpectation = {
          kind,
          matchIndex: integer(expectation.matchIndex, "Match index"),
          ...(groupNumber === undefined ? {} : { groupNumber }),
          ...(groupName === undefined ? {} : { groupName }),
          status: status as
            "participated" | "did-not-participate" | "matched-empty",
          ...(expectation.value === undefined
            ? {}
            : {
                value: stringValue(
                  expectation.value,
                  "Expected capture value",
                  MAXIMUM_REGEX_TEST_EXPECTED_UTF16,
                ),
              }),
        };
        break;
      }
      case "replacement":
        parsedExpectation = {
          kind,
          expected: stringValue(
            expectation.expected,
            "Expected replacement",
            MAXIMUM_REGEX_TEST_EXPECTED_UTF16,
          ),
        };
        break;
      case "must-complete-within":
        parsedExpectation = {
          kind,
          milliseconds: integer(
            expectation.milliseconds,
            "Completion limit",
            10_000,
          ),
        };
        break;
      case "must-time-out":
        parsedExpectation = { kind };
        break;
      default:
        throw new Error(
          `Imported test ${index + 1} uses unsupported expectation ${kind}.`,
        );
    }
    const generation =
      test.generation === undefined
        ? undefined
        : parseGeneratedCaseProvenance(
            test.generation,
            `Test ${index + 1} generation`,
          );
    if (generation) {
      if (flavour.id !== "ecmascript") {
        throw new Error(
          `Test ${index + 1} generation provenance is supported only for ECMAScript generator v1.`,
        );
      }
      const expectedKind =
        generation.intendedOutcome === "match"
          ? "should-match"
          : "should-not-match";
      if (parsedExpectation.kind !== expectedKind) {
        throw new Error(
          `Test ${index + 1} generation intendedOutcome does not match its ${parsedExpectation.kind} expectation.`,
        );
      }
    }
    const id = stringValue(test.id, `Test ${index + 1} id`, 128);
    if (id.length === 0) {
      throw new Error(`Test ${index + 1} id must not be empty.`);
    }
    if (testIds.has(id)) {
      throw new Error(`Project test id ${JSON.stringify(id)} is duplicated.`);
    }
    testIds.add(id);
    return {
      id,
      name: stringValue(
        test.name,
        `Test ${index + 1} name`,
        MAXIMUM_REGEX_TEST_NAME_UTF16,
      ),
      enabled: test.enabled !== false,
      flavour: flavour.id,
      ...(test.flavourVersion === undefined ? {} : { flavourVersion }),
      pattern: stringValue(
        test.pattern,
        `Test ${index + 1} pattern`,
        MAXIMUM_REGEX_TEST_PATTERN_UTF16,
      ),
      flags,
      options,
      ...(test.scanAll === undefined
        ? {}
        : typeof test.scanAll === "boolean"
          ? { scanAll: test.scanAll }
          : (() => {
              throw new Error(`Test ${index + 1} scanAll must be boolean.`);
            })()),
      subject: subjectValue(test.subject, `Test ${index + 1} subject`),
      ...(test.replacement === undefined
        ? {}
        : {
            replacement: stringValue(
              test.replacement,
              `Test ${index + 1} replacement`,
              DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16,
            ),
          }),
      ...(resourceLimits === undefined ? {} : { resourceLimits }),
      expectation: parsedExpectation,
      ...(generation === undefined ? {} : { generation }),
    };
  });
}

export function parseRegexProject(
  value: unknown,
  registry: RegexFlavourRegistry = AVAILABLE_REGEX_FLAVOURS,
): RegexProjectV1 {
  const input = objectValue(value);
  if (input.schemaVersion !== 1) {
    throw new Error("Only regex-tools project schema version 1 is supported.");
  }
  const flavour = registry.parse(input.flavour, "Project flavour");
  const flavourVersion = resolveRegexFlavourVersion(
    input.flavourVersion,
    flavour,
    "Project flavour version",
  ).value;
  const flags = parseRegexFlags(input.flags, flavour, "Project flags");
  const options = parseRegexOptions(input.options, flavour, "Project options");
  if (input.resourceOverrides !== undefined) {
    const overrides = objectValue(
      input.resourceOverrides,
      "Project resource overrides",
    );
    if (Object.keys(overrides).length > 0) {
      throw new Error("Project resource overrides are not implemented.");
    }
  }
  const corpusFields = Object.keys(input).filter((key) =>
    key.toLocaleLowerCase("en-US").startsWith("corpus"),
  );
  if (corpusFields.length > 0) {
    throw new Error(
      `Corpus documents and results are ephemeral and cannot be imported from a schema-v1 project (${corpusFields.join(", ")}).`,
    );
  }
  const mode = stringValue(input.mode, "Project mode", 16) as WorkspaceMode;
  if (!IMPLEMENTED_MODES.has(mode)) {
    throw new Error(`Project mode ${mode} is not implemented in this release.`);
  }
  const testTextInput =
    input.testText === undefined ? undefined : objectValue(input.testText);
  const listSource =
    input.listTemplate && typeof input.listTemplate === "object"
      ? stringValue(
          (input.listTemplate as Record<string, unknown>).source,
          "List template",
          DEFAULT_REGEX_LIMITS.maximumListTemplateUtf16,
        )
      : "$0";
  const now = new Date().toISOString();
  const project: RegexProjectV1 = {
    schemaVersion: 1,
    id: stringValue(input.id, "Project id", 128),
    name: stringValue(input.name, "Project name", 256),
    createdAt:
      typeof input.createdAt === "string" ? input.createdAt.slice(0, 64) : now,
    updatedAt:
      typeof input.updatedAt === "string" ? input.updatedAt.slice(0, 64) : now,
    flavour: flavour.id,
    flavourVersion,
    pattern: stringValue(input.pattern, "Pattern", MAXIMUM_PATTERN_UTF16),
    flags,
    options,
    replacement:
      input.replacement === undefined
        ? ""
        : stringValue(
            input.replacement,
            "Replacement",
            DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16,
          ),
    mode,
    testText: {
      included: testTextInput?.included === true,
      ...(testTextInput?.included === true
        ? {
            value: subjectValue(testTextInput.value ?? "", "Test text"),
          }
        : {}),
    },
    listTemplate: parseListTemplate(listSource),
    tests: parseRegexTests(input.tests ?? [], registry),
    ui: {
      live:
        typeof input.ui === "object" &&
        input.ui !== null &&
        (input.ui as Record<string, unknown>).live === true,
      scanAll:
        typeof input.ui === "object" &&
        input.ui !== null &&
        (input.ui as Record<string, unknown>).scanAll === true,
    },
  };
  assertProjectDocumentWithinLimit(project);
  return project;
}
