'use strict';

import type { Db } from '../types/db';

const { computeBalanceDelta, applyBalanceDelta } = require('./billsService.cts');
const { getCycleRange } = require('./statusService.cts');

// Dynamic better-sqlite3 rows / payloads — shape varies by query, callers read
// individual columns. Typed loosely on purpose (incremental migration).
type Row = Record<string, any>;

const ACCOUNTING_ACTIVE_SQL = 'COALESCE(accounting_excluded, 0) = 0';
const BANK_PAYMENT_SOURCES = new Set(['provider_sync', 'transaction_match', 'auto_match']);
const OVERRIDE_REASON = 'overridden_by_bank';

function accountingActiveSql(alias: string | null = null): string {
  const prefix = alias ? `${alias}.` : '';
  return `COALESCE(${prefix}accounting_excluded, 0) = 0`;
}

function isBankBackedPayment(payment: Row = {}): boolean {
  return BANK_PAYMENT_SOURCES.has(payment.payment_source) || payment.transaction_id != null;
}

function appendNote(existing: unknown, line: string): string {
  const current = String(existing || '').trim();
  if (current.includes(line)) return current || line;
  return current ? `${current}\n${line}`.slice(0, 500) : line.slice(0, 500);
}

function paymentMonth(paidDate: unknown): { year: number; month: number } | null {
  const match = String(paidDate || '').match(/^(\d{4})-(\d{2})-\d{2}$/);
  if (!match) return null;
  return { year: Number(match[1]), month: Number(match[2]) };
}

function cycleRangeForPayment(bill: Row, paidDate: unknown): any {
  const ym = paymentMonth(paidDate);
  if (!ym) return null;
  return getCycleRange(ym.year, ym.month, bill);
}

function reversePaymentBalance(db: Db, payment: Row): void {
  if (!payment || payment.balance_delta == null) return;
  const bill = db
    .prepare('SELECT id, current_balance FROM bills WHERE id = ?')
    .get(payment.bill_id);
  if (bill?.current_balance == null) return;

  const restored = Math.max(0, Number(bill.current_balance) - Number(payment.balance_delta)); // cents, exact integer arithmetic
  db.prepare(
    `
    UPDATE bills
    SET current_balance        = ?,
        interest_accrued_month = CASE WHEN ? THEN NULL ELSE interest_accrued_month END,
        updated_at             = datetime('now')
    WHERE id = ?
  `,
  ).run(restored, payment.interest_delta != null ? 1 : 0, bill.id);
}

function applyPaymentBalanceFromFreshBill(
  db: Db,
  billId: number,
  amount: any,
): { balance_delta: any; interest_delta: any } {
  const bill = db.prepare('SELECT * FROM bills WHERE id = ? AND deleted_at IS NULL').get(billId);
  if (!bill) return { balance_delta: null, interest_delta: null };
  const balCalc = computeBalanceDelta(bill, amount);
  applyBalanceDelta(db, billId, balCalc);
  return {
    balance_delta: balCalc?.balance_delta ?? null,
    interest_delta: balCalc?.interest_delta ?? null,
  };
}

function markProvisionalManualPaymentsOverridden(
  db: Db,
  bill: Row,
  bankPayment: Row,
): { overridden: number } {
  if (!bill || !bankPayment || !isBankBackedPayment(bankPayment)) return { overridden: 0 };
  const range = cycleRangeForPayment(bill, bankPayment.paid_date);
  if (!range) return { overridden: 0 };

  const provisionalPayments = db
    .prepare(
      `
    SELECT *
    FROM payments
    WHERE bill_id = ?
      AND id != ?
      AND payment_source = 'manual'
      AND transaction_id IS NULL
      AND deleted_at IS NULL
      AND ${ACCOUNTING_ACTIVE_SQL}
      AND paid_date BETWEEN ? AND ?
    ORDER BY paid_date DESC, id DESC
  `,
    )
    .all(bill.id, bankPayment.id, range.start, range.end);

  const note = `History only: overridden by bank payment #${bankPayment.id} on ${bankPayment.paid_date}.`;
  const update = db.prepare(`
    UPDATE payments
    SET accounting_excluded = 1,
        exclusion_reason = ?,
        excluded_at = datetime('now'),
        overridden_by_payment_id = ?,
        notes = ?,
        balance_delta = NULL,
        interest_delta = NULL,
        updated_at = datetime('now')
    WHERE id = ?
  `);

  for (const payment of provisionalPayments) {
    reversePaymentBalance(db, payment);
    update.run(OVERRIDE_REASON, bankPayment.id, appendNote(payment.notes, note), payment.id);
  }

  return { overridden: provisionalPayments.length };
}

function reactivatePaymentsOverriddenBy(db: Db, bankPaymentId: number): { reactivated: number } {
  const rows = db
    .prepare(
      `
    SELECT *
    FROM payments
    WHERE overridden_by_payment_id = ?
      AND accounting_excluded = 1
      AND deleted_at IS NULL
  `,
    )
    .all(bankPaymentId);

  const update = db.prepare(`
    UPDATE payments
    SET accounting_excluded = 0,
        exclusion_reason = NULL,
        excluded_at = NULL,
        overridden_by_payment_id = NULL,
        balance_delta = ?,
        interest_delta = ?,
        notes = ?,
        updated_at = datetime('now')
    WHERE id = ?
  `);

  for (const payment of rows) {
    const deltas = applyPaymentBalanceFromFreshBill(db, payment.bill_id, payment.amount);
    update.run(
      deltas.balance_delta,
      deltas.interest_delta,
      appendNote(payment.notes, 'Bank override removed; this manual payment counts again.'),
      payment.id,
    );
  }

  return { reactivated: rows.length };
}

function applyBankPaymentAsSourceOfTruth(
  db: Db,
  bill: Row,
  bankPayment: Row,
): { balance_delta: any; interest_delta: any } {
  markProvisionalManualPaymentsOverridden(db, bill, bankPayment);
  const deltas = applyPaymentBalanceFromFreshBill(db, bill.id, bankPayment.amount);
  db.prepare(
    `
    UPDATE payments
    SET balance_delta = ?,
        interest_delta = ?,
        updated_at = datetime('now')
    WHERE id = ?
  `,
  ).run(deltas.balance_delta, deltas.interest_delta, bankPayment.id);
  return deltas;
}

module.exports = {
  ACCOUNTING_ACTIVE_SQL,
  OVERRIDE_REASON,
  accountingActiveSql,
  isBankBackedPayment,
  markProvisionalManualPaymentsOverridden,
  reactivatePaymentsOverriddenBy,
  applyBankPaymentAsSourceOfTruth,
};
