// Validation for the v1 Finance API (developer tokens, e.g. Jarvis).
// Pure functions – no database access – so they can be unit tested.
// Amounts are integer cents (column type integer), never floats.

export type FinanceApiResult<T> = { ok: true; value: T } | { ok: false; error: string };

const MAX_AMOUNT_CENTS = 2_000_000_000; // integer column limit (≈ 20 Mio. €)
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
const MONTH_RE = /^\d{4}-\d{2}$/;
const CURRENCY_RE = /^[A-Z]{3}$/;
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;

function fail<T>(error: string): FinanceApiResult<T> {
  return { ok: false, error };
}

export function isValidDate(value: unknown): value is string {
  if (typeof value !== "string" || !DATE_RE.test(value)) return false;
  const date = new Date(`${value}T00:00:00Z`);
  return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
}

export function isValidMonth(value: unknown): value is string {
  return typeof value === "string" && MONTH_RE.test(value) && isValidDate(`${value}-01`);
}

function text(value: unknown, max: number): string | null {
  if (typeof value !== "string") return null;
  const trimmed = value.trim();
  return trimmed ? trimmed.slice(0, max) : null;
}

function amount(value: unknown): number | null {
  return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= MAX_AMOUNT_CENTS ? value : null;
}

function currency(value: unknown): string | null {
  if (value === undefined) return "EUR";
  const upper = typeof value === "string" ? value.trim().toUpperCase() : "";
  return CURRENCY_RE.test(upper) ? upper : null;
}

function oneOf<T extends string>(value: unknown, allowed: readonly T[]): T | null {
  return typeof value === "string" && (allowed as readonly string[]).includes(value) ? (value as T) : null;
}

export const TRANSACTION_TYPES = ["income", "expense"] as const;
export const TRANSACTION_STATUSES = ["cleared", "pending"] as const;
export const RECURRING_FREQUENCIES = ["weekly", "monthly", "yearly"] as const;
export const CATEGORY_TYPES = ["income", "expense", "both"] as const;

export interface TransactionCreate {
  type: (typeof TRANSACTION_TYPES)[number];
  title: string;
  description: string;
  category: string;
  amount_cents: number;
  currency: string;
  transaction_date: string;
  status: (typeof TRANSACTION_STATUSES)[number];
  external_ref: string | null;
}

export function parseTransactionCreate(body: unknown, today: string): FinanceApiResult<TransactionCreate> {
  const b = (body ?? {}) as Record<string, unknown>;
  const type = oneOf(b.type, TRANSACTION_TYPES);
  if (!type) return fail("type must be 'income' or 'expense'");
  const title = text(b.title, 200);
  if (!title) return fail("title is required");
  const amount_cents = amount(b.amount_cents);
  if (amount_cents === null) return fail("amount_cents must be a non-negative integer (cents)");
  const cur = currency(b.currency);
  if (!cur) return fail("currency must be a 3-letter code like EUR");
  const transaction_date = b.transaction_date === undefined ? today : b.transaction_date;
  if (!isValidDate(transaction_date)) return fail("transaction_date must be YYYY-MM-DD");
  const status = b.status === undefined ? "cleared" : oneOf(b.status, TRANSACTION_STATUSES);
  if (!status) return fail("status must be 'cleared' or 'pending'");
  const external_ref = b.external_ref === undefined || b.external_ref === null ? null : text(b.external_ref, 200);
  if (b.external_ref !== undefined && b.external_ref !== null && !external_ref) return fail("external_ref must be a non-empty string");
  return {
    ok: true,
    value: {
      type,
      title,
      description: text(b.description, 2000) ?? "",
      category: text(b.category, 80) ?? "Other",
      amount_cents,
      currency: cur,
      transaction_date,
      status,
      external_ref,
    },
  };
}

