<?php

namespace App\Services;

use App\Models\AccountingAccount;
use App\Models\CommunicationUnitTopup;
use App\Models\CommunicationUnitTransaction;
use App\Models\Expense;
use App\Models\Income;
use App\Models\LedgerEntry;
use App\Models\Loan;
use App\Models\LoanPayment;
use App\Models\SavingsTransaction;
use App\Models\Tenant;
use App\Models\VtuWalletTopup;
use App\Models\VtuWalletTransaction;
use Illuminate\Support\Carbon;

class AccountingService
{
    public const DEFAULT_ACCOUNTS = [
        ['1000', 'Cash on Hand', 'asset'],
        ['1100', 'Loans Receivable', 'asset'],
        ['1200', 'Communication Unit Wallet', 'asset'],
        ['1250', 'VTU Wallet', 'asset'],
        ['2000', 'Customer Savings Liability', 'liability'],
        ['2100', 'Staff Wallet Liability', 'liability'],
        ['3000', 'Owner Equity', 'equity'],
        ['4000', 'Interest Income', 'income'],
        ['4100', 'Other Income', 'income'],
        ['4200', 'Notification Income', 'income'],
        ['5000', 'Operating Expenses', 'expense'],
        ['5100', 'Penalty Income', 'income'],
        ['5200', 'Communication Cost', 'expense'],
        ['5300', 'VTU Cost', 'expense'],
    ];

    public function ensureChartOfAccounts(Tenant $tenant): void
    {
        foreach (self::DEFAULT_ACCOUNTS as [$code, $name, $type]) {
            AccountingAccount::withoutGlobalScopes()->firstOrCreate(
                ['tenant_id' => $tenant->id, 'code' => $code],
                ['name' => $name, 'type' => $type, 'balance' => 0, 'is_system' => true],
            );
        }
    }

    public function recordSavings(SavingsTransaction $transaction): void
    {
        if ($transaction->type === 'deposit') {
            $grossAmount = (float) $transaction->amount;
            $processingFee = max(0, (float) ($transaction->processing_fee_amount ?? 0));
            $netAmount = (float) ($transaction->net_amount ?? 0);

            if ($netAmount <= 0 && $processingFee < $grossAmount) {
                $netAmount = round($grossAmount - $processingFee, 2);
            }

            $this->post($transaction->tenant, '1000', 'debit', $grossAmount, 'Savings deposit '.$transaction->reference, $transaction);

            if ($netAmount > 0) {
                $this->post($transaction->tenant, '2000', 'credit', $netAmount, 'Customer savings liability '.$transaction->reference, $transaction);
            }

            if ($processingFee > 0) {
                $this->post($transaction->tenant, '4100', 'credit', $processingFee, 'Savings processing fee '.$transaction->reference, $transaction);
            }

            return;
        }

        $grossAmount = (float) $transaction->amount;
        $processingFee = max(0, (float) ($transaction->processing_fee_amount ?? 0));
        $netAmount = (float) ($transaction->net_amount ?? 0);

        if ($netAmount <= 0 && $processingFee < $grossAmount) {
            $netAmount = round($grossAmount - $processingFee, 2);
        }

        $this->post($transaction->tenant, '2000', 'debit', $grossAmount, 'Savings withdrawal '.$transaction->reference, $transaction);

        if ($netAmount > 0) {
            $this->post($transaction->tenant, '1000', 'credit', $netAmount, 'Cash paid '.$transaction->reference, $transaction);
        }

        if ($processingFee > 0) {
            $this->post($transaction->tenant, '4100', 'credit', $processingFee, 'Savings processing fee '.$transaction->reference, $transaction);
        }
    }

    public function recordLoanDisbursement(Loan $loan): void
    {
        $principal = (float) $loan->principal;
        $processingFee = (float) $loan->processing_fee;
        $netDisbursement = (float) ($loan->amount_disbursed ?: $loan->netDisbursementAmount());

        $this->post($loan->tenant, '1100', 'debit', $principal, 'Loan disbursed '.$loan->loan_number, $loan);

        if ($netDisbursement > 0) {
            $this->post($loan->tenant, '1000', 'credit', $netDisbursement, 'Cash disbursed '.$loan->loan_number, $loan);
        }

        if (($loan->processing_fee_timing ?? 'before_disbursement') === 'deduct_from_disbursement' && $processingFee > 0) {
            $this->post($loan->tenant, '4100', 'credit', min($processingFee, $principal), 'Loan processing fee deducted '.$loan->loan_number, $loan);
        }
    }

