import axios, { type AxiosRequestConfig } from 'axios';
import { fetchFineractGlAccounts, fetchFineractGlAccountsByCodes } from './omnl-ipsas-gl';
import { hoLiquidityCacheMs } from '../config/omnl-ho-liquidity-cache';

export type SignedGlBalanceRow = {
  glCode: string;
  name: string;
  side: string;
  amount: string;
};

export function fineractHeaders(): { base: string; headers: Record<string, string> } {
  const base = (process.env.OMNL_FINERACT_BASE_URL || '').replace(/\/$/, '');
  const tenant = process.env.OMNL_FINERACT_TENANT || 'omnl';
  const user =
    process.env.OMNL_FINERACT_USER?.trim() ||
    process.env.OMNL_FINERACT_USERNAME?.trim() ||
    'app.omnl';
  const pass = process.env.OMNL_FINERACT_PASSWORD || '';
  if (!base || !pass) {
    throw new Error('OMNL_FINERACT_BASE_URL and OMNL_FINERACT_PASSWORD required');
  }
  const auth = Buffer.from(`${user}:${pass}`).toString('base64');
  return {
    base,
    headers: {
      Authorization: `Basic ${auth}`,
      'Fineract-Platform-TenantId': tenant,
      Accept: 'application/json',
    },
  };
}

export function fineractConfigured(): boolean {
  return Boolean(process.env.OMNL_FINERACT_BASE_URL?.trim() && process.env.OMNL_FINERACT_PASSWORD);
}

export async function fetchOfficeJournalLines(officeId: number): Promise<
  Array<{ glAccountId: number; amount: number; entryType: string; transactionDate?: unknown }>
> {
  const { base, headers } = fineractHeaders();
  const out: Array<{ glAccountId: number; amount: number; entryType: string; transactionDate?: unknown }> = [];
  let offset = 0;
  const limit = 200;
  for (;;) {
    const cfg: AxiosRequestConfig = {
      headers,
      timeout: 25000,
      params: { officeId, limit, offset },
    };
    const { data } = await axios.get(`${base}/journalentries`, cfg);
    const batch = Array.isArray(data) ? data : (data as { pageItems?: unknown[] })?.pageItems ?? [];
    if (!Array.isArray(batch) || batch.length === 0) break;
    for (const je of batch) {
      const row = je as {
        glAccountId?: number;
        amount?: number;
        entryType?: string | { value?: string; code?: string };
        transactionDate?: unknown;
        debits?: Array<{ glAccountId?: number; amount?: number }>;
        credits?: Array<{ glAccountId?: number; amount?: number }>;
      };

      const flatEntryType =
        typeof row.entryType === 'string'
          ? row.entryType
          : row.entryType?.value || row.entryType?.code || '';

      if (row.glAccountId != null && row.amount != null && flatEntryType) {
        const normalized = flatEntryType.toUpperCase();
        out.push({
          glAccountId: row.glAccountId,
          amount: Number(row.amount || 0),
          entryType: normalized.startsWith('DEB') ? 'DEBIT' : 'CREDIT',
          transactionDate: row.transactionDate,
        });
        continue;
      }

      for (const d of row.debits ?? []) {
        if (d.glAccountId != null) {
          out.push({
            glAccountId: d.glAccountId,
            amount: Number(d.amount || 0),
            entryType: 'DEBIT',
            transactionDate: row.transactionDate,
          });
        }
      }
      for (const c of row.credits ?? []) {
        if (c.glAccountId != null) {
          out.push({
            glAccountId: c.glAccountId,
            amount: Number(c.amount || 0),
            entryType: 'CREDIT',
            transactionDate: row.transactionDate,
          });
        }
      }
    }
    if (batch.length < limit) break;
    offset += limit;
  }
  return out;
}

