import axios from 'axios';
import { electronApi } from './desktop';

/** A request body: any object the API validates (the API is the one that checks its fields). */
type Body = object;

/**
 * API base URL, resolved at REQUEST time rather than build time.
 *
 * Next.js inlines NEXT_PUBLIC_* into the bundle, and the desktop app ships a static
 * export — so a build-time-only value means repointing an installed app requires
 * rebuilding it. Instead the base is resolved on every request, highest priority first:
 *
 *   1. localStorage `api_base`     — set in-app (Server settings on the login screen)
 *   2. window.electronAPI.apiBase  — desktop config.json, readable synchronously because
 *                                    the preload passes it in; lets an installer pre-seed
 *                                    the URL before the app is ever opened
 *   3. NEXT_PUBLIC_API_BASE        — build-time default (dev, or a custom-built binary)
 *   4. the production host         — unchanged behaviour for existing builds and deploys
 *
 * Whatever server you point at must list this app's origin in its CORS allowlist
 * (`pharmacy-api/src/main.ts`), including `app://` for the packaged desktop build.
 */
const DEFAULT_API_BASE = 'https://pharmacy-api.devke.co.ke/api';
const BUILD_TIME_API_BASE = process.env.NEXT_PUBLIC_API_BASE || '';
export const API_BASE_STORAGE_KEY = 'api_base';

/**
 * Accepts what a person would actually type — `192.168.1.10:3001`, a trailing slash,
 * a bare origin — and returns something axios can use. Returns '' if unusable, so
 * callers fall through to the next source rather than issuing requests to nowhere.
 */
