<?php
declare(strict_types=1);

namespace App\Modules\Accounting;

use App\Modules\Orders\OrdersRepository;
use App\Modules\Settings\CompanySettingsRepository;
use App\Modules\Settings\FakturowniaApiClient;
use App\Modules\Settings\FakturowniaIntegrationRepository;
use App\Modules\Settings\InvoiceConfigRepository;
use Throwable;

final class InvoiceService
{
    public function __construct(
        private readonly InvoiceRepository $invoices,
        private readonly InvoiceConfigRepository $invoiceConfigs,
        private readonly CompanySettingsRepository $companySettings,
        private readonly OrdersRepository $orders,
        private readonly FakturowniaIntegrationRepository $fakturownia,
        private readonly FakturowniaApiClient $fakturowniaApi
    ) {
    }

    /**
     * @param array{
     *     order_id: int,
     *     config_id: int,
     *     buyer_tax_number?: string,
     *     buyer_name?: string,
     *     buyer_company_name?: string,
     *     buyer_street?: string,
     *     buyer_city?: string,
     *     buyer_postal_code?: string,
     *     buyer_email?: string,
     *     issue_date_override?: string,
     *     created_by?: int|null,
     * } $params
     * @return array{invoice_number: string, total_gross: string, mode: string, invoice_id: int}
     * @throws InvoiceIssueException
     */
    public function issue(array $params): array
    {
        $orderId = (int) $params['order_id'];
        $configId = (int) $params['config_id'];

        $config = $this->invoiceConfigs->findById($configId);
        if ($config === null || (int) ($config['is_active'] ?? 0) !== 1) {
            throw new InvoiceIssueException('Nieprawidlowa lub nieaktywna konfiguracja faktury.');
        }

        $details = $this->orders->findDetails($orderId);
        if ($details === null) {
            throw new InvoiceIssueException('Zamowienie nie istnieje.');
        }

        $order = is_array($details['order'] ?? null) ? $details['order'] : [];
        $items = is_array($details['items'] ?? null) ? $details['items'] : [];
        $addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
        $payments = is_array($details['payments'] ?? null) ? $details['payments'] : [];

        $issueDate = $this->resolveIssueDate((string) ($params['issue_date_override'] ?? ''));
        $saleDate = $this->resolveSaleDate($config, $order, $payments, $issueDate);
        $paymentDueDate = $this->resolvePaymentDueDate($issueDate, (int) ($config['payment_to_days'] ?? 7));
        $orderReference = $this->resolveOrderReference($config, $order);

        $sellerSnapshot = $this->buildSellerSnapshot();
        $buyerSnapshot = $this->buildBuyerSnapshot($order, $addresses, $params);
        ['items' => $itemsSnapshot, 'total_gross' => $totalGross, 'total_net' => $totalNet] = $this->buildItemsSnapshot($items, $order);

        $kind = trim((string) ($config['default_kind'] ?? 'vat')) ?: 'vat';
        $isDelegated = (int) ($config['is_delegated'] ?? 0) === 1;

        if ($isDelegated) {
            return $this->issueDelegated(
                $orderId,
                $configId,
                $config,
                $issueDate,
                $saleDate,
                $paymentDueDate,
                $sellerSnapshot,
                $buyerSnapshot,
                $itemsSnapshot,
                $totalNet,
                $totalGross,
                $orderReference,
                $kind,
                $params['created_by'] ?? null
            );
        }

        return $this->issueLocal(
            $orderId,
            $configId,
            $config,
            $issueDate,
            $saleDate,
            $paymentDueDate,
            $sellerSnapshot,
            $buyerSnapshot,
            $itemsSnapshot,
            $totalNet,
            $totalGross,
            $orderReference,
            $kind,
            $params['created_by'] ?? null
        );
    }

