<?php

namespace App\Services\Accounting;

use App\Models\AccountingAccount;
use App\Models\AccountingVoucher;
use App\Models\Order;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;

class AccountingVoucherService
{
    public const ACCOUNTS = [
        'cash' => ['1000', 'Showroom Cash', 'asset'],
        'bank_card' => ['1010', 'Bank/Card Receivable', 'asset'],
        'bkash' => ['1020', 'bKash Receivable', 'asset'],
        'nagad' => ['1030', 'Nagad Receivable', 'asset'],
        'bangla_qr' => ['1040', 'Bangla QR Receivable', 'asset'],
        'bank_account' => ['1060', 'Bank Account', 'asset'],
        'sales_income' => ['4000', 'Sales Income', 'income'],
        'sales_return' => ['4010', 'Sales Return', 'income'],
        'inventory' => ['1200', 'Inventory', 'asset'],
        'cogs' => ['5000', 'Cost of Goods Sold', 'expense'],
        'loyalty_discount' => ['5100', 'Loyalty Discount / Point Redeem Expense', 'expense'],
        'supplier_payable' => ['2100', 'Supplier Payable', 'liability'],
        'purchase_cash' => ['1050', 'Purchase Cash/Bank', 'asset'],
        'gateway_charge' => ['5200', 'Payment Gateway/Bank Charge', 'expense'],
        'customer_advance_liability' => ['2200', 'Customer Advance Liability', 'liability'],
    ];

    public function post(array $data): AccountingVoucher
    {
        try {
            return DB::transaction(function () use ($data) {
            $fingerprint = $this->sourceFingerprint($data);
            if ($existingVoucher = $this->findExistingVoucher($data, $fingerprint, true)) {
                return $existingVoucher;
            }

            $lines = collect($data['lines'] ?? [])
                ->map(fn ($line) => [
                    'account_key' => $line['account_key'] ?? null,
                    'account_code' => $line['account_code'] ?? null,
                    'account_name' => $line['account_name'] ?? null,
                    'account_type' => $line['account_type'] ?? null,
                    'debit' => round((float) ($line['debit'] ?? 0), 8),
                    'credit' => round((float) ($line['credit'] ?? 0), 8),
                    'party_type' => $line['party_type'] ?? null,
                    'party_id' => $line['party_id'] ?? null,
                    'description' => $line['description'] ?? null,
                ])
                ->filter(fn ($line) => $line['debit'] > 0 || $line['credit'] > 0)
                ->values();

            if ($lines->count() < 2) {
                throw ValidationException::withMessages([
                    'voucher' => translate('Accounting voucher requires at least two lines.'),
                ]);
            }

            $totalDebit = round((float) $lines->sum('debit'), 8);
            $totalCredit = round((float) $lines->sum('credit'), 8);
            if (abs($totalDebit - $totalCredit) > 0.00001) {
                throw ValidationException::withMessages([
                    'voucher' => translate('Accounting voucher debit and credit must be equal.'),
                ]);
            }

            $voucherPayload = [
                'voucher_no' => $this->nextVoucherNo($data['source_type'] ?? 'voucher'),
                'source_type' => $data['source_type'] ?? 'manual',
                'source_id' => $data['source_id'] ?? null,
                'source_fingerprint' => $fingerprint,
                'source_reference' => $data['source_reference'] ?? null,
                'sale_channel' => $data['sale_channel'] ?? null,
                'status' => $data['status'] ?? 'posted',
                'voucher_date' => $data['voucher_date'] ?? now()->toDateString(),
                'total_debit' => $totalDebit,
                'total_credit' => $totalCredit,
                'note' => $data['note'] ?? null,
                'created_by_admin_id' => $data['created_by_admin_id'] ?? null,
            ];
            if (! Schema::hasColumn('accounting_vouchers', 'source_fingerprint')) {
                unset($voucherPayload['source_fingerprint']);
            }

            $voucher = AccountingVoucher::create($voucherPayload);

            foreach ($lines as $line) {
                $account = $this->accountForLine($line);
                $voucher->lines()->create([
                    'account_id' => $account->id,
                    'debit' => $line['debit'],
                    'credit' => $line['credit'],
                    'party_type' => $line['party_type'],
                    'party_id' => $line['party_id'],
                    'description' => $line['description'],
                ]);
            }

            return $voucher->fresh('lines.account');
        });
        } catch (QueryException $exception) {
            if ($existingVoucher = $this->findExistingVoucher($data, $this->sourceFingerprint($data))) {
                return $existingVoucher;
            }

            throw $exception;
        }
    }

