<?php

namespace App\Services\QuickBooks;

use App\Models\AuditLog;
use App\Models\Voucher;
use App\Services\BooksClosingService;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;

class QuickBooksJournalBulkImporter
{
    private const CHUNK_SIZE = 150;

    public function __construct(
        private readonly int $companyId,
        private readonly int $userId,
        private readonly QuickBooksReferenceRegistry $registry,
    ) {
    }

    /**
     * @param array<int, array<string, mixed>> $transactions
     */
    public function import(array $transactions): array
    {
        DB::connection()->disableQueryLog();

        $imported = 0;
        $skipped = 0;
        $errors = [];
        $existing = $this->existingQuickBooksNumbers();

        foreach (array_chunk($transactions, self::CHUNK_SIZE) as $chunk) {
            DB::transaction(function () use ($chunk, $existing, &$imported, &$skipped, &$errors) {
                foreach ($chunk as $transaction) {
                    $voucherNumber = (string) $transaction['trans_no'];

                    if (isset($existing[$voucherNumber]) || isset($existing['QBJ-' . $voucherNumber])) {
                        $skipped++;
                        continue;
                    }

                    try {
                        $this->importTransaction($transaction, $voucherNumber);
                        $existing[$voucherNumber] = true;
                        $existing['QBJ-' . $voucherNumber] = true;
                        $imported++;
                    } catch (\Throwable $e) {
                        $errors[] = "Transaction {$transaction['trans_no']}: {$e->getMessage()}";
                    }
                }
            });
        }

        if ($imported > 0) {
            AuditLog::record('import', 'vouchers', null, null, [
                'imported' => $imported,
                'skipped' => $skipped,
                'errors' => count($errors),
                'source' => 'quickbooks_journal',
            ], 'QuickBooks journal data imported');
        }

        return compact('imported', 'skipped', 'errors');
    }

    /**
     * @param array<string, mixed> $transaction
     */
    private function importTransaction(array $transaction, string $voucherNumber): void
    {
        $referenceNo = trim((string) ($transaction['reference_no'] ?? ''));
        $lines = [];
        $totalDebit = 0.0;
        $totalCredit = 0.0;

        foreach ($transaction['lines'] as $line) {
            $accountId = $this->registry->accountId($line['account']);
            if (!$accountId) {
                throw new \RuntimeException("Account '{$line['account']}' is not available.");
            }

            $debit = (float) $line['debit'];
            $credit = (float) $line['credit'];

            if ($debit <= 0 && $credit <= 0) {
                continue;
            }

            $lines[] = [
                'account_id' => $accountId,
                'description' => $this->formatMemo($line['memo'] ?? null, $referenceNo),
                'line_total' => $debit > 0 ? $debit : $credit,
                'dr_cr' => $debit > 0 ? 'dr' : 'cr',
            ];

            $totalDebit += $debit;
            $totalCredit += $credit;
        }

        if (count($lines) < 2) {
            throw new \RuntimeException('Journal entry must have at least two lines.');
        }

        if (abs($totalDebit - $totalCredit) > 0.02) {
            throw new \RuntimeException('Journal entry is not balanced.');
        }

        $date = $transaction['date'] instanceof Carbon
            ? $transaction['date']
            : ($transaction['date'] ? Carbon::parse($transaction['date']) : now());

        app(BooksClosingService::class)->assertCanTransact($this->companyId, $date);

        $fiscalYearId = $this->registry->fiscalYearId($date);
        if (!$fiscalYearId) {
            throw new \RuntimeException('No fiscal year is configured for this company.');
        }

        $party = null;
        $partyName = trim((string) ($transaction['party_name'] ?? ''));
        if ($partyName !== '') {
            $party = $this->registry->party($partyName);
        }

        $now = now();
        $reference = $referenceNo !== '' ? $referenceNo : 'QB Trans #' . $transaction['trans_no'];
        $narration = $this->formatMemo($transaction['narration'] ?? null, $referenceNo)
            ?: ($partyName !== '' ? $partyName : null);

        $voucherId = DB::table('vouchers')->insertGetId([
            'company_id' => $this->companyId,
            'fiscal_year_id' => $fiscalYearId,
            'voucher_type' => 'journal',
            'voucher_number' => $voucherNumber,
            'voucher_date' => $date->toDateString(),
            'reference' => $reference,
            'party_id' => $party['id'] ?? null,
            'party_type' => $party['type'] ?? null,
            'narration' => $narration,
            'total_amount' => $totalDebit,
            'grand_total' => $totalDebit,
            'status' => 'posted',
            'created_by' => $this->userId,
            'posted_by' => $this->userId,
            'posted_at' => $now,
            'extra_data' => json_encode(['quickbooks_trans_no' => $transaction['trans_no']]),
            'created_at' => $now,
            'updated_at' => $now,
        ]);

        $ledgerRows = [];
        foreach ($lines as $index => $line) {
            $lineId = DB::table('voucher_lines')->insertGetId([
                'company_id' => $this->companyId,
                'voucher_id' => $voucherId,
                'line_number' => $index + 1,
                'account_id' => $line['account_id'],
                'description' => $line['description'],
                'line_total' => $line['line_total'],
                'dr_cr' => $line['dr_cr'],
                'created_at' => $now,
                'updated_at' => $now,
            ]);

            $isDebit = $line['dr_cr'] === 'dr';
            $ledgerRows[] = [
                'company_id' => $this->companyId,
                'fiscal_year_id' => $fiscalYearId,
                'account_id' => $line['account_id'],
                'voucher_id' => $voucherId,
                'voucher_line_id' => $lineId,
                'entry_date' => $date->toDateString(),
                'debit' => $isDebit ? $line['line_total'] : 0,
                'credit' => $isDebit ? 0 : $line['line_total'],
                'reference' => $voucherNumber,
                'narration' => $line['description'],
                'party_id' => $party['id'] ?? null,
                'party_type' => $party['type'] ?? null,
                'created_at' => $now,
                'updated_at' => $now,
            ];
        }

        foreach (array_chunk($ledgerRows, 250) as $ledgerChunk) {
            DB::table('ledger_entries')->insert($ledgerChunk);
        }
    }

    /**
     * @return array<string, bool>
     */
    private function existingQuickBooksNumbers(): array
    {
        $existing = [];

        $vouchers = Voucher::withoutGlobalScopes()
            ->where('company_id', $this->companyId)
            ->where('voucher_type', 'journal')
            ->get(['voucher_number', 'extra_data']);

        foreach ($vouchers as $voucher) {
            if ($voucher->voucher_number) {
                $existing[$voucher->voucher_number] = true;
            }

            $extra = json_decode($voucher->extra_data ?? '{}', true);
            if (!empty($extra['quickbooks_trans_no'])) {
                $transNo = (string) $extra['quickbooks_trans_no'];
                $existing[$transNo] = true;
                $existing['QBJ-' . $transNo] = true;
            }
        }

        return $existing;
    }

    private function formatMemo(?string $memo, string $referenceNo): ?string
    {
        $memo = trim((string) $memo);
        $referenceNo = trim($referenceNo);

        if ($referenceNo === '') {
            return $memo !== '' ? $memo : null;
        }

        if ($memo === '') {
            return $referenceNo;
        }

        $prefix = $referenceNo . ' - ';
        if (str_starts_with(strtolower($memo), strtolower($prefix))) {
            return $memo;
        }

        return $prefix . $memo;
    }
}
