// Domain vocabulary for /invoices — the Officer front end over the officer-invoiceshelf sidecar.
//
// Types are written against the LIVE 2.4.2 instance's actual responses, not the 3.0.0-alpha.1 checkout in
// _references. Two things about those responses drive most of the odd choices here:
//
//  1. Money is an integer count of minor units (cents), and the API is inconsistent about whether it hands
//     it over as a number or a decimal string — `invoice.total` is 450000 but `customer.due_amount` is
//     "1830000". Hence the `Money` alias and `toMinor()` in format.ts. Never do arithmetic on these
//     without going through it.
//  2. Every record embeds its whole company, including the role/ability list — a single invoice row carries
//     ~40KB of super-admin abilities nobody asked for. That is upstream's shape and we cannot change it,
//     so the types below deliberately declare `company?: unknown`: naming those fields would invite code
//     that reads them, and the interesting data is never in there.

export const INVOICES_SECTIONS = [
  { id: 'dashboard', label: 'Dashboard' },
  { id: 'invoices', label: 'Invoices' },
  { id: 'estimates', label: 'Estimates' },
  { id: 'recurring', label: 'Recurring' },
  { id: 'payments', label: 'Payments' },
  { id: 'expenses', label: 'Expenses' },
  { id: 'customers', label: 'Customers' },
  { id: 'items', label: 'Items' },
  { id: 'reports', label: 'Reports' },
  { id: 'settings', label: 'Connection' },
] as const;

export type InvoicesSectionId = (typeof INVOICES_SECTIONS)[number]['id'];

export const DEFAULT_INVOICES_SECTION: InvoicesSectionId = 'dashboard';

export const isInvoicesSection = (value: string | undefined): value is InvoicesSectionId =>
  INVOICES_SECTIONS.some((s) => s.id === value);

export const invoicesSectionPath = (id: InvoicesSectionId) => `/invoices/${id}`;

/**
 * A record open inside a list section — `/invoices/invoices?selected=12`. `useInvoicesSection` reads
 * `selected` out of the query string, so this is the addressable form of "this row is open" and the
 * thing a row anchor points at.
 */
export const invoicesRecordPath = (section: InvoicesSectionId, id: number) =>
  `${invoicesSectionPath(section)}?selected=${id}`;

/** A list section filtered to one customer — `useListFilters` reads `customer`. */
export const invoicesCustomerPath = (section: InvoicesSectionId, customerId: number) =>
  `${invoicesSectionPath(section)}?customer=${customerId}`;

/**
 * The payment editor, opened blank against an invoice — `/invoices/payments?edit=new&pay=12`.
 * `InvoicesView` reads both params (`edit` → which editor, `pay` → which invoice it prefills from), so
 * "record a payment for this invoice" is a whole address and not a callback.
 */
export const invoicesNewPaymentPath = (invoiceId: number) =>
  `${invoicesSectionPath('payments')}?edit=new&pay=${invoiceId}`;

/** The sidecar resource slug behind each list section. `dashboard` and `reports` have none. */
export const SECTION_RESOURCE = {
  invoices: 'invoices',
  estimates: 'estimates',
  recurring: 'recurring-invoices',
  payments: 'payments',
  expenses: 'expenses',
  customers: 'customers',
  items: 'items',
} as const satisfies Partial<Record<InvoicesSectionId, string>>;

export type ListSectionId = keyof typeof SECTION_RESOURCE;

export const isListSection = (id: InvoicesSectionId): id is ListSectionId => id in SECTION_RESOURCE;

// ── money ────────────────────────────────────────────────────────────────────────────────────────

/** An integer count of minor units, which upstream may hand over as either a number or a decimal string. */
export type Money = number | string | null | undefined;

export type Currency = {
  id: number;
  name: string;
  code: string;
  symbol: string;
  precision: number | string;
  thousand_separator: string;
  decimal_separator: string;
  /** true → "1.234,56 €"; false → "€1.234,56". */
  swap_currency_symbol: boolean | number;
};

// ── status vocabulary ────────────────────────────────────────────────────────────────────────────

