// What the merchant may type in, checked before anything is stored. Pure functions: the forms parse
// the text with the helpers below, the server validates the typed values again (includes/class-validator.php has the
// same limits: never trust the page).
import { __, sprintf } from "@wordpress/i18n";
import { METHODS, type AllocationMethod } from "./allocation";

export const NAME_MAX = 80;
export const SOURCE_MAX = 80;
export const TITLE_MAX = 255;
// The largest amount accepted (about 21 million in the shop currency), so shops whose currency has big nominal
// amounts (dong, rupiah, won) are not refused. Same limit as Validator::MAX_CENTS in includes/class-validator.php.
export const MAX_CENTS = 2_000_000_000;
// How many identical units one piece of a lot can hold, and how many pieces a lot can hold.
export const MAX_UNITS = 100_000;
export const MAX_PIECES = 500;

export type Errors = Record<string, string>;
export type Validated<T> = { ok: true; value: T } | { ok: false; errors: Errors };

// WooCommerce ids are post ids: whole numbers, kept as text.
const WOO_ID = /^[1-9][0-9]{0,18}$/;
const PRODUCT_ID = WOO_ID;
const VARIANT_ID = WOO_ID;

// "12.5", "12,5", "1 234,50", "1,234.50" and "1.234,50" all work. A lone separator is a decimal
// point ("1,234" is 1.234); a separator that appears several times is a thousands separator.
export function parseDecimal(text: string): number | null {
  const s = text.replace(/[\s  ]/g, "");
  if (!/^[+-]?(?:\d+[.,]?\d*|[.,]\d+)$|^[+-]?\d{1,3}(?:[.,]\d{3})+(?:[.,]\d+)?$/.test(s)) return null;
  const lastDot = s.lastIndexOf(".");
  const lastComma = s.lastIndexOf(",");
  let normalized = s;
  if (lastDot >= 0 && lastComma >= 0) {
    const decimal = lastDot > lastComma ? "." : ",";
    const thousands = decimal === "." ? "," : ".";
    normalized = s.split(thousands).join("").replace(decimal, ".");
  } else if (lastDot >= 0 || lastComma >= 0) {
    const sep = lastDot >= 0 ? "." : ",";
    const count = s.split(sep).length - 1;
    normalized = count > 1 ? s.split(sep).join("") : s.replace(sep, ".");
  }
  const value = Number(normalized);
  return Number.isFinite(value) ? value : null;
}

// Money typed as text to integer cents, without the 1.005 * 100 = 100.49999 trap.
export function parseMoneyToCents(text: string): number | null {
  const value = parseDecimal(text);
  return value === null ? null : Math.round(Number((value * 100).toPrecision(12)));
}

const isInt = (n: unknown, min: number, max: number): n is number => typeof n === "number" && Number.isInteger(n) && n >= min && n <= max;
const clean = (s: unknown): string => (typeof s === "string" ? s.replace(/\s+/g, " ").trim() : "");

const DATE = /^(\d{4})-(\d{2})-(\d{2})$/;

// "2026-10-14": a real calendar day, not "2026-02-31".
export function isCalendarDate(text: string): boolean {
  const match = DATE.exec(text);
  if (!match) return false;
  const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
  const date = new Date(Date.UTC(year, month - 1, day));
  return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
}

export interface LotPieceDraft {
  // For a simple product both ids are the product; for a variation, `productId` is the parent and `variantId` the variation.
  productId: string;
  variantId: string;
  // What the merchant sees ("Blue vase" or "Linen shirt - M"): kept so the lot stays readable if the product is deleted.
  title: string;
  units: number;
  // The cost of one unit fixed by the merchant (equal and price methods), or null.
  pinnedCents: number | null;
  // The cost of one unit typed by the merchant (manual method), or null.
  manualCents: number | null;
}

