import { readFileSync, existsSync } from 'fs';
import { hoLiquidityCacheMs } from '../config/omnl-ho-liquidity-cache';
import { resolve } from 'path';
import type { SignedGlBalanceRow } from './omnl-fineract-office-gl';
import { getCachedOfficeSignedGlBalances, fineractConfigured } from './omnl-fineract-office-gl';
import { runTripleStateReconcile } from './omnl-triple-reconcile';

export type CashInBankAuditStatus = 'aligned' | 'breaks' | 'pending' | 'unavailable';
export type SupplyNostroMatchStatus = 'matched' | 'break' | 'pending' | 'unavailable';

export interface CashInBankGlLeg {
  glCode: string;
  name: string;
  side: 'Dr' | 'Cr';
  amountUsd: number;
}

export interface SupplyNostroMatchInfo {
  status: SupplyNostroMatchStatus;
  multiplier: number;
  toleranceBps: number;
  headOfficeId: number;
  externalNostroGlCode: string;
  /** totalSupplyUsd = multiplier × externalNostroCashUsd (HO GL 13010 Dr). */
  formula: string;
  totalSupplyUsd?: number;
  externalNostroCashUsd?: number;
  expectedSupplyUsd?: number;
  deltaUsd?: number;
  deltaBps?: number;
}

export interface CashInBankAuditInfo {
  status: CashInBankAuditStatus;
  breaksCount: number;
  tripleStateAligned?: boolean;
  supplyNostroMatch?: SupplyNostroMatchStatus;
  policyId: string;
  policyRef: string;
  error?: string;
}

export interface CashInBankAuditSnapshot {
  generatedAt: string;
  officeId: number;
  /** External correspondent nostro cash only (GL 13010 Dr) — not reserve GL 1000. */
  cashInBankUsd: number;
  externalNostroGlCode: string;
  nostroGlCodes: string[];
  legs: CashInBankGlLeg[];
  audit: CashInBankAuditInfo;
  supplyNostroMatch?: SupplyNostroMatchInfo;
  fineractConfigured: boolean;
}

type NostroPolicy = {
  policyId?: string;
  nostroGlCodes?: string[];
  externalNostroGlCode?: string;
  headOfficeId?: number;
  supplyNostroMatchMultiplier?: number;
  supplyNostroMatchToleranceBps?: number;
};

let cache: { expiresAt: number; snapshot: CashInBankAuditSnapshot } | null = null;

function repoRoot(): string {
  if (process.env.PROXMOX_ROOT?.trim()) return process.env.PROXMOX_ROOT.trim();
  if (process.env.PHOENIX_REPO_ROOT?.trim()) return process.env.PHOENIX_REPO_ROOT.trim();
  return resolve(__dirname, '../../../../..');
}

export function loadNostroPolicy(): {
  policyId: string;
  nostroGlCodes: string[];
  externalNostroGlCode: string;
  headOfficeId: number;
  supplyNostroMatchMultiplier: number;
  supplyNostroMatchToleranceBps: number;
  policyRef: string;
} {
  const candidates = [
    process.env.OMNL_NOSTRO_POLICY_PATH?.trim(),
    resolve(repoRoot(), 'config/compliance/omnl-nostro-backed-netting.v1.json'),
    resolve(__dirname, '../../../../config/compliance/omnl-nostro-backed-netting.v1.json'),
  ].filter(Boolean) as string[];
  const path = candidates.find((c) => existsSync(c));
  if (!path) {
    return {
      policyId: 'OMNL-NOSTRO-BACKED-NETTING-V1',
      nostroGlCodes: ['13010', '1000'],
      externalNostroGlCode: '13010',
      headOfficeId: 1,
      supplyNostroMatchMultiplier: 1.2,
      supplyNostroMatchToleranceBps: 100,
      policyRef: 'config/compliance/omnl-nostro-backed-netting.v1.json',
    };
  }
  const parsed = JSON.parse(readFileSync(path, 'utf8')) as NostroPolicy;
  return {
    policyId: String(parsed.policyId || 'OMNL-NOSTRO-BACKED-NETTING-V1'),
    nostroGlCodes: Array.isArray(parsed.nostroGlCodes) && parsed.nostroGlCodes.length
      ? parsed.nostroGlCodes.map(String)
      : ['13010', '1000'],
    externalNostroGlCode: String(parsed.externalNostroGlCode || '13010'),
    headOfficeId: Number(parsed.headOfficeId ?? 1),
    supplyNostroMatchMultiplier: Number(parsed.supplyNostroMatchMultiplier ?? 1.2),
    supplyNostroMatchToleranceBps: Number(parsed.supplyNostroMatchToleranceBps ?? 100),
    policyRef: path.replace(`${repoRoot()}/`, ''),
  };
}

