<?php

namespace App\Services\Customer;

use App\Models\Customer\Customer;
use App\Models\Sale\Sale;
use Illuminate\Support\Facades\DB;

/**
 * Müşteriye ödeme alınmadan borçlandırma (cari limit) kuralları.
 *
 * Tek doğruluk kaynağı burasıdır: müşteri kartı, müşteri listesi ve satış
 * kartı aynı hesabı okur. İki soruyu yanıtlar:
 *
 *   1) Bu müşterinin ŞU AN ne kadar açık borcu var? (kullanılan limit)
 *   2) Şu tutarda yeni bir borç doğarsa limit aşılır mı? (izin / uyarı)
 *
 * Borç sayılan satışlar: durumu `pending, confirmed, in_progress, completed`
 * olanların kalan tutarı. Potansiyel / teklif / revize satışlar henüz borç
 * doğurmaz; iptal ve iade edilenler de sayılmaz — aksi halde kapanmış bir
 * teklif müşterinin limitini boşuna doldururdu.
 *
 * Para birimi: limit kendi biriminde tutulur, satışlar başka birimde olabilir.
 * Karşılaştırma acentenin ayarlardaki kurlarıyla limit birimine çevrilerek
 * yapılır (SalesController::currencyRates ile aynı mantık).
 */
class CustomerCreditService
{
    /** Kalan tutarı gerçek borç sayılan satış durumları */
    public const DEBT_STATUSES = ['pending', 'confirmed', 'in_progress', 'completed'];

    /** Kur/sembol haritası acente başına bir kez okunur (liste sayfasında N+1 olmasın) */
    protected array $rateCache = [];
    protected array $symbolCache = [];
    protected array $agencyCache = [];

    /**
     * Ayarlardaki kurlara göre para birimi → TL çarpanı haritası (TRY = 1).
     * Pasif bırakılmış kurlar da dahildir; eski satışlar o birimde olabilir.
     */
    public function rates(int $agencyId): array
    {
        if (isset($this->rateCache[$agencyId])) {
            return $this->rateCache[$agencyId];
        }

        $agency = $this->agency($agencyId);
        if (!$agency) {
            return $this->rateCache[$agencyId] = ['TRY' => 1.0];
        }

        $agency->getActiveCurrencies(); // hiç kur tanımı yoksa varsayılanları oluşturur

        $rates = [];
        foreach ($agency->currencies()->get() as $c) {
            $rate = (float) $c->exchange_rate;
            $rates[strtoupper((string) $c->code)] = $rate > 0 ? $rate : 1.0;
        }
        $rates['TRY'] = 1.0;

        return $this->rateCache[$agencyId] = $rates;
    }

    /** Bir tutarı kaynak biriminden hedef birime çevirir (TL üzerinden). */
    public function convert(float $amount, ?string $from, ?string $to, int $agencyId): float
    {
        $rates = $this->rates($agencyId);
        $fromRate = $rates[strtoupper((string) ($from ?: 'TRY'))] ?? 1.0;
        $toRate = $rates[strtoupper((string) ($to ?: 'TRY'))] ?? 1.0;

        return $toRate > 0 ? ($amount * $fromRate) / $toRate : $amount * $fromRate;
    }

    /**
     * Müşterinin açık borcu — limitin para biriminde.
     *
     * @param int|null $excludeSaleId Düzenlenmekte olan satış hesaba katılmaz;
     *                                onun yeni hâli ayrıca eklenir.
     */
    public function openBalance(Customer $customer, ?int $excludeSaleId = null): float
    {
        $rows = Sale::where('agency_id', $customer->agency_id)
            ->where('customer_id', $customer->id)
            ->whereIn('status', self::DEBT_STATUSES)
            ->when($excludeSaleId, fn ($q) => $q->where('id', '!=', $excludeSaleId))
            ->select('currency', DB::raw('SUM(remaining_amount) as t'))
            ->groupBy('currency')
            ->get();

        $total = 0.0;
        foreach ($rows as $row) {
            $total += $this->convert((float) $row->t, $row->currency, $customer->credit_limit_currency, $customer->agency_id);
        }

        return round(max(0.0, $total), 2);
    }

    /**
     * Liste ekranı için toplu açık borç hesabı (müşteri id → tutar).
     * Tek sorgu; her müşteri kendi limit para biriminde döner.
     *
     * @param  \Illuminate\Support\Collection|array $customers
     * @return array<int, float>
     */
    public function openBalances($customers, int $agencyId): array
    {
        $customers = collect($customers);
        $ids = $customers->pluck('id')->all();
        if (!$ids) {
            return [];
        }

        $rows = Sale::where('agency_id', $agencyId)
            ->whereIn('customer_id', $ids)
            ->whereIn('status', self::DEBT_STATUSES)
            ->select('customer_id', 'currency', DB::raw('SUM(remaining_amount) as t'))
            ->groupBy('customer_id', 'currency')
            ->get();

        $limitCurrency = $customers->pluck('credit_limit_currency', 'id')->all();

        $out = array_fill_keys($ids, 0.0);
        foreach ($rows as $row) {
            $cid = (int) $row->customer_id;
            $out[$cid] = ($out[$cid] ?? 0.0) + $this->convert(
                (float) $row->t,
                $row->currency,
                $limitCurrency[$cid] ?? 'TRY',
                $agencyId
            );
        }

        return array_map(fn ($v) => round(max(0.0, $v), 2), $out);
    }

