<?php

namespace App\Services;

use App\Models\Account;
use App\Models\Customer;
use App\Models\Item;
use App\Models\OpeningBalance;
use App\Models\Voucher;
use App\Models\Vendor;
use App\Models\VoucherLine;
use App\Support\OpeningBalanceRules;
use Illuminate\Support\Facades\DB;
use RuntimeException;

class OpeningBalancePostingService
{
    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function postAccounts(int $companyId, array $rows, string $asOfDate): ?int
    {
        $this->reverseExistingBatch($companyId, 'account', 0);

        $accountIds = collect($rows)->pluck('entity_id')->map(fn ($id) => (int) $id)->unique();
        $accounts = Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('id', $accountIds)
            ->get()
            ->keyBy('id');

        $journalLines = [];
        $postingMeta = [];

        foreach ($rows as $row) {
            $account = $accounts->get((int) ($row['entity_id'] ?? 0));
            if (!$account || $this->isOpeningBalanceEquity($account)) {
                continue;
            }

            $posting = OpeningBalanceRules::forAccountType($account->type, (float) ($row['balance'] ?? 0));
            if (!$posting) {
                continue;
            }

            $journalLines[] = [
                'account_id' => $account->id,
                'dr_cr' => $posting['dr_cr'],
                'amount' => $posting['amount'],
                'description' => "Opening balance: {$account->code} {$account->name}",
            ];

            $postingMeta[$account->id] = $posting;
        }

        return $this->finalizeBatch(
            $companyId,
            'account',
            0,
            $journalLines,
            $postingMeta,
            $asOfDate,
            'Opening balances — Accounts'
        );
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function postCustomers(int $companyId, array $rows, string $asOfDate): ?int
    {
        $this->reverseExistingBatch($companyId, 'customer', 0);

        $customerIds = collect($rows)->pluck('entity_id')->map(fn ($id) => (int) $id)->unique();
        $customers = Customer::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('id', $customerIds)
            ->get()
            ->keyBy('id');

        $journalLines = [];
        $postingMeta = [];

        foreach ($rows as $row) {
            $customer = $customers->get((int) ($row['entity_id'] ?? 0));
            if (!$customer) {
                continue;
            }

            $posting = OpeningBalanceRules::forCustomer((float) ($row['balance'] ?? 0));
            if (!$posting) {
                continue;
            }

            $arAccount = $this->resolveArAccount($customer, $companyId);

            $journalLines[] = [
                'account_id' => $arAccount->id,
                'dr_cr' => $posting['dr_cr'],
                'amount' => $posting['amount'],
                'description' => "Opening balance: Customer {$customer->name}",
                'party_id' => $customer->id,
                'party_type' => 'customer',
            ];

            $postingMeta[$customer->id] = $posting;
        }

        return $this->finalizeBatch(
            $companyId,
            'customer',
            0,
            $journalLines,
            $postingMeta,
            $asOfDate,
            'Opening balances — Customers'
        );
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function postVendors(int $companyId, array $rows, string $asOfDate): ?int
    {
        $this->reverseExistingBatch($companyId, 'vendor', 0);

        $vendorIds = collect($rows)->pluck('entity_id')->map(fn ($id) => (int) $id)->unique();
        $vendors = Vendor::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('id', $vendorIds)
            ->get()
            ->keyBy('id');

        $journalLines = [];
        $postingMeta = [];

        foreach ($rows as $row) {
            $vendor = $vendors->get((int) ($row['entity_id'] ?? 0));
            if (!$vendor) {
                continue;
            }

            $posting = OpeningBalanceRules::forVendor((float) ($row['balance'] ?? 0));
            if (!$posting) {
                continue;
            }

            $apAccount = $this->resolveApAccount($vendor, $companyId);

            $journalLines[] = [
                'account_id' => $apAccount->id,
                'dr_cr' => $posting['dr_cr'],
                'amount' => $posting['amount'],
                'description' => "Opening balance: Vendor {$vendor->name}",
                'party_id' => $vendor->id,
                'party_type' => 'vendor',
            ];

            $postingMeta[$vendor->id] = $posting;
        }

        return $this->finalizeBatch(
            $companyId,
            'vendor',
            0,
            $journalLines,
            $postingMeta,
            $asOfDate,
            'Opening balances — Vendors'
        );
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function postItems(int $companyId, array $rows, string $asOfDate, int $warehouseId): ?int
    {
        $this->reverseExistingBatch($companyId, 'item', $warehouseId);

        $itemIds = collect($rows)->pluck('entity_id')->map(fn ($id) => (int) $id)->unique();
        $items = Item::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('id', $itemIds)
            ->get()
            ->keyBy('id');

        $journalLines = [];
        $postingMeta = [];

        foreach ($rows as $row) {
            $item = $items->get((int) ($row['entity_id'] ?? 0));
            if (!$item) {
                continue;
            }

            $quantity = (float) ($row['quantity'] ?? 0);
            $rate = (float) ($row['rate'] ?? $row['unit_cost'] ?? 0);
            $amount = round($quantity * $rate, 2);

            $posting = OpeningBalanceRules::forItem($amount);
            if (!$posting) {
                continue;
            }

            $inventoryAccount = $this->resolveInventoryAccount($item, $companyId);

            $journalLines[] = [
                'account_id' => $inventoryAccount->id,
                'dr_cr' => $posting['dr_cr'],
                'amount' => $posting['amount'],
                'description' => "Opening balance: Item {$item->code} {$item->name}",
            ];

            $postingMeta[$item->id] = $posting;
        }

        return $this->finalizeBatch(
            $companyId,
            'item',
            $warehouseId,
            $journalLines,
            $postingMeta,
            $asOfDate,
            'Opening balances — Items'
        );
    }

    public function reverseExistingBatch(int $companyId, string $entityType, int $warehouseId = 0): void
    {
        $voucherIds = OpeningBalance::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('entity_type', $entityType)
            ->where('warehouse_id', $warehouseId)
            ->whereNotNull('voucher_id')
            ->distinct()
            ->pluck('voucher_id');

        foreach ($voucherIds as $voucherId) {
            $voucher = Voucher::withoutGlobalScopes()->find($voucherId);
            if (!$voucher || !$voucher->isPosted()) {
                continue;
            }

            if (!empty($voucher->extra_data['reversed_by'])) {
                continue;
            }

            $reversal = VoucherPostingService::reverse($voucher, 'Opening balance updated', now()->toDateString());
            VoucherPostingService::post($reversal);
        }
    }

    private function finalizeBatch(
        int $companyId,
        string $entityType,
        int $warehouseId,
        array $journalLines,
        array $postingMeta,
        string $asOfDate,
        string $narration
    ): ?int {
        if (empty($journalLines)) {
            return null;
        }

        $equityAccount = $this->resolveOpeningBalanceEquityAccount($companyId);
        $balancedLines = $this->withEquityOffset($journalLines, $equityAccount->id);

        $voucherId = $this->createAndPostJournal(
            $companyId,
            $balancedLines,
            $asOfDate,
            $entityType,
            $warehouseId,
            $narration
        );

        $this->linkVoucherToRows($companyId, $entityType, $warehouseId, $postingMeta, $voucherId);

        return $voucherId;
    }

    /**
     * @param  array<int, array<string, mixed>>  $journalLines
     * @return array<int, array<string, mixed>>
     */
    private function withEquityOffset(array $journalLines, int $equityAccountId): array
    {
        $totalDebit = collect($journalLines)->where('dr_cr', 'dr')->sum('amount');
        $totalCredit = collect($journalLines)->where('dr_cr', 'cr')->sum('amount');

        $offset = OpeningBalanceRules::equityOffset((float) $totalDebit, (float) $totalCredit);
        if ($offset) {
            $journalLines[] = [
                'account_id' => $equityAccountId,
                'dr_cr' => $offset['dr_cr'],
                'amount' => $offset['amount'],
                'description' => 'Opening Balance Equity offset',
            ];
        }

        return $journalLines;
    }

    /**
     * @param  array<int, array<string, mixed>>  $journalLines
     */
    private function createAndPostJournal(
        int $companyId,
        array $journalLines,
        string $asOfDate,
        string $entityType,
        int $warehouseId,
        string $narration
    ): int {
        $fiscalYear = app(FiscalYearService::class)->requireOpenForDate($companyId, $asOfDate);
        app(BooksClosingService::class)->assertCanTransact($companyId, $asOfDate);

        $voucherNumber = VoucherNumberService::generate($companyId, 'journal', new \DateTime($asOfDate));

        $voucher = Voucher::withoutGlobalScopes()->create([
            'company_id' => $companyId,
            'fiscal_year_id' => $fiscalYear->id,
            'voucher_type' => 'journal',
            'voucher_number' => $voucherNumber,
            'voucher_date' => $asOfDate,
            'reference' => 'OB-' . strtoupper($entityType),
            'narration' => $narration,
            'status' => 'draft',
            'created_by' => auth()->id(),
            'extra_data' => [
                'source' => 'opening_balance',
                'entity_type' => $entityType,
                'warehouse_id' => $warehouseId,
            ],
        ]);

        $lineNumber = 1;
        foreach ($journalLines as $line) {
            VoucherLine::withoutGlobalScopes()->create([
                'company_id' => $companyId,
                'voucher_id' => $voucher->id,
                'line_number' => $lineNumber++,
                'account_id' => $line['account_id'],
                'description' => $line['description'] ?? $narration,
                'line_total' => $line['amount'],
                'dr_cr' => $line['dr_cr'],
            ]);
        }

        $total = collect($journalLines)->sum('amount');
        $voucher->update([
            'total_amount' => $total,
            'grand_total' => $total,
        ]);

        VoucherPostingService::post($voucher);

        return $voucher->id;
    }

    /**
     * @param  array<int, array{dr_cr: string, amount: float}>  $postingMeta
     */
    private function linkVoucherToRows(
        int $companyId,
        string $entityType,
        int $warehouseId,
        array $postingMeta,
        int $voucherId
    ): void {
        foreach ($postingMeta as $entityId => $meta) {
            OpeningBalance::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('entity_type', $entityType)
                ->where('entity_id', $entityId)
                ->where('warehouse_id', $warehouseId)
                ->update([
                    'voucher_id' => $voucherId,
                    'dr_cr' => $meta['dr_cr'],
                    'posted_amount' => $meta['amount'],
                ]);
        }
    }

    private function resolveOpeningBalanceEquityAccount(int $companyId): Account
    {
        $account = Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_group', false)
            ->where(function ($query) {
                $query->where('code', '3010')
                    ->orWhere('detail_type', 'Opening Balance Equity');
            })
            ->first();

        if (!$account) {
            throw new RuntimeException(
                'Opening Balance Equity account (code 3010) was not found. Please seed or create the chart of accounts first.'
            );
        }

        return $account;
    }

    private function resolveArAccount(Customer $customer, int $companyId): Account
    {
        if ($customer->account_receivable_id) {
            $account = Account::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('id', $customer->account_receivable_id)
                ->where('is_group', false)
                ->first();

            if ($account) {
                return $account;
            }
        }

        return $this->findSystemAccount($companyId, '1100', 'Accounts Receivable (A/R)', 'asset');
    }

    private function resolveApAccount(Vendor $vendor, int $companyId): Account
    {
        if ($vendor->account_payable_id) {
            $account = Account::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('id', $vendor->account_payable_id)
                ->where('is_group', false)
                ->first();

            if ($account) {
                return $account;
            }
        }

        return $this->findSystemAccount($companyId, '2010', 'Accounts Payable (A/P)', 'liability');
    }

    private function resolveInventoryAccount(Item $item, int $companyId): Account
    {
        if ($item->inventory_account_id) {
            $account = Account::withoutGlobalScopes()
                ->where('company_id', $companyId)
                ->where('id', $item->inventory_account_id)
                ->where('is_group', false)
                ->first();

            if ($account) {
                return $account;
            }
        }

        return $this->findSystemAccount($companyId, '1200', 'Inventory', 'asset');
    }

    private function findSystemAccount(int $companyId, string $code, string $detailType, string $type): Account
    {
        $account = Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('is_group', false)
            ->where(function ($query) use ($code, $detailType) {
                $query->where('code', $code)->orWhere('detail_type', $detailType);
            })
            ->first();

        if (!$account) {
            throw new RuntimeException("Required {$type} account ({$code} / {$detailType}) was not found in the chart of accounts.");
        }

        return $account;
    }

    private function isOpeningBalanceEquity(Account $account): bool
    {
        return $account->code === '3010' || $account->detail_type === 'Opening Balance Equity';
    }
}