function glLegFromRow(row: SignedGlBalanceRow): CashInBankGlLeg | null {
  const amountUsd = Number(row.amount);
  if (!Number.isFinite(amountUsd) || amountUsd <= 0) return null;
  const side = row.side === 'Dr' ? 'Dr' : 'Cr';
  return { glCode: row.glCode, name: row.name, side, amountUsd };
}

/** Sum Dr balances for one GL code (external nostro uses 13010 only). */
export function externalNostroCashFromGlRows(
  rows: SignedGlBalanceRow[],
  externalNostroGlCode: string,
): { cashInBankUsd: number; legs: CashInBankGlLeg[] } {
  const row = rows.find((r) => r.glCode === externalNostroGlCode);
  if (!row) {
    return { cashInBankUsd: 0, legs: [] };
  }
  const leg = glLegFromRow(row);
  if (!leg || leg.side !== 'Dr') {
    return { cashInBankUsd: 0, legs: leg ? [leg] : [] };
  }
  return { cashInBankUsd: leg.amountUsd, legs: [leg] };
}

/** @deprecated Prefer externalNostroCashFromGlRows — sums all listed GL Dr legs. */
export function cashInBankUsdFromGlRows(
  rows: SignedGlBalanceRow[],
  nostroGlCodes: string[],
): { cashInBankUsd: number; legs: CashInBankGlLeg[] } {
  const legs: CashInBankGlLeg[] = [];
  let cashInBankUsd = 0;
  for (const code of nostroGlCodes) {
    const row = rows.find((r) => r.glCode === code);
    if (!row) continue;
    const leg = glLegFromRow(row);
    if (!leg) continue;
    legs.push(leg);
    if (leg.side === 'Dr') cashInBankUsd += leg.amountUsd;
  }
  return { cashInBankUsd, legs };
}

function supplyMatchMeta(policy = loadNostroPolicy()): Pick<
  SupplyNostroMatchInfo,
  'multiplier' | 'toleranceBps' | 'headOfficeId' | 'externalNostroGlCode' | 'formula'
> {
  return {
    multiplier: policy.supplyNostroMatchMultiplier,
    toleranceBps: policy.supplyNostroMatchToleranceBps,
    headOfficeId: policy.headOfficeId,
    externalNostroGlCode: policy.externalNostroGlCode,
    formula: `totalSupplyUsd = ${policy.supplyNostroMatchMultiplier} × externalNostroCashUsd`,
  };
}

/** totalSupplyUsd should equal multiplier × HO external nostro cash (GL 13010 Dr, office 1). */
export function evaluateSupplyNostroMatch(input: {
  totalSupplyUsd?: number;
  externalNostroCashUsd?: number;
  multiplier?: number;
  toleranceBps?: number;
  headOfficeId?: number;
}): SupplyNostroMatchInfo {
  const policy = loadNostroPolicy();
  const meta = supplyMatchMeta(policy);
  const multiplier = input.multiplier ?? meta.multiplier;
  const toleranceBps = input.toleranceBps ?? meta.toleranceBps;
  const headOfficeId = input.headOfficeId ?? meta.headOfficeId;
  const totalSupplyUsd = input.totalSupplyUsd;
  const externalNostroCashUsd = input.externalNostroCashUsd;

  if (totalSupplyUsd == null || !(totalSupplyUsd > 0)) {
    return {
      status: 'pending',
      ...meta,
      multiplier,
      toleranceBps,
      headOfficeId,
      totalSupplyUsd,
      externalNostroCashUsd,
    };
  }
  if (externalNostroCashUsd == null || !(externalNostroCashUsd > 0)) {
    return {
      status: 'unavailable',
      ...meta,
      multiplier,
      toleranceBps,
      headOfficeId,
      totalSupplyUsd,
      externalNostroCashUsd,
    };
  }

  const expectedSupplyUsd = externalNostroCashUsd * multiplier;
  const deltaUsd = totalSupplyUsd - expectedSupplyUsd;
  const deltaBps = Math.abs(deltaUsd / expectedSupplyUsd) * 10_000;
  const status: SupplyNostroMatchStatus = deltaBps <= toleranceBps ? 'matched' : 'break';

  return {
    status,
    multiplier,
    toleranceBps,
    headOfficeId,
    externalNostroGlCode: meta.externalNostroGlCode,
    formula: meta.formula,
    totalSupplyUsd,
    externalNostroCashUsd,
    expectedSupplyUsd,
    deltaUsd,
    deltaBps,
  };
}

