<?php

namespace App\Services\Billing;

use App\Helpers\CurrencyHelper;
use App\Models\Payment;
use App\Models\SystemSetting;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;

/**
 * InvoiceService — generates invoice numbers, renders the PDF, and
 * snapshots EU-compliant billing data onto the Payment row.
 *
 * Why snapshot the billing data on the payment instead of joining at PDF
 * render time:
 * - Tax law requires the invoice to reflect the customer's address,
 *   tax-id, and VAT number AS OF the purchase date. If the user updates
 *   their address next month, regenerating a PDF for an old payment must
 *   still show the OLD address.
 * - Company billing setup (our seller info — site_name, tax_id, VAT,
 *   address) can be regenerated on the fly because the seller is always
 *   "current" — that's our information, not the customer's snapshot.
 *
 * Invoice number format: `INV-YYYYMM-NNNN` where NNNN is a 4-digit
 * counter that resets every month. Sequential, no gaps. Stored in
 * SystemSetting('invoice_counter_YYYYMM') so we don't need a separate
 * counters table.
 */
class InvoiceService
{
    /**
     * Path inside the configured disk where invoice PDFs are stored.
     * Default disk is `local` so PDFs aren't web-accessible; they're
     * served through a signed-controller route.
     */
    protected const PDF_DIR = 'invoices';

    /**
     * Assign an invoice number if the payment doesn't have one.
     * Idempotent — calling twice does NOT create a second number.
     *
     * Uses a DB transaction + advisory locking via UPDATE to avoid
     * two concurrent payments getting the same counter value on
     * busy stores.
     */
    public function assignNumber(Payment $payment): string
    {
        if (!empty($payment->invoice_number)) {
            return $payment->invoice_number;
        }

        $period = now()->format('Ym');
        $key = 'invoice_counter_' . $period;

        // Transaction so concurrent assignNumber() calls serialize
        $number = DB::transaction(function () use ($key, $period) {
            $current = (int) SystemSetting::get($key, '0');
            $next = $current + 1;
            SystemSetting::set($key, (string) $next, 'invoicing');
            return sprintf('INV-%s-%04d', $period, $next);
        });

        $payment->invoice_number = $number;
        $payment->save();

        return $number;
    }

    /**
     * Snapshot the buyer's billing info onto the payment. Called once
     * at checkout completion. Subsequent profile edits don't touch
     * historical invoices.
     */
    public function snapshotBilling(Payment $payment, array $data): void
    {
        $payment->fill([
            'billing_company'    => $data['company']    ?? null,
            'billing_tax_id'     => $data['tax_id']     ?? null,
            'billing_vat_number' => $data['vat_number'] ?? null,
            'billing_address'    => $data['address']    ?? null,
            'billing_email'      => $data['email']      ?? null,
        ])->save();
    }

    /**
     * Render the invoice PDF and persist it to storage. Returns the
     * relative path. If a PDF already exists on the payment, regenerate
     * (admin might have updated company info — the PDF should reflect
     * the latest seller details while keeping buyer snapshot frozen).
     */
    public function generatePdf(Payment $payment): string
    {
        if (empty($payment->invoice_number)) {
            $this->assignNumber($payment);
            $payment->refresh();
        }

        $pdf = Pdf::loadView('invoices.template', [
            'payment' => $payment,
            'seller'  => $this->getSellerInfo(),
            'buyer'   => $this->getBuyerSnapshot($payment),
            'lines'   => $this->buildLineItems($payment),
        ])->setPaper('a4');

        $path = self::PDF_DIR . '/' . $payment->invoice_number . '.pdf';
        Storage::disk('local')->put($path, $pdf->output());

        $payment->invoice_pdf_path = $path;
        $payment->save();

        return $path;
    }

    /**
     * Return the path to the invoice PDF, generating on demand if missing.
     */
    public function ensurePdf(Payment $payment): string
    {
        if ($payment->invoice_pdf_path && Storage::disk('local')->exists($payment->invoice_pdf_path)) {
            return $payment->invoice_pdf_path;
        }
        return $this->generatePdf($payment);
    }

    /**
     * Seller info (us). Always derived from current SystemSetting values.
     */
    public function getSellerInfo(): array
    {
        // Pick the best logo for an invoice (white background). Try the
        // dedicated `invoice_logo` setting first, fall back to `logo_light`
        // (admin already uploads this for the rest of the site), then
        // `logo_dark` as last resort. Returning a fully-qualified URL keeps
        // the HTML preview AND the PDF renderer (DomPDF resolves absolute
        // URLs only) on the same code path.
        $logoPath = SystemSetting::get('invoice_logo', '')
            ?: SystemSetting::get('logo_light', '')
            ?: SystemSetting::get('logo_dark', '');
        $logoUrl = $logoPath ? asset('storage/'.$logoPath) : null;

        return [
            // Company name on invoices — resolve via BrandingHelper so
            // the same de-leak guard ("MailTrixy" → empty) we apply
            // everywhere else also covers PDF/HTML invoices. Previously
            // the issuer line printed "MailTrixy" on the actual invoice
            // PDF for resellers who never set Company Name or Site Name.
            'name'     => SystemSetting::get('company_name', \App\Helpers\BrandingHelper::name()),
            'logo_url' => $logoUrl,
            'tax_id'   => SystemSetting::get('company_tax_id', ''),
            'vat'      => SystemSetting::get('company_vat_number', ''),
            'address'  => $this->buildAddressBlock(
                SystemSetting::get('company_address_line1'),
                SystemSetting::get('company_address_line2'),
                SystemSetting::get('company_city'),
                SystemSetting::get('company_state'),
                SystemSetting::get('company_postal'),
                SystemSetting::get('company_country'),
            ),
            'email'    => SystemSetting::get('company_email', SystemSetting::get('support_email', '')),
            'phone'    => SystemSetting::get('company_phone', SystemSetting::get('support_phone', '')),
            'website'  => SystemSetting::get('company_website', config('app.url')),
            'bank'     => SystemSetting::get('company_bank_details', ''),
        ];
    }

