// TypeScript types for the Jorge Cuadros & Asociados API responses.
// Decimals arrive as strings, dates as ISO strings.

export type Currency = "USD" | "MXN";

export type Role = "ADMIN" | "MANAGER" | "STAFF" | "VIEWER";

export type Ability =
  | "customer:create"
  | "customer:update"
  | "customer:delete"
  | "policy:create"
  | "policy:update"
  | "policy:delete"
  | "policy:ingest"
  | "policy:ocr-review"
  | "renewal:send"
  | "property:create"
  | "property:update"
  | "property:delete"
  | "ledger:create"
  | "ledger:void"
  | "bank:create"
  | "bank:void"
  | "bank:manage-accounts"
  | "statement:ingest"
  | "statement:review"
  | "lookup:manage"
  | "user:manage"
  | "db:manage"
  | "notification:send"
  | "setting:manage";

export interface AuthUser {
  id: string;
  name: string;
  email: string;
  role: Role;
  active: boolean;
  // Text-size preference, stored per account so it follows the person across
  // machines. localStorage still holds a copy, but only as a pre-paint cache —
  // this value is the source of truth. See lib/ui-scale.ts.
  uiScale: number;
  // Resolved server-side from role (abilitiesFor in the API); the UI only ever
  // reads this map, never re-derives the rules. Server still enforces.
  abilities: Record<Ability, boolean>;
}

export interface UserRow {
  id: string;
  name: string;
  email: string;
  role: Role;
  active: boolean;
  createdAt: string;
  updatedAt: string;
}

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

export type OpsJobKind = "BACKUP" | "RESTORE" | "REIMPORT" | "SYNC";
export type OpsJobStatus = "RUNNING" | "SUCCESS" | "FAILED";

export interface OpsJob {
  id: string;
  kind: OpsJobKind;
  status: OpsJobStatus;
  log: string;
  params: Record<string, unknown> | null;
  createdById: string | null;
  startedAt: string;
  finishedAt: string | null;
}

/**
 * Health of the MySQL read replica that my.jorgecuadros.com queries.
 *
 * `secondsBehind` is null whenever MySQL reports NULL, which it does when
 * EITHER thread is down — so null means "unknown", never "up to date". Read
 * `healthy`/`problem` rather than inferring health from the lag.
 */
export interface ReplicationStatus {
  configured: boolean;
  healthy: boolean;
  host: string | null;
  ioRunning: string | null;
  sqlRunning: string | null;
  secondsBehind: number | null;
  lastIoError: string | null;
  lastSqlError: string | null;
  sourceHost: string | null;
  problem: string | null;
  checkedAt: string;
}

/** One of the four legacy Access files expected in the ingest folder. */
export interface IngestFile {
  name: string;
  present: boolean;
  size: number | null;
  modifiedAt: string | null;
}

export interface BackupFile {
  name: string;
  size: number;
  createdAt: string;
}

export interface CustomerStats {
  customers: number;
  withUtilities: number;
  withInsurance: number;
  bothLines: number;
  policies: number;
  properties: number;
  transactions: number;
}

export type BusinessLine = "utility" | "insurance" | "both";

export interface CustomerListItem {
  id: string;
  /** May be "(SIN NOMBRE)" — the legacy record had no name and none was recoverable. */
  name: string;
  /** Legacy table the name was recovered from; null when it came from DATGRAL. */
  nameSource: string | null;
  city: string | null;
  state: string | null;
  email: string | null;
  phone: string | null;
  mobile: string | null;
  status: boolean;
  archived: boolean;
  propertyCount: number;
  policyCount: number;
  transactionCount: number;
  hasUtilities: boolean;
  hasInsurance: boolean;
}