    /**
     * @param array<string, mixed> $config
     * @param array<string, mixed> $sellerSnapshot
     * @param array<string, mixed>|null $buyerSnapshot
     * @param list<array<string, mixed>> $itemsSnapshot
     * @return array{invoice_number: string, total_gross: string, mode: string, invoice_id: int}
     */
    private function issueLocal(
        int $orderId,
        int $configId,
        array $config,
        string $issueDate,
        string $saleDate,
        string $paymentDueDate,
        array $sellerSnapshot,
        ?array $buyerSnapshot,
        array $itemsSnapshot,
        float $totalNet,
        float $totalGross,
        string $orderReference,
        string $kind,
        ?int $createdBy
    ): array {
        $invoiceNumber = $this->invoices->nextLocalNumber(
            $configId,
            (string) ($config['number_format'] ?? 'FV/%N/%M/%Y'),
            (string) ($config['numbering_type'] ?? 'monthly')
        );

        $invoiceId = $this->invoices->insertLocal([
            'order_id' => $orderId,
            'config_id' => $configId,
            'invoice_number' => $invoiceNumber,
            'issue_date' => $issueDate,
            'sale_date' => $saleDate,
            'payment_due_date' => $paymentDueDate,
            'seller_data_json' => json_encode($sellerSnapshot, JSON_UNESCAPED_UNICODE),
            'buyer_data_json' => $buyerSnapshot !== null ? json_encode($buyerSnapshot, JSON_UNESCAPED_UNICODE) : null,
            'items_json' => json_encode($itemsSnapshot, JSON_UNESCAPED_UNICODE),
            'total_net' => number_format($totalNet, 2, '.', ''),
            'total_gross' => number_format($totalGross, 2, '.', ''),
            'order_reference_value' => $orderReference !== '' ? $orderReference : null,
            'kind' => $kind,
            'created_by' => $createdBy,
        ]);

        return [
            'invoice_id' => $invoiceId,
            'invoice_number' => $invoiceNumber,
            'total_gross' => number_format($totalGross, 2, '.', ''),
            'mode' => 'local',
        ];
    }

    /**
     * @param array<string, mixed> $config
     * @param array<string, mixed> $sellerSnapshot
     * @param array<string, mixed>|null $buyerSnapshot
     * @param list<array<string, mixed>> $itemsSnapshot
     * @return array{invoice_number: string, total_gross: string, mode: string, invoice_id: int}
     */
    private function issueDelegated(
        int $orderId,
        int $configId,
        array $config,
        string $issueDate,
        string $saleDate,
        string $paymentDueDate,
        array $sellerSnapshot,
        ?array $buyerSnapshot,
        array $itemsSnapshot,
        float $totalNet,
        float $totalGross,
        string $orderReference,
        string $kind,
        ?int $createdBy
    ): array {
        $integrationId = (int) ($config['integration_id'] ?? 0);
        if ($integrationId <= 0) {
            throw new InvoiceIssueException('Konfiguracja delegowana nie wskazuje konta Fakturowni.');
        }

        $account = $this->fakturownia->findByIntegrationId($integrationId);
        if ($account === null) {
            throw new InvoiceIssueException('Globalna konfiguracja Fakturowni nie istnieje (id=' . $integrationId . ').');
        }

        $prefix = trim((string) ($account['account_prefix'] ?? ''));
        if ($prefix === '') {
            throw new InvoiceIssueException('Globalna konfiguracja Fakturowni nie ma ustawionego prefiksu (subdomeny).');
        }

        $apiToken = $this->fakturownia->getDecryptedToken($integrationId);
        if ($apiToken === null || $apiToken === '') {
            throw new InvoiceIssueException('Brak tokenu API dla konta Fakturownia.');
        }

        $payload = $this->buildFakturowniaPayload(
            $kind,
            $issueDate,
            $saleDate,
            $paymentDueDate,
            $sellerSnapshot,
            $buyerSnapshot,
            $itemsSnapshot,
            $orderReference,
            (int) ($config['payment_to_days'] ?? 7),
            (string) ($account['department_id'] ?? '')
        );

        try {
            $response = $this->fakturowniaApi->createInvoice([
                'account_prefix' => $prefix,
                'api_token' => $apiToken,
            ], $payload);
        } catch (Throwable $e) {
            throw new InvoiceIssueException('Fakturownia: ' . $e->getMessage());
        }

        $externalId = trim((string) ($response['id'] ?? ''));
        $externalNumber = trim((string) ($response['number'] ?? ''));
        $externalPdfUrl = trim((string) ($response['pdf_url'] ?? $response['view_url'] ?? ''));

        if ($externalId === '' || $externalNumber === '') {
            throw new InvoiceIssueException('Fakturownia zwrocila niekompletna odpowiedz (brak id/number).');
        }

        $invoiceId = $this->invoices->insertDelegated([
            'order_id' => $orderId,
            'config_id' => $configId,
            'invoice_number' => $externalNumber,
            'issue_date' => $issueDate,
            'sale_date' => $saleDate,
            'payment_due_date' => $paymentDueDate,
            'seller_data_json' => json_encode($sellerSnapshot, JSON_UNESCAPED_UNICODE),
            'buyer_data_json' => $buyerSnapshot !== null ? json_encode($buyerSnapshot, JSON_UNESCAPED_UNICODE) : null,
            'items_json' => json_encode($itemsSnapshot, JSON_UNESCAPED_UNICODE),
            'total_net' => number_format($totalNet, 2, '.', ''),
            'total_gross' => number_format($totalGross, 2, '.', ''),
            'order_reference_value' => $orderReference !== '' ? $orderReference : null,
            'external_invoice_id' => $externalId,
            'external_pdf_url' => $externalPdfUrl !== '' ? $externalPdfUrl : null,
            'kind' => $kind,
            'created_by' => $createdBy,
        ]);

        return [
            'invoice_id' => $invoiceId,
            'invoice_number' => $externalNumber,
            'total_gross' => number_format($totalGross, 2, '.', ''),
            'mode' => 'delegated',
        ];
    }

