<?php

namespace App\Services;

use App\Models\ChartOfAccount;
use App\Models\CustomerLedger;
use App\Models\JournalEntry;
use App\Models\Sale;
use App\Models\SupplierLedger;
use App\Models\Purchase;
use App\Models\PurchaseReturn;
use App\Models\SalesReturn;
use Illuminate\Support\Facades\Auth;
use RuntimeException;

class AccountingService
{
    public const CASH = '1110';
    public const BANK = '1120';
    public const RECEIVABLE = '1130';
    public const INVENTORY = '1140';
    public const INPUT_VAT = '1150';
    public const PAYABLE = '2110';
    public const OUTPUT_VAT = '2120';
    public const SALES = '4110';
    public const COGS = '5110';

    public function __construct(private DocumentSequenceService $sequences) {}

    public function postJournal(array $header, array $lines): JournalEntry
    {
        $debit = '0.00';
        $credit = '0.00';
        foreach ($lines as $line) {
            $debit = bcadd($debit, (string) ($line['debit'] ?? 0), 2);
            $credit = bcadd($credit, (string) ($line['credit'] ?? 0), 2);
        }
        if (bccomp($debit, $credit, 2) !== 0 || bccomp($debit, '0', 2) !== 1) {
            throw new RuntimeException("Journal entry is not balanced. Debit {$debit}; credit {$credit}.");
        }

        $entry = JournalEntry::create([
            'journal_no' => $this->sequences->getNextNumber('journal') ?? throw new RuntimeException('Unable to generate journal number.'),
            'entry_date' => $header['entry_date'],
            'reference_type' => $header['reference_type'] ?? null,
            'reference_id' => $header['reference_id'] ?? null,
            'reference_number' => $header['reference_number'] ?? null,
            'description' => $header['description'] ?? null,
            'total_debit' => $debit,
            'total_credit' => $credit,
            'status' => 'posted',
            'branch_id' => $header['branch_id'] ?? null,
            'financial_year_id' => $header['financial_year_id'] ?? null,
            'created_by' => Auth::id(),
            'posted_at' => now(),
        ]);

        foreach ($lines as $line) {
            $account = isset($line['account_id'])
                ? ChartOfAccount::findOrFail($line['account_id'])
                : ChartOfAccount::where('code', $line['account_code'])->firstOrFail();
            $entry->lines()->create([
                'account_id' => $account->id,
                'party_type' => $line['party_type'] ?? null,
                'party_id' => $line['party_id'] ?? null,
                'description' => $line['description'] ?? $header['description'] ?? null,
                'debit' => $line['debit'] ?? 0,
                'credit' => $line['credit'] ?? 0,
            ]);
        }

        return $entry;
    }

    public function postSale(Sale $sale): JournalEntry
    {
        $lines = [];
        foreach ($sale->payments as $payment) {
            $lines[] = ['account_code' => $this->paymentAccount($payment->payment_method), 'debit' => $payment->amount, 'credit' => 0];
        }
        if (bccomp((string) $sale->balance_total, '0', 2) === 1) {
            $lines[] = ['account_code' => self::RECEIVABLE, 'party_type' => 'customer', 'party_id' => $sale->customer_id, 'debit' => $sale->balance_total, 'credit' => 0];
        }
        $lines[] = ['account_code' => self::SALES, 'debit' => 0, 'credit' => $sale->taxable_total];
        if (bccomp((string) $sale->vat_total, '0', 2) === 1) {
            $lines[] = ['account_code' => self::OUTPUT_VAT, 'debit' => 0, 'credit' => $sale->vat_total];
        }
        if (bccomp((string) $sale->cost_total, '0', 2) === 1) {
            $lines[] = ['account_code' => self::COGS, 'debit' => $sale->cost_total, 'credit' => 0];
            $lines[] = ['account_code' => self::INVENTORY, 'debit' => 0, 'credit' => $sale->cost_total];
        }

        $journal = $this->postJournal([
            'entry_date' => $sale->invoice_date,
            'reference_type' => 'sale',
            'reference_id' => $sale->id,
            'reference_number' => $sale->invoice_no,
            'description' => 'Sales invoice '.$sale->invoice_no,
            'branch_id' => $sale->branch_id,
            'financial_year_id' => $sale->financial_year_id,
        ], $lines);

        CustomerLedger::create([
            'customer_id' => $sale->customer_id, 'transaction_date' => $sale->invoice_date,
            'transaction_type' => 'SALE', 'reference_number' => $sale->invoice_no,
            'reference_id' => $sale->id, 'description' => 'Sales invoice',
            'debit' => $sale->grand_total, 'credit' => 0, 'journal_entry_id' => $journal->id,
        ]);
        if (bccomp((string) $sale->paid_total, '0', 2) === 1) {
            CustomerLedger::create([
                'customer_id' => $sale->customer_id, 'transaction_date' => $sale->invoice_date,
                'transaction_type' => 'SALE_PAYMENT', 'reference_number' => $sale->invoice_no,
                'reference_id' => $sale->id, 'description' => 'Payment received with invoice',
                'debit' => 0, 'credit' => $sale->paid_total, 'journal_entry_id' => $journal->id,
            ]);
        }
        return $journal;
    }

