<?php

namespace SoftminalB2B\Services\Pdf;

if (!defined('ABSPATH')) {
    exit;
}

use SoftminalB2B\Services\Settings;
use SoftminalB2B\Support\Tier;

/**
 * A PDF invoice for one WooCommerce order (FEATURES G4, Free tier).
 *
 * Free ships fixed templates (PHP views); Pro replaces the template with
 * block-editor output through the same renderer. All order access is via the
 * WC_Order CRUD so it works under HPOS, and every value is escaped in the
 * template — order data is attacker-influenced (names, addresses) and dompdf
 * renders HTML.
 */
class Invoice
{
    private \WC_Order $order;
    private PdfEngine $engine;

    public function __construct(\WC_Order $order, ?PdfEngine $engine = null)
    {
        $this->order  = $order;
        $this->engine = $engine ?? new DompdfEngine();
    }

    /** The default template a fresh install uses. */
    public const DEFAULT_TEMPLATE = 'classic';

    /**
     * The chosen template. Falls back to the default when the stored value is
     * stale, or when a Pro template is selected on a free install (so Pro
     * deactivation never leaves invoices unrenderable).
     */
    public static function template(): string
    {
        $templates = self::templates();
        $chosen    = (string) Settings::get('invoice_template', self::DEFAULT_TEMPLATE);

        if (!isset($templates[$chosen])) {
            return self::DEFAULT_TEMPLATE;
        }

        if (!empty($templates[$chosen]['pro']) && !Tier::isPro()) {
            return self::DEFAULT_TEMPLATE;
        }

        return $chosen;
    }

    /**
     * The design catalog: five free plus ten Pro. Only the five free template
     * files ship in this plugin. The ten Pro entries are metadata only (label +
     * accent + style) so the picker can advertise them as an upsell; their
     * actual render files live in the Pro add-on, which registers them through
     * the `smb2b_invoice_templates` / `smb2b_invoice_template_file` filters and
     * reads `style` via design(). A free install therefore cannot produce a Pro
     * design — there is no template code here to run — which is exactly what
     * keeps the free plugin free of locked functionality.
     *
     * @return array<string, array{label:string, pro:bool, accent:string, style:string}>
     */
    private static function catalog(): array
    {
        return [
            // Free — each has its own self-contained template file.
            'classic'  => ['label' => __('Classic', 'softminal-b2b-wholesale-for-woocommerce'), 'pro' => false, 'accent' => '#111827', 'style' => ''],
            'modern'   => ['label' => __('Modern', 'softminal-b2b-wholesale-for-woocommerce'),  'pro' => false, 'accent' => '#4f46e5', 'style' => ''],
            'minimal'  => ['label' => __('Minimal', 'softminal-b2b-wholesale-for-woocommerce'), 'pro' => false, 'accent' => '#334155', 'style' => ''],
            'bold'     => ['label' => __('Bold', 'softminal-b2b-wholesale-for-woocommerce'),    'pro' => false, 'accent' => '#0d9488', 'style' => ''],
            'letter'   => ['label' => __('Letterhead', 'softminal-b2b-wholesale-for-woocommerce'), 'pro' => false, 'accent' => '#b45309', 'style' => ''],

            // Pro — rendered through _pro_base.php with the given accent + style.
            'aurora'   => ['label' => 'Aurora',   'pro' => true, 'accent' => '#7c3aed', 'style' => 'sidebar'],
            'harbor'   => ['label' => 'Harbor',   'pro' => true, 'accent' => '#0369a1', 'style' => 'ribbon'],
            'forest'   => ['label' => 'Forest',   'pro' => true, 'accent' => '#15803d', 'style' => 'ribbon'],
            'ember'    => ['label' => 'Ember',    'pro' => true, 'accent' => '#c2410c', 'style' => 'sidebar'],
            'graphite' => ['label' => 'Graphite', 'pro' => true, 'accent' => '#334155', 'style' => 'stack'],
            'rosewood' => ['label' => 'Rosewood', 'pro' => true, 'accent' => '#be123c', 'style' => 'ribbon'],
            'midnight' => ['label' => 'Midnight', 'pro' => true, 'accent' => '#1e293b', 'style' => 'sidebar'],
            'sunbeam'  => ['label' => 'Sunbeam',  'pro' => true, 'accent' => '#b45309', 'style' => 'stack'],
            'teal'     => ['label' => 'Teal Pro', 'pro' => true, 'accent' => '#0f766e', 'style' => 'ribbon'],
            'plum'     => ['label' => 'Plum',     'pro' => true, 'accent' => '#86198f', 'style' => 'stack'],
        ];
    }

