interface ParsedSemanticVersion {
  major: number;
  minor: number;
  patch: number;
  prerelease: string[];
}

function parseNumericIdentifier(value: string): number | null {
  return /^\d+$/.test(value) ? Number.parseInt(value, 10) : null;
}

function parseSemanticVersion(rawValue: string | null | undefined): ParsedSemanticVersion | null {
  const normalized = normalizeSemanticVersion(rawValue);

  if (!normalized) {
    return null;
  }

  const match = normalized.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/);

  if (!match) {
    return null;
  }

  const prerelease = match[4]
    ? match[4]
      .split('.')
      .map((part) => part.trim())
      .filter(Boolean)
    : [];

  return {
    major: Number.parseInt(match[1], 10),
    minor: Number.parseInt(match[2] ?? '0', 10),
    patch: Number.parseInt(match[3] ?? '0', 10),
    prerelease
  };
}

function comparePrereleaseIdentifiers(left: string, right: string): number {
  const leftNumeric = parseNumericIdentifier(left);
  const rightNumeric = parseNumericIdentifier(right);

  if (leftNumeric !== null && rightNumeric !== null) {
    return leftNumeric - rightNumeric;
  }

  if (leftNumeric !== null) {
    return -1;
  }

  if (rightNumeric !== null) {
    return 1;
  }

  return left.localeCompare(right);
}

function comparePrerelease(left: string[], right: string[]): number {
  if (left.length === 0 && right.length === 0) {
    return 0;
  }

  if (left.length === 0) {
    return 1;
  }

  if (right.length === 0) {
    return -1;
  }

  const maxLength = Math.max(left.length, right.length);

  for (let index = 0; index < maxLength; index += 1) {
    const leftValue = left[index];
    const rightValue = right[index];

    if (!leftValue) {
      return -1;
    }

    if (!rightValue) {
      return 1;
    }

    const comparison = comparePrereleaseIdentifiers(leftValue, rightValue);

    if (comparison !== 0) {
      return comparison;
    }
  }

  return 0;
}

export function normalizeSemanticVersion(rawValue: string | null | undefined): string | null {
  if (typeof rawValue !== 'string') {
    return null;
  }

  const trimmedValue = rawValue.trim();

  if (!trimmedValue) {
    return null;
  }

  return trimmedValue.replace(/^v/i, '').split('+')[0] ?? null;
}

export function compareSemanticVersions(
  leftVersion: string | null | undefined,
  rightVersion: string | null | undefined
): number {
  const left = parseSemanticVersion(leftVersion);
  const right = parseSemanticVersion(rightVersion);

  if (!left && !right) {
    return 0;
  }

  if (!left) {
    return -1;
  }

  if (!right) {
    return 1;
  }

  if (left.major !== right.major) {
    return left.major - right.major;
  }

  if (left.minor !== right.minor) {
    return left.minor - right.minor;
  }

  if (left.patch !== right.patch) {
    return left.patch - right.patch;
  }

  return comparePrerelease(left.prerelease, right.prerelease);
}

export function sortSemanticVersionsDescending(versions: string[]): string[] {
  const normalizedVersions = versions
    .map((version) => normalizeSemanticVersion(version))
    .filter((version): version is string => !!version);

  return [...new Set(normalizedVersions)].sort((left, right) => compareSemanticVersions(right, left));
}