    public function recordLoanPayment(LoanPayment $payment): void
    {
        $this->post($payment->tenant, '1000', 'debit', (float) $payment->amount, 'Loan repayment '.$payment->reference, $payment);

        if ((float) $payment->principal_component > 0) {
            $this->post($payment->tenant, '1100', 'credit', (float) $payment->principal_component, 'Principal recovered '.$payment->reference, $payment);
        }

        if ((float) $payment->interest_component > 0) {
            $this->post($payment->tenant, '4000', 'credit', (float) $payment->interest_component, 'Interest received '.$payment->reference, $payment);
        }

        if ((float) $payment->fee_component > 0) {
            $this->post($payment->tenant, '4100', 'credit', (float) $payment->fee_component, 'Loan fee received '.$payment->reference, $payment);
        }

        if ((float) $payment->penalty_component > 0) {
            $this->post($payment->tenant, '5100', 'credit', (float) $payment->penalty_component, 'Penalty received '.$payment->reference, $payment);
        }
    }

    public function recordIncome(Income $income): void
    {
        $this->post($income->tenant, '1000', 'debit', (float) $income->amount, 'Income received '.$income->reference, $income);
        $this->post($income->tenant, '4100', 'credit', (float) $income->amount, $income->category, $income);
    }

    public function recordCommunicationUnitTopup(CommunicationUnitTopup $topup): void
    {
        $amount = (float) $topup->amount;

        if ($amount <= 0) {
            return;
        }

        $this->ensureChartOfAccounts($topup->tenant);
        $this->post($topup->tenant, '1200', 'debit', $amount, $topup->channelLabel().' units purchased '.$topup->reference, $topup);
        $this->post($topup->tenant, '1000', 'credit', $amount, 'Cash paid for '.$topup->channelLabel().' units '.$topup->reference, $topup);
    }

    public function recordCommunicationUnitUsage(CommunicationUnitTransaction $transaction): void
    {
        if ($transaction->type !== CommunicationUnitTransaction::TYPE_USAGE) {
            return;
        }

        $tenant = $transaction->tenant;
        $this->ensureChartOfAccounts($tenant);

        $memberCharge = (float) $transaction->amount;
        $costAmount = (float) $transaction->cost_amount;

        if ($memberCharge > 0) {
            $this->post($tenant, '2000', 'debit', $memberCharge, $transaction->channelLabel().' notification fee charged '.$transaction->reference, $transaction);
            $this->post($tenant, '4200', 'credit', $memberCharge, $transaction->channelLabel().' notification income '.$transaction->reference, $transaction);
        }

        if ($costAmount > 0) {
            $this->post($tenant, '5200', 'debit', $costAmount, $transaction->channelLabel().' unit cost '.$transaction->reference, $transaction);
            $this->post($tenant, '1200', 'credit', $costAmount, $transaction->channelLabel().' units consumed '.$transaction->reference, $transaction);
        }
    }

    public function recordVtuWalletTopup(VtuWalletTopup $topup): void
    {
        $amount = (float) $topup->amount;

        if ($amount <= 0) {
            return;
        }

        $this->ensureChartOfAccounts($topup->tenant);
        $this->post($topup->tenant, '1250', 'debit', $amount, 'VTU wallet funded '.$topup->reference, $topup);
        $this->post($topup->tenant, '1000', 'credit', $amount, 'Cash paid for VTU wallet '.$topup->reference, $topup);
    }

    public function recordVtuWalletUsage(VtuWalletTransaction $transaction): void
    {
        if ($transaction->type !== VtuWalletTransaction::TYPE_USAGE) {
            return;
        }

        $amount = (float) $transaction->amount;

        if ($amount <= 0) {
            return;
        }

        $this->ensureChartOfAccounts($transaction->tenant);
        $this->post($transaction->tenant, '5300', 'debit', $amount, 'VTU provider cost '.$transaction->reference, $transaction);
        $this->post($transaction->tenant, '1250', 'credit', $amount, 'VTU wallet consumed '.$transaction->reference, $transaction);
    }

