// Client for the external General Ledger service, reached through ERPCore's generic
// reverse-proxy at /api/v1/gl/* (docs/12-GENERAL-LEDGER-INTEGRATION.md). Deliberately NOT
// built on lib/api-client.ts's apiRequest/apiRequestWithETag: those assume ERPCore's own
// RFC 7807 ProblemDetails error shape and a bare-DTO success body. GL wraps every response
// (success AND error) in its own `{ statusCode, success, message, data }` envelope instead,
// and — a documented GL quirk — success bodies are camelCase while error bodies are
// PascalCase, so this module unwraps both forms itself rather than trusting one casing.
import {
  CashAccountType,
  CashAndBankAccountDto,
  CashBankAccountType,
  CreateBankAccountRequest,
  CreateCashAccountRequest,
  CreateCashOrBankAccountResponse,
  GlAccountListResult,
  GlBudget,
  GlFilePayload,
  ReportOutputFormat,
  ReportType,
  TrialBalanceRow,
  BalanceSheetResponse,
  GeneralLedgerRow,
  ProfitAndLossResponse,
  CashFlowResponse,
  BudgetVsActualRow,
  TaxSummaryResponse,
  TaxSummaryParams,
  GlPagedResult,
  ChequeBook,
  ChequeBookStatus,
  ChequePage,
  CreateChequeBookRequest,
  IssueChequePageRequest,
  UpdateChequePageStatusRequest,
  ReceivedCheque,
  ReceivedChequeStatus,
  ReceivedFromType,
  CreateReceivedChequeRequest,
  UpdateReceivedChequeStatusRequest,
  CHEQUE_BOOK_STATUS_BY_CODE,
  CHEQUE_PAGE_ISSUE_STATUS_BY_CODE,
  PAYEE_TYPE_BY_CODE,
  RECEIVED_FROM_TYPE_BY_CODE,
  RECEIVED_CHEQUE_STATUS_BY_CODE,
} from "@/types/general-ledger"

const GL_BASE = "/api/v1/gl"

/** Duck-type compatible with lib/error-map.ts's ApiErrorLike — `detail` carries GL's own message. */
export class GlApiError extends Error {
  status: number
  detail: string

  constructor(status: number, message: string) {
    super(message)
    this.status = status
    this.detail = message
  }
}

interface GlEnvelope<T> {
  statusCode?: number
  StatusCode?: number
  success?: boolean
  Success?: boolean
  message?: string
  Message?: string
  data?: T
  Data?: T
  // Surfaces only when the proxy itself fails before reaching GL (e.g. ERPCore's own
  // 503 GL_SERVICE_UNAVAILABLE ProblemDetails) rather than GL's own envelope.
  title?: string
  detail?: string
}

type GlQueryValue = string | number | undefined

async function glRequest<T>(
  path: string,
  options: { method?: string; query?: Record<string, GlQueryValue>; body?: unknown } = {}
): Promise<T> {
  const { method = "GET", query, body } = options
  const search = new URLSearchParams()
  if (query) {
    for (const [key, value] of Object.entries(query)) {
      if (value === undefined || value === "") continue
      search.set(key, String(value))
    }
  }
  const qs = search.toString()

  const response = await fetch(`${GL_BASE}${path}${qs ? `?${qs}` : ""}`, {
    method,
    credentials: "include", // the proxy is ErpAccess-gated, same as every other v1 endpoint
    headers: {
      Accept: "application/json",
      ...(body !== undefined ? { "Content-Type": "application/json" } : {}),
    },
    body: body !== undefined ? JSON.stringify(body) : undefined,
  })

  let envelope: GlEnvelope<T> | null = null
  try {
    envelope = (await response.json()) as GlEnvelope<T>
  } catch {
    // Non-JSON body — e.g. an unreachable proxy hop. Falls through to the generic message below.
  }

  const success = envelope?.success ?? envelope?.Success ?? false
  if (!response.ok || !success) {
    const message =
      envelope?.message ?? envelope?.Message ?? envelope?.detail ?? envelope?.title ??
      response.statusText ?? "General Ledger service request failed"
    throw new GlApiError(response.status, message)
  }

  return (envelope?.data ?? envelope?.Data) as T
}

