import { __decorate, __metadata } from "tslib";
import DataLoader from 'dataloader';
import { GraphQLError } from 'graphql';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { dateToTimelessDateString, reassureOwnerIdExists } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { identifyInterestTransactionIds } from '../../ledger/helpers/bank-deposit-ledger-generation.helper.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import { BankDepositsProvider } from './bank-deposits.provider.js';
const getTransactionsByBankDeposits = sql `
    SELECT 
      cbd.new_deposit_id,
      t.*
    FROM accounter_schema.charges_bank_deposits cbd
    INNER JOIN accounter_schema.transactions t
      ON cbd.id = t.charge_id
    WHERE new_deposit_id IN $$depositIds;`;
const getBankDepositsByChargeIds = sql `
  SELECT bd.id, bd.name, bd.currency, bd.account_id, bd.open_date, bd.close_date, cbd.id as charge_id
  FROM accounter_schema.charges_bank_deposits cbd
  INNER JOIN accounter_schema.bank_deposits bd
    ON cbd.new_deposit_id = bd.id
  WHERE cbd.id IN $$chargeIds;`;
const getDepositTransactionsByChargeId = sql `
    SELECT
      cbd.new_deposit_id,
      t.*
    FROM accounter_schema.charges_bank_deposits cbd
    LEFT JOIN accounter_schema.transactions t
      ON cbd.id = t.charge_id
    WHERE new_deposit_id IN (
      SELECT cbd2.new_deposit_id
      FROM accounter_schema.charges_bank_deposits cbd2
      WHERE cbd2.id = $chargeId
    )
    AND ($includeCharge OR t.charge_id <> $chargeId);`;
const upsertBankDepositCharge = sql `
  INSERT INTO accounter_schema.charges_bank_deposits (id, new_deposit_id, owner_id, account_id)
  VALUES ($chargeId, $depositId, $ownerId, $accountId)
  ON CONFLICT (id) DO UPDATE SET new_deposit_id = EXCLUDED.new_deposit_id, owner_id = EXCLUDED.owner_id, account_id = EXCLUDED.account_id;
`;
const unlinkChargesFromBankDepositsByChargeIds = sql `
  DELETE FROM accounter_schema.charges_bank_deposits
  WHERE id IN $$chargeIds;
`;
const getAllDepositsWithTransactions = sql `
    SELECT
      bd.id as new_deposit_id,
      bd.name as deposit_name,
      bd.currency as deposit_currency,
      bd.open_date,
      bd.close_date,
      t.*
    FROM accounter_schema.bank_deposits bd
    LEFT JOIN accounter_schema.charges_bank_deposits cbd
        ON cbd.new_deposit_id = bd.id
    LEFT JOIN accounter_schema.transactions t
        ON cbd.id = t.charge_id
    ORDER BY bd.id, COALESCE(t.debit_date, t.event_date);`;