    public function recordExpense(Expense $expense): void
    {
        $this->post($expense->tenant, '5000', 'debit', (float) $expense->amount, $expense->category, $expense);
        $this->post($expense->tenant, '1000', 'credit', (float) $expense->amount, 'Expense paid '.$expense->reference, $expense);
    }

    public function reverseSource(object $source, ?string $reason = null): void
    {
        $tenant = $source->tenant ?? null;
        $sourceId = $source->id ?? null;

        if (! $tenant || ! $sourceId) {
            return;
        }

        $entries = LedgerEntry::withoutGlobalScopes()
            ->with('account')
            ->where('tenant_id', $tenant->id)
            ->where('source_type', $source::class)
            ->where('source_id', $sourceId)
            ->where('reference', 'not like', 'REV-%')
            ->get();

        foreach ($entries as $entry) {
            $account = $entry->account;

            if (! $account) {
                continue;
            }

            $reverseType = $entry->type === 'debit' ? 'credit' : 'debit';
            $description = 'Reversal: '.($entry->description ?: $entry->reference);

            if (filled($reason)) {
                $description .= ' | '.$reason;
            }

            LedgerEntry::withoutGlobalScopes()->create([
                'tenant_id' => $tenant->id,
                'accounting_account_id' => $account->id,
                'created_by' => auth()->id(),
                'entry_date' => today(),
                'type' => $reverseType,
                'amount' => $entry->amount,
                'reference' => 'REV-'.$entry->reference,
                'description' => $description,
                'source_type' => $source::class,
                'source_id' => $sourceId,
            ]);

            $account->increment('balance', $this->signedAmount($account->type, $reverseType, (float) $entry->amount));
        }
    }

    public function recordManualJournal(Tenant $tenant, AccountingAccount $debitAccount, AccountingAccount $creditAccount, float $amount, string $entryDate, string $reference, ?string $description, ?int $createdBy): void
    {
        $date = Carbon::parse($entryDate)->toDateString();

        $this->postManualEntry($tenant, $debitAccount, 'debit', $amount, $date, $reference, $description, $createdBy);
        $this->postManualEntry($tenant, $creditAccount, 'credit', $amount, $date, $reference, $description, $createdBy);
    }

    public function post(Tenant $tenant, string $accountCode, string $type, float $amount, string $description, object $source): void
    {
        $account = AccountingAccount::withoutGlobalScopes()
            ->where('tenant_id', $tenant->id)
            ->where('code', $accountCode)
            ->firstOrFail();

        LedgerEntry::withoutGlobalScopes()->create([
            'tenant_id' => $tenant->id,
            'accounting_account_id' => $account->id,
            'created_by' => auth()->id(),
            'entry_date' => today(),
            'type' => $type,
            'amount' => $amount,
            'reference' => $source->reference ?? $source->loan_number ?? NumberGenerator::reference('LED'),
            'description' => $description,
            'source_type' => $source::class,
            'source_id' => $source->id ?? null,
        ]);

        $account->increment('balance', $this->signedAmount($account->type, $type, $amount));
    }

    private function signedAmount(string $accountType, string $entryType, float $amount): float
    {
        $normalDebit = in_array($accountType, ['asset', 'expense'], true);

        return ($normalDebit && $entryType === 'debit') || (! $normalDebit && $entryType === 'credit')
            ? $amount
            : -$amount;
    }

    private function postManualEntry(Tenant $tenant, AccountingAccount $account, string $type, float $amount, string $entryDate, string $reference, ?string $description, ?int $createdBy): void
    {
        LedgerEntry::withoutGlobalScopes()->create([
            'tenant_id' => $tenant->id,
            'accounting_account_id' => $account->id,
            'created_by' => $createdBy,
            'entry_date' => $entryDate,
            'type' => $type,
            'amount' => $amount,
            'reference' => $reference,
            'description' => $description,
            'source_type' => 'manual_journal',
            'source_id' => null,
        ]);

        $account->increment('balance', $this->signedAmount($account->type, $type, $amount));
    }
}
