<?php

namespace App\Services;

use App\Helpers\NumberHelper;
use App\Models\ChequePrintQueueItem;
use App\Models\Setting;
use App\Models\Voucher;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class ChequePrintService
{
    public function __construct(
        private readonly LedgerReportService $ledgerReports,
    ) {
    }

    public function queuedVoucherIds(int $companyId): array
    {
        return ChequePrintQueueItem::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->pluck('voucher_id')
            ->map(fn ($id) => (int) $id)
            ->all();
    }

    public function queueItems(int $companyId): Collection
    {
        return $this->eligibleVouchers($companyId);
    }

    public function eligibleVouchers(int $companyId): Collection
    {
        return $this->eligibleVouchersQuery($companyId)
            ->with(['lines.account:id,code,name,is_bank,is_cash'])
            ->orderByDesc('voucher_date')
            ->orderByDesc('id')
            ->get()
            ->map(fn (Voucher $voucher) => $this->presentVoucher($voucher, $companyId));
    }

    public function availableVouchers(int $companyId): Collection
    {
        $queuedIds = $this->queuedVoucherIds($companyId);

        return $this->eligibleVouchersQuery($companyId)
            ->when($queuedIds !== [], fn (Builder $query) => $query->whereNotIn('id', $queuedIds))
            ->with(['lines.account:id,code,name,is_bank,is_cash'])
            ->orderByDesc('voucher_date')
            ->orderByDesc('id')
            ->get()
            ->map(fn (Voucher $voucher) => $this->presentVoucher($voucher, $companyId));
    }

    public function eligibleVouchersQuery(int $companyId): Builder
    {
        return Voucher::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('status', 'posted')
            ->whereIn('voucher_type', ['bank_payment', 'contra'])
            ->where(function (Builder $query) {
                $query->whereNotNull('extra_data->cheque_no')
                    ->whereRaw("TRIM(JSON_UNQUOTE(JSON_EXTRACT(extra_data, '$.cheque_no'))) <> ''");
            })
            ->where(function (Builder $query) {
                $query->whereNull('extra_data->cheque_printed_at')
                    ->orWhereRaw("TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(extra_data, '$.cheque_printed_at')), '')) = ''");
            })
            ->where(function (Builder $query) {
                $query->where('voucher_type', 'bank_payment')
                    ->orWhere(function (Builder $contra) {
                        $contra->where('voucher_type', 'contra')
                            ->whereExists(function ($sub) {
                                $sub->select(DB::raw(1))
                                    ->from('voucher_lines')
                                    ->join('accounts', 'accounts.id', '=', 'voucher_lines.account_id')
                                    ->whereColumn('voucher_lines.voucher_id', 'vouchers.id')
                                    ->where('voucher_lines.dr_cr', 'cr')
                                    ->where('accounts.is_bank', true);
                            });
                    });
            });
    }

    public function eligibleVoucherIds(int $companyId, array $voucherIds): array
    {
        if ($voucherIds === []) {
            return [];
        }

        return $this->eligibleVouchersQuery($companyId)
            ->whereIn('id', $voucherIds)
            ->pluck('id')
            ->map(fn ($id) => (int) $id)
            ->all();
    }

    public function addToQueue(int $companyId, array $voucherIds, ?int $userId = null): int
    {
        $eligibleIds = $this->eligibleVoucherIds($companyId, $voucherIds);

        $added = 0;
        foreach ($eligibleIds as $voucherId) {
            $item = ChequePrintQueueItem::withoutGlobalScopes()->firstOrCreate(
                ['company_id' => $companyId, 'voucher_id' => $voucherId],
                ['added_by' => $userId],
            );
            if ($item->wasRecentlyCreated) {
                $added++;
            }
        }

        return $added;
    }

    public function removeFromQueue(int $companyId, array $voucherIds): int
    {
        return ChequePrintQueueItem::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereIn('voucher_id', $voucherIds)
            ->delete();
    }

    public function markPrinted(Voucher $voucher): void
    {
        $extra = $voucher->extra_data ?? [];
        $extra['cheque_printed_at'] = now()->toIso8601String();
        $voucher->update(['extra_data' => $extra]);
    }

    public function presentVoucher(Voucher $voucher, int $companyId, bool $plainPayeeName = false): array
    {
        $extra = $voucher->extra_data ?? [];
        $payee = $this->resolvePayeeName($voucher, $companyId, $plainPayeeName);
        $amount = (float) ($voucher->grand_total ?: $voucher->total_amount ?: 0);

        return [
            'id' => $voucher->id,
            'voucher_type' => $voucher->voucher_type,
            'type_label' => $voucher->getTypeLabel(),
            'voucher_date' => $voucher->voucher_date?->format($this->dateFormat($companyId)) ?? '—',
            'voucher_number' => $voucher->voucher_number ?? '—',
            'payee' => $payee,
            'cheque_no' => $extra['cheque_no'] ?? '—',
            'cheque_date' => !empty($extra['cheque_date'])
                ? Carbon::parse($extra['cheque_date'])->format($this->dateFormat($companyId))
                : '—',
            'amount' => NumberHelper::formatAmount($amount, $companyId),
            'amount_raw' => $amount,
            'amount_words' => NumberHelper::amountInWordsForCompany($amount, $companyId),
            'print_date' => !empty($extra['cheque_date'])
                ? Carbon::parse($extra['cheque_date'])->format($this->dateFormat($companyId))
                : ($voucher->voucher_date?->format($this->dateFormat($companyId)) ?? ''),
        ];
    }

    /** @return array{0: string, 1: string} */
    public function splitAmountWords(string $words, int $lineLength = 42): array
    {
        $words = trim($words);
        if ($words === '') {
            return ['', ''];
        }

        if (strlen($words) <= $lineLength) {
            return [$words, ''];
        }

        $parts = preg_split('/\s+/', $words) ?: [];
        $line1 = '';
        $line2 = '';

        foreach ($parts as $part) {
            $candidate = $line1 === '' ? $part : $line1 . ' ' . $part;
            if (strlen($candidate) <= $lineLength || $line1 === '') {
                $line1 = $candidate;
            } else {
                $line2 = $line2 === '' ? $part : $line2 . ' ' . $part;
            }
        }

        return [$line1, $line2];
    }

    private function resolvePayeeName(Voucher $voucher, int $companyId, bool $plainPayeeName = false): string
    {
        $resolveParty = fn (string $partyType, int $partyId): ?string => $plainPayeeName
            ? $this->ledgerReports->profitLossPartyName($partyType . ':' . $partyId)
            : $this->ledgerReports->profitLossPartyLabel($partyType . ':' . $partyId);

        if ($voucher->party_id && $voucher->party_type) {
            $label = $resolveParty($voucher->party_type, $voucher->party_id);
            if ($label) {
                return $label;
            }
        }

        foreach ($voucher->lines as $line) {
            if ($line->party_id && $line->party_type) {
                $label = $resolveParty($line->party_type, $line->party_id);
                if ($label) {
                    return $label;
                }
            }
        }

        foreach ($voucher->lines as $line) {
            $account = $line->account;
            if ($account && !$account->is_bank && !$account->is_cash && $line->dr_cr === 'dr') {
                return trim(($account->code ? $account->code . ' ' : '') . $account->name);
            }
        }

        return $voucher->narration ?: '—';
    }

    private function dateFormat(int $companyId): string
    {
        return match ((string) Setting::get($companyId, 'date_format', 'DD/MM/YYYY')) {
            'MM/DD/YYYY' => 'm-d-Y',
            'YYYY-MM-DD' => 'Y-m-d',
            'DD-MM-YYYY' => 'd-m-Y',
            'DD.MM.YYYY' => 'd.m.Y',
            default => 'd/m/Y',
        };
    }
}
