<?php

namespace App\Services;

use App\Core\Billing\BillingManager;
use App\Jobs\SendEmailJob;
use App\Models\ClientDomain;
use App\Models\ClientProduct;
use App\Models\Currency;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Setting;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class DueInvoiceService
{
    protected BillingManager $billingManager;
    protected TaxCalculationService $taxService;
    protected InvoiceService $invoiceService;

    public function __construct(
        BillingManager $billingManager,
        TaxCalculationService $taxService,
        InvoiceService $invoiceService,
        protected \App\Services\DiscountManager $discountManager
    ) {
        $this->billingManager = $billingManager;
        $this->taxService = $taxService;
        $this->invoiceService = $invoiceService;
    }

    /**
     * Generate due invoices for a specific client
     */
    public function generateForClient(int $clientId, bool $sendEmail = true): array
    {
        $results = ['invoices_created' => 0, 'lines_appended' => 0, 'items_found' => 0, 'skipped' => 0, 'errors' => []];

        try {
            $items = $this->collectDueItems($clientId);
            $results['items_found'] = count($items);

            if (empty($items)) {
                return $results;
            }

            // PRC-370 / M2 — Cumulative routing. For each due item, try
            // to find an existing unpaid invoice for the same service;
            // if one exists, append a line + push due_date. Otherwise
            // batch by due_date and create a fresh invoice as before.
            $freshItems = [];
            foreach ($items as $item) {
                try {
                    $target = $this->invoiceService->findCumulativeTargetInvoice(
                        $item['related_type'],
                        $item['related_service_id']
                    );

                    if ($target !== null) {
                        $this->appendCycleLine($target, $item, $sendEmail);
                        $results['lines_appended']++;
                        continue;
                    }
                } catch (\Exception $e) {
                    $results['errors'][] = $e->getMessage();
                    Log::error('Cumulative line append failed', [
                        'client_id' => $clientId,
                        'item' => $item,
                        'error' => $e->getMessage(),
                    ]);
                    continue;
                }

                $freshItems[] = $item;
            }

            // Group items by due_date
            $grouped = $this->groupItemsByDueDate($freshItems);

            foreach ($grouped as $dueDate => $groupedItems) {
                try {
                    $invoice = $this->createInvoiceForItems($clientId, $dueDate, $groupedItems, $sendEmail);
                    if ($invoice) {
                        $results['invoices_created']++;
                    }
                } catch (\Exception $e) {
                    $results['errors'][] = $e->getMessage();
                    Log::error('Due invoice creation failed', [
                        'client_id' => $clientId,
                        'due_date' => $dueDate,
                        'error' => $e->getMessage(),
                    ]);

                    $this->fireServiceRenewalFailed($clientId, $e->getMessage());
                }
            }
        } catch (\Exception $e) {
            $results['errors'][] = $e->getMessage();
            Log::error('Due invoice generation failed for client', [
                'client_id' => $clientId,
                'error' => $e->getMessage(),
            ]);

            $this->fireServiceRenewalFailed($clientId, $e->getMessage());
        }

        return $results;
    }

    /**
     * PRC-622 — 'Service Renewal Failed' admin bildirimi
     * (config/communication_events.php eşlemesi).
     */
    private function fireServiceRenewalFailed(int $clientId, string $error): void
    {
        app(\App\Core\Hook\HookBridge::class)->fireAction('ServiceRenewalFailed', [
            'client_id' => $clientId,
            'user_id' => $clientId,
            'error' => $error,
        ]);
    }

    /**
     * PRC-628 — 'Service Renewal' client bildirimi. Yalnız RENEWAL context'inde
     * (next_due_date push'undan üretilen due-invoice) ve YALNIZ ürün (hosting)
     * hizmetleri için fire eder — domainlerin kendi renewal/uyarı şablonları var,
     * ilk satın alma bu yoldan geçmez (collectDueItems yalnız status=active +
     * next_due_date dolu hizmetleri toplar). 'Invoice Created/Due Generated'
     * fatura odaklıdır; bu hizmet odaklı yenileme bildirimidir, çakışmaz.
     * entity 'client_product' → şablonun {DOMAIN} merge field'ı doğal gelir.
     */
    private function fireServiceRenewalNotice(array $item): void
    {
        if (($item['type'] ?? null) !== 'product') {
            return;
        }

        $service = $item['service'] ?? null;
        if (! $service instanceof ClientProduct) {
            return;
        }

        try {
            SendEmailJob::dispatch('Service Renewal', 'client_product', $service->id, [
                'DOMAIN' => $service->domain ?? '',
            ]);
        } catch (\Exception $e) {
            Log::warning('PRC-628 Service Renewal notice dispatch failed', [
                'client_product_id' => $service->id,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Generate due invoices for all clients
     */
    public function generateForAllClients(bool $sendEmail = true): array
    {
        $totalResults = ['invoices_created' => 0, 'clients_processed' => 0, 'errors' => []];

        // Get all clients with active services or domains
        $clientIds = collect()
            ->merge(
                ClientProduct::where('status', 'active')
                    ->whereNotNull('next_due_date')
                    ->distinct()
                    ->pluck('client_id')
            )
            ->merge(
                ClientDomain::where('status', 'active')
                    ->whereNotNull('next_due_date')
                    ->distinct()
                    ->pluck('client_id')
            )
            ->unique()
            ->values();

        foreach ($clientIds as $clientId) {
            $result = $this->generateForClient($clientId, $sendEmail);
            $totalResults['invoices_created'] += $result['invoices_created'];
            $totalResults['clients_processed']++;

            if (!empty($result['errors'])) {
                $totalResults['errors'] = array_merge($totalResults['errors'], $result['errors']);
            }
        }

        return $totalResults;
    }

    /**
     * Collect all due items for a client
     */
    protected function collectDueItems(int $clientId): array
    {
        $items = [];
        $now = Carbon::now();

        // PRC-369 — Per-cycle lead-time. Each service has its own cutoff
        // based on its billing cycle (monthly=7d, yearly=30d, domain=45d
        // by default). We over-fetch using the MAX cycle lead-time then
        // filter per-row to apply the correct per-cycle cutoff.
        $termsTable = \App\Services\Billing\PaymentTermsResolver::table();
        $maxLead = max($termsTable);

        // Products
        $products = ClientProduct::where('client_id', $clientId)
            ->where('status', 'active')
            ->whereNotNull('next_due_date')
            ->where('next_due_date', '<=', $now->copy()->addDays($maxLead)->endOfDay())
            ->whereNotIn('billingcycle', ['free', 'onetime', 'Free', 'One Time'])
            ->with('product')
            ->get();

        // PRC-370 / M2 — When cumulative mode is on, skip the
        // hasExistingInvoice gate (the cumulative routing in
        // generateForClient() handles existing unpaid invoices by
        // appending a line, with per-cycle idempotency).
        $cumulative = (bool) Setting::getValue('continuous_invoice_generation', true);

        foreach ($products as $product) {
            // PRC-812 — allow_renewal=false ürünler yenileme-fatura üretiminden
            // atlanır (upgrade'e zorlama). Ürün ilişkisi çözülemezse (legacy/orphan
            // satır) mevcut davranış korunur (renew = default true). Regresyon yok:
            // yalnız açıkça flag'lenmiş ürünler etkilenir. Hizmet due'ya gelince
            // yenileme yok → mevcut lifecycle (expire/suspend) beklenen şekilde işler.
            if ($product->product && $product->product->allow_renewal === false) {
                continue;
            }

            $lead = \App\Services\Billing\PaymentTermsResolver::daysForCycle($product->billingcycle);
            if ($product->next_due_date->gt($now->copy()->addDays($lead)->endOfDay())) {
                continue; // outside this product's per-cycle window
            }

            if (! $cumulative && $this->hasExistingInvoice('product', $product->id)) {
                continue;
            }

            $renewalPrice = $this->billingManager->getProductRenewalPricePublic($product);

            // PRC-442 — Pricing lookup başarısız olursa 0 dönüyor; 0.00'lık
            // renewal invoice/line üretmek yerine bu service'i atla + uyar.
            // Cron her run'da tekrar deneyecek; gerçek fiyat bulununca işler.
            // Ücretsiz renewal (gelir kaybı) + 0.00 duplicate line önlenir.
            if ($renewalPrice <= 0) {
                Log::warning('PRC-442 product renewal skipped — price resolved to 0', [
                    'client_product_id' => $product->id,
                    'product_id' => $product->product_id,
                    'billing_cycle' => $product->billingcycle,
                ]);
                continue;
            }

            $items[] = [
                'type' => 'product',
                'service' => $product,
                'description' => __('admin/billing.due_product_renewal', [
                    'product' => $product->product->name ?? 'Unknown',
                    'domain' => $product->domain ?? '',
                ]),
                'unit_price' => $renewalPrice,
                'due_date' => $product->next_due_date->format('Y-m-d'),
                'related_type' => 'product',
                'related_id' => $product->product_id,
                'related_service_id' => $product->id,
                'product_type' => 'hosting',
            ];
        }

        // Domains — always use the `domain` cycle lead-time.
        $domainLead = \App\Services\Billing\PaymentTermsResolver::daysForCycle('domain');
        $domains = ClientDomain::where('client_id', $clientId)
            ->where('status', 'active')
            ->whereNotNull('next_due_date')
            ->where('next_due_date', '<=', $now->copy()->addDays($domainLead)->endOfDay())
            ->with('tld')
            ->get();

        foreach ($domains as $domain) {
            if (! $cumulative && $this->hasExistingInvoice('domain', $domain->id)) {
                continue;
            }

            $renewalPrice = $this->billingManager->getDomainRenewalPricePublic($domain);

            // PRC-442 — Same 0-price guard for domains.
            if ($renewalPrice <= 0) {
                Log::warning('PRC-442 domain renewal skipped — price resolved to 0', [
                    'client_domain_id' => $domain->id,
                    'domain' => $domain->domain,
                ]);
                continue;
            }

            $items[] = [
                'type' => 'domain',
                'service' => $domain,
                'description' => __('admin/billing.due_domain_renewal', [
                    'domain' => $domain->domain,
                ]),
                'unit_price' => $renewalPrice,
                'due_date' => $domain->next_due_date->format('Y-m-d'),
                'related_type' => 'domain',
                'related_id' => $domain->tld_id,
                'related_service_id' => $domain->id,
                'product_type' => 'domain',
            ];
        }

        return $items;
    }

    /**
     * PRC-370 / M2 — Build a cycle line and append it to an existing
     * unpaid invoice. The line includes `period_start` (today, treated
     * as the cycle's invoiced day) and `period_end` (next_due_date),
     * which payment-time logic uses to push `next_due_date` forward.
     */
    protected function appendCycleLine(Invoice $invoice, array $item, bool $sendEmail = true): void
    {
        $service = $item['service'];

        // Period bounds: start = today (cycle billed now), end = next_due_date.
        // Payment handler uses period_end to push the service's next_due
        // forward; if period_end is null, the push falls back to other
        // logic in the renewal pipeline.
        $periodEnd = $service->next_due_date instanceof Carbon
            ? $service->next_due_date->copy()
            : Carbon::parse($item['due_date']);

        // PRC-370 / PRC-442 — per-cycle idempotency. Cron runs daily;
        // without this check a service still inside its lead-time window
        // would get a fresh line every day until payment pushes next_due
        // forward.
        //
        // PRC-442 re-test: the FIRST cron run for a service goes through
        // the fresh-invoice path (createInvoiceForItems), which does NOT
        // stamp period_end on its line. A later run then routes the same
        // service into appendCycleLine; an exact whereDate('period_end')
        // match misses the null-period_end fresh line and a duplicate is
        // appended (total doubles every run). A cumulative TARGET invoice
        // is by definition still unpaid, and next_due is only pushed at
        // payment time — so while the invoice stays unpaid there can be
        // at most ONE open cycle per service on it. Therefore a null
        // period_end line for this service IS the current (only) cycle:
        // treat it as already billed.
        $alreadyBilled = $invoice->items()
            ->where('related_type', $item['related_type'])
            ->where('related_service_id', $item['related_service_id'])
            ->where(function ($q) use ($periodEnd) {
                $q->whereDate('period_end', $periodEnd->toDateString())
                    ->orWhereNull('period_end');
            })
            ->exists();
        if ($alreadyBilled) {
            return;
        }

        $lineData = [
            'description' => $item['description'],
            'quantity' => 1,
            'unit_price' => $item['unit_price'],
            'line_total' => $item['unit_price'],
            'related_type' => $item['related_type'],
            'related_id' => $item['related_id'],
            'related_service_id' => $item['related_service_id'],
            'period_start' => now()->toDateString(),
            'period_end' => $periodEnd->toDateString(),
        ];

        // New due_date = cycle's next_due (= when this cycle's lead-time ends).
        $newDueDate = Carbon::parse($item['due_date']);

        $this->invoiceService->appendCycleToExistingInvoice($invoice, $lineData, $newDueDate);

        // PRC-628 — yeni renewal cycle satırı eklendiğinde hizmet yenileme
        // bildirimi (alreadyBilled erken-return'ü idempotency sağlar: aynı
        // cycle ikinci kez fire etmez). PRC-628 final review I1 — $sendEmail
        // guard'ı: admin "fatura üret ama mail gönderme" dediğinde (sendEmail
        // false) 'Invoice Due Generated' gibi 'Service Renewal' de gitmez.
        if ($sendEmail) {
            $this->fireServiceRenewalNotice($item);
        }

        // PRC-405 — spec section 4.5: "Line 2 eklendiğinde Line 1 için late
        // fee line eklenir". Triggers per-line fee upsert for any prior
        // overdue cycle on this cumulative invoice. Idempotent: already-fee'd
        // cycles get their amount refreshed if another month elapsed, not
        // double-billed.
        $this->invoiceService->applyLateFee($invoice->fresh('items'));
    }

    /**
     * Check if an unpaid/pending invoice already exists for this service
     */
    protected function hasExistingInvoice(string $relatedType, int $relatedServiceId): bool
    {
        return InvoiceItem::where('related_type', $relatedType)
            ->where(function ($q) use ($relatedServiceId) {
                $q->where('related_service_id', $relatedServiceId)
                    ->orWhere(function ($q2) use ($relatedServiceId) {
                        $q2->whereNull('related_service_id')
                            ->where('related_id', $relatedServiceId);
                    });
            })
            ->whereHas('invoice', function ($q) {
                $q->whereIn('status', [
                    Invoice::STATUS_PENDING,
                    Invoice::STATUS_UNPAID,
                ]);
            })
            ->exists();
    }

    /**
     * Group items by due date
     */
    protected function groupItemsByDueDate(array $items): array
    {
        $grouped = [];
        foreach ($items as $item) {
            $grouped[$item['due_date']][] = $item;
        }
        return $grouped;
    }

    /**
     * Create invoice for grouped items
     */
    protected function createInvoiceForItems(int $clientId, string $dueDate, array $items, bool $sendEmail): ?Invoice
    {
        $client = \App\Models\Client::findOrFail($clientId);
        $invoiceItems = [];
        $subtotal = 0;
        $discountTotal = 0;   // PRC-809 — renewal promosyon indirimi toplamı
        $renewalUsages = [];  // fatura sonrası PromotionUsage kaydı için

        // PRC-1099 — renewal satır fiyatı getProductRenewalPrice ile client
        // currency'de fiyatlanır (client->currency_id ?? default). Promosyon
        // currency-scope guard'ı için satır para birimini aynı kaynaktan çöz.
        $lineCurrency = Currency::find($client->currency_id)
            ?? Currency::where('is_default', true)->first();

        foreach ($items as $item) {
            $lineTotal = $item['unit_price'];
            $subtotal += $lineTotal;

            // PRC-809 — renewal promosyon indirimi (yalnız ürün item'ları; domain hariç).
            if (($item['related_type'] ?? '') === 'product') {
                $cpId = $item['related_service_id'] ?? $item['related_id'] ?? null;
                $cp = $cpId ? \App\Models\ClientProduct::find($cpId) : null;
                if ($cp) {
                    $rd = $this->discountManager->calculateRenewalDiscount($cp, (float) $lineTotal, $client, $lineCurrency);
                    if (($rd['discount_amount'] ?? 0) > 0) {
                        $discountTotal += $rd['discount_amount'];
                        $renewalUsages[] = [
                            'promotion_id' => $rd['promotion_id'],
                            'client_product_id' => $cp->id,
                            'discount' => $rd['discount_amount'],
                            'original' => (float) $lineTotal,
                        ];
                    }
                }
            }

            $invoiceItems[] = [
                'description' => $item['description'],
                'quantity' => 1,
                'unit_price' => $item['unit_price'],
                'line_total' => $lineTotal,
                'related_type' => $item['related_type'],
                'related_id' => $item['related_id'],
                'related_service_id' => $item['related_service_id'],
            ];
        }

        // Determine product type for tax (use first item's type)
        $productType = $items[0]['product_type'] ?? 'hosting';

        // PRC-809 — vergi indirim SONRASI matrah üzerinden (subtotal − discount).
        $taxableBase = max(0, $subtotal - $discountTotal);

        $taxResult = $this->taxService->calculateTaxAmount(
            amount: $taxableBase,
            client: $client,
            productType: $productType,
            product: null,
            countryCode: null
        );

        $invoice = $this->billingManager->createInvoice([
            'client_id' => $clientId,
            'due_date' => $dueDate,
            'subtotal' => $subtotal,
            'discount_amount' => $discountTotal,
            'tax_rate' => $taxResult['rate'],
            'tax_amount' => $taxResult['tax_amount'],
            'total_amount' => $taxResult['total'],
            'items' => $invoiceItems,
            'notes' => __('admin/billing.due_invoice_note'),
        ]);

        // Update status to unpaid
        $invoice->update(['status' => Invoice::STATUS_UNPAID]);

        // PRC-809 — renewal promosyon kullanımlarını kaydet (cycles sayacı bunları
        // client_product bazında sayar) + toplam kullanım (uses) artır.
        foreach ($renewalUsages as $u) {
            \App\Models\PromotionUsage::create([
                'promotion_id' => $u['promotion_id'],
                'client_id' => $clientId,
                'client_product_id' => $u['client_product_id'],
                'discount_amount' => $u['discount'],
                'original_amount' => $u['original'],
                'final_amount' => max(0, $u['original'] - $u['discount']),
                'currency_id' => $invoice->currency_id,
                'used_at' => now(),
                'metadata' => ['context' => 'renewal', 'invoice_id' => $invoice->id],
            ]);
            \App\Models\Promotion::find($u['promotion_id'])?->incrementUsage();
        }

        // PRC-628 — fresh renewal invoice yolunda da hizmet yenileme bildirimi
        // (yalnız ürün item'ları; her item bir kez, faturanın tek üretimiyle).
        // PRC-628 final review I1 — 'Invoice Due Generated' ile aynı $sendEmail
        // koşuluna alındı (admin mail-suppress dediğinde Service Renewal de
        // susar).
        if ($sendEmail) {
            foreach ($items as $renewalItem) {
                $this->fireServiceRenewalNotice($renewalItem);
            }
        }

        if ($sendEmail) {
            try {
                SendEmailJob::dispatch('Invoice Due Generated', 'invoice', $invoice->id, [
                    'INVOICE_ID' => $invoice->id,
                    'INVOICE_NUMBER' => $invoice->invoice_number,
                    'CLIENT_NAME' => $client->first_name . ' ' . $client->last_name,
                    'TOTAL' => number_format($invoice->total_amount, 2),
                    'DUE_DATE' => Carbon::parse($dueDate)->format('d.m.Y'),
                    'ITEMS_TABLE' => $invoice->getItemsHtmlTable(),
                ]);
            } catch (\Exception $e) {
                Log::warning('Due invoice email failed', [
                    'invoice_id' => $invoice->id,
                    'error' => $e->getMessage(),
                ]);
            }
        }

        return $invoice;
    }
}
