<?php

namespace App\Services;

use App\Models\ChartOfAccount;
use App\Models\CustomerLedger;
use App\Models\Expense;
use App\Models\Payment;
use App\Models\Receipt;
use App\Models\SupplierLedger;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use RuntimeException;

class TreasuryService
{
    public function __construct(
        private DocumentSequenceService $sequences,
        private FinancialPeriodService $periods,
        private AccountingService $accounting,
        private AuditLogService $audit,
        private BranchAccessService $branches,
    ) {}

    public function receipt(array $data, array $allocations = []): Receipt
    {
        return DB::transaction(function () use ($data, $allocations) {
            $branchId = $this->branches->assertBranch(isset($data['branch_id']) ? (int) $data['branch_id'] : null, 'receipts');
            $year = $this->periods->forDate($data['receipt_date']);
            $amount = $this->positive($data['amount']);
            $allocated = '0.00';
            foreach ($allocations as $row) $allocated = bcadd($allocated, (string) $row['amount'], 2);
            if (bccomp($allocated, $amount, 2) === 1) throw new RuntimeException('Invoice allocations cannot exceed the receipt amount.');
            $receipt = Receipt::create([
                'branch_id' => $branchId,
                'receipt_no' => $this->sequences->getNextNumber('receipt') ?? throw new RuntimeException('Unable to generate receipt number.'),
                'receipt_date' => $data['receipt_date'], 'customer_id' => $data['customer_id'],
                'amount' => $amount, 'payment_method' => strtolower($data['payment_method']),
                'reference' => $data['reference'] ?? null, 'notes' => $data['notes'] ?? null,
                'created_by' => Auth::id(), 'status' => 'posted',
            ]);
            foreach ($allocations as $row) {
                if (bccomp((string) $row['amount'], '0', 2) !== 1) continue;
                $sale = \App\Models\Sale::whereKey($row['sale_id'])->where('customer_id', $receipt->customer_id)->where('status', 'finalized')->lockForUpdate()->firstOrFail();
                if (bccomp((string) $row['amount'], (string) $sale->balance_total, 2) === 1) throw new RuntimeException('Receipt allocation exceeds an invoice balance.');
                $receipt->allocations()->create(['sale_id' => $sale->id, 'amount' => $row['amount']]);
                $sale->paid_total = bcadd((string) $sale->paid_total, (string) $row['amount'], 2);
                $sale->balance_total = bcsub((string) $sale->balance_total, (string) $row['amount'], 2);
                $sale->save();
            }
            $journal = $this->accounting->postJournal([
                'entry_date' => $receipt->receipt_date, 'reference_type' => 'receipt', 'reference_id' => $receipt->id,
                'reference_number' => $receipt->receipt_no, 'description' => 'Customer receipt '.$receipt->receipt_no,
                'financial_year_id' => $year->id, 'branch_id' => $branchId,
            ], [
                ['account_code' => $this->accounting->paymentAccount($receipt->payment_method), 'debit' => $amount, 'credit' => 0],
                ['account_code' => AccountingService::RECEIVABLE, 'party_type' => 'customer', 'party_id' => $receipt->customer_id, 'debit' => 0, 'credit' => $amount],
            ]);
            $receipt->update(['journal_entry_id' => $journal->id]);
            CustomerLedger::create([
                'customer_id' => $receipt->customer_id, 'transaction_date' => $receipt->receipt_date,
                'transaction_type' => 'RECEIPT', 'reference_number' => $receipt->receipt_no,
                'reference_id' => $receipt->id, 'description' => 'Customer receipt',
                'debit' => 0, 'credit' => $amount, 'journal_entry_id' => $journal->id,
            ]);
            $this->audit->log('RECEIPT_CREATED', 'receipts', $receipt->id, null, ['receipt_no' => $receipt->receipt_no]);
            return $receipt->fresh(['customer', 'allocations']);
        });
    }