/** Decodes a base64 payload and triggers a browser download — no server round-trip needed. */
function downloadBase64File(base64: string, fileName: string, contentType: string) {
  const byteChars = atob(base64)
  const byteNumbers = new Array(byteChars.length)
  for (let i = 0; i < byteChars.length; i++) byteNumbers[i] = byteChars.charCodeAt(i)
  const blob = new Blob([new Uint8Array(byteNumbers)], { type: contentType })
  const url = URL.createObjectURL(blob)
  const link = document.createElement("a")
  link.href = url
  link.download = fileName
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
  URL.revokeObjectURL(url)
}

/** Shared by downloadPdf/downloadCsv — same report call, only `outputFormat` differs. */
async function downloadReportFile(
  reportType: ReportType,
  outputFormat: ReportOutputFormat.Pdf | ReportOutputFormat.Csv,
  params: Record<string, GlQueryValue>
): Promise<void> {
  const payload = await glRequest<GlFilePayload>("/reports", {
    query: { reportType, outputFormat, ...params },
  })
  downloadBase64File(payload.contentBase64, payload.fileName, payload.contentType)
}

export const reportsApi = {
  trialBalance(asOfDate: string) {
    return glRequest<TrialBalanceRow[]>("/reports", {
      query: { reportType: ReportType.TrialBalance, outputFormat: ReportOutputFormat.Json, asOfDate },
    })
  },

  /** Confirmed classified-statement shape (2026-07-31 rework) — see types/general-ledger.ts's BalanceSheetResponse note. */
  balanceSheet(asOfDate: string) {
    return glRequest<BalanceSheetResponse>("/reports", {
      query: { reportType: ReportType.BalanceSheet, outputFormat: ReportOutputFormat.Json, asOfDate },
    })
  },

  // GL's `accountCode` param is optional (renamed from `accountId` in GL's 2026-07-22 revision,
  // CLAUDE.md Rule 8.2 on GL's side — behavior unchanged): omitted, this returns the true General
  // Ledger — every postable account's own transactions together, each with its own running
  // balance (resets per account), sorted by accountCode then entryDate. Supplying accountCode
  // switches to "Account Ledger" mode (one account + its descendants, one running balance) — not
  // used by this page; add it back with an accountCode param if a single-account view is needed later.
  generalLedger(periodStart: string, periodEnd: string) {
    return glRequest<GeneralLedgerRow[]>("/reports", {
      query: { reportType: ReportType.GeneralLedger, outputFormat: ReportOutputFormat.Json, periodStart, periodEnd },
    })
  },

  /** Nested-sections shape as of the 2026-07-22 rework — see types/general-ledger.ts's ProfitAndLossResponse note. */
  profitAndLoss(periodStart: string, periodEnd: string) {
    return glRequest<ProfitAndLossResponse>("/reports", {
      query: { reportType: ReportType.ProfitAndLoss, outputFormat: ReportOutputFormat.Json, periodStart, periodEnd },
    })
  },

  /** Confirmed structured-statement shape (2026-07-22 rework) — see types/general-ledger.ts's CashFlowResponse note; everything nests under `operatingActivities`. */
  cashFlow(periodStart: string, periodEnd: string) {
    return glRequest<CashFlowResponse>("/reports", {
      query: { reportType: ReportType.CashFlow, outputFormat: ReportOutputFormat.Json, periodStart, periodEnd },
    })
  },

  budgetVsActual(budgetId: number) {
    return glRequest<BudgetVsActualRow[]>("/reports", {
      query: { reportType: ReportType.BudgetVsActual, outputFormat: ReportOutputFormat.Json, budgetId },
    })
  },

  /** Income Tax Computation — new report (2026-07-22). Optional params get no client-side default; an untouched field sends nothing, letting GL's own server-side defaulting be the single source of truth. */
  taxSummary(params: TaxSummaryParams) {
    return glRequest<TaxSummaryResponse>("/reports", {
      query: { reportType: ReportType.TaxSummary, outputFormat: ReportOutputFormat.Json, ...params },
    })
  },

  /** Same report call as the Json variants above, only `outputFormat` differs — the PDF bytes come from the same endpoint. */
  downloadPdf(reportType: ReportType, params: Record<string, GlQueryValue>): Promise<void> {
    return downloadReportFile(reportType, ReportOutputFormat.Pdf, params)
  },

  /** Same mechanism as downloadPdf, `outputFormat=Csv` — GL's bytes are downloaded unmodified, not reshaped/reformatted client-side. */
  downloadCsv(reportType: ReportType, params: Record<string, GlQueryValue>): Promise<void> {
    return downloadReportFile(reportType, ReportOutputFormat.Csv, params)
  },
}