    /**
     * The picker-facing template list: label, pro flag and accent per slug.
     * Filterable so a third-party add-on can register more.
     *
     * @return array<string, array{label:string, pro:bool, accent:string}>
     */
    public static function templates(): array
    {
        $out = [];

        foreach (self::catalog() as $slug => $c) {
            $out[$slug] = ['label' => $c['label'], 'pro' => $c['pro'], 'accent' => $c['accent']];
        }

        $out = apply_filters('smb2b_invoice_templates', $out);

        // Normalise third-party additions to the expected shape.
        foreach ($out as $slug => $entry) {
            if (!is_array($entry)) {
                $out[$slug] = ['label' => (string) $entry, 'pro' => true, 'accent' => '#4f46e5'];
            } else {
                $out[$slug] = [
                    'label'  => (string) ($entry['label'] ?? $slug),
                    'pro'    => (bool) ($entry['pro'] ?? false),
                    'accent' => (string) ($entry['accent'] ?? '#4f46e5'),
                ];
            }
        }

        return $out;
    }

    /**
     * Accent + header style for a Pro design, read by _pro_base.php.
     *
     * @return array{accent:string, style:string}
     */
    public static function design(string $slug): array
    {
        $c = self::catalog()[$slug] ?? null;

        return [
            'accent' => (string) ($c['accent'] ?? '#4f46e5'),
            'style'  => (string) ($c['style'] ?? 'ribbon'),
        ];
    }

    public function filename(): string
    {
        $number = $this->order->get_order_number();

        return sanitize_file_name(sprintf('invoice-%s.pdf', $number));
    }

    /**
     * Render to PDF bytes.
     */
    public function pdf(): string
    {
        return $this->engine->render($this->html());
    }

    /**
     * Write the PDF to a temp file and return its path, for email attachment.
     */
    public function toTempFile(): ?string
    {
        $bytes = $this->pdf();

        if ($bytes === '') {
            return null;
        }

        $dir  = get_temp_dir();
        $path = trailingslashit($dir) . $this->filename();

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
        if (file_put_contents($path, $bytes) === false) {
            return null;
        }

        return $path;
    }

    /**
     * The invoice HTML. A Pro template can replace it wholesale via the filter.
     */
    public function html(): string
    {
        $data = $this->data();

        $custom = apply_filters('smb2b_invoice_html', null, $this->order, $data);

        if (is_string($custom) && $custom !== '') {
            return $custom;
        }

        return self::renderTemplate(self::template(), $data);
    }

    /**
     * Render one template slug with a data array to HTML. Shared by the real
     * invoice and the admin picker's live preview.
     *
     * @param array<string, mixed> $data
     */
    public static function renderTemplate(string $slug, array $data): string
    {
        /**
         * Resolve a template slug to the PHP file that renders it. Pro points
         * its own slugs at files in the Pro plugin's directory; core only ships
         * the five free templates here.
         *
         * @param string $file default path under core's templates dir
         * @param string $slug
         */
        $file = apply_filters(
            'smb2b_invoice_template_file',
            __DIR__ . '/templates/' . $slug . '.php',
            $slug
        );

        if (!is_string($file) || !file_exists($file)) {
            $file = __DIR__ . '/templates/' . self::DEFAULT_TEMPLATE . '.php';
        }

        // Guarantee the partials path even when a caller passes bare sample data.
        if (empty($data['partials'])) {
            $data['partials'] = __DIR__ . '/templates';
        }

        ob_start();
        // $data is in scope for the template.
        include $file;

        return (string) ob_get_clean();
    }

