type MatchKey = "roleId" | "chapter" | "location" | "seniority" | "workType";

type MatchableRateCardLine = Partial<Record<MatchKey, string | null>>;
type RateCardLineMatchCriteria = Partial<Record<MatchKey, string | undefined>>;
type RateCardLineMatchWeights = Partial<Record<MatchKey, number>>;

type RateCardLineMatchResult<TLine> = {
  line: TLine;
  score: number;
  mismatch: boolean;
};

function normalizeRateCardMatchValue(
  value: string,
  caseInsensitive: boolean,
): string {
  return caseInsensitive ? value.toLowerCase() : value;
}

function rateCardMatchValuesEqual(input: {
  lineValue: string;
  criteriaValue: string;
  caseInsensitive: boolean;
}): boolean {
  return normalizeRateCardMatchValue(input.lineValue, input.caseInsensitive) ===
    normalizeRateCardMatchValue(input.criteriaValue, input.caseInsensitive);
}

export function scoreRateCardLineMatch<TLine extends MatchableRateCardLine>(
  line: TLine,
  input: {
    criteria: RateCardLineMatchCriteria;
    weights: RateCardLineMatchWeights;
    caseInsensitiveKeys?: readonly MatchKey[];
  },
): RateCardLineMatchResult<TLine> {
  const caseInsensitiveKeys = new Set(input.caseInsensitiveKeys ?? []);
  let score = 0;
  let mismatch = false;

  for (const [key, criteriaValue] of Object.entries(input.criteria) as Array<[MatchKey, string | undefined]>) {
    const lineValue = line[key];
    if (!criteriaValue || !lineValue) {
      continue;
    }

    if (rateCardMatchValuesEqual({
      lineValue,
      criteriaValue,
      caseInsensitive: caseInsensitiveKeys.has(key),
    })) {
      score += input.weights[key] ?? 0;
      continue;
    }

    mismatch = true;
  }

  return { line, score, mismatch };
}

export function pickBestRateCardLineMatch<
  TLine extends MatchableRateCardLine,
>(
  lines: TLine[],
  input: {
    criteria: RateCardLineMatchCriteria;
    weights: RateCardLineMatchWeights;
    caseInsensitiveKeys?: readonly MatchKey[];
  },
): TLine | null {
  const candidates = lines
    .map((line) => scoreRateCardLineMatch(line, input))
    .filter((candidate) => !candidate.mismatch)
    .sort((left, right) => right.score - left.score);

  return candidates[0]?.line ?? null;
}