function resolveAuditStatus(input: {
  fineractOk: boolean;
  fineractError?: string;
  cashInBankUsd: number;
  tripleStateAligned?: boolean;
  breaksCount: number;
  tripleStateRan: boolean;
  supplyNostroMatch?: SupplyNostroMatchStatus;
}): CashInBankAuditInfo {
  const policy = loadNostroPolicy();

  if (!input.fineractOk) {
    return {
      status: 'unavailable',
      breaksCount: input.breaksCount,
      tripleStateAligned: input.tripleStateAligned,
      supplyNostroMatch: input.supplyNostroMatch,
      policyId: policy.policyId,
      policyRef: policy.policyRef,
      error: input.fineractError || 'OMNL Fineract not configured',
    };
  }

  if (input.supplyNostroMatch === 'break') {
    return {
      status: 'breaks',
      breaksCount: input.breaksCount,
      tripleStateAligned: input.tripleStateAligned,
      supplyNostroMatch: input.supplyNostroMatch,
      policyId: policy.policyId,
      policyRef: policy.policyRef,
      error: `Total supply USD != ${policy.supplyNostroMatchMultiplier}× external nostro (GL ${policy.externalNostroGlCode})`,
    };
  }

  if (input.tripleStateRan) {
    if (input.tripleStateAligned && input.cashInBankUsd > 0 && input.supplyNostroMatch === 'matched') {
      return {
        status: 'aligned',
        breaksCount: input.breaksCount,
        tripleStateAligned: true,
        supplyNostroMatch: input.supplyNostroMatch,
        policyId: policy.policyId,
        policyRef: policy.policyRef,
      };
    }
    return {
      status: 'breaks',
      breaksCount: input.breaksCount,
      tripleStateAligned: input.tripleStateAligned,
      supplyNostroMatch: input.supplyNostroMatch,
      policyId: policy.policyId,
      policyRef: policy.policyRef,
    };
  }

  if (input.cashInBankUsd > 0) {
    return {
      status: input.supplyNostroMatch === 'matched' ? 'aligned' : 'pending',
      breaksCount: input.breaksCount,
      supplyNostroMatch: input.supplyNostroMatch,
      policyId: policy.policyId,
      policyRef: policy.policyRef,
      error: input.fineractError,
    };
  }

  return {
    status: 'breaks',
    breaksCount: input.breaksCount,
    tripleStateAligned: input.tripleStateAligned,
    supplyNostroMatch: input.supplyNostroMatch,
    policyId: policy.policyId,
    policyRef: policy.policyRef,
    error: input.fineractError || `No external nostro Dr balance on GL ${policy.externalNostroGlCode}`,
  };
}

export function mergeCashInBankAuditWithSupplyMatch(
  audit: CashInBankAuditInfo,
  supplyNostroMatch: SupplyNostroMatchInfo,
): CashInBankAuditInfo {
  return resolveAuditStatus({
    fineractOk: audit.status !== 'unavailable',
    fineractError: audit.error,
    cashInBankUsd: supplyNostroMatch.externalNostroCashUsd ?? 0,
    tripleStateAligned: audit.tripleStateAligned,
    breaksCount: audit.breaksCount,
    tripleStateRan: audit.tripleStateAligned != null,
    supplyNostroMatch: supplyNostroMatch.status,
  });
}