    private function resolveIssueDate(string $override): string
    {
        $override = trim($override);
        if ($override !== '' && strtotime($override) !== false) {
            return date('Y-m-d H:i:s', (int) strtotime($override));
        }
        return date('Y-m-d H:i:s');
    }

    /**
     * @param array<string, mixed> $config
     * @param array<string, mixed> $order
     * @param list<array<string, mixed>> $payments
     */
    private function resolveSaleDate(array $config, array $order, array $payments, string $issueDate): string
    {
        $source = (string) ($config['sale_date_source'] ?? 'issue_date');

        if ($source === 'order_date') {
            $ordered = trim((string) ($order['external_created_at'] ?? $order['ordered_at'] ?? ''));
            if ($ordered !== '') {
                $ts = strtotime($ordered);
                return $ts !== false ? date('Y-m-d H:i:s', $ts) : $issueDate;
            }
        }

        if ($source === 'payment_date' && $payments !== []) {
            $payment = $payments[0] ?? [];
            $payDate = trim((string) ($payment['payment_date'] ?? ''));
            if ($payDate !== '') {
                $ts = strtotime($payDate);
                return $ts !== false ? date('Y-m-d H:i:s', $ts) : $issueDate;
            }
        }

        return $issueDate;
    }

    private function resolvePaymentDueDate(string $issueDate, int $paymentToDays): string
    {
        $ts = strtotime($issueDate);
        if ($ts === false) {
            $ts = time();
        }
        return date('Y-m-d 00:00:00', $ts + max(0, $paymentToDays) * 86400);
    }

    /**
     * @param array<string, mixed> $config
     * @param array<string, mixed> $order
     */
    private function resolveOrderReference(array $config, array $order): string
    {
        $ref = (string) ($config['order_reference'] ?? 'none');

        if ($ref === 'orderpro') {
            return (string) ($order['internal_order_number'] ?? '');
        }
        if ($ref === 'integration') {
            return (string) ($order['external_order_number'] ?? $order['external_order_id'] ?? '');
        }
        return '';
    }

    /**
     * @return array<string, mixed>
     */
    private function buildSellerSnapshot(): array
    {
        $seller = $this->companySettings->getSettings();

        return [
            'company_name' => $seller['company_name'] ?? '',
            'tax_number' => $seller['tax_number'] ?? '',
            'street' => $seller['street'] ?? '',
            'city' => $seller['city'] ?? '',
            'postal_code' => $seller['postal_code'] ?? '',
            'phone' => $seller['phone'] ?? '',
            'email' => $seller['email'] ?? '',
            'bank_account' => $seller['bank_account'] ?? '',
            'bdo_number' => $seller['bdo_number'] ?? '',
            'regon' => $seller['regon'] ?? '',
            'court_register' => $seller['court_register'] ?? '',
        ];
    }