// The real persisted statuses. DUE and OVERDUE are NOT among them — upstream computes those for filters
// and badges (`overdue` is a derived boolean on the invoice, `DUE` is a filter-only token), so they appear
// in the tone map and the filter vocabulary but never as a value you can write back.
export type InvoiceStatus = 'DRAFT' | 'SENT' | 'VIEWED' | 'COMPLETED';
export type PaidStatus = 'UNPAID' | 'PARTIALLY_PAID' | 'PAID';
export type EstimateStatus = 'DRAFT' | 'SENT' | 'VIEWED' | 'EXPIRED' | 'ACCEPTED' | 'REJECTED';
export type RecurringStatus = 'ACTIVE' | 'ON_HOLD' | 'COMPLETED';

/**
 * Tailwind classes per status token, matched hue-for-hue to upstream's badge components
 * (InvoiceStatusBadge / EstimateStatusBadge / PaidStatusBadge / RecurringInvoiceStatusBadge).
 *
 * The distinctions are finer than they look and are worth keeping: VIEWED is indigo where SENT is blue,
 * UNPAID is orange where OVERDUE is red, and PARTIALLY_PAID is cyan — so a glance down the column tells
 * you which of two similar states a row is in without reading the text.
 */
export const STATUS_TONE: Record<string, string> = {
  DRAFT: 'bg-muted text-muted-foreground ring-border',
  SENT: 'bg-blue-500/10 text-blue-600 ring-blue-500/20 dark:text-blue-400',
  VIEWED: 'bg-indigo-500/10 text-indigo-600 ring-indigo-500/20 dark:text-indigo-400',
  COMPLETED: 'bg-green-500/10 text-green-600 ring-green-500/20 dark:text-green-400',
  DUE: 'bg-amber-500/10 text-amber-600 ring-amber-500/20 dark:text-amber-400',
  OVERDUE: 'bg-red-500/10 text-red-600 ring-red-500/20 dark:text-red-400',
  UNPAID: 'bg-orange-500/10 text-orange-600 ring-orange-500/20 dark:text-orange-400',
  PARTIALLY_PAID: 'bg-cyan-500/10 text-cyan-600 ring-cyan-500/20 dark:text-cyan-400',
  PAID: 'bg-emerald-500/10 text-emerald-600 ring-emerald-500/20 dark:text-emerald-400',
  EXPIRED: 'bg-red-500/10 text-red-600 ring-red-500/20 dark:text-red-400',
  ACCEPTED: 'bg-emerald-500/10 text-emerald-600 ring-emerald-500/20 dark:text-emerald-400',
  REJECTED: 'bg-rose-500/10 text-rose-600 ring-rose-500/20 dark:text-rose-400',
  ACTIVE: 'bg-blue-500/10 text-blue-600 ring-blue-500/20 dark:text-blue-400',
  ON_HOLD: 'bg-amber-500/10 text-amber-600 ring-amber-500/20 dark:text-amber-400',
};

/** Status filter vocabulary per list, in upstream's own order. */
export const INVOICE_STATUS_FILTERS = ['DRAFT', 'DUE', 'SENT', 'VIEWED', 'COMPLETED'] as const;
export const INVOICE_PAID_FILTERS = ['UNPAID', 'PAID', 'PARTIALLY_PAID'] as const;
export const ESTIMATE_STATUS_FILTERS = ['DRAFT', 'SENT', 'VIEWED', 'EXPIRED', 'ACCEPTED', 'REJECTED'] as const;
export const RECURRING_STATUS_FILTERS = ['ACTIVE', 'ON_HOLD', 'COMPLETED'] as const;

/** Tabs above each list — `null` is "All". Mirrors upstream's tab sets exactly. */
export const INVOICE_TABS = [
  { id: null, label: 'All' },
  { id: 'DRAFT', label: 'Draft' },
  { id: 'SENT', label: 'Sent' },
  { id: 'DUE', label: 'Due' },
] as const;

export const ESTIMATE_TABS = [
  { id: null, label: 'All' },
  { id: 'DRAFT', label: 'Draft' },
  { id: 'SENT', label: 'Sent' },
] as const;

export const RECURRING_TABS = [
  { id: null, label: 'All' },
  { id: 'ACTIVE', label: 'Active' },
  { id: 'ON_HOLD', label: 'On hold' },
] as const;

/** Upstream paginates every list at 10. Matched so page numbers line up between the two UIs. */
export const PAGE_SIZE = 10;

export const humanStatus = (value: string | null | undefined): string =>
  (value ?? '')
    .replace(/_/g, ' ')
    .toLowerCase()
    .replace(/^./, (c) => c.toUpperCase());

// ── records ──────────────────────────────────────────────────────────────────────────────────────