    public function postShowroomSale(Order $order, array $payments, float $cogs = 0, float $loyaltyDiscount = 0, ?int $adminId = null): AccountingVoucher
    {
        $lines = [];
        foreach ($payments as $payment) {
            $method = $payment['payment_method'] ?? $payment['method'] ?? 'cash';
            $lines[] = [
                'account_key' => $this->paymentAccountKey($method),
                'debit' => (float) ($payment['amount'] ?? 0),
                'description' => 'Showroom sale collection via ' . $method,
            ];
        }

        if ($loyaltyDiscount > 0) {
            $lines[] = [
                'account_key' => 'loyalty_discount',
                'debit' => $loyaltyDiscount,
                'description' => 'Point redeem discount',
            ];
        }

        $lines[] = [
            'account_key' => 'sales_income',
            'credit' => (float) $order->order_amount + $loyaltyDiscount,
            'description' => 'Showroom POS sale',
        ];

        if ($cogs > 0) {
            $lines[] = ['account_key' => 'cogs', 'debit' => $cogs, 'description' => 'Showroom POS COGS'];
            $lines[] = ['account_key' => 'inventory', 'credit' => $cogs, 'description' => 'Inventory out'];
        }

        return $this->post([
            'source_type' => 'showroom_sale',
            'source_id' => $order->id,
            'source_reference' => 'POS-' . $order->id,
            'sale_channel' => 'showroom',
            'created_by_admin_id' => $adminId,
            'lines' => $lines,
        ]);
    }

    public function postWebsiteSale(Order $order, array $payments, float $cogs = 0, ?int $adminId = null): AccountingVoucher
    {
        $lines = [];
        foreach ($payments as $payment) {
            $method = $payment['payment_method'] ?? $payment['method'] ?? $order->payment_method ?? 'digital_payment';
            $lines[] = [
                'account_key' => $this->paymentAccountKey((string) $method),
                'debit' => (float) ($payment['amount'] ?? 0),
                'description' => 'Website sale collection via ' . $method,
            ];
        }

        $lines[] = [
            'account_key' => 'sales_income',
            'credit' => (float) $order->order_amount,
            'description' => 'Website sale',
        ];

        if ($cogs > 0) {
            $lines[] = ['account_key' => 'cogs', 'debit' => $cogs, 'description' => 'Website sale COGS'];
            $lines[] = ['account_key' => 'inventory', 'credit' => $cogs, 'description' => 'Website inventory out'];
        }

        return $this->post([
            'source_type' => 'website_sale',
            'source_id' => $order->id,
            'source_reference' => 'ORDER-' . $order->id,
            'sale_channel' => 'website',
            'created_by_admin_id' => $adminId,
            'lines' => $lines,
        ]);
    }

    public function postPurchase(object $purchase, float $paidAmount, ?int $adminId = null): AccountingVoucher
    {
        $total = (float) $purchase->total_amount;
        $due = max($total - $paidAmount, 0);
        $lines = [
            ['account_key' => 'inventory', 'debit' => $total, 'description' => 'Purchase stock in'],
        ];

        if ($paidAmount > 0) {
            $lines[] = ['account_key' => 'purchase_cash', 'credit' => $paidAmount, 'description' => 'Purchase payment'];
        }
        if ($due > 0) {
            $lines[] = ['account_key' => 'supplier_payable', 'credit' => $due, 'description' => 'Supplier payable'];
        }

        return $this->post([
            'source_type' => 'purchase',
            'source_id' => $purchase->id,
            'source_reference' => $purchase->purchase_no,
            'sale_channel' => 'admin/manual',
            'created_by_admin_id' => $adminId,
            'lines' => $lines,
        ]);
    }