    /**
     * Buyer snapshot — from the payment row (frozen at purchase).
     *
     * Workspace::owner() is a METHOD returning ?User directly (not an
     * Eloquent relationship), so we call it as a method. Using property
     * access `$workspace->owner` would trigger the magic relation getter
     * and explode with "must return a relationship instance".
     *
     * Public so the HTML preview controller can reuse it without
     * regenerating the PDF.
     */
    public function getBuyerSnapshot(Payment $payment): array
    {
        $workspace = $payment->workspace;
        $owner = $workspace ? $workspace->owner() : null;

        return [
            'company'    => $payment->billing_company ?: ($workspace->name ?? ''),
            'tax_id'     => $payment->billing_tax_id ?: '',
            'vat'        => $payment->billing_vat_number ?: '',
            'address'    => $payment->billing_address ?: '',
            'email'      => $payment->billing_email ?: ($owner->email ?? ''),
            'name'       => $owner->name ?? '',
        ];
    }

    /**
     * Public accessor for the line/tax breakdown used by both the PDF
     * template and the HTML preview view.
     */
    public function getLineItems(Payment $payment): array
    {
        return $this->buildLineItems($payment);
    }

    /**
     * Render a single line item from the payment plus the tax breakdown.
     * The payment.description holds the plan label; everything else is
     * computed from the snapshot fields.
     */
    protected function buildLineItems(Payment $payment): array
    {
        $currency = $payment->currency ?: 'USD';
        $subtotal = (float) ($payment->subtotal_amount ?? $payment->original_amount ?? $payment->amount);
        $tax      = (float) ($payment->tax_amount ?? 0);
        $discount = (float) ($payment->discount_amount ?? 0);
        $total    = (float) $payment->amount;

        return [
            'description' => $payment->description ?: 'Subscription',
            'currency'    => $currency,
            'subtotal'    => $subtotal,
            'discount'    => $discount,
            'tax_rate'    => (float) ($payment->tax_rate ?? 0),
            'tax'         => $tax,
            'total'       => $total,
            // Pre-formatted strings for the PDF (no Blade helpers run inside dompdf reliably)
            'subtotal_f'  => CurrencyHelper::format($subtotal, $currency),
            'discount_f'  => CurrencyHelper::format($discount, $currency),
            'tax_f'       => CurrencyHelper::format($tax, $currency),
            'total_f'     => CurrencyHelper::format($total, $currency),
        ];
    }

    protected function buildAddressBlock(?string $l1, ?string $l2, ?string $city, ?string $state, ?string $postal, ?string $country): string
    {
        $cityLine = trim(implode(' ', array_filter([trim((string) $city), trim((string) $state), trim((string) $postal)])));
        $parts = array_filter([$l1, $l2, $cityLine ?: null, $country]);
        return implode("\n", $parts);
    }

    /**
     * Compute the tax breakdown at checkout time. Returns:
     *   subtotal, tax_rate, tax_amount, total
     *
     * Inputs:
     *   $netOrGross — the price (may be net or gross depending on the plan)
     *   $rate       — tax rate (%) to apply
     *   $inclusive  — true if $netOrGross ALREADY INCLUDES tax
     *
     * Inclusive math: gross = subtotal + tax → subtotal = gross / (1 + r)
     * Exclusive math: total = subtotal + (subtotal * r)
     */
    public static function calculateTax(float $netOrGross, float $rate, bool $inclusive): array
    {
        $rate = max(0, $rate);
        if ($rate === 0.0) {
            return [
                'subtotal'   => round($netOrGross, 2),
                'tax_rate'   => 0.0,
                'tax_amount' => 0.0,
                'total'      => round($netOrGross, 2),
            ];
        }

        if ($inclusive) {
            $subtotal = $netOrGross / (1 + ($rate / 100));
            $tax = $netOrGross - $subtotal;
            $total = $netOrGross;
        } else {
            $subtotal = $netOrGross;
            $tax = $subtotal * ($rate / 100);
            $total = $subtotal + $tax;
        }

        return [
            'subtotal'   => round($subtotal, 2),
            'tax_rate'   => $rate,
            'tax_amount' => round($tax, 2),
            'total'      => round($total, 2),
        ];
    }
}
