import { EuidChecksumError, EuidFormatError, EuidSandboxPrefixError } from "./errors";
import { ALPHABET_32, mod32CheckCharacter } from "./mod32";

export type Environment = "production" | "sandbox";

const ALNUM32_RE = /^[0-9A-HJ-KMNP-TV-Z]+$/;
const LETTERS32_RE = /^[A-HJ-KMNP-TV-Z]+$/;
const LOWERCASE_RE = /[a-z]/;

const RESERVED_SANDBOX = new Set(["A", "B", "C", "D", "E", "F", "G"]);
const VALID_SANDBOX = new Set([
  "H", "J", "K", "M", "N", "P", "Q", "R", "S", "T", "V", "W", "X", "Y", "Z",
]);

const VALUE_32: Record<string, number> = Object.fromEntries(
  Array.from(ALPHABET_32).map((ch, i) => [ch, i])
);

type ParsedEuid = {
  sandbox: string | null;
  category: string;
  body: string;
  checksum: string;
};

export type ParsedEuidResult = ParsedEuid & { integer: number };

function rejectInput(euid: string): string {
  if (!/^[\x00-\x7F]+$/.test(euid)) {
    throw new EuidFormatError("EUID must be ASCII");
  }
  if (/\s/.test(euid)) {
    throw new EuidFormatError("whitespace is not permitted in EUID");
  }
  if (LOWERCASE_RE.test(euid)) {
    throw new EuidFormatError("lowercase letters are not permitted in EUID");
  }
  return euid;
}

function parseProduction(euid: string): ParsedEuid {
  if (euid.includes(":")) {
    throw new EuidFormatError("invalid production EUID syntax");
  }
  const hyphenCount = (euid.match(/-/g) ?? []).length;
  if (hyphenCount !== 1) {
    throw new EuidFormatError("invalid production EUID syntax");
  }

  const [category, tail] = euid.split("-", 2);
  if (!category || tail === undefined) {
    throw new EuidFormatError("invalid production EUID syntax");
  }
  if (category.length < 2 || category.length > 3 || !LETTERS32_RE.test(category)) {
    throw new EuidFormatError("invalid production EUID syntax");
  }

  if (tail.length < 2) {
    throw new EuidFormatError("invalid production EUID syntax");
  }
  const body = tail.slice(0, -1);
  const checksum = tail.slice(-1);

  if (body.length === 0 || body[0] === "0") {
    throw new EuidFormatError("invalid production EUID syntax");
  }
  if (!ALNUM32_RE.test(body)) {
    throw new EuidFormatError("invalid production EUID syntax");
  }
  if (!ALPHABET_32.includes(checksum)) {
    throw new EuidFormatError("invalid production EUID syntax");
  }

  return { sandbox: null, category, body, checksum };
}

function parseSandbox(euid: string): ParsedEuid {
  const colonCount = (euid.match(/:/g) ?? []).length;
  if (colonCount !== 1) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }
  const hyphenCount = (euid.match(/-/g) ?? []).length;
  if (hyphenCount !== 1) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }
  if (euid.indexOf(":") > euid.indexOf("-")) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }

  const [sandbox, rest] = euid.split(":", 2);
  if (!sandbox || rest === undefined) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }
  if (sandbox.length !== 1 || !VALID_SANDBOX.has(sandbox)) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }

  const [category, tail] = rest.split("-", 2);
  if (!category || tail === undefined) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }
  if (category.length < 2 || category.length > 3 || !LETTERS32_RE.test(category)) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }
  if (tail.length < 2) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }

  const body = tail.slice(0, -1);
  const checksum = tail.slice(-1);

  if (body.length === 0 || body[0] === "0") {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }
  if (!ALNUM32_RE.test(body)) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }
  if (!ALPHABET_32.includes(checksum)) {
    throw new EuidFormatError("invalid sandbox EUID syntax");
  }

  return { sandbox, category, body, checksum };
}

export function computeCheckCharacter(payload: string): string {
  try {
    return mod32CheckCharacter(payload);
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    throw new EuidChecksumError(msg);
  }
}