    /**
     * @param array<string, mixed> $order
     * @param list<array<string, mixed>> $addresses
     * @param array<string, mixed> $params
     * @return array<string, mixed>|null
     */
    private function buildBuyerSnapshot(array $order, array $addresses, array $params): ?array
    {
        $byType = [];
        foreach ($addresses as $addr) {
            $type = (string) ($addr['address_type'] ?? '');
            if ($type !== '' && !isset($byType[$type])) {
                $byType[$type] = $addr;
            }
        }
        $buyerAddress = $byType['invoice'] ?? $byType['customer'] ?? null;

        $autoTaxNumber = self::extractBuyerTaxNumber($order, $buyerAddress);

        $manualTax = trim((string) ($params['buyer_tax_number'] ?? ''));
        $manualName = trim((string) ($params['buyer_name'] ?? ''));
        $manualCompany = trim((string) ($params['buyer_company_name'] ?? ''));
        $manualStreet = trim((string) ($params['buyer_street'] ?? ''));
        $manualCity = trim((string) ($params['buyer_city'] ?? ''));
        $manualPostal = trim((string) ($params['buyer_postal_code'] ?? ''));
        $manualEmail = trim((string) ($params['buyer_email'] ?? ''));

        $name = $manualName !== ''
            ? $manualName
            : trim((string) ($buyerAddress['name'] ?? ''));
        $company = $manualCompany !== ''
            ? $manualCompany
            : trim((string) ($buyerAddress['company_name'] ?? ''));
        $street = $manualStreet !== ''
            ? $manualStreet
            : trim(((string) ($buyerAddress['street_name'] ?? '')) . ' ' . ((string) ($buyerAddress['street_number'] ?? '')));
        $city = $manualCity !== ''
            ? $manualCity
            : trim((string) ($buyerAddress['city'] ?? ''));
        $postal = $manualPostal !== ''
            ? $manualPostal
            : trim((string) ($buyerAddress['zip_code'] ?? ''));
        $email = $manualEmail !== ''
            ? $manualEmail
            : trim((string) ($buyerAddress['email'] ?? $order['buyer_email'] ?? ''));
        $taxNumber = $manualTax !== '' ? $manualTax : $autoTaxNumber;

        if ($name === '' && $company === '' && $taxNumber === '' && $street === '') {
            return null;
        }

        return [
            'name' => $name,
            'company_name' => $company,
            'tax_number' => $taxNumber,
            'street' => $street,
            'city' => $city,
            'postal_code' => $postal,
            'phone' => trim((string) ($buyerAddress['phone'] ?? '')),
            'email' => $email,
        ];
    }

    /**
     * Extract NIP from various payload locations (Allegro, shopPRO).
     *
     * @param array<string, mixed> $order
     * @param array<string, mixed>|null $buyerAddress
     */
    public static function extractBuyerTaxNumber(array $order, ?array $buyerAddress): string
    {
        if ($buyerAddress !== null) {
            $candidate = trim((string) ($buyerAddress['company_tax_number'] ?? ''));
            if ($candidate !== '') {
                return $candidate;
            }
        }

        $payload = $order['payload_json'] ?? null;
        if (is_string($payload) && $payload !== '') {
            $decoded = json_decode($payload, true);
            if (is_array($decoded)) {
                foreach (self::taxNumberPaths() as $path) {
                    $value = self::digValue($decoded, $path);
                    if (is_string($value) && trim($value) !== '') {
                        return trim($value);
                    }
                }
            }
        }

        return '';
    }

    /**
     * @return list<list<string>>
     */
    private static function taxNumberPaths(): array
    {
        return [
            ['invoice', 'address', 'taxId'],
            ['invoice', 'taxId'],
            ['invoice', 'nip'],
            ['buyer', 'tax_number'],
            ['buyer', 'nip'],
            ['client', 'nip'],
            ['client', 'tax_number'],
            ['nip'],
            ['tax_number'],
        ];
    }

    /**
     * @param array<string, mixed> $arr
     * @param list<string> $path
     */
    private static function digValue(array $arr, array $path): mixed
    {
        $cur = $arr;
        foreach ($path as $key) {
            if (!is_array($cur) || !array_key_exists($key, $cur)) {
                return null;
            }
            $cur = $cur[$key];
        }
        return $cur;
    }

    /**
     * @param list<array<string, mixed>> $items
     * @param array<string, mixed> $order
     * @return array{items: list<array<string, mixed>>, total_gross: float, total_net: float}
     */
    private function buildItemsSnapshot(array $items, array $order): array
    {
        $itemsSnapshot = [];
        $totalGross = 0.0;
        $totalNet = 0.0;

        foreach ($items as $item) {
            $qty = (float) ($item['quantity'] ?? 0);
            $price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : (float) ($item['price_gross'] ?? 0);
            $vat = (float) ($item['vat'] ?? 23);
            $lineGross = $qty * $price;
            $lineNet = $vat > 0 ? round($lineGross / (1 + $vat / 100), 2) : $lineGross;
            $totalGross += $lineGross;
            $totalNet += $lineNet;

            $itemsSnapshot[] = [
                'name' => $item['original_name'] ?? $item['name'] ?? '',
                'quantity' => $qty,
                'price_gross' => $price,
                'price_net' => $vat > 0 ? round($price / (1 + $vat / 100), 2) : $price,
                'vat' => $vat,
                'total_gross' => round($lineGross, 2),
                'total_net' => round($lineNet, 2),
                'sku' => $item['sku'] ?? '',
                'ean' => $item['ean'] ?? '',
            ];
        }

        $deliveryPrice = (float) ($order['delivery_price'] ?? 0);
        if ($deliveryPrice > 0) {
            $deliveryVat = 23.0;
            $deliveryNet = round($deliveryPrice / (1 + $deliveryVat / 100), 2);
            $totalGross += $deliveryPrice;
            $totalNet += $deliveryNet;
            $itemsSnapshot[] = [
                'name' => 'Koszt wysylki',
                'quantity' => 1.0,
                'price_gross' => $deliveryPrice,
                'price_net' => $deliveryNet,
                'vat' => $deliveryVat,
                'total_gross' => round($deliveryPrice, 2),
                'total_net' => round($deliveryNet, 2),
                'sku' => '',
                'ean' => '',
            ];
        }

        return [
            'items' => $itemsSnapshot,
            'total_gross' => round($totalGross, 2),
            'total_net' => round($totalNet, 2),
        ];
    }