/** Partial update: only known fields, each validated. Returns column → value. */
export function parseTransactionPatch(body: unknown): FinanceApiResult<Record<string, string | number>> {
  const b = (body ?? {}) as Record<string, unknown>;
  const out: Record<string, string | number> = {};
  if (b.type !== undefined) {
    const v = oneOf(b.type, TRANSACTION_TYPES);
    if (!v) return fail("type must be 'income' or 'expense'");
    out.type = v;
  }
  if (b.title !== undefined) {
    const v = text(b.title, 200);
    if (!v) return fail("title must not be empty");
    out.title = v;
  }
  if (b.description !== undefined) out.description = typeof b.description === "string" ? b.description.trim().slice(0, 2000) : "";
  if (b.category !== undefined) {
    const v = text(b.category, 80);
    if (!v) return fail("category must not be empty");
    out.category = v;
  }
  if (b.amount_cents !== undefined) {
    const v = amount(b.amount_cents);
    if (v === null) return fail("amount_cents must be a non-negative integer (cents)");
    out.amount_cents = v;
  }
  if (b.currency !== undefined) {
    const v = currency(b.currency);
    if (!v) return fail("currency must be a 3-letter code like EUR");
    out.currency = v;
  }
  if (b.transaction_date !== undefined) {
    if (!isValidDate(b.transaction_date)) return fail("transaction_date must be YYYY-MM-DD");
    out.transaction_date = b.transaction_date;
  }
  if (b.status !== undefined) {
    const v = oneOf(b.status, TRANSACTION_STATUSES);
    if (!v) return fail("status must be 'cleared' or 'pending'");
    out.status = v;
  }
  if (!Object.keys(out).length) return fail("no updatable fields given");
  return { ok: true, value: out };
}

export interface RecurringCreate {
  type: (typeof TRANSACTION_TYPES)[number];
  title: string;
  category: string;
  amount_cents: number;
  currency: string;
  frequency: (typeof RECURRING_FREQUENCIES)[number];
  next_due_date: string;
  is_active: boolean;
}

export function parseRecurringCreate(body: unknown, today: string): FinanceApiResult<RecurringCreate> {
  const b = (body ?? {}) as Record<string, unknown>;
  const type = b.type === undefined ? "expense" : oneOf(b.type, TRANSACTION_TYPES);
  if (!type) return fail("type must be 'income' or 'expense'");
  const title = text(b.title, 200);
  if (!title) return fail("title is required");
  const amount_cents = amount(b.amount_cents);
  if (amount_cents === null) return fail("amount_cents must be a non-negative integer (cents)");
  const cur = currency(b.currency);
  if (!cur) return fail("currency must be a 3-letter code like EUR");
  const frequency = b.frequency === undefined ? "monthly" : oneOf(b.frequency, RECURRING_FREQUENCIES);
  if (!frequency) return fail("frequency must be weekly, monthly or yearly");
  const next_due_date = b.next_due_date === undefined ? today : b.next_due_date;
  if (!isValidDate(next_due_date)) return fail("next_due_date must be YYYY-MM-DD");
  if (b.is_active !== undefined && typeof b.is_active !== "boolean") return fail("is_active must be boolean");
  return {
    ok: true,
    value: {
      type,
      title,
      category: text(b.category, 80) ?? "Other",
      amount_cents,
      currency: cur,
      frequency,
      next_due_date,
      is_active: b.is_active === undefined ? true : (b.is_active as boolean),
    },
  };
}