    /**
     * Representative data for the admin picker's preview, so a merchant sees a
     * realistic document without needing a real order.
     *
     * @return array<string, mixed>
     */
    public static function sampleData(): array
    {
        return apply_filters('smb2b_invoice_sample_data', [
            'doc_title'  => __('Invoice', 'softminal-b2b-wholesale-for-woocommerce'),
            'partials'   => __DIR__ . '/templates',
            'seller'     => [
                'name'    => (string) Settings::get('invoice_business_name', get_bloginfo('name')),
                'address' => (string) (Settings::get('invoice_business_address') ?: "1 Trade Park\nManchester M1 2AB"),
                'vat'     => (string) (Settings::get('invoice_business_vat') ?: 'GB123456789'),
                'email'   => (string) Settings::get('invoice_business_email', get_option('admin_email')),
            ],
            'logo'       => '',
            'invoice_no' => '1042',
            'date'       => date_i18n(get_option('date_format')),
            'po_number'  => 'PO-88231',
            'billing'    => "Nordic Interiors AB<br>Storgatan 1<br>111 22 Stockholm<br>Sweden",
            'shipping'   => "Nordic Interiors AB<br>Warehouse 4<br>120 30 Stockholm",
            'company'    => 'Nordic Interiors AB',
            'vat_number' => 'SE556677889901',
            'lines'      => [
                ['name' => 'Oak dining chair', 'sku' => 'CH-OAK-01', 'quantity' => 12, 'total' => wc_price(1074.00)],
                ['name' => 'Walnut side table', 'sku' => 'TB-WAL-22', 'quantity' => 4, 'total' => wc_price(596.00)],
                ['name' => 'Linen cushion, grey', 'sku' => 'CU-LIN-07', 'quantity' => 20, 'total' => wc_price(380.00)],
            ],
            'totals'     => [
                ['label' => __('Subtotal', 'softminal-b2b-wholesale-for-woocommerce'), 'value' => wc_price(2050.00)],
                ['label' => __('VAT (20%)', 'softminal-b2b-wholesale-for-woocommerce'), 'value' => wc_price(410.00)],
                ['label' => __('Total', 'softminal-b2b-wholesale-for-woocommerce'), 'value' => wc_price(2460.00)],
            ],
            'payment'    => __('Net 30 — on account', 'softminal-b2b-wholesale-for-woocommerce'),
            'note'       => (string) (Settings::get('invoice_footer_note') ?: __('Thank you for your business. Payment due within 30 days.', 'softminal-b2b-wholesale-for-woocommerce')),
            'currency'   => get_woocommerce_currency(),
            'font'       => 'Helvetica, Arial, sans-serif',
            'font_face'  => '',
            'watermark'  => '',
        ]);
    }

    /**
     * Everything a template needs, assembled once. Order access is all CRUD.
     *
     * @return array<string, mixed>
     */
    private function data(): array
    {
        $order = $this->order;
        $data  = $this->assemble($order);

        /**
         * Filter the assembled invoice data before it reaches the template.
         *
         * Add or reshape anything the templates render: extra totals rows,
         * altered seller details, additional line-item columns. The Pro block
         * customizer and the multivendor bridge both re-derive through this.
         *
         * @param array<string, mixed> $data
         * @param \WC_Order            $order
         */
        return apply_filters('smb2b_invoice_data', $data, $order);
    }