export async function buildCashInBankAuditSnapshot(options?: {
  officeId?: number;
  skipTripleState?: boolean;
  lineId?: string;
  totalSupplyUsd?: number;
  forceRefreshGl?: boolean;
}): Promise<CashInBankAuditSnapshot> {
  const policy = loadNostroPolicy();
  const officeId = options?.officeId
    ?? Number(process.env.OMNL_CASH_IN_BANK_OFFICE_ID || process.env.OMNL_DEFAULT_OFFICE_ID || 1);
  const generatedAt = new Date().toISOString();
  const configured = fineractConfigured();

  if (!configured) {
    return {
      generatedAt,
      officeId,
      cashInBankUsd: 0,
      externalNostroGlCode: policy.externalNostroGlCode,
      nostroGlCodes: policy.nostroGlCodes,
      legs: [],
      fineractConfigured: false,
      audit: resolveAuditStatus({
        fineractOk: false,
        cashInBankUsd: 0,
        breaksCount: 0,
        tripleStateRan: false,
      }),
    };
  }

  let glRows: SignedGlBalanceRow[] = [];
  let fineractError: string | undefined;
  try {
    glRows = await getCachedOfficeSignedGlBalances(officeId, { forceRefresh: options?.forceRefreshGl });
  } catch (e) {
    fineractError = e instanceof Error ? e.message : String(e);
  }

  const { cashInBankUsd, legs } = externalNostroCashFromGlRows(glRows, policy.externalNostroGlCode);

  const skipTripleState =
    options?.skipTripleState === true || process.env.OMNL_CASH_IN_BANK_SKIP_TRIPLE_STATE === '1';
  let breaksCount = 0;
  let tripleStateAligned: boolean | undefined;
  let tripleStateRan = false;

  if (!skipTripleState && !fineractError) {
    try {
      const report = await runTripleStateReconcile(options?.lineId);
      tripleStateRan = true;
      breaksCount = report.breaks.length;
      tripleStateAligned = report.aligned;
    } catch (e) {
      fineractError = fineractError || (e instanceof Error ? e.message : String(e));
    }
  }

  const supplyNostroMatch = options?.totalSupplyUsd != null
    ? evaluateSupplyNostroMatch({
      totalSupplyUsd: options.totalSupplyUsd,
      externalNostroCashUsd: cashInBankUsd,
      headOfficeId: officeId,
    })
    : undefined;

  const baseAudit = resolveAuditStatus({
    fineractOk: !fineractError,
    fineractError,
    cashInBankUsd,
    tripleStateAligned,
    breaksCount,
    tripleStateRan,
    supplyNostroMatch: supplyNostroMatch?.status,
  });

  return {
    generatedAt,
    officeId,
    cashInBankUsd,
    externalNostroGlCode: policy.externalNostroGlCode,
    nostroGlCodes: policy.nostroGlCodes,
    legs,
    fineractConfigured: true,
    supplyNostroMatch,
    audit: supplyNostroMatch
      ? mergeCashInBankAuditWithSupplyMatch(baseAudit, supplyNostroMatch)
      : baseAudit,
  };
}

/** Cached HO external-nostro snapshot with OMNL/HYBX triple-state audit (default TTL 120s). */
export async function getCachedCashInBankAuditSnapshot(options?: {
  forceRefresh?: boolean;
  officeId?: number;
  lineId?: string;
  totalSupplyUsd?: number;
  skipTripleState?: boolean;
}): Promise<CashInBankAuditSnapshot> {
  const ttlMs = Number(process.env.OMNL_CASH_IN_BANK_CACHE_MS || hoLiquidityCacheMs());
  const now = Date.now();
  if (
    !options?.forceRefresh
    && cache
    && cache.expiresAt > now
    && options?.totalSupplyUsd == null
  ) {
    return cache.snapshot;
  }
  const snapshot = await buildCashInBankAuditSnapshot({
    officeId: options?.officeId,
    lineId: options?.lineId,
    totalSupplyUsd: options?.totalSupplyUsd,
    skipTripleState: options?.skipTripleState,
    forceRefreshGl: options?.forceRefresh,
  });
  if (options?.totalSupplyUsd == null) {
    cache = { expiresAt: now + ttlMs, snapshot };
  }
  return snapshot;
}

/** Test helper — clears module cache between unit tests. */
export function resetCashInBankAuditCache(): void {
  cache = null;
}