    /**
     * Build Fakturownia API payload (https://app.fakturownia.pl/api).
     *
     * @param array<string, mixed> $sellerSnapshot
     * @param array<string, mixed>|null $buyerSnapshot
     * @param list<array<string, mixed>> $itemsSnapshot
     * @return array<string, mixed>
     */
    private function buildFakturowniaPayload(
        string $kind,
        string $issueDate,
        string $saleDate,
        string $paymentDueDate,
        array $sellerSnapshot,
        ?array $buyerSnapshot,
        array $itemsSnapshot,
        string $orderReference,
        int $paymentToDays,
        string $departmentId
    ): array {
        $issueDay = substr($issueDate, 0, 10);
        $saleDay = substr($saleDate, 0, 10);
        $dueDay = substr($paymentDueDate, 0, 10);

        // UWAGA: seller_* pola CELOWO pominiete. Konta Fakturowni z podwyzszonym
        // poziomem zabezpieczen interpretuja roznice w seller_name/tax_no/bank
        // jako proba "utworzenia nowego dzialu" i odrzucaja request HTTP 422
        // ("Poziom zabezpieczenia przed zmiana konta bankowego nie pozwala na
        // utworzenie dzialu"). Fakturownia uzywa wtedy danych sprzedawcy
        // zarejestrowanych na koncie (uzytkownik IS sprzedawca w Fakturowni).
        // Lokalny snapshot `seller_data_json` w tabeli `invoices` zachowuje
        // dane orderPRO dla audytu — niezalezne od tego co poszlo do Fakturowni.
        $invoice = [
            'kind' => $kind !== '' ? $kind : 'vat',
            'issue_date' => $issueDay,
            'sell_date' => $saleDay,
            'payment_to' => $dueDay,
            'positions' => array_map(static function (array $item): array {
                return [
                    'name' => (string) ($item['name'] ?? ''),
                    'tax' => (float) ($item['vat'] ?? 23),
                    'total_price_gross' => number_format((float) ($item['total_gross'] ?? 0), 2, '.', ''),
                    'quantity' => (float) ($item['quantity'] ?? 1),
                ];
            }, $itemsSnapshot),
        ];
        unset($paymentToDays, $sellerSnapshot);

        if ($buyerSnapshot !== null) {
            $buyerName = trim((string) ($buyerSnapshot['company_name'] ?? ''));
            if ($buyerName === '') {
                $buyerName = trim((string) ($buyerSnapshot['name'] ?? ''));
            }
            $invoice['buyer_name'] = $buyerName;
            $invoice['buyer_tax_no'] = (string) ($buyerSnapshot['tax_number'] ?? '');
            $invoice['buyer_street'] = (string) ($buyerSnapshot['street'] ?? '');
            $invoice['buyer_post_code'] = (string) ($buyerSnapshot['postal_code'] ?? '');
            $invoice['buyer_city'] = (string) ($buyerSnapshot['city'] ?? '');
            $invoice['buyer_email'] = (string) ($buyerSnapshot['email'] ?? '');
        }

        if ($orderReference !== '') {
            $invoice['additional_info_desc'] = 'Zamowienie: ' . $orderReference;
        }

        // department_id celowo pominiete — konta Fakturowni z podwyzszonym
        // poziomem zabezpieczen odrzucaja ten parametr przez API (HTTP 422
        // "Poziom zabezpieczenia ... nie pozwala na utworzenie dzialu").
        // Fakturownia uzywa wtedy domyslnego dzialu konta.
        unset($departmentId);

        return $invoice;
    }
}