export type Address = {
  id: number;
  name: string | null;
  address_street_1: string | null;
  address_street_2: string | null;
  city: string | null;
  state: string | null;
  zip: string | null;
  phone: string | null;
  country_id: number | null;
  country?: { id: number; code: string; name: string } | null;
};

export type Customer = {
  id: number;
  name: string;
  email: string | null;
  phone: string | null;
  contact_name: string | null;
  company_name: string | null;
  website: string | null;
  prefix: string | null;
  tax_id: string | null;
  currency_id: number | null;
  enable_portal: boolean;
  due_amount: Money;
  formatted_created_at: string | null;
  created_at: string | null;
  billing?: Address | null;
  shipping?: Address | null;
  // Deliberately unnamed — see the header note about embedded company payloads.
  company?: unknown;
};

export type Unit = { id: number; name: string };
export type TaxType = { id: number; name: string; percent: number | string; compound_tax: boolean | number };
export type Category = { id: number; name: string };
export type PaymentMethod = { id: number; name: string };

export type Item = {
  id: number;
  name: string;
  description: string | null;
  price: Money;
  unit_id: number | null;
  currency_id: number | null;
  tax_per_item: boolean;
  formatted_created_at: string | null;
  unit?: Unit | null;
};

/** A line on an invoice or estimate. Same shape either way. */
export type DocumentItem = {
  id: number;
  item_id: number | null;
  name: string;
  description: string | null;
  quantity: number | string;
  price: Money;
  discount_type: 'fixed' | 'percentage';
  discount: number | string;
  discount_val: Money;
  tax: Money;
  total: Money;
  unit_name: string | null;
  taxes?: DocumentTax[];
};

export type DocumentTax = {
  id?: number;
  tax_type_id: number | null;
  name?: string;
  percent: number | string;
  amount: Money;
  compound_tax?: boolean | number;
};

/** Shared spine of invoices, estimates and recurring invoices — they differ only at the edges. */
type DocumentBase = {
  id: number;
  status: string;
  notes: string | null;
  reference_number: string | null;
  customer_id: number;
  currency_id: number;
  unique_hash: string;
  template_name: string | null;
  sub_total: Money;
  total: Money;
  tax: Money;
  discount: number | string;
  discount_type: 'fixed' | 'percentage';
  discount_val: Money;
  tax_per_item: 'YES' | 'NO';
  discount_per_item: 'YES' | 'NO';
  sent: boolean;
  viewed: boolean;
  items: DocumentItem[];
  taxes?: DocumentTax[];
  customer?: Customer | null;
  formatted_created_at: string | null;
};

export type Invoice = DocumentBase & {
  invoice_number: string;
  invoice_date: string;
  due_date: string | null;
  paid_status: PaidStatus;
  due_amount: Money;
  overdue: boolean;
  allow_edit: boolean;
  invoice_pdf_url: string | null;
  formatted_invoice_date: string | null;
  formatted_due_date: string | null;
  recurring_invoice_id: number | null;
};

export type Estimate = DocumentBase & {
  estimate_number: string;
  estimate_date: string;
  expiry_date: string | null;
  estimate_pdf_url: string | null;
  formatted_estimate_date: string | null;
  formatted_expiry_date: string | null;
  /** Set once converted; the convert action is refused a second time. */
  invoice_id?: number | null;
};

export type RecurringInvoice = DocumentBase & {
  starts_at: string | null;
  next_invoice_at: string | null;
  /** A five-field cron expression, not a keyword — see FREQUENCY_PRESETS. */
  frequency: string | null;
  limit_by: 'NONE' | 'COUNT' | 'DATE' | string | null;
  limit_count: number | null;
  limit_date: string | null;
  send_automatically: boolean;
  due_amount: Money;
  formatted_starts_at: string | null;
  formatted_next_invoice_at: string | null;
  formatted_limit_date: string | null;
  /** Present only once the schedule has generated something. */
  invoices?: Invoice[];
};

/**
 * A recurring invoice's `frequency` is a raw cron expression, which is unreadable in a table column.
 *
 * These are upstream's own presets, cron string for cron string (`recurring-invoices/store.ts` →
 * `initFrequencies`), in its order — common business intervals first, then the short ones. A schedule
 * created outside these presets keeps a cron expression we do not recognise, and `frequencyLabel` shows
 * it verbatim rather than guessing.
 */