export function normaliseApiBase(raw: string | null | undefined): string {
    let value = (raw || '').trim();
    if (!value) return '';
    if (!/^https?:\/\//i.test(value)) value = `http://${value}`;
    let url: URL;
    try {
        url = new URL(value);
    } catch {
        return '';
    }
    // A bare origin is almost always meant to be the API root, which is mounted at /api.
    const path = url.pathname.replace(/\/+$/, '');
    return `${url.origin}${path || '/api'}`;
}

export function getApiBase(): string {
    if (typeof window !== 'undefined') {
        const stored = normaliseApiBase(window.localStorage.getItem(API_BASE_STORAGE_KEY));
        if (stored) return stored;
        const fromDesktop = normaliseApiBase(electronApi()?.apiBase);
        if (fromDesktop) return fromDesktop;
    }
    return BUILD_TIME_API_BASE || DEFAULT_API_BASE;
}

/** Persists a new base. Empty input clears the override and falls back down the chain. */
export function setApiBase(raw: string): string {
    const value = normaliseApiBase(raw);
    if (typeof window !== 'undefined') {
        if (value) window.localStorage.setItem(API_BASE_STORAGE_KEY, value);
        else window.localStorage.removeItem(API_BASE_STORAGE_KEY);
        // Desktop: also write config.json so the choice survives a cleared cache.
        electronApi()?.setApiBase?.(value);
    }
    return value || getApiBase();
}

/** Where the current value came from — shown in the Server settings dialog. */
export function getApiBaseSource(): 'saved' | 'desktop' | 'build' | 'default' {
    if (typeof window !== 'undefined') {
        if (normaliseApiBase(window.localStorage.getItem(API_BASE_STORAGE_KEY))) return 'saved';
        if (normaliseApiBase(electronApi()?.apiBase)) return 'desktop';
    }
    return BUILD_TIME_API_BASE ? 'build' : 'default';
}

export const resolveImageUrl = (url?: string) => {
    if (!url) return '';
    if (url.startsWith('/uploads/')) {
        const baseHost = getApiBase().replace(/\/api$/, '');
        return `${baseHost}${url}`;
    }
    return url;
};

const api = axios.create({
    headers: { 'Content-Type': 'application/json' },
});

api.interceptors.request.use((config) => {
    // Resolved per request, so a change in Server settings takes effect immediately
    // rather than on the next reload.
    config.baseURL = getApiBase();
    if (typeof window !== 'undefined') {
        const token = localStorage.getItem('token');
        if (token) config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

/** Fired on `window` when the API rejects this device's token; the layouts send the user to /login. */
export const SESSION_EXPIRED_EVENT = 'pharmapos:session-expired';

// PRD 0.4: the API now answers 401 to any request without a valid token, where it used to fall back
// to anonymous data. A 401 on a request that carried a token means the session has expired or been
// revoked, so end it. A wrong password on the login screen carries no token and is left to that screen.
/**
 * Fired on `window` with `detail: false` when a request can't reach the API at all (no response: server down,
 * network gone), and `detail: true` on the next answer after that. The web shell shows the design's offline
 * banner (spec 04 §9.4) in between.
 */
export const API_REACHABLE_EVENT = 'pharmapos:api-reachable';
let apiUnreachable = false;
const reachable = (ok: boolean) => {
    if (typeof window === 'undefined' || ok !== apiUnreachable) return;
    apiUnreachable = !ok;
    window.dispatchEvent(new CustomEvent(API_REACHABLE_EVENT, { detail: ok }));
};

api.interceptors.response.use((response) => {
    reachable(true);
    return response;
}, (error) => {
    if (error?.response) reachable(true);
    else if (error?.code !== 'ERR_CANCELED') reachable(false);
    const hadToken = !!error?.config?.headers?.Authorization;
    if (error?.response?.status === 401 && hadToken && typeof window !== 'undefined') {
        localStorage.removeItem('token');
        localStorage.removeItem('user');
        window.dispatchEvent(new Event(SESSION_EXPIRED_EVENT));
    }
    return Promise.reject(error);
});

// ─── Products ───
export const ProductsApi = {
    getAll: (search?: string, category?: string) =>
        api.get('/products', { params: { search, category } }).then(r => r.data),
    getOne: (id: number) => api.get(`/products/${id}`).then(r => r.data),
    create: (data: Body) => api.post('/products', data).then(r => r.data),
    update: (id: number, data: Body) => api.patch(`/products/${id}`, data).then(r => r.data),
    /** `reason` is one of the design's adjustment reasons; `note` shows in the activity log (Inventory API plan) */
    adjustStock: (id: number, quantity: number, reason?: string, note?: string) =>
        api.patch(`/products/${id}/stock`, { quantity, reason, note }).then(r => r.data),
    /** IN-08 (plan B): soft; refused while the product holds stock */
    delete: (id: number, reason?: string) => api.delete(`/products/${id}`, { data: reason ? { reason } : undefined }).then(r => r.data),
    deleted: () => api.get('/products/deleted').then(r => r.data as DeletedProductRow[]),
    restore: (id: number) => api.post(`/products/${id}/restore`).then(r => r.data as { message: string }),
    history: (id: number) => api.get(`/products/${id}/history`).then(r => r.data as ProductChangeRow[]),
    merges: () => api.get('/products/merges').then(r => r.data as MergeRow[]),
    mergePreview: (keep: number, merge: number) => api.get('/products/merge-preview', { params: { keep, merge } }).then(r => r.data as { keep: MergeSideRow; merge: MergeSideRow; problem: string | null }),
    /** IN-07: `id` is the duplicate, merged into `intoId` */
    merge: (id: number, intoId: number, take: { price: 'keep' | 'merge'; taxCode: 'keep' | 'merge' }, reason?: string) => api.post(`/products/${id}/merge`, { intoId, take, reason }).then(r => r.data as MergeRow),
    getCategories: () => api.get('/products/categories').then(r => r.data),
    getLowStock: () => api.get('/products/low-stock').then(r => r.data),
    getExpiring: () => api.get('/products/expiring').then(r => r.data),
    stockReport: (category?: string, branchId?: number | null) =>
        api.get('/products/stock-report', { params: { category, branchId: branchId ?? undefined } }).then(r => r.data),
    stockClosing: (asOf: string) =>
        api.get('/products/stock-closing', { params: { asOf } }).then(r => r.data),
    uploadImage: (id: number, file: File) => {
        const form = new FormData();
        form.append('image', file);
        return api.post(`/products/${id}/image`, form, {
            headers: { 'Content-Type': 'multipart/form-data' },
        }).then(r => r.data);
    },
};

// ─── Product housekeeping (plan B: IN-07, IN-08) ───
export type DeletedProductRow = { id: number; name: string; barcode: string; code: string | null; deletedBy: string; deletedAt: string; reason?: string; merged: boolean };
export type ProductChangeRow = { at: string; who: string; where: string; action: string; changes: { field: string; before?: string; after?: string }[] };
export type MergeRow = { ref: string; date: string; kept: string; merged: string; keptId: number; mergedId: number; stockMoved: number; salesMoved: number; batchesMoved: number; by: string };
export type MergeSideRow = { id: number; name: string; barcode: string; code: string | null; stock: number; batches: number; sales: number; price: number; taxCode: string };

// ─── Shelves (plan B: ST-05, IN-05) ───
export type ShelfRow = { id: number; code: string; description: string; branchId: number | null; branch: string; active: boolean; products: number };
export const ShelvesApi = {
    list: (branchId?: number) => api.get('/shelves', { params: { branchId } }).then(r => r.data as ShelfRow[]),
    create: (data: { code: string; description?: string; branchId?: number | null }) => api.post('/shelves', data).then(r => r.data as ShelfRow),
    update: (id: number, data: { code?: string; description?: string; branchId?: number | null; active?: boolean }) => api.patch(`/shelves/${id}`, data).then(r => r.data as ShelfRow),
    remove: (id: number) => api.delete(`/shelves/${id}`).then(r => r.data),
    assign: (rows: { code: string; shelf: string }[]) => api.post('/shelves/assign', { rows }).then(r => r.data as { moved: number; unchanged: number; problems: { row: number; message: string }[] }),
};

// ─── Sales ───
/** Plan E (PS-07): the promotions on a quoted basket, and what set its discounts. */
export type QuotePromotions = { applied: { promotionId: number; name: string; amount: number }[]; missed: { name: string; short: number }[]; total: number; source: 'Promotion' | 'Group' | 'Manual' | null; note: string | null };
export type SaleQuote = {
    subtotal: number; tax: number; total: number; goods: number; deliveryFee: number;
    lines: { productId: number; taxCode: string; taxRate: number; taxAmount: number; net: number }[];
    items: { productId: number; quantity: number; unitPrice: number; discount?: number; manual?: boolean; promo?: number }[];
    pricing: { groupId: number; group: string; priceGroup: string | null; discountPct: number } | null;
    promotions: QuotePromotions | null;
};

// ─── Promotions (plan E: ST-03, PS-07) ───
export type PromotionRow = {
    id: number; name: string; kind: 'Percent' | 'Fixed'; value: number; scope: 'Basket' | 'Category' | 'Product'; target: string | null; targetName: string | null;
    minBasket: number; maxBasket: number | null; start: string; end: string; branchIds: number[]; groupId: number | null; active: boolean;
    status: 'Off' | 'Scheduled' | 'Running' | 'Ended'; sales: number; given: number;
};
export type PromotionInput = Omit<PromotionRow, 'id' | 'targetName' | 'status' | 'sales' | 'given'>;
export const PromotionsApi = {
    list: () => api.get('/promotions').then(r => r.data as { promotions: PromotionRow[]; branches: { id: number; name: string }[]; categories: string[]; groups: { id: number; name: string }[] }),
    products: (q: string) => api.get('/promotions/products', { params: { q } }).then(r => r.data as { id: number; name: string; code: string | null; price: number; category: string }[]),
    create: (data: PromotionInput) => api.post('/promotions', data).then(r => r.data),
    update: (id: number, data: Partial<PromotionInput>) => api.patch(`/promotions/${id}`, data).then(r => r.data),
    remove: (id: number) => api.delete(`/promotions/${id}`).then(r => r.data),
};

// ─── Preorders (plan E: PS-14) ───
export type PreorderRow = {
    id: number; number: string; status: 'Active' | 'Collected' | 'Cancelled'; state: 'Waiting for stock' | 'Stock arrived' | 'Collected' | 'Cancelled';
    customerId: number; customer: string; phone: string; productId: number; product: string; code: string | null; quantity: number;
    expectedPrice: number; price: number; deposit: number; available: number; note: string | null; placedAt: string; placedBy: string | null;
    branchId: number | null; branch: string | null; notifiedAt: string | null; saleId: number | null; receiptId: string | null; closedAt: string | null; cancelReason: string | null; refunded: boolean;
};
export const PreordersApi = {
    list: (view: 'open' | 'closed' = 'open') => api.get('/preorders', { params: { view } }).then(r => r.data as PreorderRow[]),
    create: (data: { customerId?: number; customer?: string; phone?: string; productId: number; quantity: number; deposit?: number; method?: string; reference?: string; note?: string }) => api.post('/preorders', data).then(r => r.data),
    told: (id: number) => api.post(`/preorders/${id}/told`).then(r => r.data),
    cancel: (id: number, data: { reason: string; refund?: boolean; method?: string }) => api.post(`/preorders/${id}/cancel`, data).then(r => r.data),
    forTill: (id: number) => api.get(`/preorders/${id}/till`).then(r => r.data as { id: number; number: string; customerId: number; customer: string; phone: string; productId: number; product: string; quantity: number; deposit: number }),
};

// ─── Direct credit notes (plan E: PS-17) ───
export type DirectCreditRow = {
    id: number; number: string; customerId: number; customer: string; phone: string; amount: number; reason: string; details: string | null;
    status: 'Pending' | 'Approved' | 'Rejected'; raisedBy: number | null; raisedByName: string | null; raisedAt: string;
    decidedByName: string | null; decidedAt: string | null; decisionNote: string | null; branch: string | null;
};
export const DirectCreditsApi = {
    list: (status?: string) => api.get('/direct-credit-notes', { params: { status } }).then(r => r.data as DirectCreditRow[]),
    raise: (data: { customerId: number; amount: number; reason: string; details?: string }) => api.post('/direct-credit-notes', data).then(r => r.data),
    decide: (id: number, data: { approve: boolean; note?: string }) => api.post(`/direct-credit-notes/${id}/decide`, data).then(r => r.data),
};

export const SalesApi = {
    /** PRD 1.5 part 2: the API's totals for a basket (each line taxed by its product's tax code). POST /sales must agree. */
    /** Plan D: with `customerId` and `groupPricing`, `items` come back priced from the customer's group (lines marked `manual` untouched). */
    /** Plan E: with `promotions`, running promotions apply unless a line is priced by hand; against a group price the lower wins. */
    quote: (data: { items: { productId: number; quantity: number; unitPrice: number; discount?: number; manual?: boolean }[]; deliveryFee?: number; isEtims?: boolean; customerId?: number; groupPricing?: boolean; promotions?: boolean }) =>
        api.post('/sales/quote', data).then(r => r.data as SaleQuote),
    getAll: (from?: string, to?: string, branchId?: number | null, includeArchived?: boolean, archivedOnly?: boolean) =>
        api.get('/sales', { params: { from, to, branchId, includeArchived, archivedOnly } }).then(r => r.data),
    getOne: (id: number) => api.get(`/sales/${id}`).then(r => r.data),
    /** PS-22: one page of sales (default today) and the count and value of the whole filter. */
    list: (q: { from?: string; to?: string; sellerId?: number; cashierId?: number; q?: string; page?: number; pageSize?: number; archived?: boolean }) =>
        api.get('/sales/list', { params: q }).then(r => r.data as SalesListPage),
    /** PS-10: held now, or processed (sold or deleted) in a date range. */
    held: (view: 'held' | 'processed' = 'held', from?: string, to?: string) => api.get('/sales/held', { params: { view, from, to } }).then(r => r.data as HeldRow[]),
    deleteHeld: (id: number, reason: string) => api.delete(`/sales/held/${id}`, { data: { reason } }).then(r => r.data),
    create: (data: Body) => api.post('/sales', data).then(r => r.data),
    delete: (id: number) => api.delete(`/sales/${id}`).then(r => r.data),
    restore: (id: number) => api.patch(`/sales/${id}/restore`).then(r => r.data),
    todayRevenue: () => api.get('/sales/today-revenue').then(r => r.data),
    weekly: () => api.get('/sales/weekly').then(r => r.data),
    topProducts: () => api.get('/sales/top-products').then(r => r.data),
    stats: () => api.get('/sales/stats').then(r => r.data),
    report: (from?: string, to?: string, branchId?: number | null) =>
        api.get('/sales/report', { params: { from, to, branchId } }).then(r => r.data),
    vatReport: (from?: string, to?: string, branchId?: number | null) =>
        api.get('/sales/vat-report', { params: { from, to, branchId } }).then(r => r.data),
    initiateMpesa: (phone: string, amount: number) =>
        api.post('/sales/mpesa-stk', { phone, amount }).then(r => r.data),
    checkMpesaStatus: (checkoutRequestId: string) =>
        api.get(`/sales/mpesa-status/${checkoutRequestId}`).then(r => r.data),
};

// ─── Prescriptions ───
export const PrescriptionsApi = {
    getAll: (search?: string, status?: string) =>
        api.get('/prescriptions', { params: { search, status } }).then(r => r.data),
    getOne: (id: number) => api.get(`/prescriptions/${id}`).then(r => r.data),
    create: (data: Body) => api.post('/prescriptions', data).then(r => r.data),
    update: (id: number, data: Body) => api.patch(`/prescriptions/${id}`, data).then(r => r.data),
    dispense: (id: number) => api.patch(`/prescriptions/${id}/dispense`).then(r => r.data),
    cancel: (id: number) => api.patch(`/prescriptions/${id}/cancel`).then(r => r.data),
    delete: (id: number) => api.delete(`/prescriptions/${id}`).then(r => r.data),
    stats: () => api.get('/prescriptions/stats').then(r => r.data),
};

// ─── Suppliers ───
export const SuppliersApi = {
    getAll: (search?: string) =>
        api.get('/suppliers', { params: { search } }).then(r => r.data),
    getOne: (id: number) => api.get(`/suppliers/${id}`).then(r => r.data),
    create: (data: Body) => api.post('/suppliers', data).then(r => r.data),
    update: (id: number, data: Body) => api.patch(`/suppliers/${id}`, data).then(r => r.data),
    delete: (id: number) => api.delete(`/suppliers/${id}`).then(r => r.data),
    // Purchase Orders
    getAllPOs: (search?: string, status?: string) =>
        api.get('/suppliers/purchase-orders/all', { params: { search, status } }).then(r => r.data),
    getOnePO: (id: number) => api.get(`/suppliers/purchase-orders/${id}`).then(r => r.data),
    createPO: (data: Body) => api.post('/suppliers/purchase-orders', data).then(r => r.data),
    updatePO: (id: number, data: Body) =>
        api.patch(`/suppliers/purchase-orders/${id}`, data).then(r => r.data),
    receivePO: (id: number, items: { id: number; receivedQuantity: number }[]) =>
        api.patch(`/suppliers/purchase-orders/${id}/receive`, { items }).then(r => r.data),
    cancelPO: (id: number) =>
        api.patch(`/suppliers/purchase-orders/${id}/cancel`).then(r => r.data),
    deletePO: (id: number) => api.delete(`/suppliers/purchase-orders/${id}`).then(r => r.data),
    markPOItemImported: (id: number) =>
        api.patch(`/suppliers/purchase-order-items/${id}/import`).then(r => r.data),
    report: (from?: string, to?: string) =>
        api.get('/suppliers/report', { params: { from, to } }).then(r => r.data),
    productPurchaseReport: (from?: string, to?: string) =>
        api.get('/suppliers/purchase-orders/report', { params: { from, to } }).then(r => r.data),
    // ─── Accounts payable ───
    statements: () => api.get('/suppliers/statements').then(r => r.data),
    statement: (id: number) => api.get(`/suppliers/${id}/statement`).then(r => r.data),
    getBalance: (id: number) => api.get(`/suppliers/${id}/balance`).then(r => r.data),
    getPayments: (id: number) => api.get(`/suppliers/${id}/payments`).then(r => r.data),
    /** Plan I (Q2): `whtAmount` kept back with its `whtCertificate`, for suppliers marked for WHT. */
    recordPayment: (id: number, data: { amount: number; paymentDate?: string; method?: string; reference?: string; notes?: string; whtAmount?: number; whtCertificate?: string }) =>
        api.post(`/suppliers/${id}/payments`, data).then(r => r.data),
    // ─── Suppliers API plan (SU-01 to SU-07, 26 Oct 2026) ───
    /** SU-02: with what we owe each and what is overdue. Bank fields only for roles with Accounts/Payables Read. */
    list: (params: { search?: string; status?: string } = {}) => api.get('/suppliers/list', { params }).then(r => r.data as { total: number; rows: SupplierRow[] }),
    profile: (id: number) => api.get(`/suppliers/${id}/profile`).then(r => r.data as { supplier: SupplierRecord; owed: number; overdue: number; prices: number; creditExpected: number }),
    importCsv: (csv: string) => api.post('/suppliers/import', { csv }).then(r => r.data as { created: number; duplicates: number; invalid: number; results: { row: number; name: string; outcome: 'Created' | 'Duplicate' | 'Invalid'; message?: string }[] }),
    exportCsv: async () => {
        const res = await api.get('/suppliers/export', { responseType: 'blob' });
        downloadBlob(res.data, `suppliers-${new Date().toISOString().split("T")[0]}.csv`, 'text/csv');
    },
    /** SU-03 */
    prices: (id: number) => api.get(`/suppliers/${id}/prices`).then(r => r.data as SupplierPriceRow[]),
    setPrice: (id: number, productId: number, cost: number) => api.put(`/suppliers/${id}/prices/${productId}`, { cost }).then(r => r.data as SupplierPriceRow),
    removePrice: (id: number, productId: number) => api.delete(`/suppliers/${id}/prices/${productId}`).then(r => r.data),
    productPrices: (productId: number) => api.get(`/products/${productId}/supplier-prices`).then(r => r.data as { product: { id: number; name: string }; prices: { supplierId: number; supplier: string; cost: number; lastReceived: string | null; source: string; leadDays: number | null }[] }),
    /** SU-05 until GRNs exist */
    received: (id: number) => api.get(`/suppliers/${id}/received`).then(r => r.data as { poId: number; ref: string; status: string; orderDate: string; receivedDate: string | null; dueDate: string; value: number; lines: { product: string; ordered: number; received: number; unitCost: number; value: number }[] }[]),
    /** SU-06 */
    returnable: (id: number) => api.get(`/suppliers/${id}/returnable`).then(r => r.data as { id: number; productId: number; name: string; batch: string; expiry: string | null; onHand: number; unitCost: number; fromSupplier: boolean }[]),
    createReturn: (id: number, body: { lines: { batchId: number; quantity: number; reason: string }[]; note?: string }) => api.post(`/suppliers/${id}/returns`, body).then(r => r.data as SupplierReturnRow),
    returns: (params: { from?: string; to?: string; supplierId?: number } = {}) => api.get('/suppliers/returns', { params }).then(r => r.data as { summary: { returns: number; units: number; creditExpected: number }; rows: SupplierReturnRow[] }),
};

export type SupplierRecord = {
    id: number; name: string; kraPin: string | null; contactPerson: string | null; phone: string | null; email: string | null;
    postalAddress: string | null; address: string | null; city: string | null; country: string | null;
    bankName?: string | null; bankBranch?: string | null; bankAccount?: string | null; hasBankDetails: boolean;
    wht: boolean; creditLimit: number | null; termsDays: number; discountPct: number | null; leadDays: number | null; status: 'Active' | 'Inactive'; createdAt: string;
};
export type SupplierRow = SupplierRecord & { owed: number; overdue: number };
export type SupplierPriceRow = { productId: number; product: string; code: string | null; cost: number; lastReceived: string | null; source: 'Received' | 'Manual'; updatedBy: string | null; leadDays: number | null };
export type SupplierReturnRow = {
    id: number; ref: string; supplierId: number; supplier: string; date: string; status: string; creditExpected: number; note: string | null; by: string | null;
    lines: { productId: number; product: string; batch: string | null; expiry: string | null; quantity: number; unitCost: number; credit: number; reason: string }[];
};

// ─── Categories ───
export const CategoriesApi = {
    getAll: () => api.get('/categories').then(r => r.data),
    create: (data: { name: string; description?: string }) => api.post('/categories', data).then(r => r.data),
    update: (id: number, data: { name?: string; description?: string }) => api.patch(`/categories/${id}`, data).then(r => r.data),
    delete: (id: number) => api.delete(`/categories/${id}`).then(r => r.data),
};

// ─── Customers ───
export const CustomersApi = {
    getAll: (search?: string) => api.get('/customers', { params: { search } }).then(r => r.data),
    /** CU-01. A duplicate phone or KRA PIN is refused (409, with `duplicate`) unless `allowDuplicate`. */
    create: (data: CustomerFields & { name: string; allowDuplicate?: boolean }) => api.post('/customers', data).then(r => r.data),
    update: (id: number, data: CustomerFields & { allowDuplicate?: boolean }) => api.patch(`/customers/${id}`, data).then(r => r.data),
    /** CU-02: a page of customers with spend, visits and last visit. */
    list: (q: { search?: string; type?: string; groupId?: number | 'none'; page?: number; pageSize?: number }) => api.get('/customers/list', { params: q }).then(r => r.data as { page: number; pageSize: number; total: number; rows: CustomerRow[] }),
    /** CU-03, CU-05: the record, totals, a page of purchases, balance and headroom. */
    profile: (id: number, page = 1) => api.get(`/customers/${id}/profile`, { params: { page } }).then(r => r.data),
    importCsv: (csv: string) => api.post('/customers/import', { csv }).then(r => r.data as { created: number; duplicates: number; invalid: number; results: { row: number; name: string; outcome: 'Created' | 'Duplicate' | 'Invalid'; message?: string }[] }),
    exportCsv: async () => {
        const res = await api.get('/customers/export', { responseType: 'blob' });
        downloadBlob(res.data, `customers-${new Date().toISOString().split("T")[0]}.csv`, 'text/csv');
    },
    /** CU-07, CU-08: health data. Needs Customers/Medical records; every request is logged by the API. */
    medical: (id: number) => api.get(`/customers/${id}/medical`).then(r => r.data as MedicalRow[]),
    addMedical: (id: number, entry: { kind: string; date: string; data: Record<string, unknown> }) => api.post(`/customers/${id}/medical`, entry).then(r => r.data as MedicalRow),
    markMedicalError: (id: number, entryId: number, reason: string) => api.patch(`/customers/${id}/medical/${entryId}/error`, { reason }).then(r => r.data as MedicalRow),
    delete: (id: number) => api.delete(`/customers/${id}`).then(r => r.data),
    report: (from?: string, to?: string, branchId?: number | null) =>
        api.get('/customers/report', { params: { from, to, branchId } }).then(r => r.data),
    // ─── Accounts receivable ───
    statements: () => api.get('/customers/statements').then(r => r.data),
    statement: (id: number) => api.get(`/customers/${id}/statement`).then(r => r.data),
    getBalance: (id: number) => api.get(`/customers/${id}/balance`).then(r => r.data),
    getPayments: (id: number) => api.get(`/customers/${id}/payments`).then(r => r.data),
    recordPayment: (id: number, data: { amount: number; paymentDate?: string; method?: string; reference?: string; notes?: string }) =>
        api.post(`/customers/${id}/payments`, data).then(r => r.data),
};

export type CustomerFields = Partial<{
    name: string; phone: string; email: string; notes: string; creditLimit: number; type: 'Individual' | 'Company'; kraPin: string; etimsId: string;
    vatStatus: 'Vatable' | 'Exempt'; dateOfBirth: string; postalAddress: string; physicalAddress: string; country: string; city: string;
    contactPerson: string; bankName: string; bankAccount: string;
    // Plan D (CU-04, CU-09)
    groupId: number | null; schemeId: number | null; schemeMemberNo: string | null; schemeRelation: 'Principal' | 'Spouse' | 'Child' | null; schemePrincipal: string | null;
    schemeLimit: number | string | null; schemeValidUntil: string | null; schemeStatus: 'Active' | 'Suspended' | 'Expired' | null;
}>;
export type CustomerRow = CustomerFields & { id: number; name: string; createdAt: string; spend: number; visits: number; lastVisit: string | null; group?: string | null; scheme?: CustomerScheme | null };
/** Plan D: a customer's scheme as the list and profile show it (a membership past its end date reads Expired). */
export type CustomerScheme = { name: string; kind: string; copayPct: number; status: 'Active' | 'Suspended' | 'Expired' | null };

/* Plan D (DECISIONS.md 26 Oct 2026): price groups (IN-03), customer groups (CU-04), schemes (CU-09) */
export type PriceGroupRow = { id: number; name: string; active: boolean; prices: number; usedBy: string[] };
export type PriceGrid = { groups: { id: number; name: string; active: boolean }[]; products: { id: number; code: string; name: string; cost: number; retail: number }[]; prices: Record<string, number> };
export const PriceGroupsApi = {
    list: () => api.get('/price-groups').then(r => r.data as PriceGroupRow[]),
    grid: () => api.get('/price-groups/grid').then(r => r.data as PriceGrid),
    /** A null price removes it: the group pays retail. */
    savePrices: (prices: { productId: number; priceGroupId: number; price: number | null }[]) => api.put('/price-groups/prices', { prices }).then(r => r.data as { set: number; removed: number }),
    create: (name: string) => api.post('/price-groups', { name }).then(r => r.data),
    update: (id: number, data: { name?: string; active?: boolean }) => api.patch(`/price-groups/${id}`, data).then(r => r.data),
    remove: (id: number) => api.delete(`/price-groups/${id}`).then(r => r.data),
};
export type CustomerGroupRow = { id: number; name: string; priceGroupId: number | null; priceGroup: string | null; discountPct: number; active: boolean; members: number };
export const CustomerGroupsApi = {
    list: () => api.get('/customer-groups').then(r => r.data as CustomerGroupRow[]),
    create: (data: { name: string; priceGroupId: number | null; discountPct: number }) => api.post('/customer-groups', data).then(r => r.data),
    update: (id: number, data: Partial<{ name: string; priceGroupId: number | null; discountPct: number; active: boolean }>) => api.patch(`/customer-groups/${id}`, data).then(r => r.data),
    remove: (id: number) => api.delete(`/customer-groups/${id}`).then(r => r.data),
};
export type SchemeRow = { id: number; name: string; kind: 'Insurance' | 'Medical scheme' | 'Corporate'; copayPct: number; defaultLimit: number | null; claimWithinDays: number; active: boolean; members: number };
export const SchemesApi = {
    list: () => api.get('/schemes').then(r => r.data as SchemeRow[]),
    create: (data: Partial<Record<keyof SchemeRow, unknown>>) => api.post('/schemes', data).then(r => r.data),
    update: (id: number, data: Partial<Record<keyof SchemeRow, unknown>>) => api.patch(`/schemes/${id}`, data).then(r => r.data),
    remove: (id: number) => api.delete(`/schemes/${id}`).then(r => r.data),
};
export type MedicalRow = {
    id: number; kind: 'Diagnosis' | 'Allergy' | 'Lab result' | 'Sick-off note'; entryDate: string; data: Record<string, unknown>;
    recordedByName: string; enteredInError: boolean; errorReason: string | null; errorByName: string | null; errorAt: string | null; createdAt: string;
};

// ─── Users ───
export const UsersApi = {
    getAll: () => api.get('/users').then(r => r.data),
    getOne: (id: number) => api.get(`/users/${id}`).then(r => r.data),
    create: (data: Body) => api.post('/users', data).then(r => r.data),
    update: (id: number, data: Body) => api.patch(`/users/${id}`, data).then(r => r.data),
    delete: (id: number) => api.delete(`/users/${id}`).then(r => r.data),
};

// ─── Setup lists (IN-02) and tax codes (ST-07) ───
export type SetupListName = 'categories' | 'sub-categories' | 'brands' | 'units' | 'waste-reasons';
export const SetupListsApi = {
    list: (list: SetupListName) => api.get(`/setup-lists/${list}`).then(r => r.data),
    add: (list: SetupListName, item: { name: string; short?: string | null; parent?: string | null }) => api.post(`/setup-lists/${list}`, item).then(r => r.data),
    update: (list: SetupListName, id: string, patch: { name?: string; short?: string | null; parent?: string | null; active?: boolean }) => api.patch(`/setup-lists/${list}/${id}`, patch).then(r => r.data),
    remove: (list: SetupListName, id: string) => api.delete(`/setup-lists/${list}/${id}`).then(r => r.data),
};
export const TaxCodesApi = {
    list: () => api.get('/tax-codes').then(r => r.data),
    add: (t: { code: string; name: string; rate: number; type: string }) => api.post('/tax-codes', t).then(r => r.data),
    update: (id: number, patch: Partial<{ code: string; name: string; rate: number; type: string; active: boolean }>) => api.patch(`/tax-codes/${id}`, patch).then(r => r.data),
    remove: (id: number) => api.delete(`/tax-codes/${id}`).then(r => r.data),
};

// ─── Roles & permissions (PRD 1.1) ───
export const RolesApi = {
    /** DB-14: the panels and which each role sees */
    panelGrid: () => api.get('/roles/dashboard-panels').then(r => r.data as { panels: DashboardPanelDef[]; roles: { id: number; name: string; builtIn: boolean; locked: boolean; panels: string[] }[] }),
    setPanels: (id: number, panels: string[]) => api.put(`/roles/${id}/dashboard-panels`, { panels }).then(r => r.data),
    list: () => api.get('/roles').then(r => r.data),
    catalog: () => api.get('/roles/catalog').then(r => r.data),
    get: (id: number) => api.get(`/roles/${id}`).then(r => r.data),
    create: (name: string, copyFrom?: number) => api.post('/roles', { name, copyFrom }).then(r => r.data),
    rename: (id: number, name: string) => api.patch(`/roles/${id}`, { name }).then(r => r.data),
    remove: (id: number) => api.delete(`/roles/${id}`).then(r => r.data),
    setGrants: (id: number, grants: Record<string, string[]>) => api.put(`/roles/${id}/permissions`, { grants }).then(r => r.data),
    copyFrom: (id: number, sourceId: number) => api.post(`/roles/${id}/copy-from/${sourceId}`).then(r => r.data),
};

/** What the signed-in user's current role may do: page id -> actions. */
export const MeApi = {
    permissions: () => api.get('/auth/me/permissions').then(r => r.data as { role: string; superAdmin: boolean; branchId: number | null; grants: Record<string, string[]> }),
};

// ─── Dashboard ───
export type DashboardPanelDef = { id: string; title: string; description: string; group: 'Headline figures' | 'Money' | 'Sales' | 'Stock'; sensitive?: boolean; span?: 3 | 4 | 6 | 8 | 12; ref: string };
export const DashboardApi = {
    /** DB-14: the panels the caller's role sees */
    myPanels: () => api.get('/dashboard/my-panels').then(r => r.data as { panels: DashboardPanelDef[] }),
    /** DB-02: one panel for the applied filters; 403 when the role doesn't have it */
    panel: <T = unknown>(id: string, p: { from?: string; to?: string; branchId?: number | null }) => api.get(`/dashboard/panels/${id}`, { params: Object.fromEntries(Object.entries(p).filter(([, v]) => v != null && v !== '')) }).then(r => r.data as T),
    getStats: () => api.get('/dashboard/stats').then(r => r.data),
    // Plan O: `branchId` narrows for a role with "All branches"; a user kept to a branch always gets their own
    getInsights: (days = 90, branchId?: number | null) => api.get('/dashboard/insights', { params: { days, branchId: branchId ?? undefined } }).then(r => r.data),
    // Returns { available: false, reason } when no LLM key is configured — callers hide the block.
    getInsightsNarrative: (days = 90, branchId?: number | null) => api.get('/dashboard/insights/narrative', { params: { days, branchId: branchId ?? undefined } }).then(r => r.data),
};

/** A row of `GET /sales/list`. */
export type SalesListRow = {
    id: number; receipt: string; createdAt: string; branch: string; cashier: string; seller: string; customer?: string; method: string;
    total: number; tax: number; subtotal: number; deliveryFee: number; etims: boolean; archived: boolean;
    status: 'Completed' | 'Part returned' | 'Returned'; returnedQty: number; refunded: number;
    items: { productId: number; product: string; category: string; qty: number; unitPrice: number; discount: number; taxCode: string | null; taxRate: number | null; rx: boolean; returned: number }[];
};
export type SalesListPage = { from: string; to: string; page: number; pageSize: number; total: number; totalValue: number; rows: SalesListRow[] };
export type HeldRow = { id: number; ref: string; heldAt: string; cashier: string; customer?: string; items: number; total: number; outcome?: 'Sold' | 'Deleted'; by?: string; at?: string; saleRef?: string; reason?: string };

// ─── Auth ───
export const AuthApi = {
    login: (username: string, password: string) =>
        api.post('/auth/login', { username, password }).then(r => r.data),
    getCashiers: () => api.get('/auth/cashiers').then(r => r.data),
};

// ─── Audit ───
export const AuditApi = {
    /** ST-15: `branchId` (the user's branch) and `entity` (the module) narrow; each row carries `branchName`, and `entities` lists the modules. */
    getAll: (params?: { userId?: number; action?: string; from?: string; to?: string; limit?: number; page?: number; pharmacyId?: number; branchId?: number; entity?: string }) =>
        api.get('/audit', { params }).then(r => r.data),
};

// ─── Locations (ST-04), branch money accounts (ST-02) and alerts (ST-14): plan C ───
export type LocationRow = { id: number; name: string; physical: boolean; showInPos: boolean; isDefault: boolean; units: number; details: string | null; system: boolean; branchId: number };
export const LocationsApi = {
    list: (branchId: number) => api.get('/locations', { params: { branchId } }).then(r => r.data as LocationRow[]),
    create: (data: { branchId: number; name: string; physical?: boolean; showInPos?: boolean; details?: string }) => api.post('/locations', data).then(r => r.data as LocationRow),
    update: (id: number, data: { name?: string; physical?: boolean; showInPos?: boolean; isDefault?: boolean; details?: string }) => api.patch(`/locations/${id}`, data).then(r => r.data as LocationRow),
    remove: (id: number) => api.delete(`/locations/${id}`).then(r => r.data),
};
export type MoneyLinksRow = { branch: { id: number; name: string }; links: { role: string; accountId: number; account: string }[]; accounts: { id: number; code: string; name: string; takenBy: string | null }[] };
export const MoneyAccountsApi = {
    get: (branchId: number) => api.get(`/ledger/branches/${branchId}/money-accounts`).then(r => r.data as MoneyLinksRow),
    save: (branchId: number, links: { role: string; accountId: number }[]) => api.put(`/ledger/branches/${branchId}/money-accounts`, { links }).then(r => r.data as MoneyLinksRow),
};
export type AlertRuleRow = { kind: string; name: string; when: string; unit: string; on: boolean; threshold: number; roles: string[]; channels: { app: boolean; sms: boolean; email: boolean }; saved: boolean };
export type AlertRow = { id: string; tone: 'warning' | 'error' | 'info'; word: string; text: string; href: string; count: number };
export const AlertsApi = {
    mine: () => api.get('/alerts').then(r => r.data as { at: string; alerts: AlertRow[] }),
    rules: () => api.get('/alerts/rules').then(r => r.data as { rules: AlertRuleRow[]; roles: string[] }),
    saveRules: (rules: Pick<AlertRuleRow, 'kind' | 'on' | 'threshold' | 'roles' | 'channels'>[]) => api.put('/alerts/rules', { rules }).then(r => r.data as { rules: AlertRuleRow[]; roles: string[] }),
};

// ─── Pharmacies (SuperAdmin only) ───
export const PharmaciesApi = {
    getAll: () => api.get('/pharmacies').then(r => r.data),
    getOne: (id: number) => api.get(`/pharmacies/${id}`).then(r => r.data),
    getStats: (id: number) => api.get(`/pharmacies/${id}/stats`).then(r => r.data),
    create: (data: Body) => api.post('/pharmacies', data).then(r => r.data),
    update: (id: number, data: Body) => api.patch(`/pharmacies/${id}`, data).then(r => r.data),
    delete: (id: number) => api.delete(`/pharmacies/${id}`).then(r => r.data),
    clearSeedData: (id: number) => api.delete(`/pharmacies/${id}/seed-data`).then(r => r.data),
};

// ─── Expenses ───
export const ExpensesApi = {
    getAll: (from?: string, to?: string, category?: string, branchId?: number | null) =>
        api.get('/expenses', { params: { from, to, category, branchId } }).then(r => r.data),
    getOne: (id: number) => api.get(`/expenses/${id}`).then(r => r.data),
    create: (data: ExpenseInput) => api.post('/expenses', data).then(r => r.data as ExpenseRecord),
    update: (id: number, data: Partial<ExpenseInput>) => api.patch(`/expenses/${id}`, data).then(r => r.data as ExpenseRecord),
    /** Q5: kept and marked deleted, its posting reversed. */
    delete: (id: number, reason?: string) => api.delete(`/expenses/${id}`, { data: { reason } }).then(r => r.data),
    getCategories: () => api.get('/expenses/categories').then(r => r.data),
    summary: (from?: string, to?: string, branchId?: number | null) =>
        api.get('/expenses/summary', { params: { from, to, branchId } }).then(r => r.data),
};

/** AC-16 (Accounts API Part 1). Older clients may still send `category` and `paymentMethod`. */
export type ExpenseInput = {
    categoryAccountId?: number; paidFromAccountId?: number; category?: string; paymentMethod?: string;
    description: string; amount: number; expenseDate: string; vatClaimable?: boolean; supplierId?: number | null; cuNumber?: string | null;
    reference?: string | null; notes?: string | null; payee?: string | null; branchId?: number | null;
};
export type ExpenseRecord = {
    id: number; category: string; description: string; amount: number; expenseDate: string; paymentMethod: string; payee: string | null; reference: string | null; notes: string | null;
    categoryAccountId: number | null; paidFromAccountId: number | null; vatClaimable: boolean; supplierId: number | null; cuNumber: string | null; branchId: number | null;
    branch?: { id: number; name: string } | null; recordedBy?: { name?: string; username?: string } | null; createdAt: string;
};

// ─── General ledger (Accounts API Part 1: AC-01..AC-06, AC-14) ───
export type LedgerAccountRow = { id: number; code: string; name: string; type: 'Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense'; parent?: string; branchId: number | null; branch?: string; description?: string; system: boolean; active: boolean; hasPostings: boolean; balance: number };
export type LedgerStatementRow = { code: string; name: string; amounts: number[] };
export type LedgerTxn = { id: number; number: string; date: string; source: string; reference: string | null; details: string; amount: number; status: 'Posted' | 'Reversed'; reversalOf: number | null; by: string | null; branch: string; debit: string; credit: string; lines: { accountId: number; account: string; debit: number; credit: number; memo: string | null }[] };
const scopeParams = (p: Record<string, unknown>) => Object.fromEntries(Object.entries(p).filter(([, v]) => v !== undefined && v !== null && v !== ''));
export const LedgerApi = {
    accounts: (p: { asAt?: string; branchId?: number | null } = {}) => api.get('/ledger/accounts', { params: scopeParams(p) }).then(r => r.data as LedgerAccountRow[]),
    addAccount: (a: { code: string; name: string; type: string; parentCode?: string | null; description?: string | null }) => api.post('/ledger/accounts', a).then(r => r.data as LedgerAccountRow),
    updateAccount: (id: number, a: Partial<{ code: string; name: string; type: string; parentCode: string | null; description: string | null; active: boolean }>) => api.patch(`/ledger/accounts/${id}`, a).then(r => r.data as LedgerAccountRow),
    deleteAccount: (id: number) => api.delete(`/ledger/accounts/${id}`).then(r => r.data),
    statement: (id: number, p: { from?: string; to?: string; branchId?: number | null } = {}) => api.get(`/ledger/accounts/${id}/statement`, { params: scopeParams(p) }).then(r => r.data as { account: { id: number; code: string; name: string; type: LedgerAccountRow['type'] }; from: string; to: string; opening: number; postings: { id: string; entryId: number; date: string; ref: string; details: string; debit: number; credit: number }[] }),
    trialBalance: (p: { asAt?: string; branchId?: number | null } = {}) => api.get('/ledger/trial-balance', { params: scopeParams(p) }).then(r => r.data as { asAt: string; debit: number; credit: number; balanced: boolean; lines: { id: number; code: string; name: string; type: string; parentCode: string | null; debit: number; credit: number; balance: number }[] }),
    profitAndLoss: (periods: { from: string; to: string }[], branchId?: number | null) => api.get('/ledger/profit-and-loss', { params: scopeParams({ periods: periods.map((p) => `${p.from}:${p.to}`).join(','), branchId }) }).then(r => r.data as { periods: string[]; income: LedgerStatementRow[]; costOfSales: LedgerStatementRow[]; expenses: LedgerStatementRow[]; profit: number[] }),
    balanceSheet: (dates: string[], branchId?: number | null) => api.get('/ledger/balance-sheet', { params: scopeParams({ dates: dates.join(','), branchId }) }).then(r => r.data as { periods: string[]; assets: LedgerStatementRow[]; liabilities: LedgerStatementRow[]; equity: LedgerStatementRow[]; totals: { assets: number; liabilitiesAndEquity: number }[] }),
    transactions: (p: { from?: string; to?: string; search?: string; source?: string; branchId?: number | null; page?: number; pageSize?: number } = {}) => api.get('/ledger/transactions', { params: scopeParams(p) }).then(r => r.data as { page: number; pageSize: number; total: number; rows: LedgerTxn[] }),
    postJournal: (j: { date: string; details: string; lines: { accountId: number; debit?: number; credit?: number; memo?: string }[]; branchId?: number | null }) => api.post('/ledger/journals', j).then(r => r.data),
    reverse: (entryId: number, reason: string) => api.post(`/ledger/entries/${entryId}/reverse`, { reason }).then(r => r.data),
    openings: () => api.get('/ledger/openings').then(r => r.data as { started: string | null; lines: { accountId: number; debit: number; credit: number; from: string }[] }),
    postOpenings: (body: { date?: string; lines: { accountId: number; debit: number; credit: number }[] }) => api.post('/ledger/openings', body).then(r => r.data),
    pending: () => api.get('/ledger/pending').then(r => r.data as { id: number; source: string; sourceId: number; sourceKey: string; error: string; attempts: number; updatedAt: string }[]),
    retryPending: () => api.post('/ledger/pending/retry').then(r => r.data as { tried: number; posted: number }),
    expenseAccounts: () => api.get('/ledger/expense-accounts').then(r => r.data as { categories: { id: number; code: string; name: string }[]; payers: { id: number; code: string; name: string; branchId: number | null }[] }),
};

// ─── Exports (PDF / Excel) ───
export interface ExportColumn { key: string; label: string; type?: "text" | "number" | "money"; width?: number; }
export interface ExportSheet {
    name: string;
    columns: ExportColumn[];
    rows: Record<string, unknown>[];
    summary?: { label: string; value: string }[];
}
export interface ExportPayload { title: string; subtitle?: string; meta?: string[]; sheets: ExportSheet[]; }

/**
 * Downloads go through axios rather than window.open so the Bearer token is attached.
 * A plain URL open cannot carry the header, which is why the older CSV links had to be
 * reachable unauthenticated — do not reintroduce that pattern.
 */
const downloadBlob = (data: BlobPart, filename: string, mime: string) => {
    const url = URL.createObjectURL(new Blob([data], { type: mime }));
    const a = document.createElement("a");
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
};

const slug = (s: string) =>
    (s || "report").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "report";

export const ExportsApi = {
    excel: async (payload: ExportPayload) => {
        const res = await api.post('/exports/excel', payload, { responseType: 'blob' });
        downloadBlob(res.data, `${slug(payload.title)}-${new Date().toISOString().split("T")[0]}.xlsx`,
            'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    },
    pdf: async (payload: ExportPayload) => {
        const res = await api.post('/exports/pdf', payload, { responseType: 'blob' });
        downloadBlob(res.data, `${slug(payload.title)}-${new Date().toISOString().split("T")[0]}.pdf`, 'application/pdf');
    },
    /** RP-01: the report on screen as CSV, built by the API from the same payload as Excel and PDF */
    csvReport: async (payload: ExportPayload) => {
        const res = await api.post('/exports/csv', payload, { responseType: 'blob' });
        downloadBlob(res.data, `${slug(payload.title)}-${new Date().toISOString().split("T")[0]}.csv`, 'text/csv');
    },
    /** Authenticated CSV download — replaces the old unauthenticated window.open links */
    csv: async (path: string, filename: string) => {
        const res = await api.get(path, { responseType: 'blob' });
        downloadBlob(res.data, filename, 'text/csv');
    },
};

// ─── Settings ───
export const SettingsApi = {
    getAll: () => api.get('/settings').then(r => r.data),
    save: (data: Record<string, string>) => api.patch('/settings', data).then(r => r.data),
    /** Authenticated download of the settings backup (secrets are not in it, PRD 0.3) */
    downloadBackup: async () => {
        const res = await api.get('/settings/export', { responseType: 'blob' });
        downloadBlob(res.data, `settings-backup-${new Date().toISOString().split("T")[0]}.json`, 'application/json');
    },
};

// ─── Branches ───
export const BranchesApi = {
    getAll: () => api.get('/branches').then(r => r.data),
    create: (data: { name: string; address?: string; phone?: string; status?: string }) =>
        api.post('/branches', data).then(r => r.data),
    update: (id: number, data: Partial<{ name: string; address: string; phone: string; status: string }>) =>
        api.patch(`/branches/${id}`, data).then(r => r.data),
    delete: (id: number) => api.delete(`/branches/${id}`).then(r => r.data),
};

// ─── Shifts ───
export const ShiftsApi = {
    getActive: () => api.get('/shifts/active').then(r => r.data),
    /** PS-19: `startMpesa` and `note` from the design's open form (a note is required when the count differs from the last close). */
    open: (startBalance: number, opening?: { startMpesa: number; note?: string }) =>
        api.post('/shifts/open', { startBalance, ...opening }).then(r => r.data),
    lastClose: () => api.get('/shifts/last-close').then(r => (r.data?.shiftId ? r.data : null)),
    /** ST-02: where a drop can go from this branch (its safe and banks for cash, its banks for M-Pesa). */
    dropAccounts: () => api.get('/shifts/drop-accounts').then(r => r.data as { id: number; code: string; name: string; kind: 'Cash' | 'M-Pesa' }[]),
    /** PS-20: with `leftInDrawer`, a tender that doesn't balance needs `notes`. */
    close: (data: { actualCash: number; actualMpesa: number; actualCard: number; leftInDrawer?: number; notes?: string }) =>
        api.post('/shifts/close', data).then(r => r.data),
    /** PS-15: a reference always, a witness for cash; `accountId` from `dropAccounts()` (ST-02), or older `destination` Bank or Safe. */
    recordDrop: (data: { amount: number; type: string; accountId?: number; destination?: string; reference: string; witness?: string; notes?: string }) =>
        api.post('/shifts/drops', data).then(r => r.data),
    getHistory: (from?: string, to?: string) => {
        const params = new URLSearchParams();
        if (from) params.append('from', from);
        if (to) params.append('to', to);
        const query = params.toString() ? `?${params.toString()}` : '';
        return api.get(`/shifts/history${query}`).then(r => r.data);
    },
    getZReport: (id: number) => api.get(`/shifts/${id}/z-report`).then(r => r.data),
};

// ─── Credit Notes ───
export const CreditNotesApi = {
    getAll: () => api.get('/sales/credit-notes').then(r => r.data),
    create: (data: {
        saleId: number;
        type: string;
        reason?: string;
        /** PS-18: `reason` and `restock` per line; older screens send neither. */
        returnedItems: { productId: number; quantity: number; unitPrice?: number; reason?: string; restock?: boolean }[];
        exchangeItems?: { productId: number; quantity: number; unitPrice: number }[];
        /** Checked by the API: a different figure is refused (409) with the right `refundAmount`. */
        refundAmount?: number;
        refundMethod?: 'Cash' | 'M-Pesa' | 'Card';
    }) => api.post('/sales/credit-notes', data).then(r => r.data),
    getReturnsReport: () => api.get('/sales/credit-notes/returns-report').then(r => r.data),
    getRefundsReport: () => api.get('/sales/credit-notes/refunds-report').then(r => r.data),
};

export default api;

// ─── Reports (Reports API plan, 26 Oct 2026: RP-01..RP-15) ───
type ReportRange = { from?: string; to?: string; branchId?: number | null };
const rp = (p: Record<string, unknown>) => Object.fromEntries(Object.entries(p).filter(([, v]) => v !== undefined && v !== null && v !== ''));
export type ReportSaleRow = {
    saleId: number; receipt: string; at: string; customer: string; cashier: string; branch: string; total: number; vat: number; voided: boolean; onAccount: number;
    items: { name: string; qty: number; unitPrice: number; discount: number; total: number }[];
    payments: { method: string; amount: number; reference: string | null }[];
    returns: { ref: string; type: string; amount: number; method: string | null; at: string }[];
};
export const ReportsApi = {
    // Plan B: branch stock (RP-03, RP-04) and purchasing (IN-19)
    branchTotals: (branchId?: number) => api.get('/reports/branch-totals', { params: { branchId } }).then(r => r.data as { rows: { branchId: number; branch: string; items: number; units: number; cost?: number; selling: number }[]; inTransit: { items: number; units: number; cost?: number; selling: number } | null; total: { items: number; units: number; cost?: number; selling: number } }),
    branchComparison: (p: ReportRange) => api.get('/reports/branch-comparison', { params: rp(p) }).then(r => r.data as { period: { from: string; to: string }; branches: string[]; rows: { productId: number; item: string; unit: string; reorder: number; soldInPeriod: number; qty: Record<string, number> }[] }),
    goodsReturns: (p: ReportRange) => api.get('/reports/goods-returns', { params: rp(p) }).then(r => r.data as { summary: { returns: number; value: number }; rows: { id: number; ref: string; date: string; supplier: string; items: string; value: number; status: string; credited: number }[] }),
    purchaseAgeing: () => api.get('/reports/purchase-ageing').then(r => r.data as { asOf: string; labels: string[]; rows: { supplierId: number; supplier: string; buckets: [number, number, number, number] }[] }),
    deadStock: (p: ReportRange) => api.get('/reports/dead-stock', { params: rp(p) }).then(r => r.data as { note: string; summary: { items: number; qty: number; costValue?: number; retailValue: number }; rows: { productId: number; code: string | null; name: string; category: string | null; qty: number; costValue?: number; retailValue: number; lastSold: string | null }[] }),
    dailySales: (p: ReportRange) => api.get('/reports/daily-sales', { params: rp(p) }).then(r => r.data as { totals: { receipts: number; gross: number; net: number; vat: number }; rows: { date: string; receipts: number; gross: number; net: number; vat: number; returns: number; byMethod: Record<string, number> }[] }),
    dailyItems: (p: ReportRange) => api.get('/reports/daily-items', { params: rp(p) }).then(r => r.data as { rows: { date: string; productId: number; product: string; qty: number; revenue: number }[] }),
    soldItems: (p: ReportRange) => api.get('/reports/sold-items', { params: rp(p) }).then(r => r.data as { summary: { products: number; qty: number; revenue: number; cost?: number }; rows: { productId: number; product: string; category: string; rx: boolean; qty: number; receipts: number; revenue: number; cost?: number; margin?: number }[] }),
    payments: (method: string, p: ReportRange) => api.get('/reports/payments', { params: rp({ method, ...p }) }).then(r => r.data as { summary: { payments: number; amount: number }; rows: { id: number; saleId: number; receipt: string; at: string; amount: number; reference: string | null; cashier: string; branch: string }[] }),
    mpesaLog: (p: ReportRange) => api.get('/reports/mpesa-log', { params: rp(p) }).then(r => r.data as { summary: { prompts: number; paid: number; failed: number; pending: number; paidAmount: number; paidNoSale: number }; rows: { id: string; at: string; amount: number; phone: string; status: string; receipt: string | null; result: string | null; environment: string | null; saleId: number | null; sale: string | null }[] }),
    nonEtims: (p: ReportRange) => api.get('/reports/non-etims', { params: rp(p) }).then(r => r.data as { summary: { sales: number; total: number; vat: number; shareOfGross: number }; rows: { saleId: number; receipt: string; at: string; total: number; vat: number; method: string; cashier: string }[] }),
    users: (p: ReportRange) => api.get('/reports/users', { params: rp(p) }).then(r => r.data as { rows: { userId: number; name: string; role: string; rungUp: number; rungUpTotal: number; sold: number; soldTotal: number; returns: number; refunded: number }[] }),
    sales: (p: ReportRange & { activity?: boolean; search?: string; page?: number; pageSize?: number }) => api.get('/reports/sales', { params: rp({ ...p, activity: p.activity ? 'true' : undefined }) }).then(r => r.data as { page: number; pageSize: number; total: number; rows: ReportSaleRow[] }),
    summary: (p: ReportRange) => api.get('/reports/summary', { params: rp(p) }).then(r => r.data as {
        period: { from: string; to: string }; previous: { from: string; to: string }; totals: { receipts: number; gross: number; net: number; vat: number }; previousTotals: { receipts: number; gross: number; net: number; vat: number };
        trend: { date: string; current: number; previous: number }[]; heatmap: { days: string[]; hours: number[]; counts: number[][] };
        categories: { category: string; revenue: number; cost?: number }[]; rxOtc: { rx: number; otc: number; rxProducts: number; products: number }; byTender: { tender: string; amount: number; receipts: number }[];
    }),
    monthly: (months: number, branchId?: number | null) => api.get('/reports/monthly', { params: rp({ months, branchId }) }).then(r => r.data as { months: { month: string; receipts: number; gross: number; net: number; vat: number; byMethod: Record<string, number>; partial: boolean }[] }),
    general: (p: ReportRange) => api.get('/reports/general', { params: rp(p) }).then(r => r.data as { branchNote: string | null; invoices: number; invoicesNote: string; posSales: number; receipts: number; byTender: { tender: string; amount: number }[]; creditNotes: { count: number; amount: number }; grossAfterReturns: number; purchases: number; salesLessPurchases: number; note: string }),
};

// ─── Inventory purchasing (IN-14 to IN-18, 26 Oct 2026) ───
export type PoLine = { id: number; productId: number | null; product: string; ordered: number; received: number; outstanding: number; unitCost: number; vatRate: number; discountPct: number | null; value: number };
export type PoRow = {
    id: number; number: string; supplierId: number; supplier: string; status: string; orderDate: string; deadline: string | null; receivedDate: string | null; supplierRef: string | null;
    deliverToBranchId: number | null; deliveryNote: string | null; notes: string | null; raisedBy: string | null; raisedById: number | null; submittedAt: string | null;
    approvedBy: string | null; approvedAt: string | null; rejectReason: string | null; lines: PoLine[]; value: number;
};
export type GrnRow = {
    id: number; number: string; date: string; supplierId: number; supplier: string; poId: number; po: string; deliveryNoteNo: string; cuNumber: string; branch: string; by: string | null; note: string | null;
    subtotal: number; vat: number; total: number;
    lines: { id: number; product: string; productId: number | null; delivered: number; accepted: number; reason: string | null; batch: string | null; expiry: string | null; unitCost: number; vatRate: number; batchId: number | null }[];
};
export type PoDraftBody = { supplierId: number; supplierRef?: string; deadline?: string; deliverToBranchId?: number | null; deliveryNote?: string; notes?: string; lines: { productId: number; quantity: number; unitCost: number }[]; submit?: boolean };
const pq = (p: Record<string, unknown>) => Object.fromEntries(Object.entries(p).filter(([, v]) => v !== undefined && v !== null && v !== ''));
export const PurchasingApi = {
    list: (p: { status?: string; supplierId?: number; search?: string } = {}) => api.get('/suppliers/purchase-orders/list', { params: pq(p) }).then(r => r.data as PoRow[]),
    detail: (id: number) => api.get(`/suppliers/purchase-orders/${id}/detail`).then(r => r.data as PoRow & { grns: GrnRow[] }),
    create: (body: PoDraftBody) => api.post('/suppliers/purchase-orders/drafts', body).then(r => r.data as PoRow),
    editDraft: (id: number, body: Omit<PoDraftBody, 'supplierId' | 'submit'>) => api.put(`/suppliers/purchase-orders/${id}/draft`, body).then(r => r.data as PoRow),
    submit: (id: number) => api.post(`/suppliers/purchase-orders/${id}/submit`).then(r => r.data as PoRow),
    approve: (id: number) => api.post(`/suppliers/purchase-orders/${id}/approve`).then(r => r.data as PoRow),
    reject: (id: number, reason: string) => api.post(`/suppliers/purchase-orders/${id}/reject`, { reason }).then(r => r.data as PoRow),
    cancel: (id: number, reason: string) => api.post(`/suppliers/purchase-orders/${id}/cancel`, { reason }).then(r => r.data as PoRow),
    receive: (id: number, body: { deliveryNoteNo: string; cuNumber: string; branchId?: number | null; date?: string; note?: string; lines: { poItemId: number; delivered: number; accepted: number; reason?: string | null; batch?: string; expiry?: string }[] }) => api.post(`/suppliers/purchase-orders/${id}/grns`, body).then(r => r.data as { grn: GrnRow; warnings: string[] }),
    grns: (p: { from?: string; to?: string; supplierId?: number; search?: string } = {}) => api.get('/suppliers/goods-received', { params: pq(p) }).then(r => r.data as GrnRow[]),
    supplierGrns: (supplierId: number) => api.get(`/suppliers/${supplierId}/grns`).then(r => r.data as GrnRow[]),
    onOrder: () => api.get('/suppliers/purchase-orders/on-order').then(r => r.data as { productId: number | null; product: string; onOrder: number; orders: { poId: number; number: string; status: string; supplier: string; outstanding: number; deadline: string | null }[] }[]),
    addToDraft: (productId: number, quantity: number, supplierId?: number) => api.post('/suppliers/purchase-orders/draft-lines', pq({ productId, quantity, supplierId })).then(r => r.data as PoRow),
    correctBatch: (batchId: number, body: { batch: string; expiry: string; reason: string }) => api.patch(`/products/batches/${batchId}`, body).then(r => r.data as { id: number; batch: string; expiry: string; warning: string | null }),
};

/* ------------------------------ Stock per branch (PRD Phase 1.2: IN-09, IN-11) ------------------------------ */

export type StockLocationRow = { id: number; name: string; physical: boolean; showInPos: boolean; isDefault: boolean; active: boolean };
export type StockBranchRow = { id: number; name: string; status: string; isMain: boolean; items: number; units: number; costValue?: number; recentSales: number; locations: StockLocationRow[] };
export type StockStatus = { stockByBranch: boolean; mainBranchId: number | null; unplacedBatches: number; branches: StockBranchRow[] };
/** The design's `StockRow` (branch-stock.tsx); `qty` is keyed by location id. Cost fields are absent for roles that don't see cost. */
export type StockListRow = { productId: number; name: string; code?: string; sub?: string; category: string; subCategory?: string; brand?: string; unitCost?: number; unitPrice: number; reorderLevel: number; qty: Record<string, number>; units: number; costValue?: number };
export type MovementRow = { id: number; at: string; productId: number; product: string; action: string; change: number; newQty: number; branchId: number | null; branch: string; locationId: number | null; location: string; user: string; reference: string; note: string | null; batchId: number | null; batch: string | null };

export const InventoryApi = {
    status: () => api.get('/inventory/status').then(r => r.data as StockStatus),
    stock: (p: { search?: string; category?: string; subCategory?: string; brand?: string; locationId?: number; state?: 'all' | 'in-stock' | 'low' | 'zero-negative'; branchId?: number; page?: number; pageSize?: number } = {}) =>
        api.get('/inventory/stock', { params: pq(p) }).then(r => r.data as StockStatus & { totals: { items: number; units: number; costValue?: number; retailValue: number }; total: number; page: number; pageSize: number; rows: StockListRow[] }),
    movements: (p: { from?: string; to?: string; productId?: number; action?: string; branchId?: number; page?: number; pageSize?: number } = {}) =>
        api.get('/inventory/movements', { params: pq(p) }).then(r => r.data as { from: string; to: string; total: number; page: number; pageSize: number; rows: MovementRow[] }),
    /** Before stock is kept per branch: from the main branch to another (Q1). */
    allocate: (body: { toBranchId: number; lines: { productId: number; quantity: number }[]; note?: string }) =>
        api.post('/inventory/allocations', body).then(r => r.data as { reference: string; toBranchId: number; lines: { productId: number; quantity: number }[] }),
    setStockByBranch: (on: boolean) => api.put('/inventory/stock-by-branch', { on }).then(r => r.data as StockStatus),
};

/** A branch transfer (IN-24, IN-25): the design's `TransferRecord` with ids and status. Cost fields are absent for roles that don't see cost. */
export type TransferLineRow = { id: number; productId: number; item: string; code: string | null; unit: string; batch: string | null; expiry: string | null; sent: number; received: number | null; unitCost?: number; shortReason: string | null };
export type TransferRow = {
    id: number; ref: string; number: string; status: 'In transit' | 'Received' | 'Received short' | 'Cancelled';
    date: string; fromBranchId: number; toBranchId: number; from: string; to: string; sentById: number | null; sentBy: string;
    receivedBy?: string; receivedOn?: string; note?: string; shortNote?: string; cancelledAt: string | null; cancelReason: string | null;
    units: number; costValue?: number; lostCost?: number; lines: TransferLineRow[];
};
export type TransferOption = { batchId: number; productId: number; name: string; code: string | null; unit: string | null; batch: string | null; expiryDate: string | null; available: number; unitCost?: number };

/** A stocktake (IN-12, IN-13). Cost fields are absent for roles that don't see cost. */
export type StocktakeLineRow = {
    id: number; productId: number; name: string; code: string | null; barcode: string | null; unit: string; category: string | null;
    startSystem: number; system: number; moved: boolean; counted: number | null; note: string | null; difference: number | null;
    unitCost?: number; unitPrice: number; costChange?: number | null;
};
export type StocktakeRow = {
    id: number; number: string; status: 'Draft' | 'Applied' | 'Cancelled'; branchId: number; branch: string; locationId: number; location: string;
    category: string | null; note: string | null; startedBy: string; startedAt: string; appliedBy: string | null; appliedAt: string | null; cancelledAt: string | null; cancelReason: string | null;
};
export type StocktakeDetail = StocktakeRow & { totals: { lossCost?: number; gainCost?: number; lossPrice: number; gainPrice: number; systemCost?: number; itemsOff: number }; lines: StocktakeLineRow[] };
export type StocktakeListRow = StocktakeRow & { lines: number; counted: number; lossCost?: number; gainCost?: number };

export const StocktakesApi = {
    list: (p: { status?: 'Draft' | 'Applied' | 'Cancelled'; branchId?: number; search?: string; page?: number; pageSize?: number } = {}) =>
        api.get('/inventory/stocktakes', { params: pq(p) }).then(r => r.data as { total: number; page: number; pageSize: number; rows: StocktakeListRow[] }),
    get: (id: number) => api.get(`/inventory/stocktakes/${id}`).then(r => r.data as StocktakeDetail),
    /** One location (Q1), all products or one category. */
    start: (body: { locationId: number; category?: string; note?: string }) => api.post('/inventory/stocktakes', body).then(r => r.data as StocktakeDetail),
    /** `counted: null` clears a count. Nothing changes stock until applied. */
    saveCounts: (id: number, lines: { lineId: number; counted: number | null; note?: string | null }[]) => api.put(`/inventory/stocktakes/${id}/counts`, { lines }).then(r => r.data as StocktakeDetail),
    apply: (id: number) => api.post(`/inventory/stocktakes/${id}/apply`, {}).then(r => r.data as StocktakeDetail),
    cancel: (id: number, reason: string) => api.post(`/inventory/stocktakes/${id}/cancel`, { reason }).then(r => r.data as StocktakeDetail),
};

/** A batch that can be written off, issued or converted (plan A). Cost is absent for roles that don't see cost. */
export type OpsBatchOption = { batchId: number; productId: number; name: string; code: string | null; unit: string | null; batch: string | null; expiryDate: string | null; available: number; unitCost?: number; locationId: number | null; location: string | null };
export type WasteRow = {
    id: number; no: string; date: string; recordedAt: string; branchId: number; branch: string; staff: string; reason: string; note: string | null;
    status: 'Awaiting approval' | 'Approved' | 'Rejected'; approver?: string; decidedAt: string | null; rejectReason: string | null; value?: number;
    items: { productId: number; name: string; unit: string | null; batch: string | null; expiryDate: string | null; location: string | null; qty: number; cost?: number; reason: string }[];
};
export type ConversionRow = { id: number; no: string; date: string; parent: string; child: string; parentQty: number; childQty: number; perParent: number; parentUnitCost?: number; location: string; staff: string };
export type ConversionLinkRow = { productId: number; childId: number; perParent: number; parent: { name: string; unit: string; stock: number; unitCost?: number }; child: { name: string; unit: string; stock: number } };
export type IssueRow = { id: number; no: string; date: string; branch: string; from: string; to: string; internalUse: boolean; staff: string; reference?: string; note: string | null; items: { name: string; qty: number; unitCost?: number }[] };
export type OpsLocation = { id: number; name: string; branchId: number; branch: string; showInPos: boolean; isDefault: boolean; own: boolean };

/** Waste (IN-20), conversion (IN-21) and internal issues (IN-22, IN-23): plan A. */
export const StockOpsApi = {
    waste: (p: { from?: string; to?: string; branchId?: number; status?: string } = {}) => api.get('/inventory/waste', { params: pq(p) }).then(r => r.data as { rows: WasteRow[]; approvalAbove: number }),
    wasteReasons: () => api.get('/inventory/waste/reasons').then(r => r.data as string[]),
    wasteOptions: (p: { branchId?: number; search?: string } = {}) => api.get('/inventory/waste/options', { params: pq(p) }).then(r => r.data as OpsBatchOption[]),
    recordWaste: (body: { branchId?: number; note?: string; lines: { batchId: number; quantity: number; reason: string }[] }) => api.post('/inventory/waste', body).then(r => r.data as WasteRow),
    approveWaste: (id: number) => api.post(`/inventory/waste/${id}/approve`, {}).then(r => r.data as WasteRow),
    rejectWaste: (id: number, reason: string) => api.post(`/inventory/waste/${id}/reject`, { reason }).then(r => r.data as WasteRow),
    conversions: (p: { from?: string; to?: string; branchId?: number } = {}) => api.get('/inventory/conversions', { params: pq(p) }).then(r => r.data as ConversionRow[]),
    conversionOptions: (locationId?: number) => api.get('/inventory/conversions/options', { params: pq({ locationId }) }).then(r => r.data as { location: { id: number; name: string; branchId: number }; links: ConversionLinkRow[] }),
    setLink: (productId: number, childId: number | null, perParent?: number) => api.put(`/inventory/conversions/links/${productId}`, { childId, perParent }).then(r => r.data),
    convert: (body: { productId: number; quantity: number; locationId?: number }) => api.post('/inventory/conversions', body).then(r => r.data as ConversionRow),
    issues: (p: { from?: string; to?: string; branchId?: number } = {}) => api.get('/inventory/issues', { params: pq(p) }).then(r => r.data as IssueRow[]),
    locations: () => api.get('/inventory/issues/locations').then(r => r.data as OpsLocation[]),
    issueOptions: (locationId: number, search?: string) => api.get('/inventory/issues/options', { params: pq({ locationId, search }) }).then(r => r.data as OpsBatchOption[]),
    issue: (body: { fromLocationId: number; toLocationId: number | null; reference?: string; note?: string; lines: { batchId: number; quantity: number }[] }) => api.post('/inventory/issues', body).then(r => r.data as IssueRow),
};

export const TransfersApi = {
    list: (p: { view?: 'all' | 'receivable' | 'lost' | 'in-transit'; from?: string; to?: string; branchId?: number; page?: number; pageSize?: number } = {}) =>
        api.get('/inventory/transfers', { params: pq(p) }).then(r => r.data as { view: string; total: number; page: number; pageSize: number; rows: TransferRow[] }),
    get: (id: number) => api.get(`/inventory/transfers/${id}`).then(r => r.data as TransferRow),
    /** Batches a branch can send, earliest expiry first. */
    options: (p: { fromBranchId?: number; search?: string } = {}) => api.get('/inventory/transfers/options', { params: pq(p) }).then(r => r.data as TransferOption[]),
    send: (body: { fromBranchId?: number; toBranchId: number; note?: string; lines: ({ batchId: number; quantity: number } | { productId: number; quantity: number })[] }) =>
        api.post('/inventory/transfers', body).then(r => r.data as TransferRow),
    /** A line not listed arrived in full; a shortfall needs a reason. */
    receive: (id: number, lines: { lineId: number; received: number; reason?: string }[]) => api.post(`/inventory/transfers/${id}/receive`, { lines }).then(r => r.data as TransferRow),
    cancel: (id: number, reason: string) => api.post(`/inventory/transfers/${id}/cancel`, { reason }).then(r => r.data as TransferRow),
};

// ─── Integrations → M-Pesa (plan F: IG-01 – IG-03, IG-06, ST-18). Keys are write-only: nothing returns one. ───
export type MpesaKeyState = { set: boolean; at: string | null; by: string | null; readable: boolean };
export type MpesaBranchRow = { branchId: number; branch: string; shortcode: string; till: string; keys: MpesaKeyState; passkey: MpesaKeyState; lastTest: { ok: boolean; at: string; note: string | null } | null; c2bRegisteredAt: string | null };
export type MpesaOverview = {
    environment: 'sandbox' | 'live'; status: 'Connected' | 'Not connected' | 'Failing'; lastOk: string | null;
    encryption: { keySet: boolean; plain: number }; callbackUrl: string; callbackFromServer: boolean;
    c2b: { base: string; problem: string | null; ipCheck: boolean }; unmatched: number; branches: MpesaBranchRow[];
};
export type MpesaEventRow = { id: number; event: string; ref: string | null; ok: boolean; ignored: boolean; detail: string | null; branchId: number | null; createdAt: string };
export type C2bPaymentRow = {
    id: number; transId: string; transType: string | null; transTime: string | null; amount: number; shortCode: string | null; billRef: string | null; phone: string | null; payer: string | null;
    status: 'Unmatched' | 'Matched' | 'Not ours'; matchedHow: string | null; saleId: number | null; customerId: number | null; note: string | null; branch: string | null; createdAt: string;
    suggestion: { kind: 'sale' | 'customer'; ref: string; label: string } | null;
};
export const MpesaApi = {
    overview: () => api.get('/integrations/mpesa').then(r => r.data as MpesaOverview),
    log: (limit = 200) => api.get('/integrations/mpesa/log', { params: { limit } }).then(r => r.data as MpesaEventRow[]),
    payments: (view: 'unmatched' | 'all' = 'unmatched') => api.get('/integrations/mpesa/payments', { params: { view } }).then(r => r.data as C2bPaymentRow[]),
    lookup: (code: string | undefined, amount: number) => api.get('/integrations/mpesa/payments/lookup', { params: { code, amount } }).then(r => r.data as { code: string; amount: number; payer: string | null; at: string }),
    match: (id: number, body: { kind?: 'sale' | 'customer'; receiptId?: string; customerId?: number; notOurs?: boolean; reason?: string }) => api.post(`/integrations/mpesa/payments/${id}/match`, body).then(r => r.data),
    setEnvironment: (environment: 'sandbox' | 'live', reason?: string) => api.put('/integrations/mpesa/environment', { environment, reason }).then(r => r.data as MpesaOverview),
    saveBranch: (branchId: number, body: { shortcode: string; till: string; consumerKey?: string; consumerSecret?: string; passkey?: string }) => api.put(`/integrations/mpesa/branches/${branchId}`, body).then(r => r.data as MpesaOverview),
    test: (branchId: number) => api.post(`/integrations/mpesa/branches/${branchId}/test`).then(r => r.data as { ok: boolean; detail: string }),
    registerC2b: (branchId: number) => api.post(`/integrations/mpesa/branches/${branchId}/register-c2b`).then(r => r.data as { ok: boolean; detail: string }),
};

// ─── Accounts Part 2a (plan H): invoices (AC-07), quotations (AC-15), receivable reports (AC-09), scheme claims (AC-13) ───
export type InvoiceState = 'Cancelled' | 'Paid' | 'Overdue' | 'Part paid' | 'Unpaid';
export type InvoiceRow = {
    id: number; number: string; saleId: number; receiptId: string; date: string; dueDate: string; termsDays: number; customerId: number; customer: string; orderRef: string | null;
    branch: string | null; quotation: string | null; deliveryNoteNo: string | null; deliveredAt: string | null; receivedBy: string | null; cancelReason: string | null;
    subtotal: number; tax: number; total: number; paid: number; outstanding: number; state: InvoiceState; lines: { item: string; qty: number; price: number; vat: number }[];
};
export type InvoiceDetail = InvoiceRow & {
    party: { name: string; phone: string | null; email: string | null; address: string | null; kraPin: string | null };
    items: { code: string; name: string; unit: string | null; qty: number; price: number; discount: number; taxCode: string | null; vat: number }[];
};
export type DocLineIn = { productId: number; quantity: number; unitPrice: number; discount?: number };
export type QuotationRow = {
    id: number; number: string; date: string; validDays: number; validUntil: string; customerId: number | null; customer: string; note: string | null; isEtims: boolean;
    subtotal: number; tax: number; total: number; status: 'Open' | 'Expired' | 'Converted' | 'Cancelled'; convertedTo: string | null; cancelReason: string | null; source: 'Accounts' | 'Till'; branch: string | null; createdBy: string | null; createdAt: string;
};
export type QuotationDetail = QuotationRow & { lines: { id: number; productId: number; code: string; unit: string | null; name: string; qty: number; price: number; discount: number; taxCode: string | null; vat: number }[] };
export type QuotationCheck = { quotation: QuotationDetail; expired: boolean; lines: { id: string; productId: number; name: string; qty: number; quoted: number; current: number; inStock: number; missing: boolean }[] };
export type ClaimRow = {
    id: number; ref: string; scheme: string; schemeId: number; member: string; memberNo: string | null; sale: string; saleId: number; saleTotal: number; date: string; submitBy: string | null;
    submitted?: string; submittedBy: string | null; claimed: number; paid: number; writtenOff: number; due: number; status: 'Draft' | 'Submitted' | 'Part paid' | 'Paid' | 'Rejected' | 'Cancelled'; reason?: string; branch: string | null;
};
export type SchemeMembership = {
    customerId: number; schemeId: number; scheme: string; kind: string; active: boolean; memberNo: string | null; relation: 'Principal' | 'Spouse' | 'Child' | null; principal: string | null;
    limit: number | null; used: number; copayPct: number; validUntil: string | null; status: 'Active' | 'Suspended' | 'Expired' | null; claimWithinDays: number;
};
export type ReceivablePayment = { ref: string; date: string; party: string; customerId: number; method: string; reference?: string; amount: number; appliedTo: string };

export const InvoicesApi = {
    list: (p: { from?: string; to?: string; customerId?: number } = {}) => api.get('/invoices', { params: pq(p) }).then(r => r.data as InvoiceRow[]),
    get: (id: number) => api.get(`/invoices/${id}`).then(r => r.data as InvoiceDetail),
    terms: (customerId: number) => api.get(`/invoices/terms/${customerId}`).then(r => r.data as { termsDays: number; options: number[] }),
    create: (body: { customerId: number; items: DocLineIn[]; termsDays?: number; dueDate?: string; orderRef?: string; note?: string; quotationId?: number; total?: number }) => api.post('/invoices', body).then(r => r.data as InvoiceDetail),
    deliver: (id: number, receivedBy?: string) => api.post(`/invoices/${id}/deliver`, { receivedBy }).then(r => r.data as InvoiceDetail),
    cancel: (id: number, reason: string) => api.post(`/invoices/${id}/cancel`, { reason }).then(r => r.data as InvoiceDetail),
};
export const QuotationsApi = {
    list: (p: { from?: string; to?: string } = {}) => api.get('/quotations', { params: pq(p) }).then(r => r.data as QuotationRow[]),
    get: (id: number) => api.get(`/quotations/${id}`).then(r => r.data as QuotationDetail),
    check: (id: number) => api.get(`/quotations/${id}/check`).then(r => r.data as QuotationCheck),
    create: (body: { customerId?: number; customerName?: string; validDays?: number; isEtims?: boolean; items: DocLineIn[]; note?: string; source?: 'Till' | 'Accounts' }) => api.post('/quotations', body).then(r => r.data as QuotationDetail),
    cancel: (id: number, reason: string) => api.post(`/quotations/${id}/cancel`, { reason }).then(r => r.data as QuotationDetail),
};
export const ClaimsApi = {
    list: (view: 'open' | 'all' = 'open', p: { from?: string; to?: string } = {}) => api.get('/claims', { params: pq({ view, ...p }) }).then(r => r.data as ClaimRow[]),
    member: (customerId: number) => api.get(`/claims/member/${customerId}`).then(r => r.data as SchemeMembership),
    payments: (id: number) => api.get(`/claims/${id}/payments`).then(r => r.data as { id: number; date: string; amount: number; writtenOff: number; reference: string; reason: string | null; by: string | null }[]),
    submit: (id: number) => api.post(`/claims/${id}/submit`).then(r => r.data as ClaimRow),
    pay: (id: number, body: { amount: number; reference: string; writeOff?: boolean; reason?: string; date?: string }) => api.post(`/claims/${id}/pay`, body).then(r => r.data as ClaimRow),
    reject: (id: number, reason: string) => api.post(`/claims/${id}/reject`, { reason }).then(r => r.data as ClaimRow),
};
export const ReceivablesApi = {
    payments: (p: { from?: string; to?: string } = {}) => api.get('/receivables/payments', { params: pq(p) }).then(r => r.data as ReceivablePayment[]),
    ageing: () => api.get('/receivables/ageing').then(r => r.data as { rows: { label: string; buckets: [number, number, number, number] }[]; draftClaims: number; note: string }),
};

// ─── Accounts Part 2b (plan I): supplier bills and credit notes (AC-10), payable reports (AC-11) ───
export type SupplierBillRow = {
    id: number; no: string; supplier: string; supplierId: number; invoiceNo: string; cuNo: string; grn: string; invoiceDate: string; dueDate: string;
    amount: number; grnValue: number; paid: number; credited: number; held?: string; note: string | null; clearedBy: string | null; cancelReason: string | null; createdBy: string | null;
    status: 'Settled' | 'Held' | 'Overdue' | 'Part paid' | 'Due' | 'Cancelled';
};
export type UnbilledGrn = {
    id: number; no: string; date: string; supplier: string; supplierId: number; termsDays: number; po: string; dnNo: string; cuNo: string; location: string; value: number;
    lines: { product: string; batch: string; expiry: string; qty: number; unitCost: number; vat: 0 | 16 }[];
};
export type SupplierCreditRow = { id: number; no: string; supplierCn: string; date: string; supplier: string; supplierId: number; bill: string; returnRef?: string; amount: number; reason: string; by: string | null };
export type SupplierPaymentRow = { ref: string; date: string; party: string; supplierId: number; method: string; reference?: string; amount: number; paid: number; wht: number; appliedTo: string };
export type PayablesAgeing = { rows: { id: number; label: string; buckets: [number, number, number, number]; billed: number; paid: number; balance: number; creditOnAccount: number; oldestBillDays: number }[]; total: number; billsFrom: string | null };

export const PayablesApi = {
    setup: () => api.get('/payables/setup').then(r => r.data as { billsFrom: string | null; today: string }),
    switchOver: (date: string) => api.put('/payables/switch-over', { date }).then(r => r.data as { billsFrom: string | null; today: string }),
    bills: (p: { from?: string; to?: string; supplierId?: number; view?: 'open' | 'all' } = {}) => api.get('/payables/bills', { params: pq(p) }).then(r => r.data as SupplierBillRow[]),
    unbilled: () => api.get('/payables/unbilled').then(r => r.data as UnbilledGrn[]),
    createBill: (body: { grnId: number; invoiceNo: string; cuNo: string; invoiceDate: string; amount: string; note?: string }) => api.post('/payables/bills', body).then(r => r.data as SupplierBillRow),
    clearBill: (id: number, note?: string) => api.post(`/payables/bills/${id}/clear`, { note }).then(r => r.data as SupplierBillRow),
    cancelBill: (id: number, reason: string) => api.post(`/payables/bills/${id}/cancel`, { reason }).then(r => r.data as SupplierBillRow),
    credits: (p: { from?: string; to?: string } = {}) => api.get('/payables/credit-notes', { params: pq(p) }).then(r => r.data as SupplierCreditRow[]),
    returnsAwaiting: () => api.get('/payables/returns-awaiting').then(r => r.data as { id: number; ref: string; supplierId: number; value: number; date: string }[]),
    createCredit: (body: { supplierId: number; billId?: number; returnId?: number; supplierCn: string; amount: string; reason: string }) => api.post('/payables/credit-notes', body).then(r => r.data as SupplierCreditRow),
    payments: (p: { from?: string; to?: string } = {}) => api.get('/payables/payments', { params: pq(p) }).then(r => r.data as SupplierPaymentRow[]),
    ageing: () => api.get('/payables/ageing').then(r => r.data as PayablesAgeing),
};

// ─── HRM: employees and leave (plan K: HR-01 – HR-04) ───

export type EmployeeRecord = {
    id: number; number: string; firstName: string; lastName: string; name: string; dob: string | null; gender: string | null; phone: string | null; email: string | null;
    branchId: number | null; branch: string | null; department: string | null; designation: string | null; typeId: number | null; type: string | null;
    startDate: string | null; userId: number | null; username: string | null; payType: string;
    statutory: { nssf: boolean; shif: boolean; levy: boolean; paye: boolean }; active: boolean; missing: string[];
    /** True when the role can't see pay, ID and payment details (HRM/Salaries, Q1): the fields below are absent. */
    payHidden: boolean;
    idNumber?: string | null; rate?: number | null; kraPin?: string | null; nssfNo?: string | null; shaNo?: string | null;
    payBy?: string | null; bank?: string | null; account?: string | null; mpesa?: string | null;
};
export type EmployeeOptions = {
    branches: { id: number; name: string }[]; types: { id: number; name: string }[]; users: { id: number; username: string; name: string }[];
    departments: string[]; payTypes: string[]; canSeePay: boolean; canChangePay: boolean;
};
export type LeaveTypeRow = { id: number; name: string; days: number; carryMax: number | null; paid: boolean; document: boolean; appliesTo: string; uses: number };
export type LeaveBalanceRow = { employeeId: number; employee: string; number: string; typeId: number; type: string; paid: boolean; allocated: number; overridden: boolean; taken: number; booked: number };
export type LeaveRecordRow = { id: number; number: string; employeeId: number; employee: string; staffNo: string; type: string; paid: boolean; from: string; to: string; days: number; reason: string | null; document: string | null; status: 'Booked' | 'Cancelled'; cancelReason: string | null; by: string | null };

export const HrmApi = {
    employees: () => api.get('/hrm/employees').then(r => r.data as EmployeeRecord[]),
    employee: (id: number) => api.get(`/hrm/employees/${id}`).then(r => r.data as EmployeeRecord),
    options: (employeeId?: number) => api.get('/hrm/employees/options', { params: pq({ employeeId }) }).then(r => r.data as EmployeeOptions),
    createEmployee: (body: Record<string, unknown>) => api.post('/hrm/employees', body).then(r => r.data as EmployeeRecord),
    updateEmployee: (id: number, body: Record<string, unknown>) => api.patch(`/hrm/employees/${id}`, body).then(r => r.data as EmployeeRecord),
    setActive: (id: number, active: boolean) => api.post(`/hrm/employees/${id}/active`, { active }).then(r => r.data as EmployeeRecord),
    importEmployees: (rows: Record<string, string>[]) => api.post('/hrm/employees/import', { rows }).then(r => r.data as { created: number }),
    types: () => api.get('/hrm/employee-types').then(r => r.data as { id: number; name: string; active: boolean; uses: number }[]),
    addType: (name: string) => api.post('/hrm/employee-types', { name }).then(r => r.data),
    updateType: (id: number, body: { name?: string; active?: boolean }) => api.patch(`/hrm/employee-types/${id}`, body).then(r => r.data),
    leaveTypes: () => api.get('/hrm/leave/types').then(r => r.data as LeaveTypeRow[]),
    saveLeaveTypes: (types: { id: string; name: string; days: string; carryMax: string; paid: boolean; document: boolean; appliesTo: string }[]) => api.put('/hrm/leave/types', { types }).then(r => r.data as LeaveTypeRow[]),
    holidays: () => api.get('/hrm/leave/holidays').then(r => r.data as { date: string; name: string }[]),
    saveHolidays: (holidays: { date: string; name: string }[]) => api.put('/hrm/leave/holidays', { holidays }).then(r => r.data as { date: string; name: string }[]),
    balances: (year: number) => api.get('/hrm/leave/balances', { params: { year } }).then(r => r.data as LeaveBalanceRow[]),
    setAllocation: (body: { employeeId: number; leaveTypeId: number; year: number; days: number; reason: string }) => api.put('/hrm/leave/allocations', body).then(r => r.data as LeaveBalanceRow),
    records: (p: { from?: string; to?: string; employeeId?: number } = {}) => api.get('/hrm/leave/records', { params: pq(p) }).then(r => r.data as LeaveRecordRow[]),
    book: (body: { employeeId: number; leaveTypeId: number; from: string; to: string; reason?: string; document?: string }) => api.post('/hrm/leave/records', body).then(r => r.data as LeaveRecordRow),
    cancelLeave: (id: number, reason: string) => api.post(`/hrm/leave/records/${id}/cancel`, { reason }).then(r => r.data as LeaveRecordRow),
};

// ─── HRM: payroll (plan L: HR-05 – HR-19) ───

export type RateBand = { upTo: number | null; pct: number };
export type RateVersionRow = {
    id: number; from: string; nssfPct: number; nssfUpper: number; nssfEmployerPct: number; shifPct: number; shifMin: number; levyPct: number; levyEmployerPct: number;
    relief: number; bands: RateBand[]; deductNssf: boolean; deductShif: boolean; deductLevy: boolean; note: string;
    confirmed: boolean; confirmedBy: string | null; confirmedAt: string | null; confirmNote: string | null; scheduled: boolean;
};
export type PayrollOptions = {
    employees: { id: number; number: string; name: string; department: string | null; payType: string; rate: number | null; active: boolean }[];
    categories: { Allowance: string[]; Deduction: string[] };
    accounts: { id: number; code: string; name: string; branchId: number | null }[];
    closedMonths: string[]; today: string;
};
export type PayItemRow = { id: number; employeeId: number; employee: string; kind: 'Allowance' | 'Deduction'; category: string; amount: number; recurring: boolean; fromMonth: string; untilMonth: string | null; taxable: boolean; note: string | null; paidIn: number };
export type WorkLogRow = { id: number; employeeId: number; employee: string; kind: 'Overtime' | 'Casual' | 'Piece'; date: string; timeIn: string | null; timeOut: string | null; hours: number | null; pieces: number | null; rate: number; amount: number; note: string | null; paidBy: string | null };
export type BonusRow = { id: number; employeeId: number; employee: string; month: string; type: string; description: string; amount: number; paidBy: string | null };
export type LoanRow = { id: number; number: string; employeeId: number; employee: string; kind: 'Company loan' | 'HELB'; principal: number; instalment: number; firstMonth: string; date: string; reference: string | null; repaid: number; balance: number; status: 'Active' | 'Repaid' | 'Stopped'; stopReason: string | null; payrollsLeft: number };
export type AdvanceRow = { id: number; number: string; employeeId: number; employee: string; date: string; amount: number; method: string; paidFrom: string | null; reference: string | null; recoveredBy: string | null };
export type PayrollRunRow = {
    id: number; number: string; kind: 'Monthly' | 'Piece workers' | 'Casuals'; month: string; from: string; to: string; department: string | null; status: 'Draft' | 'Approved' | 'Reversed';
    preparedBy: string | null; preparedById: number | null; approvedBy: string | null; approvedAt: string | null; reversedAt: string | null; reverseReason: string | null;
    paidAt: string | null; paidDate: string | null; paidReference: string | null; slips: number; gross: number; net: number; paye: number;
};
export type PayslipRow = {
    id: number; employeeId: number; month: string; employeeName: string; employeeNumber: string; kraPin: string | null; nssfNo: string | null; shaNo: string | null;
    department: string | null; designation: string | null; payTo: string | null; basic: number; allowances: number; untaxedAllowances: number; overtime: number; commission: number; bonus: number;
    otherDeductions: number; loans: number; advance: number; gross: number; nssf: number; shif: number; levy: number; taxable: number; payeBeforeRelief: number; relief: number; paye: number;
    nssfEmployer: number; levyEmployer: number; totalDeductions: number; net: number; warnings: string[];
    items: { id: number; source: string; sourceId: number | null; label: string; amount: number }[];
};
export type PayrollRunDetail = PayrollRunRow & {
    employer: { name: string; pin: string }; rates: RateVersionRow | null; totals: Record<string, number>; payslips: PayslipRow[]; skipped?: string[];
};
export type P9Data = {
    year: number; employer: { name: string; pin: string }; employee: { id: number; name: string; number: string; pin: string | null }; payslips: number;
    rows: ({ month: string } & Record<'basic' | 'allowances' | 'gross' | 'nssf' | 'shif' | 'levy' | 'taxable' | 'payeBeforeRelief' | 'relief' | 'paye', number>)[];
    total: Record<'basic' | 'allowances' | 'gross' | 'nssf' | 'shif' | 'levy' | 'taxable' | 'payeBeforeRelief' | 'relief' | 'paye', number>;
};

export const PayrollApi = {
    rates: () => api.get('/payroll/rates').then(r => r.data as { versions: RateVersionRow[]; inForceId: number | null; problem: string | null; employerPin: string }),
    addRates: (body: Record<string, unknown>) => api.post('/payroll/rates', body).then(r => r.data as RateVersionRow),
    updateRates: (id: number, body: Record<string, unknown>) => api.patch(`/payroll/rates/${id}`, body).then(r => r.data as RateVersionRow),
    confirmRates: (id: number, note: string) => api.post(`/payroll/rates/${id}/confirm`, { note }).then(r => r.data as RateVersionRow),
    removeRates: (id: number) => api.delete(`/payroll/rates/${id}`).then(r => r.data),
    setEmployerPin: (pin: string) => api.put('/payroll/rates/employer-pin', { pin }).then(r => r.data as { employerPin: string }),
    options: () => api.get('/payroll/options').then(r => r.data as PayrollOptions),
    addCategory: (kind: 'Allowance' | 'Deduction', name: string) => api.post('/payroll/categories', { kind, name }).then(r => r.data),
    items: (employeeId?: number) => api.get('/payroll/items', { params: pq({ employeeId }) }).then(r => r.data as PayItemRow[]),
    addItem: (body: Record<string, unknown>) => api.post('/payroll/items', body).then(r => r.data),
    endItem: (id: number, untilMonth?: string) => api.post(`/payroll/items/${id}/end`, untilMonth ? { untilMonth } : {}).then(r => r.data),
    logs: (p: { from?: string; to?: string; employeeId?: number } = {}) => api.get('/payroll/logs', { params: pq(p) }).then(r => r.data as WorkLogRow[]),
    addLog: (body: Record<string, unknown>) => api.post('/payroll/logs', body).then(r => r.data),
    removeLog: (id: number) => api.delete(`/payroll/logs/${id}`).then(r => r.data),
    bonuses: (month?: string) => api.get('/payroll/bonuses', { params: pq({ month }) }).then(r => r.data as BonusRow[]),
    addBonus: (body: Record<string, unknown>) => api.post('/payroll/bonuses', body).then(r => r.data),
    removeBonus: (id: number) => api.delete(`/payroll/bonuses/${id}`).then(r => r.data),
    loans: () => api.get('/payroll/loans').then(r => r.data as LoanRow[]),
    addLoan: (body: Record<string, unknown>) => api.post('/payroll/loans', body).then(r => r.data),
    stopLoan: (id: number, reason: string) => api.post(`/payroll/loans/${id}/stop`, { reason }).then(r => r.data),
    advances: () => api.get('/payroll/advances').then(r => r.data as AdvanceRow[]),
    advanceLimit: (employeeId: number) => api.get(`/payroll/advances/limit/${employeeId}`).then(r => r.data as { netPay: number; open: number; available: number; problem: string | null }),
    addAdvance: (body: Record<string, unknown>) => api.post('/payroll/advances', body).then(r => r.data),
    runs: () => api.get('/payroll/runs').then(r => r.data as PayrollRunRow[]),
    run: (id: number) => api.get(`/payroll/runs/${id}`).then(r => r.data as PayrollRunDetail),
    createRun: (body: { kind: string; month?: string; from?: string; to?: string; department?: string | null; employeeIds?: number[] }) => api.post('/payroll/runs', body).then(r => r.data as PayrollRunDetail),
    recalculate: (id: number) => api.post(`/payroll/runs/${id}/recalculate`).then(r => r.data as PayrollRunDetail),
    approve: (id: number) => api.post(`/payroll/runs/${id}/approve`).then(r => r.data as PayrollRunDetail),
    reverse: (id: number, reason: string) => api.post(`/payroll/runs/${id}/reverse`, { reason }).then(r => r.data as PayrollRunDetail),
    paid: (id: number, body: { accountId: number; date: string; reference: string }) => api.post(`/payroll/runs/${id}/paid`, body).then(r => r.data as PayrollRunDetail),
    discard: (id: number) => api.delete(`/payroll/runs/${id}`).then(r => r.data),
    p9: (employeeId: number, year: number) => api.get(`/payroll/p9/${employeeId}`, { params: { year } }).then(r => r.data as P9Data),
};