export function parseRecurringPatch(body: unknown): FinanceApiResult<Record<string, string | number | boolean>> {
  const b = (body ?? {}) as Record<string, unknown>;
  const out: Record<string, string | number | boolean> = {};
  if (b.type !== undefined) {
    const v = oneOf(b.type, TRANSACTION_TYPES);
    if (!v) return fail("type must be 'income' or 'expense'");
    out.type = v;
  }
  if (b.title !== undefined) {
    const v = text(b.title, 200);
    if (!v) return fail("title must not be empty");
    out.title = v;
  }
  if (b.category !== undefined) {
    const v = text(b.category, 80);
    if (!v) return fail("category must not be empty");
    out.category = v;
  }
  if (b.amount_cents !== undefined) {
    const v = amount(b.amount_cents);
    if (v === null) return fail("amount_cents must be a non-negative integer (cents)");
    out.amount_cents = v;
  }
  if (b.currency !== undefined) {
    const v = currency(b.currency);
    if (!v) return fail("currency must be a 3-letter code like EUR");
    out.currency = v;
  }
  if (b.frequency !== undefined) {
    const v = oneOf(b.frequency, RECURRING_FREQUENCIES);
    if (!v) return fail("frequency must be weekly, monthly or yearly");
    out.frequency = v;
  }
  if (b.next_due_date !== undefined) {
    if (!isValidDate(b.next_due_date)) return fail("next_due_date must be YYYY-MM-DD");
    out.next_due_date = b.next_due_date;
  }
  if (b.is_active !== undefined) {
    if (typeof b.is_active !== "boolean") return fail("is_active must be boolean");
    out.is_active = b.is_active;
  }
  if (!Object.keys(out).length) return fail("no updatable fields given");
  return { ok: true, value: out };
}

export interface CategoryCreate {
  name: string;
  type: (typeof CATEGORY_TYPES)[number];
  color: string;
}

export function parseCategoryCreate(body: unknown): FinanceApiResult<CategoryCreate> {
  const b = (body ?? {}) as Record<string, unknown>;
  const name = text(b.name, 80);
  if (!name) return fail("name is required");
  const type = b.type === undefined ? "both" : oneOf(b.type, CATEGORY_TYPES);
  if (!type) return fail("type must be income, expense or both");
  const color = b.color === undefined ? "#6366f1" : typeof b.color === "string" && COLOR_RE.test(b.color) ? b.color : null;
  if (!color) return fail("color must be a hex color like #6366f1");
  return { ok: true, value: { name, type, color } };
}

export interface BudgetUpsert {
  category: string;
  amount_cents: number;
  currency: string;
  month: string; // first day of month, YYYY-MM-01
}

export function parseBudgetUpsert(body: unknown): FinanceApiResult<BudgetUpsert> {
  const b = (body ?? {}) as Record<string, unknown>;
  const category = text(b.category, 80);
  if (!category) return fail("category is required");
  const amount_cents = amount(b.amount_cents);
  if (amount_cents === null) return fail("amount_cents must be a non-negative integer (cents)");
  const cur = currency(b.currency);
  if (!cur) return fail("currency must be a 3-letter code like EUR");
  if (!isValidMonth(b.month)) return fail("month must be YYYY-MM");
  return { ok: true, value: { category, amount_cents, currency: cur, month: `${b.month}-01` } };
}

/** Month range [first day, first day of next month) for YYYY-MM. */
export function monthRange(month: string): { from: string; to: string } {
  const [y, m] = month.split("-").map(Number);
  const next = m === 12 ? `${y + 1}-01` : `${y}-${String(m + 1).padStart(2, "0")}`;
  return { from: `${month}-01`, to: `${next}-01` };
}

export interface SummaryRow {
  type: "income" | "expense";
  category: string;
  currency: string;
  total_cents: number | string;
  count: number | string;
}

/** Aggregate rows (from SQL GROUP BY) into a summary per currency. */
export function buildSummary(rows: SummaryRow[]) {
  const byCurrency: Record<string, { income_cents: number; expense_cents: number; net_cents: number; categories: Array<{ type: string; category: string; total_cents: number; count: number }> }> = {};
  for (const r of rows) {
    const total = Number(r.total_cents);
    const entry = (byCurrency[r.currency] ??= { income_cents: 0, expense_cents: 0, net_cents: 0, categories: [] });
    if (r.type === "income") entry.income_cents += total;
    else entry.expense_cents += total;
    entry.categories.push({ type: r.type, category: r.category, total_cents: total, count: Number(r.count) });
  }
  for (const entry of Object.values(byCurrency)) {
    entry.net_cents = entry.income_cents - entry.expense_cents;
    entry.categories.sort((a, b) => b.total_cents - a.total_cents);
  }
  return byCurrency;
}

/** Today's date in Germany (Maro's time zone), YYYY-MM-DD. */
export function todayIso(now = new Date()): string {
  return new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Berlin" }).format(now);
}