export const FREQUENCY_PRESETS = [
  { cron: '0 0 * * 0', label: 'Every week' },
  { cron: '0 0 */14 * *', label: 'Every 2 weeks' },
  { cron: '0 0 1 * *', label: 'Every month' },
  { cron: '0 0 1 */2 *', label: 'Every 2 months' },
  { cron: '0 0 1 */3 *', label: 'Every quarter' },
  { cron: '0 0 1 */6 *', label: 'Every 6 months' },
  { cron: '0 0 1 1 *', label: 'Every year' },
  { cron: '0 0 * * *', label: 'Every day' },
  { cron: '0 5 */15 * *', label: 'Every 15 days' },
  { cron: '0 * * * *', label: 'Every hour' },
  { cron: '* * * * *', label: 'Every minute' },
] as const;

export const frequencyLabel = (cron: string | null | undefined): string =>
  FREQUENCY_PRESETS.find((f) => f.cron === cron)?.label ?? cron ?? '—';

export type Payment = {
  id: number;
  payment_number: string;
  payment_date: string;
  amount: Money;
  notes: string | null;
  unique_hash: string;
  invoice_id: number | null;
  customer_id: number;
  payment_method_id: number | null;
  currency_id: number;
  transaction_id: string | null;
  payment_pdf_url: string | null;
  formatted_payment_date: string | null;
  formatted_created_at: string | null;
  customer?: Customer | null;
  invoice?: Invoice | null;
  payment_method?: PaymentMethod | null;
};

export type Expense = {
  id: number;
  expense_date: string;
  expense_number: string | null;
  amount: Money;
  notes: string | null;
  expense_category_id: number | null;
  payment_method_id: number | null;
  customer_id: number | null;
  currency_id: number;
  /** Absolute, rendered from the instance's APP_URL — usable in a browser, not from this process. */
  attachment_receipt_url: string | null;
  formatted_expense_date: string | null;
  formatted_created_at: string | null;
  /** `expense_category`, not `category` — the column is `expense_category_id` but the relation keeps the prefix. */
  expense_category?: Category | null;
  customer?: Customer | null;
  payment_method?: PaymentMethod | null;
};

// ── envelopes ────────────────────────────────────────────────────────────────────────────────────

export type PageMeta = {
  current_page: number;
  last_page: number;
  per_page: number;
  from: number | null;
  to: number | null;
  total: number;
};

export type Paginated<T> = { data: T[]; meta?: PageMeta };

export type Summary = {
  me: { id: number; name: string; email: string } | null;
  company: { id: number; name: string; unique_hash: string; address?: Address | null; logo?: string | null } | null;
  currency: Currency | null;
  settings: Record<string, string> | null;
  dashboard: Dashboard | null;
};

export type Dashboard = {
  total_amount_due: Money;
  total_customer_count: number;
  total_invoice_count: number;
  total_estimate_count: number;
  total_sales: Money;
  total_receipts: Money;
  total_expenses: Money;
  total_net_income: Money;
  recent_due_invoices: Invoice[];
  recent_estimates: Estimate[];
  chart_data: {
    months: string[];
    invoice_totals: Money[];
    expense_totals: Money[];
    receipt_totals: Money[];
    net_income_totals: Money[];
  } | null;
};

/**
 * `GET /customers/{id}/stats` — the customer plus a twelve-month chart.
 *
 * The chart keys are camelCase here (`invoiceTotals`) where the company dashboard's are snake_case
 * (`invoice_totals`). That is upstream's inconsistency, not a transcription slip; the two shapes cannot share
 * a type.
 */
export type CustomerStats = {
  data?: Customer;
  meta?: {
    chartData?: {
      months: string[];
      invoiceTotals: Money[];
      expenseTotals: Money[];
      receiptTotals: Money[];
      netProfits: Money[];
      netProfit: Money;
      salesTotal: Money;
      totalReceipts: Money;
      totalExpenses: Money;
    };
  };
};

export type Lookups = {
  customers: Customer[];
  items: Item[];
  units: Unit[];
  taxTypes: TaxType[];
  categories: Category[];
  paymentMethods: PaymentMethod[];
  currencies: Currency[];
};

export const REPORT_KINDS = [
  { id: 'sales-customers', label: 'Sales by customer' },
  { id: 'sales-items', label: 'Sales by item' },
  { id: 'tax-summary', label: 'Tax summary' },
  { id: 'profit-loss', label: 'Profit & loss' },
  { id: 'expenses', label: 'Expenses' },
] as const;

export type ReportKind = (typeof REPORT_KINDS)[number]['id'];