/**
 * Chart of Accounts — used by the Cash/Bank Accounts list page to resolve each row's `glAccountId`
 * into a readable account code/name (retrofit 2026-07-31: the create form no longer needs this at
 * all, since `glAccountCode` was removed from the create request — the GL account is auto-created).
 * The General Ledger **report** page deliberately does NOT use this: it always calls the report in
 * full-ledger mode (no `accountCode`), so every account's code/name shown come from the report's own
 * rows (`GeneralLedgerRow.accountCode`/`accountName`), not a separate `/accounts` call — see
 * docs/21-GENERAL-LEDGER-FRONTEND.md.
 */
export const glAccountsApi = {
  list(): Promise<GlAccountListResult> {
    return glRequest<GlAccountListResult>("/accounts")
  },
}

/** Used only to populate the Budget vs Actual report's budget picker. */
export const glBudgetsApi = {
  list(): Promise<GlBudget[]> {
    return glRequest<GlBudget[]>("/budgets")
  },
}

export const bankAccountsApi = {
  /** GL's own server-side union of both tables (2026-07-22 rework) — `accountType` narrows which table(s) contribute rows; client-side filters still layer on top. */
  list(accountType?: CashBankAccountType | "Both"): Promise<CashAndBankAccountDto[]> {
    return glRequest<CashAndBankAccountDto[]>("/bank-accounts", { query: { accountType } })
  },

  createBank(request: CreateBankAccountRequest): Promise<CreateCashOrBankAccountResponse> {
    return glRequest<CreateCashOrBankAccountResponse>("/bank-accounts", { method: "POST", body: request })
  },

  createCash(request: CreateCashAccountRequest): Promise<CreateCashOrBankAccountResponse> {
    return glRequest<CreateCashOrBankAccountResponse>("/cash-accounts", { method: "POST", body: request })
  },

  // No get()/update(): GL exposes no GET/PUT by id for either bank_account or cash_account today
  // (see docs/21-GENERAL-LEDGER-FRONTEND.md's "Known gap — edit").
}

/** Feeds the Cash/Bank create form's Cash Account Type picker; a name with no match creates a new type on the fly server-side (nothing to pre-create from this list). */
export const cashAccountTypesApi = {
  list(): Promise<CashAccountType[]> {
    return glRequest<CashAccountType[]>("/cash-account-types")
  },
}

/**
 * Cheque Books/Pages — cheques issued from this company's own cheque books (Cheque Management
 * module, added to GL 2026-07-30). `chequeBookNo` is the identifying value GL uses in its own
 * routes, not a numeric id. No `list()`/`get()` for pages standalone — a book's pages are always
 * read via `get(chequeBookNo, true)`'s `pages[]`, which is the only place this frontend needs them.
 */
// GL sends `ChequeBook.status`/`ChequePage.issueStatus`/`ChequePage.payeeType`/
// `ReceivedCheque.receivedFromType`/`ReceivedCheque.status` as raw integers, not their string
// name (06_Enums_Reference.md — no global JsonStringEnumConverter on GL's side; see the long
// comment above the `*_BY_CODE` maps in types/general-ledger.ts for why). These `Raw*` shapes
// describe exactly what GL's JSON actually contains for those fields; the `map*` functions below
// translate them into this frontend's normal string-enum `ChequeBook`/`ChequePage`/`ReceivedCheque`
// types immediately after each call returns, so every other file in this app can keep comparing
// against `ChequeBookStatus.Active` etc. exactly as before.
type RawChequePage = Omit<ChequePage, "issueStatus" | "payeeType"> & {
  issueStatus: number
  payeeType: number | null
}
type RawChequeBook = Omit<ChequeBook, "status" | "pages"> & {
  status: number
  pages: RawChequePage[]
}
type RawReceivedCheque = Omit<ReceivedCheque, "receivedFromType" | "status"> & {
  receivedFromType: number
  status: number
}