    public function payment(array $data, array $allocations = []): Payment
    {
        return DB::transaction(function () use ($data, $allocations) {
            $branchId = $this->branches->assertBranch(isset($data['branch_id']) ? (int) $data['branch_id'] : null, 'payments');
            $year = $this->periods->forDate($data['payment_date']);
            $amount = $this->positive($data['amount']);
            $allocated = '0.00'; foreach ($allocations as $row) $allocated = bcadd($allocated, (string) $row['amount'], 2);
            if (bccomp($allocated, $amount, 2) === 1) throw new RuntimeException('Purchase allocations cannot exceed the payment amount.');
            $payment = Payment::create([
                'branch_id' => $branchId,
                'payment_no' => $this->sequences->getNextNumber('payment') ?? throw new RuntimeException('Unable to generate payment number.'),
                'payment_date' => $data['payment_date'], 'supplier_id' => $data['supplier_id'],
                'amount' => $amount, 'payment_method' => strtolower($data['payment_method']),
                'reference' => $data['reference'] ?? null, 'notes' => $data['notes'] ?? null,
                'created_by' => Auth::id(), 'status' => 'posted',
            ]);
            foreach ($allocations as $row) {
                if (bccomp((string) $row['amount'], '0', 2) !== 1) continue;
                $purchase = \App\Models\Purchase::whereKey($row['purchase_id'])->where('supplier_id', $payment->supplier_id)->where('status', 'finalized')->lockForUpdate()->firstOrFail();
                if (bccomp((string) $row['amount'], (string) $purchase->balance_total, 2) === 1) throw new RuntimeException('Payment allocation exceeds a purchase balance.');
                $payment->allocations()->create(['purchase_id' => $purchase->id, 'amount' => $row['amount']]);
                $purchase->paid_total = bcadd((string) $purchase->paid_total, (string) $row['amount'], 2);
                $purchase->balance_total = bcsub((string) $purchase->balance_total, (string) $row['amount'], 2);
                $purchase->save();
            }
            $journal = $this->accounting->postJournal([
                'entry_date' => $payment->payment_date, 'reference_type' => 'payment', 'reference_id' => $payment->id,
                'reference_number' => $payment->payment_no, 'description' => 'Supplier payment '.$payment->payment_no,
                'financial_year_id' => $year->id, 'branch_id' => $branchId,
            ], [
                ['account_code' => AccountingService::PAYABLE, 'party_type' => 'supplier', 'party_id' => $payment->supplier_id, 'debit' => $amount, 'credit' => 0],
                ['account_code' => $this->accounting->paymentAccount($payment->payment_method), 'debit' => 0, 'credit' => $amount],
            ]);
            $payment->update(['journal_entry_id' => $journal->id]);
            SupplierLedger::create([
                'supplier_id' => $payment->supplier_id, 'transaction_date' => $payment->payment_date,
                'transaction_type' => 'PAYMENT', 'reference_number' => $payment->payment_no,
                'reference_id' => $payment->id, 'description' => 'Supplier payment',
                'debit' => $amount, 'credit' => 0, 'journal_entry_id' => $journal->id,
            ]);
            $this->audit->log('PAYMENT_CREATED', 'payments', $payment->id, null, ['payment_no' => $payment->payment_no]);
            return $payment->fresh(['supplier', 'allocations']);
        });
    }

    public function expense(array $data): Expense
    {
        return DB::transaction(function () use ($data) {
            $branchId = $this->branches->assertBranch(isset($data['branch_id']) ? (int) $data['branch_id'] : null, 'expenses');
            $year = $this->periods->forDate($data['expense_date']);
            $taxable = $this->positive($data['taxable_amount'], true);
            $vat = bcadd((string) ($data['vat_amount'] ?? 0), '0', 2);
            $total = bcadd($taxable, $vat, 2);
            $expense = Expense::create([
                'branch_id' => $branchId,
                'expense_no' => $this->sequences->getNextNumber('expense') ?? throw new RuntimeException('Unable to generate expense number.'),
                'expense_date' => $data['expense_date'], 'expense_account_id' => $data['expense_account_id'],
                'payment_account_id' => $data['payment_account_id'], 'taxable_amount' => $taxable,
                'vat_amount' => $vat, 'total_amount' => $total, 'reference' => $data['reference'] ?? null,
                'notes' => $data['notes'] ?? null, 'attachment' => $data['attachment'] ?? null,
                'created_by' => Auth::id(), 'status' => 'posted',
            ]);
            $lines = [['account_id' => $expense->expense_account_id, 'debit' => $taxable, 'credit' => 0]];
            if (bccomp($vat, '0', 2) === 1) $lines[] = ['account_code' => AccountingService::INPUT_VAT, 'debit' => $vat, 'credit' => 0];
            $lines[] = ['account_id' => $expense->payment_account_id, 'debit' => 0, 'credit' => $total];
            $journal = $this->accounting->postJournal([
                'entry_date' => $expense->expense_date, 'reference_type' => 'expense', 'reference_id' => $expense->id,
                'reference_number' => $expense->expense_no, 'description' => 'Expense '.$expense->expense_no,
                'financial_year_id' => $year->id, 'branch_id' => $branchId,
            ], $lines);
            $expense->update(['journal_entry_id' => $journal->id]);
            $this->audit->log('EXPENSE_CREATED', 'expenses', $expense->id, null, ['expense_no' => $expense->expense_no]);
            return $expense->fresh(['expenseAccount', 'paymentAccount']);
        });
    }

    private function positive(string|int|float $value, bool $allowZero = false): string
    {
        $value = bcadd((string) $value, '0', 2);
        if (bccomp($value, '0', 2) < ($allowZero ? 0 : 1)) throw new RuntimeException('Amount must be greater than zero.');
        return $value;
    }
}