    public function postBanglaQrSettlement(object $settlement, ?int $adminId = null): AccountingVoucher
    {
        $gross = round((float) $settlement->gross_amount, 8);
        $charge = round((float) $settlement->charge_amount, 8);
        $net = round((float) $settlement->net_amount, 8);

        if ($gross <= 0 || $charge < 0 || $net < 0 || abs(($net + $charge) - $gross) > 0.00001) {
            throw ValidationException::withMessages([
                'settlement' => translate('Bangla QR settlement amount is not balanced.'),
            ]);
        }

        $lines = [
            ['account_key' => 'bank_account', 'debit' => $net, 'description' => 'Bangla QR settlement received in bank'],
            ['account_key' => 'bangla_qr', 'credit' => $gross, 'description' => 'Clear Bangla QR receivable'],
        ];

        if ($charge > 0) {
            $lines[] = ['account_key' => 'gateway_charge', 'debit' => $charge, 'description' => 'Bangla QR settlement charge'];
        }

        return $this->post([
            'source_type' => 'bangla_qr_settlement',
            'source_id' => $settlement->id,
            'source_reference' => $settlement->settlement_no,
            'sale_channel' => 'showroom',
            'created_by_admin_id' => $adminId,
            'lines' => $lines,
        ]);
    }

    private function sourceFingerprint(array $data): ?string
    {
        if (! array_key_exists('source_id', $data) || blank($data['source_id'])) {
            return null;
        }

        return implode(':', [
            $data['source_type'] ?? 'manual',
            $data['source_id'],
        ]);
    }

    private function findExistingVoucher(array $data, ?string $fingerprint, bool $lock = false): ?AccountingVoucher
    {
        if (! $fingerprint && (! array_key_exists('source_id', $data) || blank($data['source_id']))) {
            return null;
        }

        $query = AccountingVoucher::query()->with('lines.account');
        if ($fingerprint && Schema::hasColumn('accounting_vouchers', 'source_fingerprint')) {
            $query->where('source_fingerprint', $fingerprint);
        } else {
            $query->where('source_type', $data['source_type'] ?? 'manual')
                ->where('source_id', $data['source_id']);
        }

        if ($lock) {
            $query->lockForUpdate();
        }

        return $query->first();
    }

    public function paymentAccountKey(string $method): string
    {
        return match ($method) {
            'cash', 'cash_on_delivery' => 'cash',
            'bkash' => 'bkash',
            'nagad' => 'nagad',
            'bank', 'card', 'bank_card' => 'bank_card',
            'bangla_qr' => 'bangla_qr',
            'wallet', 'digital_payment', 'ssl_commerz', 'ssl_commerz_payment' => 'bank_card',
            default => 'bank_card',
        };
    }

    public function accountForKey(string $key): AccountingAccount
    {
        return $this->accountForLine(['account_key' => $key]);
    }

    public function accountForPaymentMethod(string $method): AccountingAccount
    {
        return $this->accountForKey($this->paymentAccountKey($method));
    }

    private function accountForLine(array $line): AccountingAccount
    {
        if (! empty($line['account_key']) && isset(self::ACCOUNTS[$line['account_key']])) {
            [$code, $name, $type] = self::ACCOUNTS[$line['account_key']];
        } else {
            $code = $line['account_code'] ?: str($line['account_name'] ?? 'manual')->slug('_')->upper()->toString();
            $name = $line['account_name'] ?: $code;
            $type = $line['account_type'] ?: 'asset';
        }

        return AccountingAccount::firstOrCreate(
            ['code' => $code],
            ['name' => $name, 'type' => $type, 'is_system' => true, 'is_active' => true]
        );
    }

    private function nextVoucherNo(string $sourceType): string
    {
        $prefix = strtoupper(substr(preg_replace('/[^A-Za-z0-9]/', '', $sourceType), 0, 6)) ?: 'VCH';

        return $prefix . '-' . now()->format('ymdHis') . '-' . random_int(1000, 9999);
    }
}