function mapChequePage(raw: RawChequePage): ChequePage {
  return {
    ...raw,
    issueStatus: CHEQUE_PAGE_ISSUE_STATUS_BY_CODE[raw.issueStatus],
    payeeType: raw.payeeType == null ? null : PAYEE_TYPE_BY_CODE[raw.payeeType],
  }
}

function mapChequeBook(raw: RawChequeBook): ChequeBook {
  return {
    ...raw,
    status: CHEQUE_BOOK_STATUS_BY_CODE[raw.status],
    pages: (raw.pages ?? []).map(mapChequePage),
  }
}

function mapReceivedCheque(raw: RawReceivedCheque): ReceivedCheque {
  return {
    ...raw,
    receivedFromType: RECEIVED_FROM_TYPE_BY_CODE[raw.receivedFromType],
    status: RECEIVED_CHEQUE_STATUS_BY_CODE[raw.status],
  }
}

export const chequeBooksApi = {
  async list(params?: {
    bankAccountId?: number
    branchId?: number
    status?: ChequeBookStatus
    page?: number
    pageSize?: number
  }): Promise<GlPagedResult<ChequeBook>> {
    const res = await glRequest<GlPagedResult<RawChequeBook>>("/cheque-books", { query: { ...params } })
    return { ...res, items: res.items.map(mapChequeBook) }
  },

  /** `expandPages` maps to GL's `?expand=pages` — omit it for just the book's own fields. */
  async get(chequeBookNo: string, expandPages = false): Promise<ChequeBook> {
    const res = await glRequest<RawChequeBook>(`/cheque-books/${encodeURIComponent(chequeBookNo)}`, {
      query: expandPages ? { expand: "pages" } : undefined,
    })
    return mapChequeBook(res)
  },

  /** Auto-generates every leaf (`totalLeaves` `ChequePage` rows, all `Unused`) in the same call — the response's `pages[]` already has them. */
  async create(request: CreateChequeBookRequest): Promise<ChequeBook> {
    const res = await glRequest<RawChequeBook>("/cheque-books", { method: "POST", body: request })
    return mapChequeBook(res)
  },
}

export const chequePagesApi = {
  async issue(chequeNo: string, request: IssueChequePageRequest): Promise<ChequePage> {
    const res = await glRequest<RawChequePage>(`/cheque-pages/${encodeURIComponent(chequeNo)}/issue`, {
      method: "PUT",
      body: request,
    })
    return mapChequePage(res)
  },

  /** `Clear`/`Bounce`/`Cancel`/`Void` — only valid from certain `issueStatus` values, see `types/general-ledger.ts`'s `ChequePageStatusAction`. */
  async updateStatus(chequeNo: string, request: UpdateChequePageStatusRequest): Promise<ChequePage> {
    const res = await glRequest<RawChequePage>(`/cheque-pages/${encodeURIComponent(chequeNo)}/status`, {
      method: "PUT",
      body: request,
    })
    return mapChequePage(res)
  },
}

/** Received Cheques — cheques received from customers/suppliers/others, deliberately unlinked to any `ChequeBook`. */
export const receivedChequesApi = {
  async list(params?: {
    companyId?: number
    branchId?: number
    status?: ReceivedChequeStatus
    receivedFromType?: ReceivedFromType
    page?: number
    pageSize?: number
  }): Promise<GlPagedResult<ReceivedCheque>> {
    const res = await glRequest<GlPagedResult<RawReceivedCheque>>("/received-cheques", { query: { ...params } })
    return { ...res, items: res.items.map(mapReceivedCheque) }
  },

  async create(request: CreateReceivedChequeRequest): Promise<ReceivedCheque> {
    const res = await glRequest<RawReceivedCheque>("/received-cheques", { method: "POST", body: request })
    return mapReceivedCheque(res)
  },

  /** `Deposit`/`Clear`/`Return`/`Cancel` — only valid from certain statuses, see `types/general-ledger.ts`'s `ReceivedChequeStatusAction`. */
  async updateStatus(id: number, request: UpdateReceivedChequeStatusRequest): Promise<ReceivedCheque> {
    const res = await glRequest<RawReceivedCheque>(`/received-cheques/${id}/status`, { method: "PUT", body: request })
    return mapReceivedCheque(res)
  },
}