let BankDepositChargesProvider = class BankDepositChargesProvider {
    db;
    adminContextProvider;
    transactionsProvider;
    bankDepositsProvider;
    constructor(db, adminContextProvider, transactionsProvider, bankDepositsProvider) {
        this.db = db;
        this.adminContextProvider = adminContextProvider;
        this.transactionsProvider = transactionsProvider;
        this.bankDepositsProvider = bankDepositsProvider;
    }
    async batchTransactionsByBankDeposits(depositIds) {
        const transactions = await getTransactionsByBankDeposits.run({
            depositIds,
        }, this.db);
        return depositIds.map(id => transactions.filter(t => t.new_deposit_id === id));
    }
    getTransactionsByBankDepositLoader = new DataLoader((keys) => this.batchTransactionsByBankDeposits(keys));
    async batchBankDepositsByChargeIds(chargeIds) {
        const rows = await getBankDepositsByChargeIds.run({ chargeIds: Array.from(new Set(chargeIds)) }, this.db);
        const chargeIdToDepositMap = new Map();
        for (const row of rows) {
            if (row.charge_id) {
                chargeIdToDepositMap.set(row.charge_id, row);
            }
        }
        return chargeIds.map(chargeId => chargeIdToDepositMap.get(chargeId) ?? null);
    }
    getBankDepositByChargeIdLoader = new DataLoader(async (chargeIds) => this.batchBankDepositsByChargeIds(chargeIds));
    async getDepositTransactionsByChargeId(chargeId, includeCharge = false) {
        return getDepositTransactionsByChargeId.run({ chargeId, includeCharge }, this.db);
    }
    async upsertBankDepositCharge(params) {
        const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
        return upsertBankDepositCharge.run(reassureOwnerIdExists(params, ownerId), this.db);
    }
    async batchBankDepositsMetadata(depositIds) {
        const uniqueDepositIds = Array.from(new Set(depositIds));
        const depositsTransactions = await this.getTransactionsByBankDepositLoader.loadMany(uniqueDepositIds);
        const depositIdToMetadataMap = new Map();
        for (let i = 0; i < uniqueDepositIds.length; i++) {
            const depositId = uniqueDepositIds[i];
            const transactions = depositsTransactions[i] ?? [];
            if (transactions instanceof Error) {
                depositIdToMetadataMap.set(depositId, transactions);
                continue;
            }
            const interestTransactionIds = identifyInterestTransactionIds(transactions, {
                getId: r => r.id,
                getChargeId: r => r.charge_id,
                getAmount: r => Number(r.amount ?? 0),
            });
            let currentBalance = 0;
            let totalInterest = 0;
            let totalDeposit = 0;
            let potentialCloseDate = null;
            const transactionIds = [];
            for (const tx of transactions) {
                if (!tx.id)
                    continue;
                transactionIds.push(tx.id);
                const amount = Number(tx.amount ?? 0);
                if (interestTransactionIds.has(tx.id)) {
                    totalInterest += amount;
                }
                else {
                    currentBalance += amount;
                    if (amount > 0) {
                        totalDeposit += amount;
                    }
                }
                const transactionDate = tx.debit_date ?? tx.event_date;
                if (!potentialCloseDate || transactionDate > potentialCloseDate) {
                    potentialCloseDate = transactionDate;
                }
            }
            depositIdToMetadataMap.set(depositId, {
                id: depositId,
                potentialCloseDate: potentialCloseDate
                    ? dateToTimelessDateString(potentialCloseDate)
                    : null,
                currentBalance,
                totalInterest,
                totalDeposit,
                transactionIds,
            });
        }
        return depositIds.map(id => {
            const metadata = depositIdToMetadataMap.get(id);
            if (!metadata) {
                return {
                    id,
                    potentialCloseDate: null,
                    currentBalance: 0,
                    totalInterest: 0,
                    totalDeposit: 0,
                    transactionIds: [],
                };
            }
            if (metadata instanceof Error) {
                return metadata;
            }
            return metadata;
        });
    }
    getBankDepositMetadataLoader = new DataLoader(async (depositIds) => this.batchBankDepositsMetadata(depositIds));
    async getAllDepositsWithMetadata() {
        const transactionRows = await getAllDepositsWithTransactions.run(undefined, this.db);
        const depositMap = new Map();
        const interestTransactionIds = identifyInterestTransactionIds(transactionRows, {
            getId: r => r.id,
            getChargeId: r => r.charge_id,
            getAmount: r => Number(r.amount ?? 0),
        });
        for (const row of transactionRows) {
            if (!row.new_deposit_id)
                continue;
            if (!depositMap.has(row.new_deposit_id)) {
                depositMap.set(row.new_deposit_id, {
                    id: row.new_deposit_id,
                    potentialCloseDate: row.close_date,
                    currentBalance: 0,
                    totalInterest: 0,
                    totalDeposit: 0,
                    transactionIds: [],
                });
            }
            if (!row.id)
                continue;
            const deposit = depositMap.get(row.new_deposit_id);
            deposit.transactionIds.push(row.id);
            if (row.amount) {
                const amount = Number(row.amount);
                if (interestTransactionIds.has(row.id)) {
                    deposit.totalInterest += amount;
                }
                else {
                    deposit.currentBalance += amount;
                    if (amount > 0) {
                        deposit.totalDeposit += amount;
                    }
                }
            }
        }
        return Array.from(depositMap.values()).map(deposit => ({
            id: deposit.id,
            potentialCloseDate: deposit.potentialCloseDate
                ? dateToTimelessDateString(deposit.potentialCloseDate)
                : null,
            currentBalance: deposit.currentBalance,
            totalInterest: deposit.totalInterest,
            totalDeposit: deposit.totalDeposit,
            transactionIds: deposit.transactionIds,
        }));
    }
    async unlinkChargesFromBankDepositsByChargeIds(chargeIds) {
        return unlinkChargesFromBankDepositsByChargeIds.run({ chargeIds }, this.db);
    }
    async assignChargeToDeposit(chargeId, depositId) {
        const [deposit, transactions] = await Promise.all([
            // Get target deposit transactions to validate currency
            this.bankDepositsProvider.bankDepositByIdLoader.load(depositId),
            this.transactionsProvider.transactionsByChargeIDLoader.load(chargeId),
        ]);
        if (!deposit) {
            throw new GraphQLError(`Deposit ${depositId} not found`);
        }
        if (transactions.length < 1) {
            throw new GraphQLError(`No transactions found for charge ID="${chargeId}"`);
        }
        let accountId = deposit.account_id;
        let currency = deposit.currency;
        // Check currency or account conflict
        for (const transaction of transactions) {
            if (!accountId || !currency) {
                accountId ??= transaction.account_id;
                currency ??= transaction.currency;
                await this.bankDepositsProvider.updateBankDeposit({
                    depositId: deposit.id,
                    accountId,
                    currency,
                });
            }
            if (accountId !== transaction.account_id) {
                throw new Error(`Account conflict: Transactions accounts are inconsistent`);
            }
            if (currency !== transaction.currency) {
                throw new Error(`Currency conflict: Transactions currencies are inconsistent`);
            }
        }
        await this.upsertBankDepositCharge({ chargeId, depositId, accountId });
    }
    async createDepositFromCharge(chargeId, name) {
        const transactions = await this.transactionsProvider.transactionsByChargeIDLoader.load(chargeId);
        if (transactions.length === 0) {
            throw new GraphQLError('createDepositFromCharge can only be called for charges that have at least one transaction');
        }
        // Validate single currency and single account across all transactions
        const currencies = new Set(transactions.map(t => t.currency));
        if (currencies.size > 1) {
            throw new GraphQLError(`Cannot create deposit: charge has transactions in multiple currencies (${[...currencies].join(', ')})`);
        }
        const accountIds = new Set(transactions.map(t => t.account_id).filter(Boolean));
        if (accountIds.size > 1) {
            throw new GraphQLError(`Cannot create deposit: charge has transactions from multiple accounts`);
        }
        const currency = [...currencies][0];
        const accountId = [...accountIds][0] ?? null;
        // Derive open_date from the earliest debit_date, falling back to event_date
        const openDate = transactions.reduce((min, t) => {
            const d = t.debit_date ?? t.event_date;
            if (!d)
                return min;
            return min === null || d < min ? d : min;
        }, null);
        const deposit = await this.bankDepositsProvider.insertBankDeposit({
            name,
            currency,
            accountId,
            openDate,
            closeDate: null,
        });
        if (!deposit) {
            throw new GraphQLError('Failed to create bank deposit');
        }
        await this.upsertBankDepositCharge({ chargeId, depositId: deposit.id });
        this.getBankDepositByChargeIdLoader.clear(chargeId);
        return deposit;
    }
};
BankDepositChargesProvider = __decorate([
    Injectable({
        scope: Scope.Operation,
        global: true,
    }),
    __metadata("design:paramtypes", [TenantAwareDBClient,
        AdminContextProvider,
        TransactionsProvider,
        BankDepositsProvider])
], BankDepositChargesProvider);
export { BankDepositChargesProvider };
//# sourceMappingURL=bank-deposit-charges.provider.js.map