export function validate(
  euid: string,
  options: {
    environment: Environment;
    allowedSandboxPrefixes?: ReadonlySet<string> | undefined;
  }
): string {
  if (typeof euid !== "string") {
    throw new EuidFormatError("EUID must be a string");
  }

  rejectInput(euid);
  const env = options.environment;

  if (env === "production") {
    const parsed = parseProduction(euid);
    const payload = `${parsed.category}${parsed.body}`;
    const expected = mod32CheckCharacter(payload);
    if (parsed.checksum !== expected) {
      throw new EuidChecksumError("checksum mismatch");
    }
    return `${parsed.category}-${parsed.body}${parsed.checksum}`;
  }

  if (env === "sandbox") {
    const parsed = parseSandbox(euid);
    const payload = `${parsed.sandbox}${parsed.category}${parsed.body}`;
    const expected = mod32CheckCharacter(payload);
    if (parsed.checksum !== expected) {
      throw new EuidChecksumError("checksum mismatch");
    }

    // Sandbox prefix allowlist check (after regex and checksum per SPEC.md Section 8).
    const sandboxPrefix = parsed.sandbox!;
    const allowed = options.allowedSandboxPrefixes;
    if (allowed && !allowed.has(sandboxPrefix)) {
      throw new EuidSandboxPrefixError("sandbox prefix not allowed in this context");
    }

    return `${parsed.sandbox}:${parsed.category}-${parsed.body}${parsed.checksum}`;
  }

  throw new Error(`unknown environment: ${JSON.stringify(env)}`);
}

export function isValid(
  euid: string,
  options: {
    environment: Environment;
    allowedSandboxPrefixes?: ReadonlySet<string> | undefined;
  }
): boolean {
  try {
    validate(euid, options);
    return true;
  } catch {
    return false;
  }
}

// ---------------------------------------------------------------------------
// Encode / Parse
// ---------------------------------------------------------------------------

function intToBase32(n: number): string {
  if (n <= 0) {
    throw new EuidFormatError("integer must be positive");
  }
  let result = "";
  let remaining = n;
  while (remaining > 0) {
    result = ALPHABET_32[remaining % 32]! + result;
    remaining = Math.floor(remaining / 32);
  }
  return result;
}

function base32ToInt(s: string): number {
  let result = 0;
  for (const ch of s) {
    const v = VALUE_32[ch];
    if (v === undefined) {
      throw new EuidFormatError(`invalid Base32 character: ${JSON.stringify(ch)}`);
    }
    result = result * 32 + v;
  }
  return result;
}

/**
 * Encode a positive integer as a Meridian EUID.
 *
 * @param integer - Positive integer (sequence-allocated ID)
 * @param category - Category prefix (2-3 uppercase Crockford letters)
 * @param options.sandbox - Optional sandbox prefix (single letter from VALID_SANDBOX)
 * @returns The fully-formed EUID string
 */
export function encode(
  integer: number,
  category: string,
  options?: { sandbox?: string },
): string {
  // Validate category
  if (category.length < 2 || category.length > 3 || !LETTERS32_RE.test(category)) {
    throw new EuidFormatError("category must be 2-3 uppercase Crockford letters");
  }

  // Validate integer
  if (!Number.isInteger(integer) || integer < 1) {
    throw new EuidFormatError("integer must be a positive integer");
  }

  const sandbox = options?.sandbox ?? null;

  // Validate sandbox prefix
  if (sandbox !== null) {
    if (RESERVED_SANDBOX.has(sandbox)) {
      throw new EuidFormatError(`sandbox prefix '${sandbox}' is reserved (A-G)`);
    }
    if (!VALID_SANDBOX.has(sandbox)) {
      throw new EuidFormatError("sandbox prefix must be a valid Crockford letter (H-Z, excluding I/L/O/U)");
    }
  }

  const body = intToBase32(integer);

  // Build payload for checksum
  const payload = sandbox !== null
    ? `${sandbox}${category}${body}`
    : `${category}${body}`;

  const checkChar = mod32CheckCharacter(payload);

  return sandbox !== null
    ? `${sandbox}:${category}-${body}${checkChar}`
    : `${category}-${body}${checkChar}`;
}

/**
 * Parse a validated EUID into its structural components.
 *
 * @param euid - The EUID string to parse
 * @returns Object with sandbox, category, body, checksum, and integer fields
 * @throws EuidFormatError if the EUID is syntactically invalid
 * @throws EuidChecksumError if the checksum does not match
 */
export function parse(euid: string): ParsedEuidResult {
  if (typeof euid !== "string") {
    throw new EuidFormatError("EUID must be a string");
  }

  rejectInput(euid);

  const isSandbox = euid.includes(":");

  let parsed: ParsedEuid;
  let payload: string;

  if (isSandbox) {
    parsed = parseSandbox(euid);
    payload = `${parsed.sandbox}${parsed.category}${parsed.body}`;
  } else {
    parsed = parseProduction(euid);
    payload = `${parsed.category}${parsed.body}`;
  }

  const expected = mod32CheckCharacter(payload);
  if (parsed.checksum !== expected) {
    throw new EuidChecksumError("checksum mismatch");
  }

  return {
    sandbox: parsed.sandbox,
    category: parsed.category,
    body: parsed.body,
    checksum: parsed.checksum,
    integer: base32ToInt(parsed.body),
  };
}
