// What the merchant may type in, checked before anything is stored. Pure functions: the forms parse
// the text with the helpers below, the store validates the typed values again (never trust the page).
import { MAX_TARGET_MARGIN, type PriceRounding, type RecipeLine } from "./cost";

export const NAME_MAX = 80;
export const UNIT_MAX = 16;
export const TITLE_MAX = 255;
// The largest amount stored (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;
export const MAX_QUANTITY = 1_000_000_000;
export const MAX_WASTE_PERCENT = 500;
export const MAX_MINUTES = 100_000;
export const MAX_RECIPE_LINES = 100;
export const MAX_FACTOR = 100;
export const MAX_VARIANTS = 2048;

export const PRICE_ROUNDINGS: PriceRounding[] = ["none", "whole", "end99"];

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\u00a0\u202f]/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 isNum = (n: unknown, min: number, max: number): n is number => typeof n === "number" && Number.isFinite(n) && n >= min && n <= max;
const clean = (s: unknown): string => (typeof s === "string" ? s.replace(/\s+/g, " ").trim() : "");

export interface MaterialDraft {
  name: string;
  unit: string;
  packPriceCents: number;
  packQuantity: number;
  wastePercent: number;
}

export function validateMaterial(input: MaterialDraft): Validated<MaterialDraft> {
  const errors: Errors = {};
  const name = clean(input.name);
  const unit = clean(input.unit);
  if (!name) errors.name = "Enter a name.";
  else if (name.length > NAME_MAX) errors.name = `Use at most ${NAME_MAX} characters.`;
  if (!unit) errors.unit = "Enter a unit (g, ml, pc...).";
  else if (unit.length > UNIT_MAX) errors.unit = `Use at most ${UNIT_MAX} characters.`;
  if (!isInt(input.packPriceCents, 0, MAX_CENTS)) errors.packPriceCents = "Enter the price of one pack (0 or more).";
  if (!(isNum(input.packQuantity, 0, MAX_QUANTITY) && input.packQuantity > 0)) errors.packQuantity = "Enter how much one pack contains (more than 0).";
  if (!isNum(input.wastePercent, 0, MAX_WASTE_PERCENT)) errors.wastePercent = `Enter a waste between 0 and ${MAX_WASTE_PERCENT} %.`;
  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return { ok: true, value: { name, unit, packPriceCents: input.packPriceCents, packQuantity: input.packQuantity, wastePercent: input.wastePercent } };
}

export interface RecipeDraft {
  productId: string;
  productTitle: string;
  lines: RecipeLine[];
  labourMinutes: number;
  packagingCents: number;
  otherCents: number;
}

export function validateRecipe(input: RecipeDraft): Validated<RecipeDraft> {
  const errors: Errors = {};
  const productTitle = clean(input.productTitle);
  if (typeof input.productId !== "string" || !PRODUCT_ID.test(input.productId)) errors.productId = "This is not a store product.";
  if (!productTitle) errors.productTitle = "The product has no title.";
  else if (productTitle.length > TITLE_MAX) errors.productTitle = `Use at most ${TITLE_MAX} characters.`;
  if (!Array.isArray(input.lines)) errors.lines = "The recipe has no list of materials.";
  else if (input.lines.length > MAX_RECIPE_LINES) errors.lines = `A recipe can have at most ${MAX_RECIPE_LINES} materials.`;
  else if (input.lines.some((l) => typeof l?.materialId !== "string" || l.materialId === "")) errors.lines = "Choose a material on every line.";
  else if (input.lines.some((l) => !(isNum(l.quantity, 0, MAX_QUANTITY) && l.quantity > 0))) errors.lines = "Every quantity must be more than 0.";
  else if (new Set(input.lines.map((l) => l.materialId)).size !== input.lines.length) errors.lines = "Each material can appear only once.";
  if (!isNum(input.labourMinutes, 0, MAX_MINUTES)) errors.labourMinutes = "Enter the labour time in minutes (0 or more).";
  if (!isInt(input.packagingCents, 0, MAX_CENTS)) errors.packagingCents = "Enter the packaging cost (0 or more).";
  if (!isInt(input.otherCents, 0, MAX_CENTS)) errors.otherCents = "Enter the other costs (0 or more).";
  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return {
    ok: true,
    value: {
      productId: input.productId,
      productTitle,
      lines: input.lines.map((l) => ({ materialId: l.materialId, quantity: l.quantity })),
      labourMinutes: input.labourMinutes,
      packagingCents: input.packagingCents,
      otherCents: input.otherCents,
    },
  };
}

export interface SettingsDraft {
  hourlyRateCents: number;
  targetMarginPercent: number;
  priceRounding: PriceRounding;
  // Whether uninstalling the plugin also deletes the materials and recipes. Missing means no.
  deleteDataOnUninstall?: boolean;
}

export function validateSettings(input: SettingsDraft): Validated<SettingsDraft> {
  const errors: Errors = {};
  if (!isInt(input.hourlyRateCents, 0, MAX_CENTS)) errors.hourlyRateCents = "Enter your hourly rate (0 or more).";
  if (!isNum(input.targetMarginPercent, 0, MAX_TARGET_MARGIN)) errors.targetMarginPercent = `Enter a margin between 0 and ${MAX_TARGET_MARGIN} %.`;
  if (!PRICE_ROUNDINGS.includes(input.priceRounding)) errors.priceRounding = "Choose how prices are rounded.";
  if (input.deleteDataOnUninstall !== undefined && typeof input.deleteDataOnUninstall !== "boolean") errors.deleteDataOnUninstall = "Choose yes or no.";
  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return {
    ok: true,
    value: {
      hourlyRateCents: input.hourlyRateCents,
      targetMarginPercent: input.targetMarginPercent,
      priceRounding: input.priceRounding,
      deleteDataOnUninstall: input.deleteDataOnUninstall ?? false,
    },
  };
}

export function validateVariantFactors(input: Record<string, number>): Validated<Record<string, number>> {
  const entries = Object.entries(input ?? {});
  if (entries.length > MAX_VARIANTS) return { ok: false, errors: { factors: `At most ${MAX_VARIANTS} variants.` } };
  const errors: Errors = {};
  for (const [variantId, factor] of entries) {
    if (!VARIANT_ID.test(variantId)) errors[variantId] = "This is not a store variant.";
    else if (!(isNum(factor, 0, MAX_FACTOR) && factor > 0)) errors[variantId] = `Enter a coefficient between 0 and ${MAX_FACTOR}.`;
  }
  if (Object.keys(errors).length > 0) return { ok: false, errors };
  return { ok: true, value: Object.fromEntries(entries) };
}