export interface LotDraft {
  name: string;
  // Where the lot was bought (an auction, a car boot sale, a supplier): the lots are summed up by source.
  source: string;
  // A day, "2026-10-14", or "" when not known.
  purchasedAt: string;
  itemsCents: number;
  shippingCents: number;
  otherCents: number;
  method: AllocationMethod;
  // False while some costs of the lot are still to come (an invoice for the shipping, a fee).
  confirmed: boolean;
  pieces: LotPieceDraft[];
}

const piecesError = (title: string, message: string): string => (title ? `${title}: ${message}` : message);

export function validateLot(input: LotDraft): Validated<LotDraft> {
  const errors: Errors = {};
  const name = clean(input.name);
  const source = clean(input.source);
  const purchasedAt = typeof input.purchasedAt === "string" ? input.purchasedAt.trim() : "";
  if (!name) errors.name = __("Enter a name for the lot.", "lotcost");
  else if (name.length > NAME_MAX) errors.name = sprintf(__("Use at most %d characters.", "lotcost"), NAME_MAX);
  if (source.length > SOURCE_MAX) errors.source = sprintf(__("Use at most %d characters.", "lotcost"), SOURCE_MAX);
  if (purchasedAt !== "" && !isCalendarDate(purchasedAt)) errors.purchasedAt = __("Enter a date like 2026-10-14, or leave it empty.", "lotcost");
  if (!isInt(input.itemsCents, 0, MAX_CENTS)) errors.itemsCents = __("Enter the price you paid for the lot (0 or more).", "lotcost");
  if (!isInt(input.shippingCents, 0, MAX_CENTS)) errors.shippingCents = __("Enter the shipping cost (0 or more).", "lotcost");
  if (!isInt(input.otherCents, 0, MAX_CENTS)) errors.otherCents = __("Enter the other costs (0 or more).", "lotcost");
  if (!METHODS.includes(input.method)) errors.method = __("Choose how the cost is shared.", "lotcost");

  const seen = new Set<string>();
  const pieces: LotPieceDraft[] = [];
  if (!Array.isArray(input.pieces)) errors.pieces = __("The lot has no list of pieces.", "lotcost");
  else if (input.pieces.length > MAX_PIECES) errors.pieces = sprintf(__("A lot can have at most %d pieces.", "lotcost"), MAX_PIECES);
  else {
    for (const piece of input.pieces) {
      const title = clean(piece?.title);
      let problem: string | null = null;
      if (typeof piece?.productId !== "string" || !PRODUCT_ID.test(piece.productId) || typeof piece.variantId !== "string" || !VARIANT_ID.test(piece.variantId)) problem = __("This is not a product of your store.", "lotcost");
      else if (!title) problem = __("The product has no title.", "lotcost");
      else if (title.length > TITLE_MAX) problem = sprintf(__("Use at most %d characters for the title.", "lotcost"), TITLE_MAX);
      else if (!isInt(piece.units, 1, MAX_UNITS)) problem = sprintf(__("Enter a whole number of units (1 to %d).", "lotcost"), MAX_UNITS);
      else if (piece.pinnedCents !== null && !isInt(piece.pinnedCents, 0, MAX_CENTS)) problem = __("Enter a fixed cost of 0 or more.", "lotcost");
      else if (piece.manualCents !== null && !isInt(piece.manualCents, 0, MAX_CENTS)) problem = __("Enter a cost of 0 or more.", "lotcost");
      else if (seen.has(piece.variantId)) problem = __("This piece is in the lot twice.", "lotcost");
      if (problem) {
        errors.pieces = piecesError(title, problem);
        break;
      }
      seen.add(piece.variantId);
      pieces.push({ productId: piece.productId, variantId: piece.variantId, title, units: piece.units, pinnedCents: piece.pinnedCents, manualCents: piece.manualCents });
    }
  }
  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return {
    ok: true,
    value: {
      name,
      source,
      purchasedAt,
      itemsCents: input.itemsCents,
      shippingCents: input.shippingCents,
      otherCents: input.otherCents,
      method: input.method,
      confirmed: input.confirmed === true,
      pieces,
    },
  };
}