export function signedGlBalances(
  lines: Array<{ glAccountId: number; amount: number; entryType: string }>,
  glById: Map<number, { glCode: string; name: string }>,
): SignedGlBalanceRow[] {
  const net = new Map<number, number>();
  for (const l of lines) {
    const cur = net.get(l.glAccountId) ?? 0;
    net.set(l.glAccountId, cur + (l.entryType === 'DEBIT' ? l.amount : -l.amount));
  }
  const rows: SignedGlBalanceRow[] = [];
  for (const [id, val] of net.entries()) {
    if (Math.abs(val) < 0.005) continue;
    const g = glById.get(id) ?? { glCode: String(id), name: '' };
    rows.push({
      glCode: g.glCode,
      name: g.name,
      side: val > 0 ? 'Dr' : 'Cr',
      amount: Math.abs(val).toFixed(2),
    });
  }
  rows.sort((a, b) => a.glCode.localeCompare(b.glCode));
  return rows;
}

/** Signed GL balances for one Fineract office (journal-derived, same method as settlement terminal). */
export async function fetchOfficeSignedGlBalances(
  officeId: number,
  glCodesHint: string[] = ['1000', '1050', '13010', '2100', '2000'],
): Promise<SignedGlBalanceRow[]> {
  const lines = await fetchOfficeJournalLines(officeId);
  const journalIds = new Set(lines.map((l) => l.glAccountId));
  let glRows = await fetchFineractGlAccountsByCodes(glCodesHint);
  const mappedIds = new Set(glRows.map((g) => g.id));
  const missingJournalIds = [...journalIds].filter((id) => !mappedIds.has(id));
  if (missingJournalIds.length > 0) {
    const fullChart = await fetchFineractGlAccounts();
    const seen = new Set(glRows.map((g) => g.id));
    for (const row of fullChart) {
      if (row.id != null && !seen.has(row.id) && journalIds.has(row.id)) {
        glRows.push(row);
        seen.add(row.id);
      }
    }
  }
  const glById = new Map<number, { glCode: string; name: string }>();
  for (const g of glRows) {
    if (g.id != null && g.glCode) {
      glById.set(g.id, { glCode: String(g.glCode), name: String(g.name || '') });
    }
  }
  return signedGlBalances(lines, glById);
}

let officeGlCache: { officeId: number; expiresAt: number; rows: SignedGlBalanceRow[] } | null = null;
let officeGlInflight: Promise<SignedGlBalanceRow[]> | null = null;

function officeGlFetchTimeoutMs(): number {
  return Number(process.env.OMNL_OFFICE_GL_FETCH_TIMEOUT_MS || 25_000);
}

async function withOfficeGlTimeout<T>(promise: Promise<T>): Promise<T> {
  const timeoutMs = officeGlFetchTimeoutMs();
  let timer: ReturnType<typeof setTimeout> | undefined;
  try {
    return await Promise.race([
      promise,
      new Promise<T>((_, reject) => {
        timer = setTimeout(
          () => reject(new Error(`Fineract office GL fetch timed out after ${timeoutMs}ms`)),
          timeoutMs,
        );
      }),
    ]);
  } finally {
    if (timer) clearTimeout(timer);
  }
}

/** Shared office GL snapshot — dedupes concurrent Fineract journal replays (market-batch hot path). */
export async function getCachedOfficeSignedGlBalances(
  officeId: number,
  options?: { forceRefresh?: boolean },
): Promise<SignedGlBalanceRow[]> {
  const ttlMs = Number(process.env.OMNL_OFFICE_GL_CACHE_MS || hoLiquidityCacheMs());
  const now = Date.now();
  const stale = officeGlCache && officeGlCache.officeId === officeId ? officeGlCache.rows : null;
  if (
    !options?.forceRefresh
    && officeGlCache
    && officeGlCache.officeId === officeId
    && officeGlCache.expiresAt > now
  ) {
    return officeGlCache.rows;
  }
  if (officeGlInflight) {
    try {
      return await withOfficeGlTimeout(officeGlInflight);
    } catch {
      if (stale) return stale;
      throw new Error('Fineract office GL balances unavailable');
    }
  }
  officeGlInflight = withOfficeGlTimeout(fetchOfficeSignedGlBalances(officeId))
    .then((rows) => {
      officeGlCache = { officeId, expiresAt: Date.now() + ttlMs, rows };
      return rows;
    })
    .finally(() => {
      officeGlInflight = null;
    });
  try {
    return await officeGlInflight;
  } catch (e) {
    if (stale) return stale;
    throw e instanceof Error ? e : new Error(String(e));
  }
}

export function resetOfficeSignedGlCache(): void {
  officeGlCache = null;
  officeGlInflight = null;
}
