// Cross-origin API client. All requests send the session cookie (connect.sid)
// via credentials: "include". The API origin comes from NEXT_PUBLIC_API_ORIGIN.

import type {
  AuthUser,
  BalanceFilter,
  BalanceListResponse,
  BalanceSort,
  BankAccount,
  BankCleared,
  BankDirection,
  BankFacets,
  BankInstitution,
  BankListResponse,
  BankSort,
  BankStats,
  BankSummary,
  BatchCreateInput,
  ConfirmBatchInput,
  ConfirmBatchResult,
  BatchCreateResponse,
  BillingFacets,
  BillingStats,
  BusinessLine,
  ByCheckResponse,
  CreateBankAccountInput,
  CreateBankInput,
  CreateBankMovementInput,
  CreateMovementInput,
  DiscardBatchResult,
  UpdateBankAccountInput,
  ResolveOutstandingInput,
  ReviewDocumentInput,
  StatementBatch,
  StatementBatchDetail,
  StatementDocument,
  StatementDocumentStatus,
  CustomerDetail,
  CustomerInput,
  CustomerListResponse,
  CustomerStats,
  LedgerCurrency,
  LedgerDirection,
  MovementListResponse,
  MovementSort,
  PolicyDetail,
  PolicyFacets,
  PolicyInput,
  PolicyListResponse,
  PolicySort,
  PolicyStats,
  PolicyStatus,
  PolicyOcrBatch,
  PolicyOcrBatchDetail,
  PolicyOcrDocument,
  PolicyOcrReviewInput,
  PolicyOcrConfirmInput,
  PolicyOcrConfirmResult,
  LookupsResponse,
  OpsJob,
  OpsJobKind,
  ReplicationStatus,
  IngestFile,
  BackupFile,
  PropertyDetail,
  PropertyFacets,
  PropertyInput,
  PropertyListResponse,
  PropertySort,
  PropertyStats,
  ReportCatalog,
  ReportRunResult,
  ServiceInput,
  TrustInput,
  Role,
  ServiceKind,
  Statement,
  Transaction,
  TransactionDomain,
  TrustFilter,
  UserRow,
} from "./types";

// Resolve the API origin at runtime, not build time. In the browser it comes
// from window.__API_ORIGIN__, injected server-side by the root layout from the
// deploy .env (API_ORIGIN) — so one built image serves any deployment. On the
// server (SSR) read process.env directly. NEXT_PUBLIC_API_ORIGIN stays as the
// dev/build fallback.
function resolveApiOrigin(): string {
  if (typeof window !== "undefined") {
    const injected = (window as { __API_ORIGIN__?: string }).__API_ORIGIN__;
    if (injected) return injected;
  }
  return (
    process.env.API_ORIGIN ??
    process.env.NEXT_PUBLIC_API_ORIGIN ??
    "http://localhost:3001"
  );
}

export const API_ORIGIN = resolveApiOrigin();

export class ApiError extends Error {
  status: number;
  constructor(status: number, message: string) {
    super(message);
    this.status = status;
    this.name = "ApiError";
  }
}

export async function apiFetch<T>(
  path: string,
  init?: RequestInit,
): Promise<T> {
  let res: Response;
  try {
    res = await fetch(`${API_ORIGIN}${path}`, {
      credentials: "include",
      headers: {
        "Content-Type": "application/json",
        ...(init?.headers ?? {}),
      },
      ...init,
    });
  } catch (e) {
    throw new ApiError(
      0,
      "No se pudo conectar con el servidor. Verifica tu conexión.",
    );
  }

  if (!res.ok) {
    let message = `Error ${res.status}`;
    try {
      const body = await res.json();
      if (body?.message) message = body.message;
    } catch {
      /* ignore non-JSON error bodies */
    }
    throw new ApiError(res.status, message);
  }

  if (res.status === 204) return undefined as T;
  return (await res.json()) as T;
}

export function login(email: string, password: string): Promise<AuthUser> {
  return apiFetch<AuthUser>("/auth/login", {
    method: "POST",
    body: JSON.stringify({ email, password }),
  });
}

export function me(): Promise<AuthUser> {
  return apiFetch<AuthUser>("/auth/me");
}

/** Persist the caller's own text-size preference on their account. */
export function updateUiScale(uiScale: number): Promise<AuthUser> {
  return apiFetch<AuthUser>("/auth/preferences", {
    method: "PATCH",
    body: JSON.stringify({ uiScale }),
  });
}

export function logout(): Promise<{ success: boolean }> {
  return apiFetch<{ success: boolean }>("/auth/logout", { method: "POST" });
}

export interface ServiceVersion {
  service: string;
  version: string;
  gitSha: string;
  buildDate: string;
}

/** What the API container reports it is running. Unauthenticated by design. */
export function getApiVersion(): Promise<ServiceVersion> {
  return apiFetch<ServiceVersion>("/version");
}

export function getStats(): Promise<CustomerStats> {
  return apiFetch<CustomerStats>("/customers/stats");
}

export interface CustomerQuery {
  query?: string;
  page?: number;
  pageSize?: number;
  line?: BusinessLine;
}

export function listCustomers(
  q: CustomerQuery,
): Promise<CustomerListResponse> {
  const params = new URLSearchParams();
  if (q.query) params.set("query", q.query);
  if (q.page) params.set("page", String(q.page));
  if (q.pageSize) params.set("pageSize", String(q.pageSize));
  if (q.line) params.set("line", q.line);
  const qs = params.toString();
  return apiFetch<CustomerListResponse>(`/customers${qs ? `?${qs}` : ""}`);
}

export function getCustomer(id: string): Promise<CustomerDetail> {
  return apiFetch<CustomerDetail>(`/customers/${id}`);
}

