/**
 * @license
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * Fiesta REST client — the merchant-facing `live/api/v1/web/*` surface served by
 * https://fiesta.nearle.app (documented at developer.nearledaily.com under the
 * REST tab). This is the operational backend: order/delivery/location summaries,
 * the deliveries board, riders, stock statements, and customers.
 *
 * Requests go directly to `https://fiesta.nearle.app/*` — Fiesta is CORS-enabled
 * and needs no auth header for these read endpoints, so no dev proxy is required.
 *
 * This sits alongside `./api` (the Hasura/workolik REST surface the dashboard
 * uses). Components should call the TanStack hooks in `./fiestaQueries`, not
 * these functions directly.
 */

const FIESTA_BASE = 'https://fiesta.nearle.app/live/api/v1/web';
const FIESTA_MOB_BASE = 'https://fiesta.nearle.app/live/api/v1/mob';

/** Tenant / location scope shared by the merchant console (Ragul Stores, Coimbatore). */
export const FIESTA_TENANT_ID = 1087;
export const FIESTA_APPLOCATION_ID = 1;
/** Primary outlet for this tenant — the one carrying live orders/stock. */
export const FIESTA_PRIMARY_LOCATION_ID = 1097;

export type Row = Record<string, unknown>;
type QueryParams = Record<string, string | number | undefined | null>;

/**
 * The exact payload the nearledaily consumer app expects when its in-app scanner
 * reads a store QR: a JSON object `{"tenantid":N,"locationid":N}`. The app parses
 * this and resolves the outlet from it.
 *
 * IMPORTANT: it must be this JSON shape, NOT a URL — the app rejects a URL with
 * "invalid QR code content". Keep it to exactly these two keys to match the app's
 * schema; extra keys risk strict-schema rejection on the app side.
 */
export function buildStoreQrPayload(opts: { tenantid: number; locationid: number }): string {
  return JSON.stringify({ tenantid: opts.tenantid, locationid: opts.locationid });
}

async function fiestaGet<T = unknown>(endpoint: string, params: QueryParams = {}): Promise<T> {
  const qs = new URLSearchParams();
  Object.entries(params).forEach(([k, v]) => {
    // Fiesta requires some params to be present-but-empty (e.g. keyword=), so we
    // keep empty strings and only drop undefined/null.
    if (v !== undefined && v !== null) qs.append(k, String(v));
  });
  const query = qs.toString();
  const res = await fetch(`${FIESTA_BASE}/${endpoint}${query ? `?${query}` : ''}`, {
    headers: { Accept: 'application/json' },
  });
  if (!res.ok) {
    throw new Error(`Fiesta ${endpoint} failed: ${res.status} ${res.statusText}`);
  }
  return res.json() as Promise<T>;
}

async function fiestaSend<T = unknown>(
  endpoint: string,
  method: 'POST' | 'PUT' | 'DELETE',
  body?: unknown,
): Promise<T> {
  const res = await fetch(`${FIESTA_BASE}/${endpoint}`, {
    method,
    headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });
  const json = (await res.json().catch(() => null)) as
    | { message?: string; status?: boolean }
    | null;
  if (!res.ok || (json && json.status === false)) {
    throw new Error(json?.message || `Fiesta ${endpoint} failed: ${res.status} ${res.statusText}`);
  }
  return json as T;
}

/**
 * Fiesta envelopes responses as `{ code, details, message, status }`. `details`
 * is usually an array of rows, sometimes a single object, sometimes null.
 */
export function toRows<T = Row>(json: unknown): T[] {
  if (Array.isArray(json)) return json as T[];
  if (json && typeof json === 'object') {
    const d = (json as { details?: unknown }).details;
    if (Array.isArray(d)) return d as T[];
    if (d && typeof d === 'object') return [d as T];
  }
  return [];
}

export function firstRow<T = Row>(json: unknown): T | null {
  const d = (json as { details?: unknown })?.details;
  if (Array.isArray(d)) return (d.length ? (d[0] as T) : null);
  if (d && typeof d === 'object') return d as T;
  return null;
}

export function num(v: unknown): number {
  const n = typeof v === 'number' ? v : Number(v);
  return Number.isFinite(n) ? n : 0;
}

export const str = (v: unknown): string => (v == null ? '' : String(v));