    public function postPurchase(Purchase $purchase): JournalEntry
    {
        $lines = [
            ['account_code' => self::INVENTORY, 'debit' => $purchase->taxable_total, 'credit' => 0],
        ];
        if (bccomp((string) $purchase->vat_total, '0', 2) === 1) {
            $lines[] = ['account_code' => self::INPUT_VAT, 'debit' => $purchase->vat_total, 'credit' => 0];
        }
        foreach ($purchase->payments as $payment) {
            $lines[] = ['account_code' => $this->paymentAccount($payment->payment_method), 'debit' => 0, 'credit' => $payment->amount];
        }
        if (bccomp((string) $purchase->balance_total, '0', 2) === 1) {
            $lines[] = ['account_code' => self::PAYABLE, 'party_type' => 'supplier', 'party_id' => $purchase->supplier_id, 'debit' => 0, 'credit' => $purchase->balance_total];
        }

        $journal = $this->postJournal([
            'entry_date' => $purchase->purchase_date,
            'reference_type' => 'purchase', 'reference_id' => $purchase->id,
            'reference_number' => $purchase->purchase_no,
            'description' => 'Purchase invoice '.$purchase->purchase_no,
            'branch_id' => $purchase->branch_id, 'financial_year_id' => $purchase->financial_year_id,
        ], $lines);

        SupplierLedger::create([
            'supplier_id' => $purchase->supplier_id, 'transaction_date' => $purchase->purchase_date,
            'transaction_type' => 'PURCHASE', 'reference_number' => $purchase->purchase_no,
            'reference_id' => $purchase->id, 'description' => 'Purchase invoice',
            'debit' => 0, 'credit' => $purchase->grand_total, 'journal_entry_id' => $journal->id,
        ]);
        if (bccomp((string) $purchase->paid_total, '0', 2) === 1) {
            SupplierLedger::create([
                'supplier_id' => $purchase->supplier_id, 'transaction_date' => $purchase->purchase_date,
                'transaction_type' => 'PURCHASE_PAYMENT', 'reference_number' => $purchase->purchase_no,
                'reference_id' => $purchase->id, 'description' => 'Payment made with purchase',
                'debit' => $purchase->paid_total, 'credit' => 0, 'journal_entry_id' => $journal->id,
            ]);
        }
        return $journal;
    }

    public function paymentAccount(string $method): string
    {
        return in_array(strtolower($method), ['bank', 'card', 'transfer'], true) ? self::BANK : self::CASH;
    }

    public function postSalesReturn(SalesReturn $return): JournalEntry
    {
        $cost = '0.00';
        foreach ($return->items as $item) {
            $cost = bcadd($cost, bcmul((string) $item->base_quantity, (string) $item->cost_price_snapshot, 4), 2);
        }
        $lines = [
            ['account_code' => self::SALES, 'debit' => $return->taxable_total, 'credit' => 0],
            ['account_code' => self::RECEIVABLE, 'party_type' => 'customer', 'party_id' => $return->customer_id, 'debit' => 0, 'credit' => $return->grand_total],
        ];
        if (bccomp((string) $return->vat_total, '0', 2) === 1) {
            $lines[] = ['account_code' => self::OUTPUT_VAT, 'debit' => $return->vat_total, 'credit' => 0];
        }
        if (bccomp($cost, '0', 2) === 1) {
            $lines[] = ['account_code' => self::INVENTORY, 'debit' => $cost, 'credit' => 0];
            $lines[] = ['account_code' => self::COGS, 'debit' => 0, 'credit' => $cost];
        }
        $journal = $this->postJournal([
            'entry_date' => $return->return_date, 'reference_type' => 'sales_return',
            'reference_id' => $return->id, 'reference_number' => $return->return_no,
            'description' => 'Sales return '.$return->return_no,
            'branch_id' => $return->branch_id, 'financial_year_id' => $return->financial_year_id,
        ], $lines);
        CustomerLedger::create([
            'customer_id' => $return->customer_id, 'transaction_date' => $return->return_date,
            'transaction_type' => 'SALE_RETURN', 'reference_number' => $return->return_no,
            'reference_id' => $return->id, 'description' => 'Sales return / credit note',
            'debit' => 0, 'credit' => $return->grand_total, 'journal_entry_id' => $journal->id,
        ]);
        return $journal;
    }

    public function postPurchaseReturn(PurchaseReturn $return): JournalEntry
    {
        $lines = [
            ['account_code' => self::PAYABLE, 'party_type' => 'supplier', 'party_id' => $return->supplier_id, 'debit' => $return->grand_total, 'credit' => 0],
            ['account_code' => self::INVENTORY, 'debit' => 0, 'credit' => $return->taxable_total],
        ];
        if (bccomp((string) $return->vat_total, '0', 2) === 1) {
            $lines[] = ['account_code' => self::INPUT_VAT, 'debit' => 0, 'credit' => $return->vat_total];
        }
        $journal = $this->postJournal([
            'entry_date' => $return->return_date, 'reference_type' => 'purchase_return',
            'reference_id' => $return->id, 'reference_number' => $return->return_no,
            'description' => 'Purchase return '.$return->return_no,
            'branch_id' => $return->branch_id, 'financial_year_id' => $return->financial_year_id,
        ], $lines);
        SupplierLedger::create([
            'supplier_id' => $return->supplier_id, 'transaction_date' => $return->return_date,
            'transaction_type' => 'PURCHASE_RETURN', 'reference_number' => $return->return_no,
            'reference_id' => $return->id, 'description' => 'Purchase return',
            'debit' => $return->grand_total, 'credit' => 0, 'journal_entry_id' => $journal->id,
        ]);
        return $journal;
    }
}
