<?php

namespace App\Imports;

use App\Helpers\NumberHelper;
use App\Models\Account;
use App\Models\OpeningBalance;
use App\Services\OpeningBalanceService;
use App\Support\QuickBooksChartOfAccounts;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;

class ChartOfAccountsImport implements ToCollection, WithHeadingRow
{
    public int $created = 0;
    public int $updated = 0;
    public int $skipped = 0;
    public int $balancesSaved = 0;
    public array $errors = [];

    private array $codeMap = [];

    /** @var array<int, array{entity_id: int, balance: float}> */
    private array $balanceRows = [];

    public function __construct(private readonly int $companyId)
    {
    }

    public function collection(Collection $rows): void
    {
        $this->codeMap = Account::withoutGlobalScopes()
            ->where('company_id', $this->companyId)
            ->whereNull('deleted_at')
            ->whereNotNull('code')
            ->pluck('id', 'code')
            ->all();

        foreach ($rows as $offset => $row) {
            $this->importRow($this->normalizeRow($row), $offset + 2);
        }

        if ($this->balanceRows !== []) {
            try {
                $this->balancesSaved = $this->saveImportedBalances();
            } catch (\Throwable $e) {
                $this->errors[] = 'Opening balances: ' . $e->getMessage();
            }
        }
    }

    private function saveImportedBalances(): int
    {
        $importedIds = collect($this->balanceRows)->pluck('entity_id')->flip();

        $merged = OpeningBalance::withoutGlobalScopes()
            ->where('company_id', $this->companyId)
            ->where('entity_type', 'account')
            ->where('warehouse_id', 0)
            ->get()
            ->filter(fn (OpeningBalance $row) => !$importedIds->has($row->entity_id))
            ->map(fn (OpeningBalance $row) => [
                'entity_id' => $row->entity_id,
                'balance' => (float) $row->balance,
            ])
            ->values()
            ->all();

        $merged = array_merge($merged, $this->balanceRows);

        return app(OpeningBalanceService::class)->saveAccounts($this->companyId, $merged);
    }

    private function importRow(array $row, int $line): void
    {
        $code = $this->clean($this->value($row, ['account_no', 'account_number', 'number', 'code']));
        $name = $this->clean($this->value($row, ['account_name', 'name']));

        if ($name === '') {
            $this->skipped++;
            return;
        }

        $classification = QuickBooksChartOfAccounts::classificationForImport(
            $this->clean($this->value($row, ['account_type', 'section', 'type'])),
            $this->clean($this->value($row, ['detail_type', 'detail']))
        );

        $parent = $this->parentAccount($this->clean($this->value($row, ['parent_account_no', 'parent_code', 'parent'])));
        $depth = $parent ? $parent->depth + 1 : 0;
        $path = $parent ? ($parent->path ? $parent->path . '/' . $parent->id : (string) $parent->id) : null;

        $account = $this->findAccount($code, $name);
        $isNew = !$account;
        $account ??= new Account();

        if ($isNew) {
            $account->company_id = $this->companyId;
            $account->is_system = false;
        }

        $account->fill([
            'parent_id' => $parent?->id,
            'code' => $code !== '' ? $code : null,
            'name' => $name,
            'type' => $classification['type'],
            'sub_type' => $classification['sub_type'],
            'detail_type' => $classification['detail_type'],
            'is_group' => $isNew ? false : $account->is_group,
            'is_active' => $this->activeValue($this->value($row, ['status', 'is_active', 'active'])),
            'is_bank' => $this->boolValue($this->value($row, ['bank', 'is_bank']), $classification['is_bank']),
            'is_cash' => $this->boolValue($this->value($row, ['cash', 'is_cash']), $classification['is_cash']),
            'description' => $this->clean($this->value($row, ['description', 'notes'])) ?: null,
            'depth' => $depth,
            'path' => $path,
        ]);

        try {
            $account->save();
            if ($account->code) {
                $this->codeMap[$account->code] = $account->id;
            }

            $balance = $this->parseSignedBalance(
                $this->value($row, ['balance', 'opening_balance']),
                $account->type,
            );
            if ($balance !== null && $this->canImportBalance($account)) {
                $this->balanceRows[] = [
                    'entity_id' => $account->id,
                    'balance' => $balance,
                ];
            }

            $isNew ? $this->created++ : $this->updated++;
        } catch (\Throwable $e) {
            $this->errors[] = "Row {$line}: {$e->getMessage()}";
        }
    }

    private function findAccount(string $code, string $name): ?Account
    {
        $query = Account::withoutGlobalScopes()
            ->where('company_id', $this->companyId)
            ->whereNull('deleted_at');

        if ($code !== '') {
            return (clone $query)->where('code', $code)->first();
        }

        return $query->where('name', $name)->first();
    }

    private function parentAccount(string $parentCode): ?Account
    {
        if ($parentCode === '') {
            return null;
        }

        $parentId = $this->codeMap[$parentCode] ?? null;

        return $parentId ? Account::withoutGlobalScopes()->find($parentId) : null;
    }

    private function canImportBalance(Account $account): bool
    {
        if ($account->is_group) {
            return false;
        }

        $section = QuickBooksChartOfAccounts::sectionForAccount($account);

        if (in_array($section, ['income', 'other_income', 'cost_of_goods_sold', 'expenses', 'other_expense'], true)) {
            return false;
        }

        return $account->code !== '3010' && $account->detail_type !== 'Opening Balance Equity';
    }

    private function parseSignedBalance(mixed $value, string $accountType): ?float
    {
        $raw = $this->clean($value);
        if ($raw === '' || $raw === '—') {
            return null;
        }

        $suffix = null;
        if (preg_match('/\s*(dr|cr)\s*$/i', $raw, $matches)) {
            $suffix = strtolower($matches[1]);
            $raw = trim(preg_replace('/\s*(dr|cr)\s*$/i', '', $raw));
        }

        if ($raw === '' || $raw === '-') {
            return null;
        }

        $parsed = NumberHelper::parseAmount($raw, $this->companyId);
        if ($parsed === null) {
            return null;
        }

        if (abs($parsed) < 0.005) {
            return 0.0;
        }

        if ($parsed < 0) {
            return $parsed;
        }

        $amount = abs($parsed);

        if ($suffix === null) {
            return $amount;
        }

        $creditNormal = in_array($accountType, ['liability', 'equity', 'income'], true);
        $normalSuffix = $creditNormal ? 'cr' : 'dr';

        return $suffix === $normalSuffix ? $amount : -$amount;
    }

    private function normalizeRow($row): array
    {
        return collect($row)->mapWithKeys(fn ($value, $key) => [
            strtolower((string) $key) => $value,
        ])->all();
    }

    private function value(array $row, array $keys): mixed
    {
        foreach ($keys as $key) {
            if (array_key_exists($key, $row)) {
                return $row[$key];
            }
        }

        return null;
    }

    private function clean(mixed $value): string
    {
        return trim((string) $value);
    }

    private function activeValue(mixed $value): bool
    {
        $value = strtolower($this->clean($value));

        return !in_array($value, ['0', 'no', 'false', 'inactive'], true);
    }

    private function boolValue(mixed $value, bool $default): bool
    {
        $value = strtolower($this->clean($value));

        if ($value === '') {
            return $default;
        }

        return in_array($value, ['1', 'yes', 'true', 'active', 'y'], true);
    }
}