/** Fiesta date params want a bare `YYYY-MM-DD`. */
export const ymd = (d: Date) =>
  `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;

// ════════════════════════════════════════════════════════════════════════════
// ORDERS
// ════════════════════════════════════════════════════════════════════════════

export interface FiestaOrderSummary {
  total: number;
  created: number;
  pending: number;
  processing: number;
  delivered: number;
  cancelled: number;
  tenantid?: number;
  tenantname?: string;
}

export interface FiestaLocationRevenue {
  locationid: number;
  locationname: string;
  revenue: number;
}

export interface FiestaRevenueSummary {
  tenantid: number;
  tenantname: string;
  overallrevenue: number;
  locationrevenue: FiestaLocationRevenue[];
}

/** /orders/getrevenuesummary?tenantid=&locationid=&fromdate=&todate= — tenant revenue and location breakdown. */
export async function getRevenueSummary(opts: {
  tenantid: number;
  fromdate: string;
  todate: string;
  locationid?: number;
}) {
  const res = await fiestaGet('orders/getrevenuesummary', opts);
  return firstRow<{
    tenantid: number;
    locationid: number | null;
    grossrevenue: number;
    netrevenue: number;
    profit: number;
    marginpct: number;
    ordercount: number;
    deliveredcount: number;
    cancelledcount: number;
    otifpct: number;
    avgordervalue: number;
    prev_grossrevenue: number;
    prev_ordercount: number;
  }>(res);
}



/** /orders/getordersummary?tenantid=&locationid=&fromdate=&todate= — flat order counts. */
export async function getOrderSummary(
  tenantid: number,
  fromdate: string,
  todate: string,
  locationid?: number,
): Promise<FiestaOrderSummary | null> {
  const row = firstRow<Row>(await fiestaGet('orders/getordersummary', { tenantid, locationid, fromdate, todate }));
  if (!row) return null;
  return {
    total: num(row.total),
    created: num(row.created),
    pending: num(row.pending),
    processing: num(row.processing),
    delivered: num(row.delivered),
    cancelled: num(row.cancelled),
    tenantid: row.tenantid != null ? num(row.tenantid) : undefined,
    tenantname: typeof row.tenantname === 'string' ? row.tenantname : undefined,
  };
}

export interface FiestaLocationSummary {
  locationid: number;
  locationname: string;
  total: number;
  created: number;
  pending: number;
  processing: number;
  delivered: number;
  cancelled: number;
}

/** /orders/getlocationsummary?tenantid= — per-outlet order rollup. */
export async function getLocationSummary(tenantid: number): Promise<FiestaLocationSummary[]> {
  return toRows<Row>(await fiestaGet('orders/getlocationsummary', { tenantid })).map((r) => ({
    locationid: num(r.locationid),
    locationname: str(r.locationname),
    total: num(r.total),
    created: num(r.created),
    pending: num(r.pending),
    processing: num(r.processing),
    delivered: num(r.delivered),
    cancelled: num(r.cancelled),
  }));
}

/** /orders/getorderinsight?tenantid= — per-location monthly order counts. */
export async function getOrderInsight(tenantid: number): Promise<Row[]> {
  return toRows(await fiestaGet('orders/getorderinsight', { tenantid }));
}

/** /orders/getorders?tenantid=&locationid=&applocationid=&status=&fromdate=&todate=&keyword=&pageno=&pagesize= — orders board. */
export async function getOrders(opts: {
  tenantid: number;
  status: string;
  fromdate: string;
  todate: string;
  locationid?: number;
  applocationid?: number;
  keyword?: string;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('orders/getorders', {
      tenantid: opts.tenantid,
      locationid: opts.locationid,
      applocationid: opts.applocationid,
      status: opts.status,
      fromdate: opts.fromdate,
      todate: opts.todate,
      keyword: opts.keyword,
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 20,
    }),
  );
}

/** /orders/gettimeseries?tenantid=&locationid=&granularity=&fromdate=&todate= — time-series chart data */
export async function getTimeSeries(opts: {
  tenantid: number;
  granularity: 'day' | 'month' | 'year';
  fromdate: string;
  todate: string;
  locationid?: number;
}) {
  const res = await fiestaGet('orders/gettimeseries', opts);
  return toRows<{
    label: string;
    orders: number;
    revenue: number;
    delivered: number;
    cancelled: number;
    activeskus: number;
  }>(res);
}

/** /orders/getorderdetails?orderheaderid= — line items for a single order. */
export async function getOrderDetails(orderheaderid: number | string): Promise<Row[]> {
  let cleanId = String(orderheaderid).trim();
  if (cleanId.toUpperCase().startsWith('DLV-')) {
    cleanId = cleanId.substring(4);
  }
  cleanId = cleanId.split('-')[0];
  const numericId = Number(cleanId);
  const finalId = Number.isInteger(numericId) && numericId > 0 ? numericId : orderheaderid;

  return toRows(await fiestaGet('orders/getorderdetails', { orderheaderid: finalId }));
}

/** /orders/getorders?customerid=&status=&pageno=&pagesize= — one customer's order history. */
export async function getCustomerOrders(opts: {
  customerid: number | string;
  status?: string;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('orders/getorders', {
      customerid: opts.customerid,
      status: opts.status ?? '',
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 20,
    }),
  );
}

// ════════════════════════════════════════════════════════════════════════════
// DELIVERIES
// ════════════════════════════════════════════════════════════════════════════

export interface FiestaDeliverySummary {
  total: number;
  created: number;
  pending: number;
  accepted: number;
  arrived: number;
  picked: number;
  active: number;
  delivered: number;
  cancelled: number;
}

/** /deliveries/deliverysummary?tenantid=&applocationid=&locationid=&fromdate=&todate= — dispatch counts. */
export async function getDeliverySummary(opts: {
  tenantid: number;
  applocationid?: number;
  locationid?: number;
  fromdate: string;
  todate: string;
}): Promise<FiestaDeliverySummary | null> {
  const row = firstRow<Row>(
    await fiestaGet('deliveries/deliverysummary', {
      tenantid: opts.tenantid,
      applocationid: opts.applocationid, // only sent when provided (no forced default)
      locationid: opts.locationid,
      fromdate: opts.fromdate,
      todate: opts.todate,
    }),
  );
  if (!row) return null;
  return {
    total: num(row.total),
    created: num(row.created),
    pending: num(row.pending),
    accepted: num(row.accepted),
    arrived: num(row.arrived),
    picked: num(row.picked),
    active: num(row.active),
    delivered: num(row.delivered),
    cancelled: num(row.cancelled),
  };
}

/** /deliveries/getdeliveries?tenantid=&applocationid=&locationid=&status=&fromdate=&todate=&keyword=&pageno=&pagesize= — the master deliveries board. */
export async function getDeliveries(opts: {
  tenantid: number;
  fromdate: string;
  todate: string;
  status?: string;
  locationid?: number;
  applocationid?: number;
  keyword?: string;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  const rows = toRows(
    await fiestaGet('deliveries/getdeliveries', {
      tenantid: opts.tenantid,
      // NOTE: do NOT send `locationid` to getdeliveries — the backend's locationid
      // filter on THIS endpoint is broken: passing a real outlet id returns []
      // (it doesn't match against the row's own `locationid`), even though
      // deliverysummary honours the same id and the rows clearly carry it. So we
      // fetch tenant-wide here and scope by locationid client-side below; the KPI
      // strip (deliverysummary) keeps using the working server-side filter.
      applocationid: opts.applocationid,
      // The backend treats `status` as a LITERAL orderstatus filter — passing
      // 'all' matches nothing (returns []). Send empty to fetch every status and
      // let the board filter client-side by its status tabs.
      status: !opts.status || opts.status === 'all' ? '' : opts.status,
      fromdate: opts.fromdate,
      todate: opts.todate,
      keyword: opts.keyword,
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 200,
    }),
  );
  return opts.locationid ? rows.filter((r) => num(r.locationid) === opts.locationid) : rows;
}

/** /deliveries/getdeliveryinsight?tenantid= — daily delivery insight. */
export async function getDeliveryInsight(tenantid: number): Promise<Row[]> {
  return toRows(await fiestaGet('deliveries/getdeliveryinsight', { tenantid }));
}

/** /deliveries/getdeliveryreport?tenantid=&applocationid=&partnerid=&userid=&fromdate=&todate= —
 * deliveries financial report summary (per the endpoint sheet). */
export async function getDeliveryReport(opts: {
  tenantid: number;
  applocationid?: number;
  partnerid?: number;
  userid?: number;
  fromdate: string;
  todate: string;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('deliveries/getdeliveryreport', {
      tenantid: opts.tenantid,
      applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
      partnerid: opts.partnerid,
      userid: opts.userid,
      fromdate: opts.fromdate,
      todate: opts.todate,
    }),
  );
}

/** /partners/getfleetsummary?applocationid=&partnerid=&tenantid=&fromdate=&todate= —
 * fleet rider summary metrics (per the endpoint sheet). */
export async function getFleetSummary(opts: {
  applocationid?: number;
  partnerid?: number;
  tenantid: number;
  fromdate: string;
  todate: string;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('partners/getfleetsummary', {
      applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
      partnerid: opts.partnerid,
      tenantid: opts.tenantid,
      fromdate: opts.fromdate,
      todate: opts.todate,
    }),
  );
}

/** `YYYY-MM-DD HH:mm:ss` — the timestamp format the delivery endpoints expect. */
function nowStamp(): string {
  const d = new Date();
  const p = (n: number) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}

/**
 * Build a delivery record from a getorders row for POST /createdeliveries. The
 * backend keys the order (orderheaderid) and the rider (userid), copies the
 * pickup/drop snapshot, and must carry the SAME tenant/partner/applocation as
 * the order so the assignment is valid. Order field names map 1:1 (Go's JSON
 * decode is case-insensitive, so `pickupaddress` satisfies `Pickupaddress`).
 */
function deliveryFromOrder(o: Row, userid: number, assigntime: string): Row {
  return {
    orderheaderid: num(o.orderheaderid),
    orderid: str(o.orderid),
    applocationid: num(o.applocationid),
    configid: num(o.configid) || 1,
    partnerid: num(o.partnerid),
    tenantid: num(o.tenantid),
    moduleid: num(o.moduleid),
    locationid: num(o.locationid),
    categoryid: num(o.categoryid),
    subcategoryid: num(o.subcategoryid),
    userid, // the assigned rider
    customerid: num(o.customerid),
    orderstatus: 'pending',
    assigntime,
    // The Orders API exposes the scheduled delivery date as `deliverytime` (there
    // is no `deliverydate` on an order row). Copy it through so the new delivery
    // lands in the Deliveries board's date window — falling back to the order date
    // and finally the assign timestamp so the row is never written date-less
    // (a date-less delivery is excluded by getdeliveries' from/to filter).
    deliverydate: str(o.deliverydate) || str(o.deliverytime) || str(o.orderdate) || assigntime,
    itemcount: num(o.itemcount),
    orderamount: num(o.orderamount) || num(o.deliveryamt),
    deliveryamt: num(o.deliveryamt),
    deliverycharges: num(o.deliverycharge) || num(o.deliverycharges),
    paymenttype: num(o.paymenttype),
    ordernotes: str(o.ordernotes),
    pickupcustomer: str(o.pickupcustomer) || str(o.tenantname),
    pickupcontactno: str(o.pickupcontactno),
    pickupaddress: str(o.pickupaddress),
    pickuplocationid: num(o.pickuplocationid),
    pickuplat: str(o.pickuplat),
    pickuplon: str(o.pickuplong) || str(o.pickuplon),
    deliverycustomerid: num(o.deliverycustomerid),
    deliverylocationid: num(o.deliverylocationid),
    deliverycustomer: str(o.deliverycustomer),
    deliverycontactno: str(o.deliverycontactno),
    deliveryaddress: str(o.deliveryaddress),
    deliverylat: str(o.deliverylat),
    deliverylong: str(o.deliverylong),
  };
}

/**
 * Assign a rider to one or more orders — the CORRECT flow per the backend:
 *  • orders with no delivery yet (`deliveryid == 0`, i.e. freshly created) →
 *    POST /deliveries/createdeliveries (one batched call). This creates the
 *    delivery, enqueues it, AND flips the order to `pending`.
 *  • orders that already have a delivery → PUT /deliveries/updatedelivery to
 *    re-point the rider.
 * The rider (userid) MUST belong to the same tenant/partner as the orders, or
 * the backend rejects the assignment — that scoping is enforced on the rider
 * list (see getRiders' partnerid).
 */
export async function assignRiderToOrders(
  userid: number,
  orders: Row[],
): Promise<{ ok: number; failed: number; total: number }> {
  const assigntime = nowStamp();
  const toCreate = orders.filter((o) => !num(o.deliveryid));
  const toUpdate = orders.filter((o) => num(o.deliveryid));
  let ok = 0;
  let failed = 0;

  if (toCreate.length) {
    try {
      await fiestaSend('deliveries/createdeliveries', 'POST', toCreate.map((o) => deliveryFromOrder(o, userid, assigntime)));
      ok += toCreate.length;
    } catch {
      failed += toCreate.length;
    }
  }

  if (toUpdate.length) {
    const results = await Promise.allSettled(
      toUpdate.map((o) =>
        fiestaSend('deliveries/updatedelivery', 'PUT', {
          userid,
          deliveryid: num(o.deliveryid),
          orderheaderid: num(o.orderheaderid),
          orderstatus: 'pending',
          assigntime,
        }),
      ),
    );
    ok += results.filter((r) => r.status === 'fulfilled').length;
    failed += results.filter((r) => r.status === 'rejected').length;
  }

  return { ok, failed, total: orders.length };
}

// ════════════════════════════════════════════════════════════════════════════
// PARTNERS / RIDERS
// ════════════════════════════════════════════════════════════════════════════

/**
 * /partners/getriders?applocationid=&tenantid=&partnerid= — active rider fleet.
 * Scoped by tenant AND partner: a rider belongs to one tenant/partner, so an
 * order can only be assigned to a rider sharing its partnerid. Passing the
 * order's partnerid keeps the assignable list correct (an out-of-tenant rider
 * simply won't appear, which is the intended guard).
 */
export async function getRiders(opts: {
  applocationid?: number;
  tenantid: number;
  partnerid?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('partners/getriders', {
      applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
      tenantid: opts.tenantid,
      partnerid: opts.partnerid,
    }),
  );
}

/** /partners/getridershifts?applocationid= — rider shift records. */
export async function getRiderShifts(applocationid: number = FIESTA_APPLOCATION_ID): Promise<Row[]> {
  return toRows(await fiestaGet('partners/getridershifts/', { applocationid }));
}

// ════════════════════════════════════════════════════════════════════════════
// TENANTS / CUSTOMERS
// ════════════════════════════════════════════════════════════════════════════

/**
 * Throwaway/test email providers. A location whose contact email is on one of
 * these is a sandbox record, never a real outlet — used to drop test data.
 */
const DISPOSABLE_EMAIL_DOMAINS = new Set([
  'mailinator.com',
  'mailinator.net',
  'example.com',
  'example.org',
  'test.com',
  'yopmail.com',
  'guerrillamail.com',
  '10minutemail.com',
]);

/**
 * The tenant-locations endpoint for some tenants returns junk: the primary
 * outlet duplicated several times, plus orphan test records geocoded to random
 * countries (e.g. "Deborah Lara, Spain", "power, Ireland") with throwaway
 * emails. This strips both so the registry/inventory show only real outlets.
 *
 * The filter is self-calibrating (no hardcoded names/ids): it derives the
 * tenant's operating region from the most common state among its outlets, then
 * drops rows that either use a disposable email or sit outside that region. If
 * the region can't be established (no state data), nothing is region-filtered —
 * we'd rather show an extra row than hide a genuine outlet.
 */
export function cleanTenantLocations(rows: Row[]): Row[] {
  // 1. Dedupe by locationid — the API repeats the primary outlet.
  const seen = new Set<number>();
  const deduped = rows.filter((r) => {
    const id = num(r.locationid);
    if (!id || seen.has(id)) return false;
    seen.add(id);
    return true;
  });

  // 2. Find the tenant's home region (plurality of `state`).
  const stateCounts = new Map<string, number>();
  for (const r of deduped) {
    const st = str(r.state).trim().toLowerCase();
    if (st) stateCounts.set(st, (stateCounts.get(st) ?? 0) + 1);
  }
  let homeState = '';
  let max = 0;
  for (const [st, c] of stateCounts) {
    if (c > max) {
      max = c;
      homeState = st;
    }
  }

  // 3. Drop disposable-email rows and out-of-region rows.
  return deduped.filter((r) => {
    const emailDomain = (str(r.email).split('@')[1] ?? '').trim().toLowerCase();
    if (emailDomain && DISPOSABLE_EMAIL_DOMAINS.has(emailDomain)) return false;
    if (homeState) {
      const st = str(r.state).trim().toLowerCase();
      if (st && st !== homeState) return false;
    }
    return true;
  });
}

/** /tenants/gettenantlocations?tenantid= — outlet locations for a tenant (test rows stripped). */
export async function getTenantLocations(tenantid: number): Promise<Row[]> {
  return cleanTenantLocations(toRows(await fiestaGet('tenants/gettenantlocations', { tenantid })));
}

/** /tenants/getalltenants?applocationid=&status=&pageno=&pagesize= — active tenants. */
export async function getAllTenants(opts: {
  applocationid?: number;
  status?: string;
  pageno?: number;
  pagesize?: number;
} = {}): Promise<Row[]> {
  return toRows(
    await fiestaGet('tenants/getalltenants', {
      applocationid: opts.applocationid ?? FIESTA_APPLOCATION_ID,
      status: opts.status ?? 'Active',
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 20,
    }),
  );
}

/**
 * Collapse the gettenantcustomers rows to one per customer. The endpoint returns
 * one row per saved DELIVERY ADDRESS (each carries its own deliverylocationid /
 * address), so a customer with several addresses repeats many times. Key by
 * customerid (fall back to contactno), preferring the row flagged primaryaddress;
 * rows with no identity at all are kept as-is so nothing is silently dropped.
 */
export function dedupeCustomers(rows: Row[]): Row[] {
  const byCustomer = new Map<string, Row>();
  for (const r of rows) {
    const cid = num(r.customerid);
    const key = cid ? `c${cid}` : (str(r.contactno) ? `p${str(r.contactno)}` : `x${byCustomer.size}`);
    const existing = byCustomer.get(key);
    if (!existing || (num(r.primaryaddress) && !num(existing.primaryaddress))) {
      byCustomer.set(key, r);
    }
  }
  return [...byCustomer.values()];
}

/** /customers/gettenantcustomers?tenantid=&locationid=&pageno=&pagesize=&keyword= (deduped per customer). */
export async function getTenantCustomers(opts: {
  tenantid: number;
  locationid: number;
  keyword?: string;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  return dedupeCustomers(toRows(
    await fiestaGet('customers/gettenantcustomers', {
      tenantid: opts.tenantid,
      locationid: opts.locationid,
      keyword: opts.keyword ?? '',
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 20,
    }),
  ));
}

// ════════════════════════════════════════════════════════════════════════════
// PRODUCTS / STOCK
// ════════════════════════════════════════════════════════════════════════════

/** /products/getstockstatement?tenantid=&locationid=&subcategoryid=&keyword=&pageno=&pagesize= */
export async function getStockStatement(opts: {
  tenantid: number;
  locationid: number;
  subcategoryid?: number;
  keyword?: string;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('products/getstockstatement', {
      tenantid: opts.tenantid,
      locationid: opts.locationid,
      subcategoryid: opts.subcategoryid,
      keyword: opts.keyword ?? '',
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 50,
    }),
  );
}

/** /products/getproductscount?tenantid=&categoryid=&subcategoryid=&approve= */
export async function getProductsCount(opts: {
  tenantid: number;
  categoryid: number;
  subcategoryid?: number;
}): Promise<Row | null> {
  return firstRow(
    await fiestaGet('products/getproductscount', {
      tenantid: opts.tenantid,
      categoryid: opts.categoryid,
      subcategoryid: opts.subcategoryid,
      approve: 1,
    }),
  );
}

/** /products/getproductstocks?tenantid=&locationid= — live stock levels for an outlet. */
export async function getProductStocks(opts: {
  tenantid: number;
  locationid: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('products/getproductstocks', {
      tenantid: opts.tenantid,
      locationid: opts.locationid,
    }),
  );
}

/** /products/getproductlocations?tenantid=&locationid=&subcategoryid=&pageno=&pagesize= —
 * geofenced per-outlet inventory. */
export async function getProductLocations(opts: {
  tenantid: number;
  locationid: number;
  subcategoryid?: number;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('products/getproductlocations', {
      tenantid: opts.tenantid,
      locationid: opts.locationid,
      subcategoryid: opts.subcategoryid,
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 50,
    }),
  );
}

/** /products/getproducts?tenantid=&locationid=&subcategoryid=&keyword=&pageno=&pagesize= —
 * master catalog listings (global assortment). */
export async function getMasterCatalog(opts: {
  tenantid: number;
  locationid?: number;
  subcategoryid?: number;
  keyword?: string;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('products/getproducts', {
      tenantid: opts.tenantid,
      locationid: opts.locationid,
      subcategoryid: opts.subcategoryid,
      keyword: opts.keyword ?? '',
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 50,
    }),
  );
}

/** /products/getproductcategories — global product categories. */
export async function getProductCategories(): Promise<Row[]> {
  return toRows(await fiestaGet('products/getproductcategories', {}));
}

/** /products/getproductsubcategories?categoryid=&tenantid= — subcategories under a category. */
export async function getProductSubcategories(opts: {
  categoryid: number;
  tenantid?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('products/getproductsubcategories', {
      categoryid: opts.categoryid,
      tenantid: opts.tenantid,
    }),
  );
}

// ════════════════════════════════════════════════════════════════════════════
// USERS
// ════════════════════════════════════════════════════════════════════════════

/** Best-effort role label from the numeric roleid (roles aren't fully resolvable for every config). */
export function roleName(roleid: number): string {
  const map: Record<number, string> = {
    0: 'Unassigned',
    1: 'Owner',
    2: 'Manager',
    3: 'Admin',
    4: 'Staff',
    5: 'Rider',
    6: 'Cashier',
  };
  return map[roleid] || `Role ${roleid}`;
}

/** /users/getallusers?roleid=&tenantid=&pageno=&pagesize=&keyword= — staff/users under a tenant. */
export async function getAllUsers(opts: {
  tenantid: number;
  roleid?: number;
  keyword?: string;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('users/getallusers', {
      tenantid: opts.tenantid,
      roleid: opts.roleid,
      keyword: opts.keyword ?? '',
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 50,
    }),
  );
}

/** /users/getusers?userid= — a single user profile. */
export async function getUserById(userid: number): Promise<Row | null> {
  return firstRow(await fiestaGet('users/getusers', { userid }));
}

export interface CreateUserInput {
  firstname: string;
  lastname?: string;
  email: string;
  contactno: string;
  /** Optional — merchant_web's create form doesn't collect one. */
  password?: string;
  roleid: number;
  /** Role config (the selected role's configid) — matches merchant_web's create payload. */
  configid?: number;
  /** Business module id (merchant_web sends the logged-in user's; 0 when absent). */
  moduleid?: number;
  dialcode?: string;
  pin?: number;
  address?: string;
  suburb?: string;
  city?: string;
  state?: string;
  postcode?: string;
  latitude?: string;
  longitude?: string;
  /** Rider shift (only meaningful for rider-role users). */
  shiftid?: number;
  tenantid: number;
  locationid?: number;
  applocationid?: number;
  status?: string;
}

/** POST /users/create — register a new web staff user. */
export async function createUser(input: CreateUserInput): Promise<Row> {
  return fiestaSend<Row>('users/create', 'POST', {
    authname: input.email,
    firstname: input.firstname,
    lastname: input.lastname ?? '',
    password: input.password ?? '',
    email: input.email,
    dialcode: input.dialcode ?? '+91',
    contactno: input.contactno,
    roleid: input.roleid,
    configid: input.configid ?? 15,
    moduleid: input.moduleid ?? 0,
    pin: input.pin ?? 0,
    address: input.address ?? '',
    suburb: input.suburb ?? '',
    city: input.city ?? '',
    state: input.state ?? '',
    postcode: input.postcode ?? '',
    latitude: input.latitude ?? '',
    longitude: input.longitude ?? '',
    shiftid: input.shiftid ?? 0,
    tenantid: input.tenantid,
    locationid: input.locationid ?? 0,
    applocationid: input.applocationid ?? FIESTA_APPLOCATION_ID,
    status: input.status ?? 'active',
  });
}

export interface UpdateUserInput {
  userid: number;
  firstname?: string;
  lastname?: string;
  email?: string;
  contactno?: string;
  address?: string;
  suburb?: string;
  city?: string;
  state?: string;
  postcode?: string;
  status?: string;
}

/** PUT /users/update — update an existing web staff user. */
export async function updateUser(input: UpdateUserInput): Promise<Row> {
  return fiestaSend<Row>('users/update', 'PUT', input);
}

export interface CreateTenantInput {
  tenantname: string;
  companyname: string;
  primarycontact: string;
  primaryemail: string;
  address?: string;
  suburb?: string;
  city?: string;
  state?: string;
  postcode?: string;
  approved?: number;
  status?: string;
}

/** POST /tenants/createtenantuser — Onboard a new tenant and create their admin user. */
export async function createTenantUser(input: CreateTenantInput): Promise<Row> {
  const res = await fetch(`${FIESTA_MOB_BASE}/tenants/createtenantuser`, {
    method: 'POST',
    headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
    body: JSON.stringify(input),
  });
  const json = (await res.json().catch(() => null)) as { message?: string; status?: boolean } | null;
  if (!res.ok || (json && json.status === false)) {
    throw new Error(json?.message || `Tenant onboarding failed: ${res.status}`);
  }
  return json as Row;
}

export interface CreateTenantLocationInput {
  tenantid: number;
  locationname: string;
  address?: string;
  suburb?: string;
  city?: string;
  state?: string;
  postcode?: string;
  contactno?: string;
  email?: string;
  opentime?: string;
  closetime?: string;
  deliverymins?: number;
  deliveryradius?: number;
  latitude?: string;
  longitude?: string;
  status?: string;
}

/** POST /tenants/createtenantlocation — Create a new store location under a tenant. */
export async function createTenantLocation(input: CreateTenantLocationInput): Promise<Row> {
  return fiestaSend<Row>('tenants/createtenantlocation', 'POST', input);
}

// ════════════════════════════════════════════════════════════════════════════
// RIDERS / DISPATCH
// ════════════════════════════════════════════════════════════════════════════

/** /riders/getriderperiodiclogs?userid=&riderid=&fromdate=&todate=&tenantid=&applocationid= —
 * periodic GPS/status snapshots for a rider across a date range. */
export async function getRiderPeriodicLogs(opts: {
  userid?: number;
  riderid?: number;
  fromdate: string;
  todate: string;
  tenantid?: number;
  applocationid?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('riders/getriderperiodiclogs', {
      userid: opts.userid,
      riderid: opts.riderid,
      fromdate: opts.fromdate,
      todate: opts.todate,
      tenantid: opts.tenantid,
      applocationid: opts.applocationid,
    }),
  );
}

/** /riders/getriderlogs?userid=&riderid=&fromdate=&todate=&tenantid=&applocationid= —
 * full telemetry logs (GPS traces, events, etc.) for a rider. */
export async function getRiderLogs(opts: {
  userid?: number;
  riderid?: number;
  fromdate: string;
  todate: string;
  tenantid?: number;
  applocationid?: number;
  pageno?: number;
  pagesize?: number;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('riders/getriderlogs', {
      userid: opts.userid,
      riderid: opts.riderid,
      fromdate: opts.fromdate,
      todate: opts.todate,
      tenantid: opts.tenantid,
      applocationid: opts.applocationid,
      pageno: opts.pageno ?? 1,
      pagesize: opts.pagesize ?? 200,
    }),
  );
}

/** /partners/getbatchefficiency?partnerid=&tenantid=&fromdate=&todate= —
 * batch/trip efficiency metrics. */
export async function getBatchEfficiency(opts: {
  partnerid?: number;
  tenantid: number;
  fromdate: string;
  todate: string;
}): Promise<Row[]> {
  return toRows(
    await fiestaGet('partners/getbatchefficiency', {
      partnerid: opts.partnerid,
      tenantid: opts.tenantid,
      fromdate: opts.fromdate,
      todate: opts.todate,
    }),
  );
}

/** PUT /deliveries/updatedelivery — Manually assign/update a delivery's rider or status. */
export async function updateDelivery(deliveryid: number, updates: Row): Promise<Row> {
  return fiestaSend<Row>('deliveries/updatedelivery', 'PUT', {
    deliveryid,
    ...updates,
  });
}

/** POST /riders/reassigndeliveries — Batch-reassign multiple deliveries to a new rider. */
export async function reassignDeliveries(opts: {
  userid: number;
  deliveryids: number[];
}): Promise<Row> {
  return fiestaSend<Row>('riders/reassigndeliveries', 'POST', opts);
}

