import { RAMPS_MEMO_LIMITS } from "@sdp/types";

export interface MemoRow {
  key: string;
  value: string;
}

export type MemoRowErrorCode =
  | "keyRequired"
  | "keyTooLong"
  | "keyDuplicate"
  | "valueRequired"
  | "valueTooLong"
  | "tooManyFields";

export interface MemoRowError {
  row: number;
  code: MemoRowErrorCode;
}

/**
 * Creates an empty memo row for the editable memo grid.
 *
 * @returns An empty key-value row.
 */
export function emptyMemoRow(): MemoRow {
  return { key: "", value: "" };
}

/**
 * Determines whether a memo row has no key and no value.
 *
 * @param row - The editable memo row.
 * @returns True when both key and value are empty.
 */
export function isEmptyMemoRow(row: MemoRow): boolean {
  return row.key.length === 0 && row.value.length === 0;
}

/**
 * Splits pasted newline-delimited memo into key-value rows.
 *
 * @param text - Clipboard text containing tab- or comma-delimited rows.
 * @returns Parsed rows that contain both a key and a value.
 */
export function splitPastedMemoRows(text: string): MemoRow[] {
  return text
    .split("\n")
    .map((line) => {
      const tabIndex = line.indexOf("\t");
      const commaIndex = line.indexOf(",");
      const separatorIndex = tabIndex >= 0 ? tabIndex : commaIndex;
      if (separatorIndex < 0) {
        return null;
      }
      const key = line.slice(0, separatorIndex).trim();
      const value = line.slice(separatorIndex + 1).trim();
      return key.length > 0 && value.length > 0 ? { key, value } : null;
    })
    .filter((row): row is MemoRow => row !== null);
}

/**
 * Normalizes a memo key for the API payload: trimmed, with whitespace runs
 * replaced by dashes.
 *
 * @param key - Raw key input from the memo grid.
 * @returns The normalized memo key.
 */
export function normalizeMemoKey(key: string): string {
  return key.trim().replace(/\s+/g, "-");
}

/**
 * Validates memo rows against the quote API constraints, comparing keys in
 * their normalized form. Row numbers in the returned errors are 1-based
 * positions in the supplied rows array so they align with what the editing
 * grid renders; fully empty rows are ignored.
 *
 * @param rows - Editable memo rows to validate.
 * @returns Row-level validation errors.
 */
export function validateMemoRows(rows: MemoRow[]): MemoRowError[] {
  const errors: MemoRowError[] = [];
  const populatedRows = rows.filter((row) => !isEmptyMemoRow(row));
  const keyCounts = new Map<string, number>();

  for (const row of populatedRows) {
    const key = normalizeMemoKey(row.key);
    const currentCount = keyCounts.get(key);
    keyCounts.set(key, currentCount === undefined ? 1 : currentCount + 1);
  }

  rows.forEach((row, index) => {
    if (isEmptyMemoRow(row)) {
      return;
    }
    const rowNumber = index + 1;
    const key = normalizeMemoKey(row.key);
    if (key.length === 0) {
      errors.push({ row: rowNumber, code: "keyRequired" });
    } else if (key.length > RAMPS_MEMO_LIMITS.maxKeyLength) {
      errors.push({ row: rowNumber, code: "keyTooLong" });
    } else if (keyCounts.get(key) !== 1) {
      errors.push({ row: rowNumber, code: "keyDuplicate" });
    }
    if (row.value.length === 0) {
      errors.push({ row: rowNumber, code: "valueRequired" });
    } else if (row.value.length > RAMPS_MEMO_LIMITS.maxValueLength) {
      errors.push({ row: rowNumber, code: "valueTooLong" });
    }
  });

  if (populatedRows.length > RAMPS_MEMO_LIMITS.maxEntries) {
    errors.push({ row: 0, code: "tooManyFields" });
  }

  return errors;
}

/**
 * Converts populated memo rows into an API memo record with normalized keys.
 *
 * @param rows - Validated editable memo rows.
 * @returns A string-valued memo record.
 */
export function memoRowsToRecord(rows: MemoRow[]): Record<string, string> {
  return Object.fromEntries(
    rows.filter((row) => !isEmptyMemoRow(row)).map((row) => [normalizeMemoKey(row.key), row.value])
  );
}