    /**
     * Müşteri kartı / satış kartı için hazır özet.
     *
     * @param float|null $used Zaten hesaplanmışsa yeniden sorgulanmaz (liste ekranı).
     */
    public function summary(Customer $customer, ?float $used = null, ?int $excludeSaleId = null): array
    {
        $currency = strtoupper((string) ($customer->credit_limit_currency ?: 'TRY'));
        $limit = round((float) $customer->credit_limit, 2);
        $allowed = (bool) $customer->credit_allowed;
        $used = $used === null ? $this->openBalance($customer, $excludeSaleId) : round($used, 2);

        // Limit 0 iken "sınırsız" demek tehlikelidir: açık ama limitsiz kayıtta
        // her tutar geçerdi. Bu yüzden 0 = borçlandırma yapılamaz kabul edilir.
        $available = $allowed ? round(max(0.0, $limit - $used), 2) : 0.0;

        return [
            'allowed' => $allowed,
            'limit' => $limit,
            'currency' => $currency,
            'symbol' => $this->symbol($currency, $customer->agency_id),
            'used' => $used,
            'available' => $available,
            'over' => $allowed && $used > $limit,
            'usage_rate' => $allowed && $limit > 0 ? min(100, round($used / $limit * 100)) : ($used > 0 ? 100 : 0),
            'term_days' => (int) $customer->payment_term_days,
            'note' => (string) ($customer->credit_note ?? ''),
        ];
    }

    /**
     * Yeni doğacak borç limite sığıyor mu?
     *
     * @param float  $debt     Ödeme alınmadan kalacak tutar (satışın kalanı)
     * @param string $currency Bu tutarın para birimi
     * @return array{ok: bool, code: string, message: string, summary: array, requested: float, after: float}
     */
    public function evaluate(Customer $customer, float $debt, ?string $currency = 'TRY', ?int $excludeSaleId = null): array
    {
        $summary = $this->summary($customer, null, $excludeSaleId);
        $debt = round(max(0.0, $this->convert($debt, $currency, $summary['currency'], $customer->agency_id)), 2);
        $after = round($summary['used'] + $debt, 2);
        $money = fn (float $v) => $summary['symbol'] . number_format($v, 2, ',', '.');

        if ($debt <= 0.0) {
            return $this->result(true, 'no_debt', '', $summary, $debt, $after);
        }

        if (!$summary['allowed']) {
            return $this->result(false, 'not_allowed',
                $customer->full_name . ' için ödeme alınmadan borçlandırma kapalı. '
                . 'Müşteri kartındaki "Cari / Borçlandırma" bölümünden borçlandırmayı açın '
                . 'ya da ' . $money($debt) . ' tutarında tahsilat girin.',
                $summary, $debt, $after);
        }

        if ($summary['limit'] <= 0.0) {
            return $this->result(false, 'no_limit',
                $customer->full_name . ' için kredi limiti tanımlanmamış. '
                . 'Müşteri kartından bir limit girin ya da ' . $money($debt) . ' tutarında tahsilat girin.',
                $summary, $debt, $after);
        }

        if ($after > $summary['limit']) {
            return $this->result(false, 'over_limit',
                $customer->full_name . ' kredi limitini aşıyor. '
                . 'Limit ' . $money($summary['limit']) . ', mevcut açık bakiye ' . $money($summary['used'])
                . ', bu satışla birlikte ' . $money($after) . ' olacak. '
                . 'Kullanılabilir limit: ' . $money($summary['available']) . '.',
                $summary, $debt, $after);
        }

        return $this->result(true, 'within_limit', '', $summary, $debt, $after);
    }

    /** Para birimi sembolü (acentede tanımlı değilse kodun kendisi). */
    public function symbol(string $currency, int $agencyId): string
    {
        $key = $agencyId . ':' . $currency;
        if (isset($this->symbolCache[$key])) {
            return $this->symbolCache[$key];
        }

        $found = $this->agency($agencyId)?->getActiveCurrencies()->firstWhere('code', $currency);

        return $this->symbolCache[$key] = ($found?->symbol ?: $currency);
    }

    /** Acente kaydı bir kez okunur (kur ve sembol aynı kayıttan gelir). */
    protected function agency(int $agencyId): ?\App\Models\Agency\Agency
    {
        if (!array_key_exists($agencyId, $this->agencyCache)) {
            $this->agencyCache[$agencyId] = \App\Models\Agency\Agency::find($agencyId);
        }

        return $this->agencyCache[$agencyId];
    }

    protected function result(bool $ok, string $code, string $message, array $summary, float $requested, float $after): array
    {
        return [
            'ok' => $ok,
            'code' => $code,
            'message' => $message,
            'summary' => $summary,
            'requested' => $requested,
            'after' => $after,
        ];
    }
}