export function createCustomer(input: CustomerInput): Promise<CustomerDetail> {
  return apiFetch<CustomerDetail>("/customers", {
    method: "POST",
    body: JSON.stringify(input),
  });
}

export function updateCustomer(
  id: string,
  input: Partial<CustomerInput>,
): Promise<CustomerDetail> {
  return apiFetch<CustomerDetail>(`/customers/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}

export function archiveCustomer(id: string): Promise<CustomerDetail> {
  return apiFetch<CustomerDetail>(`/customers/${id}`, { method: "DELETE" });
}

export function restoreCustomer(id: string): Promise<CustomerDetail> {
  return apiFetch<CustomerDetail>(`/customers/${id}/restore`, { method: "POST" });
}

/* ------------------------------------------------------ Policies module */

/** Renewal horizon in days, shared by the list, stats and detail calls so the
 *  "por vencer" bucket means the same thing everywhere. */
export const EXPIRY_WINDOW_DAYS = 30;

export interface PolicyQuery {
  query?: string;
  page?: number;
  pageSize?: number;
  status?: PolicyStatus;
  days?: number;
  typeId?: string;
  providerId?: string;
  liquidated?: boolean;
  sort?: PolicySort;
}

export function listPolicies(q: PolicyQuery): Promise<PolicyListResponse> {
  const params = new URLSearchParams();
  if (q.query) params.set("query", q.query);
  if (q.page) params.set("page", String(q.page));
  if (q.pageSize) params.set("pageSize", String(q.pageSize));
  if (q.status) params.set("status", q.status);
  if (q.days) params.set("days", String(q.days));
  if (q.typeId) params.set("typeId", q.typeId);
  if (q.providerId) params.set("providerId", q.providerId);
  if (q.liquidated !== undefined) params.set("liquidated", String(q.liquidated));
  if (q.sort) params.set("sort", q.sort);
  const qs = params.toString();
  return apiFetch<PolicyListResponse>(`/policies${qs ? `?${qs}` : ""}`);
}

export function getPolicyStats(
  days: number = EXPIRY_WINDOW_DAYS,
): Promise<PolicyStats> {
  return apiFetch<PolicyStats>(`/policies/stats?days=${days}`);
}

export function getPolicyFacets(): Promise<PolicyFacets> {
  return apiFetch<PolicyFacets>("/policies/facets");
}

export function getPolicy(
  id: string,
  days: number = EXPIRY_WINDOW_DAYS,
): Promise<PolicyDetail> {
  return apiFetch<PolicyDetail>(`/policies/${id}?days=${days}`);
}

export function createPolicy(input: PolicyInput): Promise<PolicyDetail> {
  return apiFetch<PolicyDetail>("/policies", {
    method: "POST",
    body: JSON.stringify(input),
  });
}
export function updatePolicy(
  id: string,
  input: Partial<PolicyInput>,
): Promise<PolicyDetail> {
  return apiFetch<PolicyDetail>(`/policies/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}
export function archivePolicy(id: string): Promise<PolicyDetail> {
  return apiFetch<PolicyDetail>(`/policies/${id}`, { method: "DELETE" });
}
export function restorePolicy(id: string): Promise<PolicyDetail> {
  return apiFetch<PolicyDetail>(`/policies/${id}/restore`, { method: "POST" });
}

// Generic policy-child CRUD. `kind` is the URL segment
// (installments|vehicles|drivers|beneficiaries|claims).
export function addPolicyChild<T>(
  policyId: string,
  kind: string,
  input: T,
): Promise<unknown> {
  return apiFetch(`/policies/${policyId}/${kind}`, {
    method: "POST",
    body: JSON.stringify(input),
  });
}
export function updatePolicyChild<T>(
  policyId: string,
  kind: string,
  childId: string,
  input: T,
): Promise<unknown> {
  return apiFetch(`/policies/${policyId}/${kind}/${childId}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}
export function removePolicyChild(
  policyId: string,
  kind: string,
  childId: string,
): Promise<unknown> {
  return apiFetch(`/policies/${policyId}/${kind}/${childId}`, {
    method: "DELETE",
  });
}

/* ------------------------------------------------- Lookups (insurance ref) */

export function getLookups(): Promise<LookupsResponse> {
  return apiFetch<LookupsResponse>("/lookups");
}
export function createLookup(kind: string, input: unknown): Promise<unknown> {
  return apiFetch(`/lookups/${kind}`, { method: "POST", body: JSON.stringify(input) });
}
export function updateLookup(
  kind: string,
  id: string,
  input: unknown,
): Promise<unknown> {
  return apiFetch(`/lookups/${kind}/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}
export function removeLookup(kind: string, id: string): Promise<unknown> {
  return apiFetch(`/lookups/${kind}/${id}`, { method: "DELETE" });
}

/* ----------------------------------------------------- Utilities module */

export interface PropertyQuery {
  query?: string;
  page?: number;
  pageSize?: number;
  serviceKind?: ServiceKind;
  municipality?: string;
  bank?: string;
  trust?: TrustFilter;
  hasServices?: boolean;
  customerId?: string;
  days?: number;
  sort?: PropertySort;
}

export function listProperties(q: PropertyQuery): Promise<PropertyListResponse> {
  const params = new URLSearchParams();
  if (q.query) params.set("query", q.query);
  if (q.page) params.set("page", String(q.page));
  if (q.pageSize) params.set("pageSize", String(q.pageSize));
  if (q.serviceKind) params.set("serviceKind", q.serviceKind);
  if (q.municipality) params.set("municipality", q.municipality);
  if (q.bank) params.set("bank", q.bank);
  if (q.trust) params.set("trust", q.trust);
  if (q.hasServices !== undefined)
    params.set("hasServices", String(q.hasServices));
  if (q.customerId) params.set("customerId", q.customerId);
  if (q.days) params.set("days", String(q.days));
  if (q.sort) params.set("sort", q.sort);
  const qs = params.toString();
  return apiFetch<PropertyListResponse>(`/properties${qs ? `?${qs}` : ""}`);
}

export function getPropertyStats(
  days: number = EXPIRY_WINDOW_DAYS,
): Promise<PropertyStats> {
  return apiFetch<PropertyStats>(`/properties/stats?days=${days}`);
}

export function getPropertyFacets(): Promise<PropertyFacets> {
  return apiFetch<PropertyFacets>("/properties/facets");
}

export function getProperty(
  id: string,
  days: number = EXPIRY_WINDOW_DAYS,
): Promise<PropertyDetail> {
  return apiFetch<PropertyDetail>(`/properties/${id}?days=${days}`);
}

export function createProperty(input: PropertyInput): Promise<PropertyDetail> {
  return apiFetch<PropertyDetail>("/properties", {
    method: "POST",
    body: JSON.stringify(input),
  });
}
export function updateProperty(
  id: string,
  input: Partial<PropertyInput>,
): Promise<PropertyDetail> {
  return apiFetch<PropertyDetail>(`/properties/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}
export function archiveProperty(id: string): Promise<PropertyDetail> {
  return apiFetch<PropertyDetail>(`/properties/${id}`, { method: "DELETE" });
}
export function restoreProperty(id: string): Promise<PropertyDetail> {
  return apiFetch<PropertyDetail>(`/properties/${id}/restore`, { method: "POST" });
}

// Service child CRUD.
export function addService(propertyId: string, input: ServiceInput): Promise<unknown> {
  return apiFetch(`/properties/${propertyId}/services`, {
    method: "POST",
    body: JSON.stringify(input),
  });
}
export function updateService(
  propertyId: string,
  serviceId: string,
  input: Partial<ServiceInput>,
): Promise<unknown> {
  return apiFetch(`/properties/${propertyId}/services/${serviceId}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}
export function removeService(propertyId: string, serviceId: string): Promise<unknown> {
  return apiFetch(`/properties/${propertyId}/services/${serviceId}`, {
    method: "DELETE",
  });
}

// Trust account (1:1 upsert).
export function upsertTrust(propertyId: string, input: TrustInput): Promise<unknown> {
  return apiFetch(`/properties/${propertyId}/trust`, {
    method: "PUT",
    body: JSON.stringify(input),
  });
}
export function removeTrust(propertyId: string): Promise<unknown> {
  return apiFetch(`/properties/${propertyId}/trust`, { method: "DELETE" });
}

export function removePropertyDocument(
  propertyId: string,
  documentId: string,
): Promise<unknown> {
  return apiFetch(`/properties/${propertyId}/documents/${documentId}`, {
    method: "DELETE",
  });
}

export function propertyDocumentDownloadUrl(
  propertyId: string,
  documentId: string,
): string {
  return `${API_ORIGIN}/properties/${propertyId}/documents/${documentId}/download`;
}

export function uploadPropertyDocument(
  propertyId: string,
  file: File,
  type?: string,
): Promise<unknown> {
  const q = type ? `?type=${encodeURIComponent(type)}` : "";
  return uploadFile(`/properties/${propertyId}/documents${q}`, file);
}

export function removePolicyDocument(
  policyId: string,
  documentId: string,
): Promise<unknown> {
  return apiFetch(`/policies/${policyId}/documents/${documentId}`, {
    method: "DELETE",
  });
}

export function policyDocumentDownloadUrl(
  policyId: string,
  documentId: string,
): string {
  return `${API_ORIGIN}/policies/${policyId}/documents/${documentId}/download`;
}

export function uploadPolicyDocument(
  policyId: string,
  file: File,
  type?: string,
): Promise<unknown> {
  const q = type ? `?type=${encodeURIComponent(type)}` : "";
  return uploadFile(`/policies/${policyId}/documents${q}`, file);
}

/* ------------------------------------------- Billing / statements module */

export interface MovementQuery {
  query?: string;
  page?: number;
  pageSize?: number;
  domain?: TransactionDomain;
  currency?: LedgerCurrency;
  direction?: LedgerDirection;
  typeId?: string;
  source?: string;
  customerId?: string;
  /** Restrict to captured-but-unpaid rows (the NOPAGO worklist). */
  outstanding?: boolean;
  /** Exact check number — the by-check reconciliation lookup. */
  checkNumber?: string;
  /** `YYYY-MM-DD`, inclusive on both ends. */
  from?: string;
  to?: string;
  sort?: MovementSort;
}

export function listMovements(q: MovementQuery): Promise<MovementListResponse> {
  const params = new URLSearchParams();
  if (q.query) params.set("query", q.query);
  if (q.page) params.set("page", String(q.page));
  if (q.pageSize) params.set("pageSize", String(q.pageSize));
  if (q.domain) params.set("domain", q.domain);
  if (q.currency) params.set("currency", q.currency);
  if (q.direction) params.set("direction", q.direction);
  if (q.typeId) params.set("typeId", q.typeId);
  if (q.source) params.set("source", q.source);
  if (q.customerId) params.set("customerId", q.customerId);
  if (q.outstanding !== undefined) params.set("outstanding", String(q.outstanding));
  if (q.checkNumber) params.set("checkNumber", q.checkNumber);
  if (q.from) params.set("from", q.from);
  if (q.to) params.set("to", q.to);
  if (q.sort) params.set("sort", q.sort);
  const qs = params.toString();
  return apiFetch<MovementListResponse>(`/billing${qs ? `?${qs}` : ""}`);
}

export interface BalanceQuery {
  query?: string;
  page?: number;
  pageSize?: number;
  currency?: LedgerCurrency;
  balance?: BalanceFilter;
  domain?: TransactionDomain;
  sort?: BalanceSort;
}

export function listBalances(q: BalanceQuery): Promise<BalanceListResponse> {
  const params = new URLSearchParams();
  if (q.query) params.set("query", q.query);
  if (q.page) params.set("page", String(q.page));
  if (q.pageSize) params.set("pageSize", String(q.pageSize));
  if (q.currency) params.set("currency", q.currency);
  if (q.balance) params.set("balance", q.balance);
  if (q.domain) params.set("domain", q.domain);
  if (q.sort) params.set("sort", q.sort);
  const qs = params.toString();
  return apiFetch<BalanceListResponse>(`/billing/balances${qs ? `?${qs}` : ""}`);
}

export function getBillingStats(): Promise<BillingStats> {
  return apiFetch<BillingStats>("/billing/stats");
}

export function getBillingFacets(): Promise<BillingFacets> {
  return apiFetch<BillingFacets>("/billing/facets");
}

export function getStatement(customerId: string): Promise<Statement> {
  return apiFetch<Statement>(`/billing/customers/${customerId}`);
}

/** Append a new ledger movement. Booked movements are never edited — fix
 *  mistakes with voidMovement + a fresh capture. */
export function createMovement(
  input: CreateMovementInput,
): Promise<Transaction> {
  return apiFetch<Transaction>("/billing", {
    method: "POST",
    body: JSON.stringify(input),
  });
}

/** Reverse a movement by marking it voided; totals drop it. 400 if already void. */
export function voidMovement(id: string): Promise<Transaction> {
  return apiFetch<Transaction>(`/billing/${id}/void`, { method: "POST" });
}

/** Capture many customers' receipts against one check, in one transaction. The
 *  returned `items` are positionally parallel to `input.lines`. */
export function createMovementBatch(
  input: BatchCreateInput,
): Promise<BatchCreateResponse> {
  return apiFetch<BatchCreateResponse>("/billing/batch", {
    method: "POST",
    body: JSON.stringify(input),
  });
}

/** Clear an outstanding (NOPAGO) row: stamps the check number + resolution date
 *  and starts counting it toward the balance. 400 if not outstanding or voided. */
export function resolveOutstanding(
  id: string,
  input: ResolveOutstandingInput,
): Promise<Transaction> {
  return apiFetch<Transaction>(`/billing/${id}/resolve-outstanding`, {
    method: "POST",
    body: JSON.stringify(input),
  });
}

/** Everything captured against one check, with its reconciliation total. */
export function getByCheck(checkNumber: string): Promise<ByCheckResponse> {
  return apiFetch<ByCheckResponse>(
    `/billing/by-check?checkNumber=${encodeURIComponent(checkNumber)}`,
  );
}

/* ------------------------------------------------- Bank register (chequera) */

/**
 * Every read below is scoped to one chequera. `bankAccountId` is required, not
 * defaulted to "all accounts": the office's registers are in different
 * currencies, and a combined total would be a figure that never existed.
 */
export interface BankQuery {
  bankAccountId: string;
  query?: string;
  page?: number;
  pageSize?: number;
  direction?: BankDirection;
  cleared?: BankCleared;
  /** `YYYY-MM-DD`, inclusive on both ends. */
  from?: string;
  to?: string;
  sort?: BankSort;
}

export function listBankMovements(q: BankQuery): Promise<BankListResponse> {
  const params = new URLSearchParams({ bankAccountId: q.bankAccountId });
  if (q.query) params.set("query", q.query);
  if (q.page) params.set("page", String(q.page));
  if (q.pageSize) params.set("pageSize", String(q.pageSize));
  if (q.direction) params.set("direction", q.direction);
  if (q.cleared) params.set("cleared", q.cleared);
  if (q.from) params.set("from", q.from);
  if (q.to) params.set("to", q.to);
  if (q.sort) params.set("sort", q.sort);
  return apiFetch<BankListResponse>(`/bank?${params.toString()}`);
}

export function getBankStats(bankAccountId: string): Promise<BankStats> {
  return apiFetch<BankStats>(
    `/bank/stats?bankAccountId=${encodeURIComponent(bankAccountId)}`,
  );
}

export function getBankFacets(bankAccountId: string): Promise<BankFacets> {
  return apiFetch<BankFacets>(
    `/bank/facets?bankAccountId=${encodeURIComponent(bankAccountId)}`,
  );
}

export function getBankSummary(
  bankAccountId: string,
  year?: number,
): Promise<BankSummary> {
  const params = new URLSearchParams({ bankAccountId });
  if (year) params.set("year", String(year));
  return apiFetch<BankSummary>(`/bank/summary?${params.toString()}`);
}

/* --------------------------------------------- Chequera accounts (catalog) */

/** The account picker's source. Includes closed accounts, which stay readable. */
export function listBankAccounts(): Promise<BankAccount[]> {
  return apiFetch<BankAccount[]>("/bank/accounts");
}

export function listBankInstitutions(): Promise<BankInstitution[]> {
  return apiFetch<BankInstitution[]>("/bank/banks");
}

export function createBankInstitution(
  input: CreateBankInput,
): Promise<BankInstitution> {
  return apiFetch<BankInstitution>("/bank/banks", {
    method: "POST",
    body: JSON.stringify(input),
  });
}

export function updateBankInstitution(
  id: string,
  input: Partial<CreateBankInput>,
): Promise<BankInstitution> {
  return apiFetch<BankInstitution>(`/bank/banks/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}

export function createBankAccount(
  input: CreateBankAccountInput,
): Promise<unknown> {
  return apiFetch("/bank/accounts", {
    method: "POST",
    body: JSON.stringify(input),
  });
}

/** No `currency` — an account's booked movements are denominated in it. */
export function updateBankAccount(
  id: string,
  input: UpdateBankAccountInput,
): Promise<unknown> {
  return apiFetch(`/bank/accounts/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}

/** Append a new chequera movement. Booked rows are never edited — fix mistakes
 *  with voidBankMovement + a fresh capture. */
export function createBankMovement(
  input: CreateBankMovementInput,
): Promise<unknown> {
  return apiFetch("/bank", {
    method: "POST",
    body: JSON.stringify(input),
  });
}

/** Reverse a chequera movement by marking it voided; totals drop it. */
export function voidBankMovement(id: string): Promise<unknown> {
  return apiFetch(`/bank/${id}/void`, { method: "POST" });
}

/* ------------------------------------------------- Users / administration */

export function listUsers(): Promise<UserRow[]> {
  return apiFetch<UserRow[]>("/users");
}

export interface CreateUserInput {
  name: string;
  email: string;
  password: string;
  role: Role;
  active?: boolean;
}

export function createUser(input: CreateUserInput): Promise<UserRow> {
  return apiFetch<UserRow>("/users", {
    method: "POST",
    body: JSON.stringify(input),
  });
}

export interface UpdateUserInput {
  name?: string;
  email?: string;
  role?: Role;
  active?: boolean;
}

export function updateUser(id: string, input: UpdateUserInput): Promise<UserRow> {
  return apiFetch<UserRow>(`/users/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}

export function resetUserPassword(id: string, password: string): Promise<UserRow> {
  return apiFetch<UserRow>(`/users/${id}/reset-password`, {
    method: "POST",
    body: JSON.stringify({ password }),
  });
}

export function deleteUser(id: string): Promise<void> {
  return apiFetch<void>(`/users/${id}`, { method: "DELETE" });
}

/* ------------------------------------------- DB operations (admin only) */

export function listIngest(): Promise<IngestFile[]> {
  return apiFetch<IngestFile[]>("/ops/ingest");
}

/** Live upload stats reported to `uploadFile`'s `onProgress` callback. */
export type UploadProgress = {
  loaded: number;
  /** 0 when the browser can't compute the request size. */
  total: number;
  /** 0..1, or null when `total` is unknown. */
  fraction: number | null;
  /** Smoothed transfer rate. */
  bytesPerSecond: number;
  /** null until a rate and a total are both known. */
  secondsRemaining: number | null;
  /** True once the bytes are sent and we're waiting on the server's reply. */
  finishing: boolean;
};

/**
 * Multipart upload — not JSON, so it bypasses apiFetch's Content-Type. `path`
 * is API-relative (may include a query string); `filename` overrides the part
 * name sent to the server. Uses XHR rather than fetch because fetch has no way
 * to report request-body progress.
 */
export function uploadFile(
  path: string,
  file: File,
  filename?: string,
  onProgress?: (p: UploadProgress) => void,
): Promise<unknown> {
  const body = new FormData();
  body.append("file", file, filename ?? file.name);

  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("POST", `${API_ORIGIN}${path}`);
    xhr.withCredentials = true;

    if (onProgress) {
      // Exponentially smoothed rate — raw per-chunk deltas jump around too
      // much to read.
      let lastAt = performance.now();
      let lastLoaded = 0;
      let rate = 0;

      xhr.upload.onprogress = (e) => {
        const now = performance.now();
        const dt = (now - lastAt) / 1000;
        if (dt >= 0.15) {
          const sample = (e.loaded - lastLoaded) / dt;
          rate = rate === 0 ? sample : rate * 0.7 + sample * 0.3;
          lastAt = now;
          lastLoaded = e.loaded;
        }
        const total = e.lengthComputable ? e.total : 0;
        onProgress({
          loaded: e.loaded,
          total,
          fraction: total ? e.loaded / total : null,
          bytesPerSecond: rate,
          secondsRemaining:
            total && rate > 0 ? (total - e.loaded) / rate : null,
          finishing: false,
        });
      };
      // Bytes are out the door; the server still has to write the file.
      xhr.upload.onload = () => {
        onProgress({
          loaded: file.size,
          total: file.size,
          fraction: 1,
          bytesPerSecond: rate,
          secondsRemaining: 0,
          finishing: true,
        });
      };
    }

    xhr.onload = () => {
      let parsed: unknown;
      try {
        parsed = xhr.responseText ? JSON.parse(xhr.responseText) : undefined;
      } catch {
        parsed = undefined;
      }
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(parsed);
        return;
      }
      const message =
        (parsed as { message?: string } | undefined)?.message ??
        `Error ${xhr.status}`;
      reject(new ApiError(xhr.status, message));
    };
    xhr.onerror = () =>
      reject(new ApiError(0, "Fallo de red durante la carga."));
    xhr.onabort = () => reject(new ApiError(0, "Carga cancelada."));
    xhr.ontimeout = () => reject(new ApiError(0, "Tiempo de carga agotado."));

    xhr.send(body);
  });
}

export function uploadIngest(
  name: string,
  file: File,
  onProgress?: (p: UploadProgress) => void,
): Promise<unknown> {
  return uploadFile(
    `/ops/ingest/${encodeURIComponent(name)}`,
    file,
    name,
    onProgress,
  );
}

export function deleteIngest(name: string): Promise<unknown> {
  return apiFetch(`/ops/ingest/${encodeURIComponent(name)}`, { method: "DELETE" });
}

export function listBackups(): Promise<BackupFile[]> {
  return apiFetch<BackupFile[]>("/ops/backups");
}

export function backupDownloadUrl(name: string): string {
  return `${API_ORIGIN}/ops/backups/${encodeURIComponent(name)}/download`;
}

export function deleteBackup(name: string): Promise<unknown> {
  return apiFetch(`/ops/backups/${encodeURIComponent(name)}`, { method: "DELETE" });
}

/**
 * Health of the read replica my.jorgecuadros.com serves customers from.
 *
 * A stopped replica does not error — it answers with stale balances — so this
 * is the only place the failure is visible.
 */
export function getReplicationStatus(): Promise<ReplicationStatus> {
  return apiFetch<ReplicationStatus>("/ops/replication");
}

export function listOpsJobs(): Promise<OpsJob[]> {
  return apiFetch<OpsJob[]>("/ops/jobs");
}

export function getOpsJob(id: string): Promise<OpsJob> {
  return apiFetch<OpsJob>(`/ops/jobs/${id}`);
}

/** Start a mutating op. `file` is required for RESTORE. 409 if one is running. */
export function startOpsJob(kind: OpsJobKind, file?: string): Promise<OpsJob> {
  return apiFetch<OpsJob>("/ops/jobs", {
    method: "POST",
    body: JSON.stringify({ kind, file }),
  });
}

/* ----------------------------------------------------------- Reports module */

export function getReportCatalog(): Promise<ReportCatalog> {
  return apiFetch<ReportCatalog>("/reports");
}

export function runReport(
  slug: string,
  params: Record<string, string | undefined>,
): Promise<ReportRunResult> {
  const qs = new URLSearchParams();
  for (const [k, v] of Object.entries(params)) {
    if (v != null && v !== "") qs.set(k, v);
  }
  const tail = qs.toString();
  return apiFetch<ReportRunResult>(`/reports/${slug}${tail ? `?${tail}` : ""}`);
}

/* ------------------------------------------------- Mass email notifications */

export type NotificationType =
  | "OUTSTANDING_PAYMENT"
  | "PAYMENT_CONFIRMATION"
  | "ACCOUNT_STATUS"
  | "TRUST_PAYMENT_CONFIRMATION"
  | "RENEWAL_NOTICE";

export type NotificationServicio = "CUSTOMERS" | "TRUST" | "POLICIES";

/** Which servicios each /notificaciones tab reads out of the shared log. */
export const SERVICIOS_LOG_SCOPE: NotificationServicio[] = ["CUSTOMERS", "TRUST"];
export const POLIZAS_LOG_SCOPE: NotificationServicio[] = ["POLICIES"];

export type NotificationStatus =
  | "SENT"
  | "FAILED"
  | "SKIPPED_NO_EMAIL"
  | "SKIPPED_GATE";

export interface NotificationLogRow {
  id: string;
  sendDate: string;
  notificationType: NotificationType;
  level: number | null;
  servicio: NotificationServicio;
  customerId: string | null;
  customerName: string;
  customerEmail: string;
  subject: string;
  debug: boolean;
  status: NotificationStatus;
  providerMessageId: string | null;
  error: string | null;
}

export interface NotificationLogPage {
  items: NotificationLogRow[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
}

export interface NotificationStats {
  byType: { notificationType: NotificationType; status: NotificationStatus; _count: { _all: number } }[];
  byStatus: { status: NotificationStatus; _count: { _all: number } }[];
  byServicio: { servicio: NotificationServicio; status: NotificationStatus; _count: { _all: number } }[];
  lastRun: { sendDate: string; notificationType: NotificationType } | null;
  transport: { available: boolean; devFallback: boolean };
}

export type NotificationFlags = {
  debug?: boolean;
  ignoreDayRestriction?: boolean;
  useEmailLimit?: boolean;
};

/** Job 1 (Outstanding) response — legacy `result` field. */
export interface OutstandingResponse {
  result: "success";
  notificationType: "sendPaymentConfirmation";
  reason: string;
  statusCode: 200;
  sent: number;
  skipped: number;
  failed: number;
  debug: boolean;
  type: "OUTSTANDING_PAYMENT";
}

/** Job 2 (Payment Confirmation) response. */
export interface PaymentConfirmResponse {
  request: "success";
  notificationType: "sendPaymentConfirmation";
  confirmationSent: string;
  statusCode: 200;
  sent: number;
  skipped: number;
  failed: number;
  debug: boolean;
  type: "PAYMENT_CONFIRMATION";
}

/** Job 3 (Account Status) response. */
export interface AccountStatusResponse {
  request: "success";
  notificationType: "sendAccountStatus";
  statusSent: string;
  statusReport: string;
  statusCode: 200;
  red: number;
  yellow: number;
  total: number;
  sent: number;
  skipped: number;
  failed: number;
  debug: boolean;
  type: "ACCOUNT_STATUS";
}

/** Job 4 (Trust Confirmation) response. */
export interface TrustConfirmResponse {
  request: "success";
  notificationType: "sendTrustPaymentConfirmation";
  confirmationSent: string;
  statusCode: 200;
  sent: number;
  skipped: number;
  failed: number;
  debug: boolean;
  type: "TRUST_PAYMENT_CONFIRMATION";
}

export type NotificationJobResponse =
  | OutstandingResponse
  | PaymentConfirmResponse
  | AccountStatusResponse
  | TrustConfirmResponse;

export function runOutstandingPayments(
  flags: NotificationFlags = {},
): Promise<OutstandingResponse> {
  return apiFetch<OutstandingResponse>("/notifications/outstanding-payments", {
    method: "POST",
    body: JSON.stringify(flags),
  });
}

export function runPaymentConfirmation(
  flags: NotificationFlags = {},
): Promise<PaymentConfirmResponse> {
  return apiFetch<PaymentConfirmResponse>("/notifications/payment-confirmation", {
    method: "POST",
    body: JSON.stringify(flags),
  });
}

export function runAccountStatus(
  flags: NotificationFlags = {},
): Promise<AccountStatusResponse> {
  return apiFetch<AccountStatusResponse>("/notifications/account-status", {
    method: "POST",
    body: JSON.stringify(flags),
  });
}

export function runTrustConfirmation(
  flags: NotificationFlags = {},
): Promise<TrustConfirmResponse> {
  return apiFetch<TrustConfirmResponse>("/notifications/trust-payment-confirmation", {
    method: "POST",
    body: JSON.stringify(flags),
  });
}

export type NotificationJobKind = "outstanding" | "payment" | "account" | "trust";

export interface NotificationRunAllJobResult {
  kind: NotificationJobKind;
  ok: boolean;
  result?: NotificationJobResponse;
  error?: string;
}

/** Aggregate response of the "Ejecutar todos" sweep. */
export interface NotificationRunAllResponse {
  request: "success";
  notificationType: "runAllNotifications";
  statusCode: 200;
  debug: boolean;
  sent: number;
  skipped: number;
  failed: number;
  errors: number;
  jobs: NotificationRunAllJobResult[];
  type: "RUN_ALL";
}

export function runAllNotifications(
  flags: NotificationFlags = {},
): Promise<NotificationRunAllResponse> {
  return apiFetch<NotificationRunAllResponse>("/notifications/run-all", {
    method: "POST",
    body: JSON.stringify(flags),
  });
}

export interface NotificationLogQuery {
  page?: number;
  pageSize?: number;
  type?: NotificationType;
  /** One or more servicios; omitted = the whole log. */
  servicio?: NotificationServicio[];
  status?: NotificationStatus;
  view?: "sent" | "failed" | "skipped" | "all";
}

export function listNotificationLog(
  q: NotificationLogQuery = {},
): Promise<NotificationLogPage> {
  const qs = new URLSearchParams();
  if (q.page) qs.set("page", String(q.page));
  if (q.pageSize) qs.set("pageSize", String(q.pageSize));
  if (q.type) qs.set("type", q.type);
  if (q.servicio?.length) qs.set("servicio", q.servicio.join(","));
  if (q.status) qs.set("status", q.status);
  if (q.view) qs.set("view", q.view);
  const tail = qs.toString();
  return apiFetch<NotificationLogPage>(`/notifications/log${tail ? `?${tail}` : ""}`);
}

/** Where a setting's current value came from — shown so an operator can tell
 *  "nobody has set this, you are seeing the deploy's value" from "somebody
 *  set this on purpose". */
export type SettingSource = "db" | "env" | "default";

export interface NotificationAdminEmails {
  value: string[];
  source: SettingSource;
  updatedAt: string | null;
  updatedById: string | null;
}

export function getNotificationAdminEmails(): Promise<NotificationAdminEmails> {
  return apiFetch<NotificationAdminEmails>("/notifications/settings/admin-emails");
}

export function setNotificationAdminEmails(
  emails: string[],
): Promise<NotificationAdminEmails> {
  return apiFetch<NotificationAdminEmails>("/notifications/settings/admin-emails", {
    method: "PUT",
    body: JSON.stringify({ emails }),
  });
}

/* ----------------------------------------------------- envío scheduling */

/** The two automatic envíos, one per /notificaciones tab. */
export type ScheduleKind = "servicios" | "polizas";

export interface NotificationSchedule {
  enabled: boolean;
  /** Local hour/minute in America/Tijuana. */
  hour: number;
  minute: number;
  /** 0 = domingo … 6 = sábado. Vacío = todos los días. */
  weekdays: number[];
}

export interface ResolvedSchedule {
  value: NotificationSchedule;
  source: SettingSource;
  updatedAt: string | null;
  updatedById: string | null;
  /** Expression the value compiles to, shown verbatim in the UI. */
  cron: string;
  nextRun: string | null;
}

export type NotificationSchedules = Record<ScheduleKind, ResolvedSchedule>;

export function getNotificationSchedules(): Promise<NotificationSchedules> {
  return apiFetch<NotificationSchedules>("/notifications/settings/schedule");
}

export function setNotificationSchedule(
  kind: ScheduleKind,
  schedule: NotificationSchedule,
): Promise<ResolvedSchedule> {
  return apiFetch<ResolvedSchedule>(`/notifications/settings/schedule/${kind}`, {
    method: "PUT",
    body: JSON.stringify(schedule),
  });
}

export function getNotificationStats(
  servicio?: NotificationServicio[],
): Promise<NotificationStats> {
  const tail = servicio?.length
    ? `?servicio=${encodeURIComponent(servicio.join(","))}`
    : "";
  return apiFetch<NotificationStats>(`/notifications/stats${tail}`);
}

/** Build a download URL for a report's file output. The session cookie
 *  travels with the browser's same-origin navigation, so a plain `href`
 *  is enough — no fetch-with-credentials dance. */
export function reportDownloadUrl(
  slug: string,
  format: "csv" | "xlsx" | "pdf" | "print",
  params: Record<string, string | undefined>,
): string {
  const qs = new URLSearchParams();
  for (const [k, v] of Object.entries(params)) {
    if (v != null && v !== "") qs.set(k, v);
  }
  const tail = qs.toString();
  return `${API_ORIGIN}/reports/${slug}/${format}${tail ? `?${tail}` : ""}`;
}

/* ------------------------------------- Statement OCR intake (recibos) */

/**
 * Whether this deployment can ingest scans — automatic capture is hidden
 * without it. OCR reads the page, object storage keeps it; both are required.
 */
export function getStatementStatus(): Promise<{
  ocrAvailable: boolean;
  storageAvailable: boolean;
}> {
  return apiFetch("/statements/status");
}

export function listStatementBatches(
  page = 1,
  pageSize = 25,
): Promise<{
  items: StatementBatch[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
}> {
  return apiFetch(`/statements/batches?page=${page}&pageSize=${pageSize}`);
}

export function getStatementBatch(id: string): Promise<StatementBatchDetail> {
  return apiFetch(`/statements/batches/${id}`);
}

export function listStatementDocuments(
  batchId: string,
  status?: StatementDocumentStatus,
): Promise<StatementDocument[]> {
  const q = status ? `?status=${status}` : "";
  return apiFetch(`/statements/batches/${batchId}/documents${q}`);
}

/** Multi-file upload — one batch is usually several multi-page scans. */
export async function uploadStatementBatch(
  files: File[],
  serviceKind: ServiceKind,
  label?: string,
): Promise<StatementBatch> {
  const body = new FormData();
  for (const f of files) body.append("files", f, f.name);
  const qs = new URLSearchParams({ serviceKind });
  if (label) qs.set("label", label);

  const res = await fetch(`${API_ORIGIN}/statements/batches?${qs}`, {
    method: "POST",
    credentials: "include",
    body,
  });
  if (!res.ok) {
    let message = `Error ${res.status}`;
    try {
      const b = await res.json();
      if (b?.message) message = b.message;
    } catch {
      /* non-JSON error body */
    }
    throw new Error(message);
  }
  return res.json();
}

export function reviewStatementDocument(
  id: string,
  input: ReviewDocumentInput,
): Promise<StatementDocument> {
  return apiFetch(`/statements/documents/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}

export function rejectStatementDocument(id: string): Promise<StatementDocument> {
  return apiFetch(`/statements/documents/${id}/reject`, { method: "POST" });
}

export function confirmStatementBatch(
  batchId: string,
  input: ConfirmBatchInput,
): Promise<ConfirmBatchResult> {
  return apiFetch(`/statements/batches/${batchId}/confirm`, {
    method: "POST",
    body: JSON.stringify(input),
  });
}

/** Abandon a batch pending review; rejects every page that is not posted. */
export function discardStatementBatch(batchId: string): Promise<DiscardBatchResult> {
  return apiFetch(`/statements/batches/${batchId}/discard`, { method: "POST" });
}

/** The rendered page image. A plain <img src> — the cookie rides along. */
export function statementPageUrl(documentId: string): string {
  return `${API_ORIGIN}/statements/documents/${documentId}/page`;
}

/* ----------------------------------------------------- Policy OCR (GMX) */

export function getPolicyOcrStatus(): Promise<{
  ocrAvailable: boolean;
  storageAvailable: boolean;
}> {
  return apiFetch("/policy-ocr/status");
}

export function listPolicyOcrBatches(
  page = 1,
  pageSize = 25,
): Promise<{
  items: PolicyOcrBatch[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
}> {
  return apiFetch(`/policy-ocr/batches?page=${page}&pageSize=${pageSize}`);
}

export function getPolicyOcrBatch(id: string): Promise<PolicyOcrBatchDetail> {
  return apiFetch(`/policy-ocr/batches/${id}`);
}

export function listPolicyOcrDocuments(batchId: string): Promise<PolicyOcrDocument[]> {
  return apiFetch(`/policy-ocr/batches/${batchId}/documents`);
}

export async function uploadPolicyOcrBatch(
  files: File[],
  label?: string,
): Promise<PolicyOcrBatch> {
  const body = new FormData();
  for (const f of files) body.append("files", f, f.name);
  const qs = new URLSearchParams();
  if (label) qs.set("label", label);

  const res = await fetch(
    `${API_ORIGIN}/policy-ocr/batches${qs.toString() ? `?${qs}` : ""}`,
    {
      method: "POST",
      credentials: "include",
      body,
    },
  );
  if (!res.ok) {
    let message = `Error ${res.status}`;
    try {
      const b = await res.json();
      if (b?.message) message = b.message;
    } catch {
      /* non-JSON error body */
    }
    throw new Error(message);
  }
  return res.json();
}

export function reviewPolicyOcrDocument(
  id: string,
  input: PolicyOcrReviewInput,
): Promise<PolicyOcrDocument> {
  return apiFetch(`/policy-ocr/documents/${id}`, {
    method: "PATCH",
    body: JSON.stringify(input),
  });
}

export function rejectPolicyOcrDocument(id: string): Promise<PolicyOcrDocument> {
  return apiFetch(`/policy-ocr/documents/${id}/reject`, { method: "POST" });
}

export function confirmPolicyOcrBatch(
  batchId: string,
  input: PolicyOcrConfirmInput,
): Promise<PolicyOcrConfirmResult> {
  return apiFetch(`/policy-ocr/batches/${batchId}/confirm`, {
    method: "POST",
    body: JSON.stringify(input),
  });
}

/** Abandon a batch pending review; rejects every page that is not applied. */
export function discardPolicyOcrBatch(batchId: string): Promise<DiscardBatchResult> {
  return apiFetch(`/policy-ocr/batches/${batchId}/discard`, { method: "POST" });
}

/**
 * URL for the source PDF of a parsed policy document. The endpoint returns
 * the original upload (one PDF = one parsed policy), not a rendered page
 * image, so the review screen embeds it in an iframe.
 */
export function policyOcrDocumentUrl(documentId: string): string {
  return `${API_ORIGIN}/policy-ocr/documents/${documentId}/page`;
}