export interface CustomerListResponse {
  items: CustomerListItem[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
}

export interface LegacyRef {
  id: string;
  sourceSystem: string; // "utilities" | "insurance"
  sourceTable: string;
  legacyId: string;
}

export type ServiceKind =
  | "WATER"
  | "ELECTRIC"
  | "GAS"
  | "CABLE"
  | "PROPERTY_TAX"
  | "FEDERAL_ZONE"
  | "ALARM"
  | "TELEPHONE"
  | "OTHER"
  | string;

export interface Service {
  id: string;
  kind: ServiceKind;
  accountNumber: string | null;
  meterNumber: string | null;
  route: string | null;
  dueDay: string | null;
  active: boolean;
  notes: string | null;
}

export interface TrustAccount {
  id: string;
  bankName: string | null;
  trustNumber: string | null;
  bankFee: string | null;
  dueDate1: string | null;
  dueDate2: string | null;
}

export interface DocumentRef {
  id?: string;
  documentType: string | null;
  storageKey: string | null;
}

export interface Property {
  id: string;
  addressLine1: string | null;
  addressLine2: string | null;
  phone1: string | null;
  phone2: string | null;
  phone3: string | null;
  zone: string | null;
  services: Service[];
  trustAccount: TrustAccount | null;
  documents: DocumentRef[];
}

export interface Installment {
  id: string;
  sequence: number;
  amount: string | null;
  currency: string | null;
  dueDate: string | null;
  paidDate: string | null;
  checkNumber: string | null;
  isCash: boolean;
}

export interface Vehicle {
  id: string;
  make: string | null;
  model: string | null;
  modelYear: number | string | null;
  licensePlate: string | null;
  bodyType: string | null;
  engineNumber?: string | null;
  vinNumber?: string | null;
  stateCode?: string | null;
  notes?: string | null;
}

export interface InsuredDriver {
  id: string;
  fullName: string | null;
  licenseNumber: string | null;
  birthDate?: string | null;
  sex?: string | null;
  occupation?: string | null;
  licenseState?: string | null;
}

export interface Beneficiary {
  id: string;
  name: string | null;
  phone: string | null;
  email: string | null;
  address?: string | null;
}

export interface NamedRef {
  name: string | null;
  shortDescription?: string | null;
}

export interface Policy {
  id: string;
  policyNumber: string | null;
  policyType: NamedRef | null;
  insuranceProvider: NamedRef | null;
  agentName: string | null;
  policyFrom: string | null;
  policyTo: string | null;
  netPremium: string | null;
  total: string | null;
  currency: string | null;
  liquidated: boolean;
  installments: Installment[];
  vehicles: Vehicle[];
  insuredDrivers: InsuredDriver[];
  beneficiaries: Beneficiary[];
  claims: unknown[];
  documents: DocumentRef[];
}

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

/**
 * Vigencia bucket computed by the API from `policyTo`. "undated" is a real
 * bucket: a large share of migrated policies carry no end date at all.
 */
export type PolicyStatus = "active" | "expiring" | "expired" | "undated";

export type PolicySort =
  | "expiry_desc"
  | "expiry_asc"
  | "customer"
  | "number"
  | "premium_desc";

export interface PolicyListItem {
  id: string;
  policyNumber: string | null;
  agentName: string | null;
  policyFrom: string | null;
  policyTo: string | null;
  netPremium: string | null;
  total: string | null;
  currency: string | null;
  liquidated: boolean;
  customerId: string;
  customerName: string;
  customerCity: string | null;
  policyType: { id: string; name: string } | null;
  insuranceProvider: { id: string; name: string } | null;
  status: PolicyStatus;
  /** Days until `policyTo`; negative when already expired, null when undated. */
  daysToExpiry: number | null;
  vehicleCount: number;
  installmentCount: number;
  documentCount: number;
  archived: boolean;
}

/** Editable policy-header fields — shared by the create/edit form and API. */
export interface PolicyInput {
  policyNumber: string;
  customerId: string;
  policyTypeId?: string;
  insuranceProviderId?: string;
  agentName?: string;
  policyDate?: string;
  policyFrom?: string;
  policyTo?: string;
  coveragePeriodDays?: number;
  netPremium?: number;
  policyFee?: number;
  brokerFee?: number;
  commission?: number;
  total?: number;
  currency?: Currency;
  observations?: string;
  notes?: string;
  endorsement?: boolean;
  liquidated?: boolean;
  liquidationNumber?: string;
  liquidationDate?: string;
}

export interface InstallmentInput {
  sequence: number;
  amount?: number;
  currency?: Currency;
  dueDate?: string;
  paidDate?: string;
  checkNumber?: string;
  isCash?: boolean;
}
export interface VehicleInput {
  make?: string;
  model?: string;
  modelYear?: string;
  bodyType?: string;
  engineNumber?: string;
  licensePlate?: string;
  vinNumber?: string;
  stateCode?: string;
  notes?: string;
}
export interface DriverInput {
  fullName?: string;
  birthDate?: string;
  sex?: string;
  occupation?: string;
  licenseNumber?: string;
  licenseState?: string;
}
export interface BeneficiaryInput {
  name?: string;
  address?: string;
  phone?: string;
  email?: string;
}
export interface ClaimInput {
  claimType?: string;
  incidentDate?: string;
  reportedDate?: string;
  description?: string;
  adjusterId?: string;
  claimedAmount?: number;
  settledAmount?: number;
  settlementDate?: string;
  checkNumber?: string;
  resolved?: boolean;
  resolutionNotes?: string;
}

/* Lookups (insurance reference data) */
export interface ProviderRow {
  id: string;
  name: string;
  _count?: { policies: number };
}
export interface PolicyTypeRow {
  id: string;
  name: string;
  shortDescription: string | null;
  _count?: { policies: number };
}
export interface AdjusterRow {
  id: string;
  company: string | null;
  city: string | null;
  name: string | null;
  phone: string | null;
  beeper: string | null;
}
export interface LookupsResponse {
  providers: ProviderRow[];
  types: PolicyTypeRow[];
  adjusters: AdjusterRow[];
}

export interface PolicyListResponse {
  items: PolicyListItem[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
}

export interface PremiumRow {
  currency: string;
  total: string | null;
  netPremium: string | null;
  count: number;
}

export interface PolicyStats {
  total: number;
  active: number;
  expiring: number;
  expired: number;
  undated: number;
  liquidated: number;
  pending: number;
  days: number;
  premiumInForce: PremiumRow[];
}

export interface Facet {
  id: string;
  name: string;
  count: number;
}

export interface PolicyFacets {
  types: Facet[];
  providers: Facet[];
}

export interface Adjuster {
  id: string;
  name: string | null;
  phone?: string | null;
  email?: string | null;
}

export interface Claim {
  id: string;
  claimType: string | null;
  incidentDate: string | null;
  reportedDate: string | null;
  description: string | null;
  claimedAmount: string | null;
  settledAmount: string | null;
  settlementDate: string | null;
  status?: string | null;
  adjuster: Adjuster | null;
  adjusterId?: string | null;
  checkNumber?: string | null;
  resolved?: boolean;
  resolutionNotes?: string | null;
}

export interface PolicyCustomerRef {
  id: string;
  name: string;
  nameSource: string | null;
  city: string | null;
  state: string | null;
  phone: string | null;
  mobile: string | null;
  email: string | null;
}

export interface PolicyDetail {
  id: string;
  policyNumber: string | null;
  agentName: string | null;
  policyDate: string | null;
  policyFrom: string | null;
  policyTo: string | null;
  coveragePeriodDays: number | null;
  netPremium: string | null;
  policyFee: string | null;
  brokerFee: string | null;
  commission: string | null;
  total: string | null;
  currency: string | null;
  observations: string | null;
  notes: string | null;
  /** Legacy coverage columns the target schema doesn't model, kept verbatim. */
  coveragesJson: Record<string, unknown> | null;
  endorsement: boolean;
  liquidated: boolean;
  liquidationNumber: string | null;
  liquidationDate: string | null;
  legacySourceDb: string | null;
  legacySourceTable: string | null;
  legacyId: string | null;
  archivedAt: string | null;
  status: PolicyStatus;
  daysToExpiry: number | null;
  customer: PolicyCustomerRef;
  policyType: NamedRef & { id: string } | null;
  insuranceProvider: (NamedRef & { id: string }) | null;
  installments: Installment[];
  vehicles: Vehicle[];
  insuredDrivers: InsuredDriver[];
  beneficiaries: Beneficiary[];
  claims: Claim[];
  documents: DocumentRef[];
  properties: {
    id: string;
    addressLine1: string | null;
    addressLine2: string | null;
    zone: string | null;
  }[];
}

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

/**
 * Trust (fideicomiso) renewal bucket computed by the API from the trust's
 * `dueDate2` — the *next* annual due date. "undated" is a real bucket: a
 * handful of migrated trusts carry no dates at all.
 */
export type TrustStatus = "active" | "expiring" | "expired" | "undated";

/** `with`/`without` filter the whole property set; the rest are trust buckets. */
export type TrustFilter = "with" | "without" | TrustStatus;

export type PropertySort =
  | "customer"
  | "address"
  | "services_desc"
  | "trust_due_asc"
  | "trust_due_desc";

export interface TrustSummary {
  bankName: string | null;
  trustNumber: string | null;
  bankFee: string | null;
  dueDate1: string | null;
  dueDate2: string | null;
  status: TrustStatus;
  /** Days until `dueDate2`; negative when overdue, null when undated. */
  daysToDue: number | null;
}

export interface PropertyListItem {
  id: string;
  addressLine1: string | null;
  addressLine2: string | null;
  zone: string | null;
  phones: string[];
  customerId: string;
  customerName: string;
  customerCity: string | null;
  customerState: string | null;
  /** Municipality, read off the predial service's notes. */
  municipality: string | null;
  services: { id: string; kind: ServiceKind; active: boolean }[];
  serviceCount: number;
  activeServiceCount: number;
  documentCount: number;
  trust: TrustSummary | null;
  archived: boolean;
}

/** Editable property-header fields — shared by the form and the API. */
export interface PropertyInput {
  customerId: string;
  policyId?: string;
  addressLine1?: string;
  addressLine2?: string;
  phone1?: string;
  phone2?: string;
  phone3?: string;
  zone?: string;
}
export interface ServiceInput {
  kind: ServiceKind;
  accountNumber?: string;
  meterNumber?: string;
  route?: string;
  dueDay?: string;
  active?: boolean;
  notes?: string;
}
export interface TrustInput {
  bankName?: string;
  trustNumber?: string;
  bankFee?: number;
  dueDate1?: string;
  dueDate2?: string;
}

export interface PropertyListResponse {
  items: PropertyListItem[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
}

export interface PropertyStats {
  properties: number;
  owners: number;
  services: number;
  withoutServices: number;
  trusts: number;
  trustExpiring: number;
  trustExpired: number;
  documents: number;
  days: number;
  byKind: { kind: ServiceKind; count: number; active: number }[];
}

export interface PropertyFacets {
  kinds: { kind: ServiceKind; count: number }[];
  municipalities: { name: string; count: number }[];
  banks: { name: string; count: number }[];
}

export interface PropertyOwnerRef {
  id: string;
  name: string;
  nameSource: string | null;
  addressLine1: string | null;
  city: string | null;
  state: string | null;
  phone: string | null;
  mobile: string | null;
  email: string | null;
  _count: { properties: number; policies: number };
}

export interface PropertyDetail {
  id: string;
  customerId: string;
  addressLine1: string | null;
  addressLine2: string | null;
  phone1: string | null;
  phone2: string | null;
  phone3: string | null;
  zone: string | null;
  archivedAt: string | null;
  legacySourceTable: string | null;
  legacyId: string | null;
  customer: PropertyOwnerRef;
  services: Service[];
  trustAccount: (TrustAccount & { propertyId?: string }) | null;
  documents: DocumentRef[];
  policy: {
    id: string;
    policyNumber: string | null;
    policyTo: string | null;
    policyType: { name: string } | null;
  } | null;
  municipality: string | null;
  trustStatus: TrustStatus;
  daysToTrustDue: number | null;
  siblings: {
    id: string;
    addressLine1: string | null;
    addressLine2: string | null;
    zone: string | null;
    serviceCount: number;
  }[];
  /** Owner-level utility movements — the legacy data never tied a payment to
   *  a specific property, so these belong to the customer, not to this file. */
  customerTransactions: Transaction[];
  customerLedger: { currency: string; total: string | null; count: number }[];
}

export type TransactionDomain = "UTILITY" | "INSURANCE" | "TRUST" | string;

export interface TransactionType {
  nameEn: string | null;
  nameEs: string | null;
}

export interface Transaction {
  id: string;
  transactionDate: string | null;
  domain: TransactionDomain;
  amount: string | null;
  currency: string | null;
  reference: string | null;
  period?: string | null;
  message: string | null;
  checkNumber: string | null;
  /** App-voided (`voidedAt` set). UI strikes; totals exclude. */
  voidedAt?: string | null;
  type: TransactionType | null;
}

export interface TransactionSummaryRow {
  domain: TransactionDomain;
  currency: string;
  total: string;
  count: number;
}

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

/**
 * Which side of the ledger a movement sits on. `transactions.amount` is signed:
 * a charge (cargo) is negative, a credit (abono) is positive, so the balance is
 * simply the sum — negative means the customer owes the office.
 */
export type LedgerDirection = "charge" | "credit";

/** The only two currencies in the ledger. Totals are never summed across them. */
export type LedgerCurrency = "MXN" | "USD";

export type BalanceFilter = "all" | "owing" | "credit" | "settled";

export type MovementSort =
  | "date_desc"
  | "date_asc"
  | "amount_desc"
  | "amount_asc"
  | "customer";

export type BalanceSort = "owing_desc" | "credit_desc" | "recent" | "customer";

export interface Movement {
  id: string;
  transactionDate: string | null;
  domain: TransactionDomain;
  amount: string;
  currency: LedgerCurrency;
  direction: LedgerDirection;
  reference: string | null;
  period: string | null;
  checkNumber: string | null;
  message: string | null;
  /** Legacy table the row came from — `datos2`, `EFECTIVO`, `fee15`, … */
  source: string | null;
  type: TransactionType | null;
  /** App-voided (`voidedAt` set). UI strikes; totals exclude. */
  voided: boolean;
  /** Legacy "NOPAGO": captured but unpaid (no funds). Shown tagged, and kept
   *  out of every balance until resolved via resolveOutstanding(). */
  outstanding?: boolean;
}

/** Payload for POST /billing — a new ledger movement. Sign convention: negative
 *  = cargo (charge), positive = abono (credit). */
export interface CreateMovementInput {
  customerId: string;
  domain: TransactionDomain;
  amount: number;
  transactionDate: string;
  currency?: Currency;
  typeId?: string;
  period?: string;
  reference?: string;
  checkNumber?: string;
  message?: string;
  /** Legacy NOPAGO — captured but unpaid; excluded from balances until resolved. */
  outstanding?: boolean;
}

/** One customer's line inside a check batch; check-level fields sit on the parent. */
export interface BatchLineInput {
  customerId: string;
  amount: number;
  reference?: string;
  period?: string;
  message?: string;
  outstanding?: boolean;
}

/** Payload for POST /billing/batch — many receipts cut against one check. */
export interface BatchCreateInput {
  domain: TransactionDomain;
  transactionDate: string;
  checkNumber: string;
  currency?: Currency;
  typeId?: string;
  lines: BatchLineInput[];
}

export interface BatchCreateResponse {
  /** Positionally parallel to the submitted `lines`. */
  items: Transaction[];
  checkNumber: string;
  currency: LedgerCurrency;
  source: "MANUAL" | "BATCH" | "OCR";
  count: number;
  outstandingCount: number;
  /** Excludes outstanding lines — this is the figure to reconcile against the
   *  physical check. */
  total: string;
}

/** Payload for POST /billing/:id/resolve-outstanding. */
export interface ResolveOutstandingInput {
  checkNumber: string;
  resolvedDate: string;
}

export interface ByCheckItem {
  id: string;
  transactionDate: string | null;
  domain: TransactionDomain;
  amount: string;
  currency: LedgerCurrency;
  direction: LedgerDirection;
  reference: string | null;
  period: string | null;
  message: string | null;
  outstanding: boolean;
  type: TransactionType | null;
  customerId: string;
  customerName: string;
  customerNameSource: string | null;
}

/** GET /billing/by-check — everything cut against one check, for reconciliation. */
export interface ByCheckResponse {
  checkNumber: string;
  items: ByCheckItem[];
  count: number;
  outstandingCount: number;
  totals: { currency: LedgerCurrency; total: string; count: number }[];
}

export interface MovementListItem extends Movement {
  customerId: string;
  customerName: string;
  customerNameSource: string | null;
  customerCity: string | null;
}

export interface CurrencyTotals {
  currency: LedgerCurrency;
  net: string | null;
  count: number;
  charges: string | null;
  chargeCount: number;
  credits: string | null;
  creditCount: number;
}

export interface MovementListResponse {
  items: MovementListItem[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
  /** Totals for the whole filtered set, not just the current page. */
  totals: CurrencyTotals[];
}

export interface CurrencyBalance {
  currency: LedgerCurrency;
  balance: string;
  charges: string;
  credits: string;
}

export interface BalanceListItem {
  id: string;
  name: string;
  nameSource: string | null;
  city: string | null;
  state: string | null;
  movements: number;
  utilityMovements: number;
  insuranceMovements: number;
  lastMovement: string | null;
  balances: CurrencyBalance[];
}

export interface BalanceListResponse {
  items: BalanceListItem[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
  currency: LedgerCurrency;
}

export interface BillingStats {
  movements: number;
  ledgerCustomers: number;
  /** Customers whose ledger spans utilities *and* insurance. */
  crossLineCustomers: number;
  firstMovement: string | null;
  lastMovement: string | null;
  byCurrency: (CurrencyTotals & { owing: number; inCredit: number })[];
  byDomain: {
    domain: TransactionDomain;
    currency: LedgerCurrency;
    net: string | null;
    count: number;
  }[];
}

export interface BillingFacets {
  types: Facet[];
  sources: { name: string; count: number }[];
  years: { year: number; count: number }[];
}

export interface StatementSummary {
  currency: LedgerCurrency;
  charges: string;
  credits: string;
  balance: string;
  chargeCount: number;
  creditCount: number;
  count: number;
  firstMovement: string | null;
  lastMovement: string | null;
}

export interface StatementDomainRow {
  domain: TransactionDomain;
  currency: LedgerCurrency;
  charges: string;
  credits: string;
  balance: string;
  count: number;
}

export interface StatementTypeRow {
  name: string;
  currency: LedgerCurrency;
  total: string;
  count: number;
}

export interface StatementMovement extends Movement {
  /** Balance in this row's currency after the movement was applied. */
  balanceAfter: string;
}

export interface Statement {
  customer: {
    id: string;
    name: string;
    nameSource: string | null;
    addressLine1: string | null;
    city: string | null;
    state: string | null;
    phone: string | null;
    mobile: string | null;
    email: string | null;
    customerSince: string | null;
    preferredCurrency: string | null;
    status: boolean;
    propertyCount: number;
    policyCount: number;
  };
  summary: StatementSummary[];
  byDomain: StatementDomainRow[];
  byType: StatementTypeRow[];
  movements: StatementMovement[];
}

export interface CustomerDetail {
  id: string;
  name: string;
  nameSource: string | null;
  addressLine1: string | null;
  addressLine2: string | null;
  city: string | null;
  state: string | null;
  zipCode: string | null;
  country: string | null;
  phone: string | null;
  mobile: string | null;
  fax: string | null;
  email: string | null;
  emailOptOut: boolean;
  notes: string | null;
  identificationType: string | null;
  identificationNumber: string | null;
  identificationExpiration: string | null;
  customerSince: string | null;
  status: boolean;
  minimumBalance: string | number | null;
  feeAmount: string | number | null;
  preferredCurrency: string | null;
  archivedAt: string | null;
  legacyRefs: LegacyRef[];
  properties: Property[];
  policies: Policy[];
  transactions: Transaction[];
  transactionSummary: TransactionSummaryRow[];
}

/** Editable customer fields — shared by the create/edit form and the API. */
export interface CustomerInput {
  name: string;
  addressLine1?: string;
  addressLine2?: string;
  city?: string;
  state?: string;
  zipCode?: string;
  country?: string;
  phone?: string;
  mobile?: string;
  fax?: string;
  email?: string;
  emailOptOut?: boolean;
  notes?: string;
  identificationType?: string;
  identificationNumber?: string;
  identificationExpiration?: string;
  customerSince?: string;
  status?: boolean;
  minimumBalance?: number;
  feeAmount?: number;
  preferredCurrency?: Currency;
}

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

/**
 * The office's own checking accounts — one register per chequera, no customer
 * link. See `bank.service.ts`. Positive is a deposit, negative a payment, and
 * exactly zero a cancelled cheque. Every figure below belongs to exactly one
 * `BankAccount` and is denominated in that account's currency; two accounts'
 * figures are never combined.
 */
export type BankDirection = "income" | "expense" | "void";

/** A bank the office holds chequeras at. */
export interface BankInstitution {
  id: string;
  name: string;
  /** "MX" | "US" — informational. */
  country: string | null;
}

/** One chequera. Its `currency` is what every figure on the page is read in. */
export interface BankAccount {
  id: string;
  label: string;
  currency: Currency;
  /** Soft hint about which line of business it serves; never enforced. */
  businessLine: TransactionDomain | null;
  /** Closed accounts stay readable but take no new movements. */
  active: boolean;
  bankId: string;
  bankName: string;
  bankCountry: string | null;
}

export interface CreateBankInput {
  name: string;
  country?: string;
}

export interface CreateBankAccountInput {
  bankId: string;
  label: string;
  /** Fixed at creation — an account's booked history is denominated in it. */
  currency: Currency;
  businessLine?: TransactionDomain;
  active?: boolean;
}

export interface UpdateBankAccountInput {
  bankId?: string;
  label?: string;
  businessLine?: TransactionDomain;
  active?: boolean;
}

export type BankCleared = "cleared" | "pending";

export type BankSort =
  | "date_desc"
  | "date_asc"
  | "amount_desc"
  | "amount_asc"
  | "reference";

export interface BankListItem {
  id: string;
  transactionDate: string;
  /** "INGRESO" / "EGRESO" as recorded in the source. */
  transactionType: string | null;
  /** Cheque number on egresos, deposit slip on ingresos. */
  reference: string | null;
  /** The payee or payer — a name, not a category. */
  concept: string | null;
  amount: string;
  direction: BankDirection;
  cleared: boolean;
  transferred: boolean;
  notes: string | null;
  /** "CIENTO CINCUENTA MIL PESOS 00/100" — egresos only. */
  amountInWords: string | null;
  source: string | null;
  /** App-voided (`voidedAt` set). UI strikes; totals exclude. */
  voided: boolean;
}

/** Payload for POST /bank — a new chequera movement. Sign convention: positive
 *  = ingreso, negative = egreso. The currency comes from the account. */
export interface CreateBankMovementInput {
  /** Which chequera it lands in. Required. */
  bankAccountId: string;
  amount: number;
  transactionDate: string;
  concept?: string;
  reference?: string;
  transactionType?: string;
  cleared?: boolean;
  transferred?: boolean;
  notes?: string;
  amountInWords?: string;
}

export interface BankTotals {
  income: string;
  incomeCount: number;
  expense: string;
  expenseCount: number;
  net: string;
  voidCount: number;
}

export interface BankListResponse {
  items: BankListItem[];
  total: number;
  page: number;
  pageSize: number;
  pageCount: number;
  /** Totals for the whole filtered set, not just the current page. */
  totals: BankTotals;
}

export interface BankStats extends BankTotals {
  movements: number;
  firstMovement: string | null;
  lastMovement: string | null;
  /** `operado` is false — recorded but not yet cleared by the bank. */
  pending: number;
  transferred: number;
}

export interface BankFacets {
  years: { year: number; count: number }[];
}

export interface BankPeriodRow {
  /** Year, or month number 1–12 in the monthly table. */
  period: number;
  count: number;
  income: string;
  expense: string;
  net: string;
  /** Net movement since the register opened — not a bank balance. */
  cumulative: string;
}

export interface BankSummary {
  year: number | null;
  years: (BankPeriodRow & { opening: string })[];
  months: BankPeriodRow[];
  /** Cumulative figure the selected year opened on. */
  opening: string;
}

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

export type ReportDomain =
  | "clientes"
  | "polizas"
  | "servicios"
  | "estado-cuenta"
  | "chequera";

export type ReportFormat = "tabular" | "statement" | "letter";

/** Param declaration a report exposes to its filter form. */
export type ReportParam =
  | { key: string; label: string; kind: "text"; placeholder?: string; defaultValue?: string }
  | { key: string; label: string; kind: "number"; defaultValue?: string }
  | { key: string; label: string; kind: "date"; endOfDay?: boolean; defaultValue?: string }
  | {
      key: string;
      label: string;
      kind: "select";
      options: { value: string; label: string }[];
      defaultValue?: string;
    }
  | { key: string; label: string; kind: "customer-picker" };

export interface ReportColumn {
  key: string;
  label: string;
  type: "text" | "number" | "money" | "date";
  align?: "left" | "right";
  width?: number;
}

export interface ReportDef {
  slug: string;
  title: string;
  description: string;
  domain: ReportDomain;
  legacyName: string | null;
  format: ReportFormat;
  params: ReportParam[];
  columns: ReportColumn[];
}

export interface ReportRunResult {
  columns: ReportColumn[];
  rows: Array<Record<string, unknown>>;
  totals?: Record<string, string | number>;
  subtitle?: string;
}

export interface ReportCatalog {
  items: ReportDef[];
}

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

export type StatementBatchStatus =
  | "UPLOADED"
  | "PROCESSING"
  | "READY_FOR_REVIEW"
  | "COMPLETED"
  | "FAILED"
  /** Abandoned by staff before anything was posted. */
  | "DISCARDED";

export type StatementDocumentStatus =
  | "PENDING_OCR"
  | "OCR_FAILED"
  | "NEEDS_REVIEW"
  | "MATCHED"
  | "CONFIRMED"
  | "POSTED"
  | "REJECTED";

export interface StatementBatch {
  id: string;
  serviceKind: ServiceKind;
  status: StatementBatchStatus;
  label: string | null;
  fileCount: number;
  error: string | null;
  createdAt: string;
  completedAt: string | null;
  uploadedBy?: { name: string };
  _count?: { documents: number };
}

export interface StatementBatchDetail extends StatementBatch {
  byStatus: Partial<Record<StatementDocumentStatus, number>>;
  /** Sum of the amounts still awaiting posting. */
  pendingTotal: string;
}

export interface StatementDocument {
  id: string;
  pageNumber: number;
  status: StatementDocumentStatus;
  provider: string | null;
  ocrConfidence: string | null;
  extractedAccountRef: string | null;
  extractedAmount: string | null;
  extractedPeriod: string | null;
  extractedDueDate: string | null;
  extractedCadastralKey: string | null;
  matchNote: string | null;
  matchedCustomer: { id: string; name: string } | null;
  matchedPropertyService: {
    id: string;
    kind: ServiceKind;
    accountNumber: string | null;
    meterNumber: string | null;
    property: { id: string; addressLine1: string | null };
  } | null;
  postedTransactionId: string | null;
}

export interface ReviewDocumentInput {
  accountRef?: string;
  amount?: number;
  period?: string;
  dueDate?: string;
  matchedPropertyServiceId?: string;
  matchedCustomerId?: string;
  status?: "MATCHED" | "NEEDS_REVIEW" | "CONFIRMED";
}

/** Check-level fields shared by every line posted from a batch. */
export interface ConfirmBatchInput {
  checkNumber: string;
  transactionDate: string;
  currency?: Currency;
  typeId?: string;
  outstanding?: boolean;
  includeReviewed?: boolean;
}

export interface ConfirmBatchResult {
  posted: number;
  total: string;
  checkNumber: string;
}

/** Shared by both OCR domains: how many pages the discard rejected. */
export interface DiscardBatchResult {
  batchId: string;
  rejected: number;
}

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

export type PolicyOcrBatchStatus =
  | "UPLOADED"
  | "PROCESSING"
  | "READY_FOR_REVIEW"
  | "COMPLETED"
  | "FAILED"
  /** Abandoned by staff before anything was applied. */
  | "DISCARDED";

export type PolicyOcrDocumentStatus =
  | "PENDING_OCR"
  | "OCR_FAILED"
  | "NEEDS_REVIEW"
  | "MATCHED"
  | "CONFIRMED"
  | "POSTED"
  | "REJECTED";

export interface PolicyOcrBatch {
  id: string;
  provider: string;
  status: PolicyOcrBatchStatus;
  label: string | null;
  fileCount: number;
  error: string | null;
  createdAt: string;
  completedAt: string | null;
  uploadedBy?: { name: string };
  _count?: { documents: number };
}

export interface PolicyOcrBatchDetail extends PolicyOcrBatch {
  byStatus: Partial<Record<PolicyOcrDocumentStatus, number>>;
}

export interface PolicyOcrCoverage {
  risk: string;
  insuredAmount: number | null;
  deductible: string | null;
  lossParticipation: string | null;
}

export interface PolicyOcrMatchCandidate {
  policyId: string;
  customerId: string;
  customerName: string;
  policyNumber: string;
}

export interface PolicyOcrDocument {
  id: string;
  pageNumber: number;
  status: PolicyOcrDocumentStatus;
  provider: string | null;
  ocrConfidence: string | null;
  extractedPolicyNumber: string | null;
  extractedInsuredName: string | null;
  extractedAdditionalInsured: string | null;
  extractedAgentName: string | null;
  extractedLegalAddress: string | null;
  extractedZip: string | null;
  extractedPolicyFrom: string | null;
  extractedPolicyTo: string | null;
  extractedPolicyDate: string | null;
  extractedCurrency: string | null;
  extractedNetPremium: string | null;
  extractedPolicyFee: string | null;
  extractedBrokerFee: string | null;
  extractedTotal: string | null;
  extractedCoveragesJson: PolicyOcrCoverage[] | null;
  extractedPremiumPayment: string | null;
  matchedPolicy: {
    id: string;
    policyNumber: string | null;
    customerId: string;
    customer: { name: string };
  } | null;
  matchedCustomer: { id: string; name: string } | null;
  matchCandidates: PolicyOcrMatchCandidate[] | null;
  matchNote: string | null;
}

export interface PolicyOcrReviewInput {
  policyNumber?: string;
  insuredName?: string;
  additionalInsured?: string;
  agentName?: string;
  legalAddress?: string;
  zip?: string;
  policyFrom?: string;
  policyTo?: string;
  policyDate?: string;
  currency?: string;
  netPremium?: number;
  policyFee?: number;
  brokerFee?: number;
  total?: number;
  premiumPayment?: string;
  coveragesJson?: PolicyOcrCoverage[];
  matchedPolicyId?: string;
  matchedCustomerId?: string;
  forceConfirm?: boolean;
}

export interface PolicyOcrConfirmDocument {
  documentId: string;
  policyId?: string;
  customerId?: string;
  policyNumber?: string;
  insuredName?: string;
  additionalInsured?: string;
  agentName?: string;
  legalAddress?: string;
  zip?: string;
  policyFrom?: string;
  policyTo?: string;
  policyDate?: string;
  currency?: string;
  netPremium?: number;
  policyFee?: number;
  brokerFee?: number;
  total?: number;
  premiumPayment?: string;
  coveragesJson?: PolicyOcrCoverage[];
  postPremium?: boolean;
}

export interface PolicyOcrConfirmInput {
  documents: PolicyOcrConfirmDocument[];
}

export interface PolicyOcrConfirmResult {
  applied: number;
  policies: string[];
  postedTransactions: number;
}
