<?php

namespace App\Services;

use App\Models\Account;
use App\Models\Customer;
use App\Models\Item;
use App\Models\OpeningBalance;
use App\Models\Vendor;
use App\Support\OpeningBalanceRules;
use App\Support\QuickBooksChartOfAccounts;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class OpeningBalanceService
{
    public function __construct(private readonly OpeningBalancePostingService $posting)
    {
    }

    /**
     * @param  array{section?: string|null}  $filters
     */
    public function accounts(int $companyId, array $filters = []): Collection
    {
        $sectionFilter = $filters['section'] ?? null;

        $accounts = Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereNull('deleted_at')
            ->where('is_group', false)
            ->orderBy('code')
            ->orderBy('name')
            ->get(['id', 'code', 'name', 'type', 'sub_type', 'detail_type', 'is_bank', 'is_cash']);

        $ledger = $this->ledgerNetBalances($companyId, $accounts->pluck('id'));
        $saved = $this->savedMap($companyId, 'account');
        $sections = QuickBooksChartOfAccounts::sections();

        return $accounts->map(function (Account $account) use ($ledger, $saved, $sections) {
            $section = QuickBooksChartOfAccounts::sectionForAccount($account);
            $account->qb_section = $section;
            $account->account_type_label = $sections[$section]['label'] ?? ucfirst((string) $account->type);
            $account->show_balance = !in_array($section, ['income', 'other_income', 'cost_of_goods_sold', 'expenses', 'other_expense'], true)
                && !$this->isOpeningBalanceEquity($account);
            $net = (float) ($ledger[$account->id] ?? 0);
            $account->current_balance = in_array($account->type, ['liability', 'equity', 'income'], true) ? -$net : $net;
            $row = $saved->get($account->id);
            $balance = (float) ($row->balance ?? 0);
            $account->opening_balance = $balance;
            $account->as_of_date = $row?->as_of_date?->format('Y-m-d');
            $account->opening_dr_cr = $row?->dr_cr ?? OpeningBalanceRules::forAccountType($account->type, $balance)['dr_cr'] ?? null;
            $account->posted_amount = (float) ($row->posted_amount ?? 0);

            return $account;
        })
            ->filter(fn (Account $account) => $account->show_balance)
            ->when($sectionFilter, fn (Collection $rows) => $rows->filter(
                fn (Account $account) => $account->qb_section === $sectionFilter
            ))
            ->values();
    }

    public function customers(int $companyId): Collection
    {
        $customers = Customer::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereNull('deleted_at')
            ->where('is_job', false)
            ->orderBy('name')
            ->get(['id', 'code', 'name']);

        $saved = $this->savedMap($companyId, 'customer');

        return $customers->map(function (Customer $customer) use ($saved) {
            $row = $saved->get($customer->id);
            $balance = (float) ($row->balance ?? 0);
            $customer->opening_balance = $balance;
            $customer->as_of_date = $row?->as_of_date?->format('Y-m-d');
            $customer->opening_dr_cr = $row?->dr_cr ?? OpeningBalanceRules::forCustomer($balance)['dr_cr'] ?? null;
            $customer->posted_amount = (float) ($row->posted_amount ?? 0);

            return $customer;
        });
    }

    public function vendors(int $companyId): Collection
    {
        $vendors = Vendor::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereNull('deleted_at')
            ->orderBy('name')
            ->get(['id', 'code', 'name']);

        $saved = $this->savedMap($companyId, 'vendor');

        return $vendors->map(function (Vendor $vendor) use ($saved) {
            $row = $saved->get($vendor->id);
            $balance = (float) ($row->balance ?? 0);
            $vendor->opening_balance = $balance;
            $vendor->as_of_date = $row?->as_of_date?->format('Y-m-d');
            $vendor->opening_dr_cr = $row?->dr_cr ?? OpeningBalanceRules::forVendor($balance)['dr_cr'] ?? null;
            $vendor->posted_amount = (float) ($row->posted_amount ?? 0);

            return $vendor;
        });
    }

    /**
     * @param  array{warehouse_id?: int|string|null, type?: string|null, category_id?: int|string|null}  $filters
     */
    public function items(int $companyId, array $filters = []): Collection
    {
        $warehouseId = (int) ($filters['warehouse_id'] ?? 0);
        if ($warehouseId <= 0) {
            return collect();
        }

        $query = Item::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereNull('deleted_at')
            ->with(['category:id,name']);

        if (!empty($filters['type'])) {
            $query->where('type', $filters['type']);
        }

        if (!empty($filters['category_id'])) {
            $query->where('category_id', (int) $filters['category_id']);
        }

        $items = $query
            ->orderBy('code')
            ->orderBy('name')
            ->get(['id', 'code', 'name', 'type', 'category_id', 'opening_stock', 'default_warehouse_id']);

        $saved = $this->savedItemMap($companyId, $warehouseId);

        return $items->map(function (Item $item) use ($saved, $warehouseId) {
            $row = $saved->get($item->id);
            $fallbackQty = ($item->type === 'inventory' && (int) $item->default_warehouse_id === $warehouseId)
                ? (float) ($item->opening_stock ?? 0)
                : 0;

            $item->opening_quantity = (float) ($row->quantity ?? $fallbackQty);
            $item->opening_rate = (float) ($row->unit_cost ?? 0);
            $item->opening_amount = $item->opening_quantity * $item->opening_rate;
            $item->as_of_date = $row?->as_of_date?->format('Y-m-d');
            $item->opening_dr_cr = $row?->dr_cr ?? OpeningBalanceRules::forItem($item->opening_amount)['dr_cr'] ?? null;
            $item->posted_amount = (float) ($row->posted_amount ?? 0);

            return $item;
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function saveAccounts(int $companyId, array $rows, ?string $asOfDate = null): int
    {
        $date = $asOfDate ?: now()->toDateString();
        $accounts = Account::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('id', collect($rows)->pluck('entity_id')->map(fn ($id) => (int) $id))
            ->get()
            ->keyBy('id');

        return DB::transaction(function () use ($companyId, $rows, $date, $accounts) {
            $count = $this->saveRows($companyId, 'account', $rows, $date, function (array $row) use ($accounts) {
                $balance = (float) ($row['balance'] ?? 0);
                $posting = OpeningBalanceRules::forAccountType(
                    $accounts->get((int) $row['entity_id'])?->type ?? 'asset',
                    $balance
                );

                return [
                    'warehouse_id' => 0,
                    'balance' => $balance,
                    'quantity' => 0,
                    'unit_cost' => 0,
                    'dr_cr' => $posting['dr_cr'] ?? null,
                    'posted_amount' => $posting['amount'] ?? 0,
                ];
            });

            $this->posting->postAccounts($companyId, $rows, $date);

            return $count;
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function saveCustomers(int $companyId, array $rows, ?string $asOfDate = null): int
    {
        $date = $asOfDate ?: now()->toDateString();

        return DB::transaction(function () use ($companyId, $rows, $date) {
            $count = $this->saveRows($companyId, 'customer', $rows, $date, function (array $row) {
                $balance = (float) ($row['balance'] ?? 0);
                $posting = OpeningBalanceRules::forCustomer($balance);

                return [
                    'warehouse_id' => 0,
                    'balance' => $balance,
                    'quantity' => 0,
                    'unit_cost' => 0,
                    'dr_cr' => $posting['dr_cr'] ?? null,
                    'posted_amount' => $posting['amount'] ?? 0,
                ];
            });

            $this->posting->postCustomers($companyId, $rows, $date);

            return $count;
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function saveVendors(int $companyId, array $rows, ?string $asOfDate = null): int
    {
        $date = $asOfDate ?: now()->toDateString();

        return DB::transaction(function () use ($companyId, $rows, $date) {
            $count = $this->saveRows($companyId, 'vendor', $rows, $date, function (array $row) {
                $balance = (float) ($row['balance'] ?? 0);
                $posting = OpeningBalanceRules::forVendor($balance);

                return [
                    'warehouse_id' => 0,
                    'balance' => $balance,
                    'quantity' => 0,
                    'unit_cost' => 0,
                    'dr_cr' => $posting['dr_cr'] ?? null,
                    'posted_amount' => $posting['amount'] ?? 0,
                ];
            });

            $this->posting->postVendors($companyId, $rows, $date);

            return $count;
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    public function saveItems(int $companyId, array $rows, ?string $asOfDate = null): int
    {
        $date = $asOfDate ?: now()->toDateString();
        $warehouseId = (int) ($rows[0]['warehouse_id'] ?? 0);

        return DB::transaction(function () use ($companyId, $rows, $date, $warehouseId) {
            $count = $this->saveRows($companyId, 'item', $rows, $date, function (array $row) {
                $quantity = (float) ($row['quantity'] ?? 0);
                $rate = (float) ($row['rate'] ?? $row['unit_cost'] ?? 0);
                $amount = round($quantity * $rate, 2);
                $posting = OpeningBalanceRules::forItem($amount);

                return [
                    'warehouse_id' => (int) ($row['warehouse_id'] ?? 0),
                    'balance' => $amount,
                    'quantity' => $quantity,
                    'unit_cost' => $rate,
                    'dr_cr' => $posting['dr_cr'] ?? null,
                    'posted_amount' => $posting['amount'] ?? 0,
                ];
            });

            foreach ($rows as $row) {
                $entityId = (int) ($row['entity_id'] ?? 0);
                $rowWarehouseId = (int) ($row['warehouse_id'] ?? 0);
                if ($entityId <= 0 || $rowWarehouseId <= 0) {
                    continue;
                }

                $item = Item::withoutGlobalScopes()
                    ->where('company_id', $companyId)
                    ->where('id', $entityId)
                    ->first(['id', 'type', 'default_warehouse_id']);

                if ($item && $item->type === 'inventory' && (int) $item->default_warehouse_id === $rowWarehouseId) {
                    $item->update(['opening_stock' => (float) ($row['quantity'] ?? 0)]);
                }
            }

            if ($warehouseId > 0) {
                $this->posting->postItems($companyId, $rows, $date, $warehouseId);
            }

            return $count;
        });
    }

    private function savedMap(int $companyId, string $entityType): Collection
    {
        return OpeningBalance::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('entity_type', $entityType)
            ->where('warehouse_id', 0)
            ->get()
            ->keyBy('entity_id');
    }

    private function savedItemMap(int $companyId, int $warehouseId): Collection
    {
        return OpeningBalance::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('entity_type', 'item')
            ->where('warehouse_id', $warehouseId)
            ->get()
            ->keyBy('entity_id');
    }

    private function ledgerNetBalances(int $companyId, Collection $accountIds): Collection
    {
        if ($accountIds->isEmpty()) {
            return collect();
        }

        return DB::table('ledger_entries')
            ->where('company_id', $companyId)
            ->whereIn('account_id', $accountIds)
            ->select('account_id')
            ->selectRaw('COALESCE(SUM(debit),0) - COALESCE(SUM(credit),0) as net_balance')
            ->groupBy('account_id')
            ->pluck('net_balance', 'account_id');
    }

    /**
     * @param  array<int, array<string, mixed>>  $rows
     */
    private function saveRows(int $companyId, string $entityType, array $rows, ?string $asOfDate, callable $mapper): int
    {
        $count = 0;
        $date = $asOfDate ?: now()->toDateString();

        foreach ($rows as $row) {
            $entityId = (int) ($row['entity_id'] ?? 0);
            if ($entityId <= 0) {
                continue;
            }

            $values = $mapper($row);
            $warehouseId = (int) ($values['warehouse_id'] ?? 0);
            unset($values['warehouse_id']);

            OpeningBalance::withoutGlobalScopes()->updateOrCreate(
                [
                    'company_id' => $companyId,
                    'entity_type' => $entityType,
                    'entity_id' => $entityId,
                    'warehouse_id' => $warehouseId,
                ],
                array_merge($values, ['as_of_date' => $date])
            );

            $count++;
        }

        return $count;
    }

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