    /**
     * @return array<string, mixed>
     */
    private function assemble(\WC_Order $order): array
    {

        $lines = [];

        foreach ($order->get_items() as $item) {
            /** @var \WC_Order_Item_Product $item */
            $lines[] = [
                'name'     => $item->get_name(),
                'quantity' => $item->get_quantity(),
                'total'    => $this->pdfMoney($order->get_formatted_line_subtotal($item)),
                'sku'      => $this->itemSku($item),
            ];
        }

        return [
            'doc_title'    => __('Invoice', 'softminal-b2b-wholesale-for-woocommerce'),
            // Where the shared template partials live, so a Pro template in
            // another directory can still `require $data['partials'].'/_content.php'`.
            'partials'     => __DIR__ . '/templates',
            'seller'       => $this->seller(),
            'logo'         => $this->logoDataUri(),
            'invoice_no'   => $order->get_order_number(),
            'date'         => wc_format_datetime($order->get_date_created()),
            'billing'      => $order->get_formatted_billing_address() ?: '',
            'shipping'     => $order->get_formatted_shipping_address() ?: '',
            'company'      => (string) $order->get_billing_company(),
            'vat_number'   => (string) $order->get_meta('_smb2b_vat_number'),
            'lines'        => $lines,
            'totals'       => $this->totals(),
            'payment'      => $order->get_payment_method_title(),
            'note'         => (string) Settings::get('invoice_footer_note', ''),
            'currency'     => $order->get_currency(),

            // Presentation knobs the templates read. Free renders in Helvetica
            // with no watermark; Pro fills `font`/`font_face`/`watermark` via the
            // `smb2b_invoice_data` filter from its customization settings.
            'font'         => 'Helvetica, Arial, sans-serif',
            'font_face'    => '',
            'watermark'    => '',
        ];
    }

    private function itemSku(\WC_Order_Item_Product $item): string
    {
        $product = $item->get_product();

        return $product ? (string) $product->get_sku() : '';
    }

    /**
     * @return array<int, array{label:string, value:string}>
     */
    private function totals(): array
    {
        $rows = [];

        foreach ($this->order->get_order_item_totals() as $total) {
            $rows[] = [
                'label' => (string) ($total['label'] ?? ''),
                'value' => $this->pdfMoney((string) ($total['value'] ?? '')),
            ];
        }

        return $rows;
    }

    /**
     * Make a WooCommerce-formatted money string safe for the PDF.
     *
     * dompdf's built-in fonts (Helvetica) can only draw Latin-1 glyphs, so a
     * non-Latin currency symbol — the Bengali Taka ৳, the Euro €, the Rupee ₹ —
     * comes out as "?". For those currencies we drop the symbol and prefix the
     * ISO code instead ("BDT 60.00"), which always renders. Latin-1 symbols
     * ($, £, ¥, ¢) render fine and are left exactly as WooCommerce formatted them.
     */
    private function pdfMoney(string $formatted): string
    {
        $symbol = html_entity_decode(get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8');

        // Symbol is within Latin-1 → dompdf can draw it; keep WooCommerce's HTML.
        if (!preg_match('/[^\x{0000}-\x{00FF}]/u', $symbol)) {
            return $formatted;
        }

        // Otherwise strip the unrenderable symbol and show the ISO code.
        $amount = html_entity_decode(wp_strip_all_tags($formatted), ENT_QUOTES, 'UTF-8');
        $amount = trim(str_replace($symbol, '', $amount));

        return esc_html(get_woocommerce_currency() . ' ' . $amount);
    }

    /**
     * @return array<string, string>
     */
    private function seller(): array
    {
        return [
            'name'    => (string) Settings::get('invoice_business_name', get_bloginfo('name')),
            'address' => (string) Settings::get('invoice_business_address', ''),
            'vat'     => (string) Settings::get('invoice_business_vat', ''),
            'email'   => (string) Settings::get('invoice_business_email', get_option('admin_email')),
        ];
    }

    /**
     * The logo as a data URI so dompdf never has to fetch a remote URL. Returns
     * an empty string when no logo is set or it cannot be read.
     */
    private function logoDataUri(): string
    {
        $attachmentId = (int) Settings::get('invoice_logo_id', 0);

        if ($attachmentId <= 0) {
            return '';
        }

        $path = get_attached_file($attachmentId);

        if (!$path || !file_exists($path)) {
            return '';
        }

        $mime = get_post_mime_type($attachmentId);

        if (!in_array($mime, ['image/png', 'image/jpeg', 'image/gif'], true)) {
            return '';
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
        $bytes = file_get_contents($path);

        if ($bytes === false) {
            return '';
        }

        return 'data:' . $mime . ';base64,' . base64_encode($bytes);
    }
}
