<?php
declare(strict_types=1);

require_once __DIR__ . '/bootstrap.php';
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/shopify.php';
require_once __DIR__ . '/pandadoc.php';
require_once __DIR__ . '/pandadoc_signing.php';
require_once __DIR__ . '/drive.php';
require_once __DIR__ . '/hubspot_sync.php';

function pd_money_fmt_usd(float $amount): string {
    return '$' . number_format($amount, 2, '.', ',');
}

function pd_try_load_admin_quote_helpers(): bool {
    static $loaded = false;
    if ($loaded) {
        return function_exists('admin_get_quote_description_text')
            || function_exists('admin_build_options_values_only_text');
    }
    $loaded = true;
    $paths = [
        dirname(__DIR__, 2) . '/admin_config_helpers.php',
        dirname(__DIR__) . '/admin_config_helpers.php',
        dirname(__DIR__, 2) . '/admin/admin_config_helpers.php',
    ];
    foreach ($paths as $path) {
        if (is_file($path)) {
            require_once $path;
            break;
        }
    }
    return function_exists('admin_get_quote_description_text')
        || function_exists('admin_build_options_values_only_text');
}

function pd_line_item_custom_attribute_skip_key(string $key): bool {
    $k = trim($key);
    if ($k === '') {
        return true;
    }
    if (strcasecmp($k, 'Config ID') === 0) {
        return true;
    }
    if (function_exists('admin_quote_description_attribute_key')
        && strcasecmp($k, admin_quote_description_attribute_key()) === 0) {
        return true;
    }
    if (strcasecmp($k, 'quote_description') === 0) {
        return true;
    }
    if (strcasecmp($k, '_configured_price') === 0) {
        return true;
    }
    if (preg_match('/^configured\s+price$/i', preg_replace('/\s+/', ' ', $k)) === 1) {
        return true;
    }
    if (strcasecmp($k, 'custom_image_url') === 0) {
        return true;
    }
    if (preg_match('/^internal_image(_\d+)?$/i', $k) === 1) {
        return true;
    }
    return false;
}

function pd_config_id_from_line_item_attrs(array $attrs): int {
    foreach ($attrs as $attr) {
        if (!is_array($attr)) {
            continue;
        }
        if (strcasecmp(trim((string)($attr['key'] ?? '')), 'Config ID') === 0) {
            return max(0, (int)preg_replace('/\D/', '', (string)($attr['value'] ?? '0')));
        }
    }
    return 0;
}

/** @return array{title: string, description: string, image: string} */
function pd_quote_line_from_db(int $configId): array {
    global $pdo;
    $out = ['title' => '', 'description' => '', 'image' => ''];
    if ($configId <= 0 || !isset($pdo) || !($pdo instanceof PDO)) {
        return $out;
    }
    if (!pd_try_load_admin_quote_helpers()) {
        return $out;
    }
    try {
        // Select the full row so config-level image columns are available too.
        $stmt = $pdo->prepare('SELECT * FROM door_configurations WHERE id = ? LIMIT 1');
        $stmt->execute([$configId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return $out;
        }
        $opts = json_decode((string)($row['options_json'] ?? ''), true);
        if (!is_array($opts)) {
            $opts = [];
        }
        $config = is_array($row) ? $row : [];
        $config['product_title'] = (string)($row['product_title'] ?? '');
        if (function_exists('admin_get_configuration_quote_title')) {
            $out['title'] = admin_get_configuration_quote_title($opts, $config);
        }
        if (function_exists('admin_get_quote_description_text')) {
            $out['description'] = admin_get_quote_description_text($opts, $config);
        } elseif (function_exists('admin_build_options_values_only_text')) {
            $out['description'] = admin_build_options_values_only_text($opts);
        }
        // Resolve the configuration's image (custom image, then product default).
        if (function_exists('admin_config_quote_image_url')) {
            $out['image'] = (string) admin_config_quote_image_url($opts, $config);
        }
    } catch (Throwable $e) {
        pd_log('pd_quote_line_from_db failed', ['config_id' => $configId, 'error' => $e->getMessage()]);
    }
    return $out;
}

function pd_normalize_image_url(string $url): string {
    $url = trim($url);
    if ($url === '') {
        return '';
    }
    pd_try_load_admin_quote_helpers();
    if (function_exists('admin_normalize_quote_image_url')) {
        return (string) admin_normalize_quote_image_url($url);
    }
    if (strpos($url, '//') === 0) {
        return 'https:' . $url;
    }
    return $url;
}

/** Case-insensitive custom attribute lookup on a Shopify line item. */
function pd_line_item_attr_value(array $attrs, string $wantKey): string {
    $want = strtolower(trim($wantKey));
    if ($want === '') {
        return '';
    }
    foreach ($attrs as $attr) {
        if (!is_array($attr)) {
            continue;
        }
        $key = strtolower(trim((string)($attr['key'] ?? '')));
        if ($key === $want) {
            return trim((string)($attr['value'] ?? ''));
        }
    }
    return '';
}

/**
 * Resolve the public image URL for a line item: custom_image_url, then product_image_url,
 * then configuration DB (admin_config_quote_image_url).
 */
function pd_line_item_image_url(array $attrs, int $configId = 0): string {
    foreach (['custom_image_url', 'product_image_url'] as $key) {
        $url = pd_normalize_image_url(pd_line_item_attr_value($attrs, $key));
        if ($url !== '') {
            return $url;
        }
    }
    if ($configId > 0) {
        $dbLine = pd_quote_line_from_db($configId);
        return pd_normalize_image_url((string)($dbLine['image'] ?? ''));
    }
    return '';
}

/**
 * Image block names from document details (GET /documents/{id}/details → images[].name).
 *
 * @return list<string>
 */
function pd_extract_image_block_names_from_document(string $documentId): array {
    if (!function_exists('pandadoc_get_document_details')) {
        return [];
    }
    $details = pandadoc_get_document_details($documentId);
    if (empty($details['ok']) || !is_array($details['data'] ?? null)) {
        return [];
    }
    $names = [];
    $images = $details['data']['images'] ?? [];
    if (!is_array($images)) {
        return [];
    }
    foreach ($images as $img) {
        if (!is_array($img)) {
            continue;
        }
        $name = trim((string)($img['name'] ?? ''));
        if ($name !== '') {
            $names[] = $name;
        }
    }
    return $names;
}

function pd_filter_skipped_image_block_names(array $names): array {
    $envGet = static function (string $k, string $d): string {
        return (string)(function_exists('pd_env') ? pd_env($k, $d) : (getenv($k) ?: $d));
    };
    $skipRaw = trim($envGet('PANDADOC_IMAGE_BLOCK_SKIP', 'Image 1,Image_1'));
    $skip = array_map('strtolower', array_values(array_filter(array_map('trim', explode(',', $skipRaw)))));
    $out = [];
    foreach ($names as $name) {
        $name = trim((string) $name);
        if ($name === '' || in_array(strtolower($name), $skip, true)) {
            continue;
        }
        $out[] = $name;
    }
    return $out;
}

/**
 * Resolve PandaDoc image block names: env override, else document details (skip logo blocks).
 *
 * @return list<string>
 */
function pd_resolve_pandadoc_image_block_names(string $documentId): array {
    $envGet = static function (string $k, string $d): string {
        return (string)(function_exists('pd_env') ? pd_env($k, $d) : (getenv($k) ?: $d));
    };
    $configured = trim($envGet('PANDADOC_PRODUCT_IMAGE_BLOCKS', ''));
    if ($configured !== '') {
        return array_values(array_filter(array_map('trim', explode(',', $configured)), static function ($n) {
            return $n !== '';
        }));
    }
    $fromDoc = pd_filter_skipped_image_block_names(pd_extract_image_block_names_from_document($documentId));
    if (!empty($fromDoc)) {
        return $fromDoc;
    }
    return ['Image 2', 'Image 3'];
}

/**
 * PATCH image blocks on a draft document (self-contained — does not require pandadoc_apply_image_blocks).
 *
 * @param list<array{name: string, url: string}> $blocks
 * @return array{ok: bool, reason?: string, applied?: int, http?: int, data?: mixed}
 */
function pd_pandadoc_patch_image_blocks(string $documentId, array $blocks): array {
    $images = [];
    foreach ($blocks as $block) {
        if (!is_array($block)) {
            continue;
        }
        $name = trim((string)($block['name'] ?? ''));
        $url = trim((string)($block['url'] ?? ''));
        if ($name === '' || $url === '') {
            continue;
        }
        $images[] = ['name' => $name, 'urls' => [$url]];
    }
    if (empty($images)) {
        return ['ok' => false, 'reason' => 'no_images'];
    }

    if (function_exists('pandadoc_apply_image_blocks')) {
        return pandadoc_apply_image_blocks($documentId, $blocks);
    }
    if (function_exists('pandadoc_update_document')) {
        $res = pandadoc_update_document($documentId, ['images' => $images]);
        if (!empty($res['ok'])) {
            return ['ok' => true, 'applied' => count($images), 'http' => $res['code'] ?? 0, 'data' => $res['data'] ?? null];
        }
        return [
            'ok' => false,
            'reason' => 'image_update_failed',
            'http' => $res['code'] ?? 0,
            'data' => $res['data'] ?? null,
        ];
    }
    if (!function_exists('pandadoc_http')) {
        return ['ok' => false, 'reason' => 'pandadoc_http_missing'];
    }
    $res = pandadoc_http('PATCH', '/documents/' . rawurlencode($documentId), ['images' => $images], 'update_document_images');
    if (!empty($res['ok'])) {
        return ['ok' => true, 'applied' => count($images), 'http' => $res['code'] ?? 0, 'data' => $res['data'] ?? null];
    }
    return [
        'ok' => false,
        'reason' => 'image_update_failed',
        'http' => $res['code'] ?? 0,
        'data' => $res['data'] ?? null,
    ];
}

function pd_default_product_image_block_names(): array {
    $envGet = static function (string $k, string $d): string {
        return (string)(function_exists('pd_env') ? pd_env($k, $d) : (getenv($k) ?: $d));
    };
    $blockNamesRaw = trim($envGet('PANDADOC_PRODUCT_IMAGE_BLOCKS', 'Image 2,Image 3'));
    return array_values(array_filter(array_map('trim', explode(',', $blockNamesRaw)), static function ($n) {
        return $n !== '';
    }));
}

function pd_image_visibility_token_name(string $blockName): string {
    $envGet = static function (string $k, string $d): string {
        return (string)(function_exists('pd_env') ? pd_env($k, $d) : (getenv($k) ?: $d));
    };
    $suffix = trim($envGet('PANDADOC_IMAGE_VISIBILITY_TOKEN_SUFFIX', ' Show'));
    return trim($blockName) . $suffix;
}

/**
 * Per line item → image block slot (includes empty slots so visibility tokens can hide placeholders).
 *
 * @param list<string>|null $blockNames
 * @return list<array{name: string, url: string, show: bool, line_index: int}>
 */
function pd_build_product_image_slots_from_order(array $order, ?array $blockNames = null): array {
    if ($blockNames === null) {
        $blockNames = pd_default_product_image_block_names();
    }
    if (empty($blockNames)) {
        return [];
    }

    $nodes = $order['lineItems']['nodes'] ?? [];
    if (!is_array($nodes)) {
        return [];
    }

    $slots = [];
    $lineIndex = 0;
    foreach ($nodes as $li) {
        if (!is_array($li)) {
            continue;
        }
        if ($lineIndex >= count($blockNames)) {
            break;
        }
        $attrs = is_array($li['customAttributes'] ?? null) ? $li['customAttributes'] : [];
        $configId = pd_config_id_from_line_item_attrs($attrs);
        $url = pd_line_item_image_url($attrs, $configId);
        $slots[] = [
            'name' => $blockNames[$lineIndex],
            'url' => $url,
            'show' => $url !== '',
            'line_index' => $lineIndex + 1,
        ];
        $lineIndex++;
    }

    return $slots;
}

/** @return array<string, array{value: string}> */
function pd_build_image_visibility_fields_from_slots(array $slots): array {
    $fields = [];
    foreach ($slots as $slot) {
        if (!is_array($slot)) {
            continue;
        }
        $name = trim((string)($slot['name'] ?? ''));
        if ($name === '') {
            continue;
        }
        $fields[pd_image_visibility_token_name($name)] = [
            'value' => !empty($slot['show']) ? 'yes' : 'no',
        ];
    }
    return $fields;
}

/**
 * @return list<array{name: string, value: string}>
 */
function pd_build_image_visibility_tokens_from_slots(array $slots): array {
    $tokens = [];
    foreach (pd_build_image_visibility_fields_from_slots($slots) as $name => $cfg) {
        $tokens[] = ['name' => $name, 'value' => (string)($cfg['value'] ?? '')];
    }
    return $tokens;
}

function pd_empty_image_placeholder_url(): string {
    $envGet = static function (string $k, string $d): string {
        return (string)(function_exists('pd_env') ? pd_env($k, $d) : (getenv($k) ?: $d));
    };
    return pd_normalize_image_url(trim($envGet('PANDADOC_EMPTY_IMAGE_URL', '')));
}

function pd_should_fill_empty_image_slots(): bool {
    $envGet = static function (string $k, string $d): string {
        return (string)(function_exists('pd_env') ? pd_env($k, $d) : (getenv($k) ?: $d));
    };
    $v = strtolower(trim($envGet('PANDADOC_FILL_EMPTY_IMAGE_SLOTS', '0')));
    return in_array($v, ['1', 'true', 'yes', 'on'], true);
}

/**
 * @return array{ok: bool, reason?: string, http?: int, data?: mixed}
 */
function pd_pandadoc_patch_tokens(string $documentId, array $tokens): array {
    if (empty($tokens)) {
        return ['ok' => false, 'reason' => 'no_tokens'];
    }
    $body = ['tokens' => $tokens];
    if (function_exists('pandadoc_update_document')) {
        $res = pandadoc_update_document($documentId, $body);
    } elseif (function_exists('pandadoc_http')) {
        $res = pandadoc_http('PATCH', '/documents/' . rawurlencode($documentId), $body, 'update_document_tokens');
    } else {
        return ['ok' => false, 'reason' => 'pandadoc_http_missing'];
    }
    if (!empty($res['ok'])) {
        return ['ok' => true, 'http' => $res['code'] ?? 0, 'data' => $res['data'] ?? null];
    }
    return [
        'ok' => false,
        'reason' => 'token_update_failed',
        'http' => $res['code'] ?? 0,
        'data' => $res['data'] ?? null,
    ];
}

/**
 * Map order line items to PandaDoc image blocks (item 1 → first block name, item 2 → second, …).
 * Only returns slots that have a product image URL.
 *
 * @param list<string>|null $blockNames
 * @return list<array{name: string, url: string, line_index: int}>
 */
function pd_build_product_image_blocks_from_order(array $order, ?array $blockNames = null): array {
    $slots = pd_build_product_image_slots_from_order($order, $blockNames);
    $blocks = [];
    $skipped = [];
    foreach ($slots as $slot) {
        if (!empty($slot['show']) && trim((string)($slot['url'] ?? '')) !== '') {
            $blocks[] = [
                'name' => (string) $slot['name'],
                'url' => (string) $slot['url'],
                'line_index' => (int) $slot['line_index'],
            ];
            continue;
        }
        $skipped[] = [
            'line_index' => (int) ($slot['line_index'] ?? 0),
            'block' => (string) ($slot['name'] ?? ''),
        ];
    }

    pd_log('PandaDoc product image blocks built', [
        'order_name' => (string)($order['name'] ?? ''),
        'block_names' => $blockNames ?? pd_default_product_image_block_names(),
        'blocks' => array_map(static function (array $b) {
            return ['name' => $b['name'], 'line_index' => $b['line_index'], 'url' => $b['url']];
        }, $blocks),
        'skipped_lines_without_image' => $skipped,
    ]);

    return $blocks;
}

/** Build PandaDoc create-document `images` payload from order line items. */
function pd_build_pandadoc_images_payload_from_order(array $order, ?array $blockNames = null): array {
    $payload = [];
    foreach (pd_build_product_image_blocks_from_order($order, $blockNames) as $block) {
        $payload[] = [
            'name' => (string) $block['name'],
            'urls' => [(string) $block['url']],
        ];
    }
    return $payload;
}

/**
 * After document.draft, populate template Image blocks from order line item images.
 *
 * @return array{ok: bool, reason?: string, applied?: int, http?: int, data?: mixed}
 */
function pd_apply_product_images_from_order(string $documentId, array $order): array {
    $available = pd_extract_image_block_names_from_document($documentId);
    $blockNames = pd_resolve_pandadoc_image_block_names($documentId);
    $slots = pd_build_product_image_slots_from_order($order, $blockNames);
    $visibilityTokens = pd_build_image_visibility_tokens_from_slots($slots);

    $out = [
        'block_names' => $blockNames,
        'available_image_blocks' => $available,
        'visibility_tokens' => $visibilityTokens,
        'slots' => array_map(static function (array $slot) {
            return [
                'name' => $slot['name'] ?? '',
                'line_index' => $slot['line_index'] ?? 0,
                'show' => !empty($slot['show']),
            ];
        }, $slots),
    ];

    if (!empty($visibilityTokens)) {
        $tokenRes = pd_pandadoc_patch_tokens($documentId, $visibilityTokens);
        $out['visibility_token_patch'] = $tokenRes;
        if (empty($tokenRes['ok'])) {
            pd_log('PandaDoc image visibility tokens patch failed (continuing)', [
                'document_id' => $documentId,
                'result' => $tokenRes,
            ]);
        }
    }

    $blocks = [];
    foreach ($slots as $slot) {
        if (!empty($slot['show']) && trim((string)($slot['url'] ?? '')) !== '') {
            $blocks[] = [
                'name' => (string) $slot['name'],
                'url' => (string) $slot['url'],
            ];
            continue;
        }
        if (pd_should_fill_empty_image_slots()) {
            $placeholder = pd_empty_image_placeholder_url();
            if ($placeholder !== '') {
                $blocks[] = [
                    'name' => (string) $slot['name'],
                    'url' => $placeholder,
                    'placeholder' => true,
                ];
            }
        }
    }

    if (empty($blocks)) {
        if (!empty($out['visibility_token_patch']['ok'])) {
            return array_merge($out, ['ok' => true, 'applied' => 0, 'reason' => 'visibility_tokens_only']);
        }
        return array_merge($out, ['ok' => false, 'reason' => 'no_line_item_images']);
    }

    $res = pd_pandadoc_patch_image_blocks($documentId, $blocks);
    if (!empty($res['ok'])) {
        return array_merge($out, $res);
    }

    $autoNames = pd_filter_skipped_image_block_names($available);
    if (!empty($autoNames) && $autoNames !== $blockNames) {
        $retrySlots = pd_build_product_image_slots_from_order($order, $autoNames);
        $retryBlocks = [];
        foreach ($retrySlots as $slot) {
            if (!empty($slot['show']) && trim((string)($slot['url'] ?? '')) !== '') {
                $retryBlocks[] = ['name' => (string) $slot['name'], 'url' => (string) $slot['url']];
            }
        }
        if (!empty($retryBlocks)) {
            $retry = pd_pandadoc_patch_image_blocks($documentId, $retryBlocks);
            $retry['retried_with_document_block_names'] = $autoNames;
            if (!empty($retry['ok'])) {
                return array_merge($out, $retry);
            }
            $res = $retry;
        }
    }

    return array_merge($out, $res);
}

function pd_quote_description_from_line_item_attrs(array $attrs): string {
    $quoteDescription = '';
    $values = [];
    $quoteKey = function_exists('admin_quote_description_attribute_key')
        ? admin_quote_description_attribute_key()
        : 'quote_description';

    foreach ($attrs as $attr) {
        if (!is_array($attr)) {
            continue;
        }
        $key = trim((string)($attr['key'] ?? ''));
        $value = trim((string)preg_replace("/\r?\n/", ' ', (string)($attr['value'] ?? '')));
        if ($key === '' || $value === '') {
            continue;
        }
        if (strcasecmp($key, $quoteKey) === 0) {
            $quoteDescription = $value;
            continue;
        }
        if (pd_line_item_custom_attribute_skip_key($key)) {
            continue;
        }
        $values[] = $value;
    }

    if ($quoteDescription !== '') {
        return $quoteDescription;
    }
    return implode(' / ', $values);
}

function pd_build_rows_from_order(array $order): array {
    $rows = [];
    $items = $order['lineItems']['nodes'] ?? [];
    if (!is_array($items)) return $rows;

    $i = 0;
    foreach ($items as $item) {
        if (!is_array($item)) continue;
        $i++;

        $title = trim((string)($item['title'] ?? ''));
        $qty = (int)($item['quantity'] ?? 0);
        $price = (string)($item['originalUnitPriceSet']['shopMoney']['amount'] ?? '');

        $attrs = $item['customAttributes'] ?? [];
        if (!is_array($attrs)) {
            $attrs = [];
        }
        $configId = pd_config_id_from_line_item_attrs($attrs);
        $dbLine = pd_quote_line_from_db($configId);
        if ($dbLine['title'] !== '') {
            $title = $dbLine['title'];
        }

        $descText = $dbLine['description'] !== ''
            ? $dbLine['description']
            : pd_quote_description_from_line_item_attrs($attrs);

        $desc = $title;
        if ($descText !== '') {
            $desc .= "\n" . $descText;
        }

        $rows[] = [
            'sr' => $i,
            'description' => $desc,
            'qty' => $qty,
            'price' => $price,
        ];
    }

    return $rows;
}

function pd_build_pricing_tables_from_order(array $order): array {
    $rows = pd_build_rows_from_order($order);
    $currency = (string)($order['subtotalPriceSet']['shopMoney']['currencyCode'] ?? 'USD');
    $taxAmount = (float)($order['totalTaxSet']['shopMoney']['amount'] ?? '0');
    $feeAmount = (float)($order['totalShippingPriceSet']['shopMoney']['amount'] ?? '0');

    pd_log('Shopify order pricing table (PandaDoc Pricing Table 2)', [
        'order_name' => (string)($order['name'] ?? ''),
        'order_gid' => (string)($order['id'] ?? ''),
        'currency' => $currency,
        'line_items' => count($rows),
        'tax' => $taxAmount,
        'fee' => $feeAmount,
    ]);

    if (empty($rows)) return [];

    $pdRows = [];
    foreach ($rows as $r) {
        if (!is_array($r)) continue;
        $desc = (string)($r['description'] ?? '');
        $sr = (int)($r['sr'] ?? 0);
        $qty = (int)($r['qty'] ?? 0);
        $priceRaw = (string)($r['price'] ?? '0');
        $price = (float)$priceRaw;

        // Use first line as Name, rest as Description (keeps table clean)
        $parts = preg_split("/\r?\n/", $desc);
        $name = trim((string)($parts[0] ?? 'Item'));
        $descRest = trim(implode("\n", array_slice($parts ?: [], 1)));

        $rowData = [
            'Name' => $name,
            'Description' => $descRest,
            'Price' => $price,
            'QTY' => $qty,
            'SKU' => (string)$sr,
            // Keep line Tax/Fee at 0% so only summary totals apply.
            'Tax' => ['type' => 'percent', 'value' => 0.0],
            'Fee' => ['type' => 'percent', 'value' => 0.0],
        ];

        $pdRows[] = [
            'options' => [
                'optional' => false,
                'optional_selected' => true,
                'qty_editable' => false,
            ],
            'data' => $rowData,
        ];
    }

    return [
        [
            'name' => 'Pricing Table 2',
            'data_merge' => true,
            'options' => [
                'currency' => $currency,
                'Tax' => [
                    'type' => 'absolute',
                    'name' => 'Tax',
                    'value' => $taxAmount,
                ],
                'Fee' => [
                    'type' => 'absolute',
                    'name' => 'Fee',
                    'value' => $feeAmount,
                ],
            ],
            'sections' => [
                [
                    'title' => 'Items',
                    'default' => true,
                    'rows' => $pdRows,
                ],
            ],
        ],
    ];
}

/**
 * Build quote-builder line items from a Shopify order.
 *
 * Note: PandaDoc's public Quote API tags every API-managed item as
 * reference_type "public-api" and ignores inline thumbnail/images, so per-line
 * product images are not supported here. We populate data only.
 *
 * @return array{items: list<array<string,mixed>>, currency: string, subtotal: float}
 */
function pd_build_quote_items_from_order(array $order): array {
    $nodes = $order['lineItems']['nodes'] ?? [];
    $currency = (string)($order['subtotalPriceSet']['shopMoney']['currencyCode'] ?? 'USD');
    if (!is_array($nodes)) return ['items' => [], 'currency' => $currency, 'subtotal' => 0.0];

    $items = [];
    $subtotal = 0.0;
    $i = 0;
    foreach ($nodes as $li) {
        if (!is_array($li)) continue;
        $i++;

        $title = trim((string)($li['title'] ?? ''));
        $qty = (int)($li['quantity'] ?? 0);
        $price = (string)($li['originalUnitPriceSet']['shopMoney']['amount'] ?? '0');

        $attrs = is_array($li['customAttributes'] ?? null) ? $li['customAttributes'] : [];
        $configId = pd_config_id_from_line_item_attrs($attrs);
        $dbLine = pd_quote_line_from_db($configId);
        if ($dbLine['title'] !== '') $title = $dbLine['title'];
        if ($title === '') $title = 'Item ' . $i;

        $descText = $dbLine['description'] !== ''
            ? $dbLine['description']
            : pd_quote_description_from_line_item_attrs($attrs);

        $shopifySku = trim((string)($li['sku'] ?? ''));
        $displaySku = $shopifySku !== ''
            ? $shopifySku
            : ($configId > 0 ? ('CFG-' . $configId) : ('LINE-' . $i));

        $items[] = [
            'sku' => $displaySku,
            'name' => $title,
            'description' => $descText,
            'qty' => (string)$qty,
            'price' => $price,
            'type' => 'product',
            'options' => ['qty_editable' => false, 'selected' => true],
        ];
        $subtotal += (float)$price * max(0, $qty);
    }

    return ['items' => $items, 'currency' => $currency, 'subtotal' => $subtotal];
}

/**
 * Build the quote-level summary (tax + shipping) from a Shopify order.
 *
 * Shopify provides absolute amounts; PandaDoc summary adjustments are either
 * "percent" or "flat". Defaults to "percent" (matching a template configured
 * with % tax/fee) by deriving the rate from the subtotal so totals still match
 * the order. Override per env:
 *   PANDADOC_QUOTE_SUMMARY_TYPE = percent|flat   (default percent)
 *   PANDADOC_QUOTE_TAX_NAME      = adjustment name in template (default "Tax")
 *   PANDADOC_QUOTE_SHIPPING_NAME = adjustment name in template (default "Shipping")
 *
 * @return array{taxes?: array<string,array{type:string,value:float}>, fees?: array<string,array{type:string,value:float}>}
 */
function pd_build_quote_summary_from_order(array $order, float $subtotalBase): array {
    $taxAmount = (float)($order['totalTaxSet']['shopMoney']['amount'] ?? 0);
    $feeAmount = (float)($order['totalShippingPriceSet']['shopMoney']['amount'] ?? 0);

    $envGet = static function (string $k, string $d): string {
        return (string)(function_exists('pd_env') ? pd_env($k, $d) : (getenv($k) ?: $d));
    };
    $type = strtolower(trim($envGet('PANDADOC_QUOTE_SUMMARY_TYPE', 'percent')));
    if (!in_array($type, ['percent', 'flat'], true)) $type = 'percent';
    $taxName = trim($envGet('PANDADOC_QUOTE_TAX_NAME', 'Tax'));
    $feeName = trim($envGet('PANDADOC_QUOTE_SHIPPING_NAME', 'Shipping'));

    $toValue = static function (float $amount) use ($type, $subtotalBase): float {
        if ($type === 'percent') {
            return $subtotalBase > 0 ? round($amount / $subtotalBase * 100, 4) : 0.0;
        }
        return round($amount, 2);
    };

    $summary = [];
    if ($taxAmount > 0 && $taxName !== '') {
        $summary['taxes'] = [$taxName => ['type' => $type, 'value' => $toValue($taxAmount)]];
    }
    if ($feeAmount > 0 && $feeName !== '') {
        $summary['fees'] = [$feeName => ['type' => $type, 'value' => $toValue($feeAmount)]];
    }
    return $summary;
}

/**
 * After the document reaches document.draft, populate its Quote block with the
 * order's line items plus a tax/shipping summary. Requires the template to
 * contain a Quote block (added in the PandaDoc editor; no API exists for that).
 *
 * @return array{ok: bool, reason?: string, quote_id?: string, items?: int, http?: int, data?: mixed}
 */
function pd_apply_quote_from_order(string $documentId, array $order, string $quoteName = ''): array {
    $details = pandadoc_get_document_details($documentId);
    if (empty($details['ok']) || !is_array($details['data'] ?? null)) {
        return ['ok' => false, 'reason' => 'details_failed', 'http' => $details['code'] ?? 0, 'data' => $details['data'] ?? null];
    }

    $quotes = pandadoc_extract_quotes_from_details($details['data']);
    if (empty($quotes)) {
        return ['ok' => false, 'reason' => 'no_quote_block_in_document'];
    }

    // Target the quote block by name (default "Quote 1"); falls back to first.
    if ($quoteName === '') {
        $quoteName = (string)(function_exists('pd_env') ? pd_env('PANDADOC_QUOTE_NAME', 'Quote 1') : 'Quote 1');
    }
    $q = pandadoc_pick_quote($quotes, $quoteName);
    if ($q === null) return ['ok' => false, 'reason' => 'quote_not_found', 'quote_name' => $quoteName];

    $built = pd_build_quote_items_from_order($order);
    if (empty($built['items'])) return ['ok' => false, 'reason' => 'no_items'];

    $section = [
        'name' => $q['sections'][0]['name'] ?? 'Items',
        'items' => $built['items'],
        'settings' => ['selection_type' => 'custom', 'optional' => false, 'selected' => true],
    ];
    if (!empty($q['sections'][0]['id'])) $section['id'] = $q['sections'][0]['id'];

    $summary = pd_build_quote_summary_from_order($order, (float)($built['subtotal'] ?? 0));
    $res = pandadoc_update_quote($documentId, $q['quote_id'], [$section], $summary, (string)($built['currency'] ?? ''));
    if (!empty($res['ok'])) {
        return ['ok' => true, 'quote_id' => $q['quote_id'], 'items' => count($built['items'])];
    }
    return ['ok' => false, 'reason' => 'quote_update_failed', 'http' => $res['code'] ?? 0, 'data' => $res['data'] ?? null];
}

function pd_write_order_rows_txt(int $shopifyOrderId, array $rows, string $filePrefix = 'order'): void {
    $rootDir = dirname(__DIR__, 1);
    $file = $rootDir . '/' . $filePrefix . '_' . $shopifyOrderId . '_rows.txt';
    $payload = [
        'order_id' => $shopifyOrderId,
        'rows' => $rows,
    ];
    @file_put_contents($file, json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}

function pd_templates(): array {
    $doc1 = pd_env('PANDADOC_DOC1_TEMPLATE_UUID', '');
    $doc3 = pd_env('PANDADOC_DOC3_TEMPLATE_UUID', '');
    return ['doc1' => trim((string)$doc1), 'doc3' => trim((string)$doc3)];
}

function pd_default_drive_pdf_url(): string {
    return trim((string)pd_env('PANDADOC_DRIVE_PDF_URL', ''));
}

function pd_extract_custom_metafield(array $order, string $key): ?array {
    $mfs = $order['metafields']['nodes'] ?? [];
    if (!is_array($mfs)) return null;
    foreach ($mfs as $mf) {
        if (!is_array($mf)) continue;
        if (($mf['key'] ?? null) === $key) return $mf;
    }
    return null;
}

/** Summarize custom metafields for webhook/debug logs. */
function pd_summarize_custom_metafields(array $entity): array {
    $out = [];
    $mfs = $entity['metafields']['nodes'] ?? [];
    if (!is_array($mfs)) {
        return $out;
    }
    foreach ($mfs as $mf) {
        if (!is_array($mf)) {
            continue;
        }
        $key = (string)($mf['key'] ?? '');
        if ($key === '') {
            continue;
        }
        $out[$key] = $mf['value'] ?? null;
    }
    return $out;
}

/** Resolve Google Drive PDF URL from known metafield key variants. */
function pd_extract_drive_pdf_url_from_entity(array $entity): ?string {
    foreach (['drive_pdf_url', 'google_drive_link', 'google_drive_url', 'drive_pdf_link'] as $key) {
        $mf = pd_extract_custom_metafield($entity, $key);
        if (!is_array($mf)) {
            continue;
        }
        $url = trim((string)($mf['value'] ?? ''));
        if ($url !== '') {
            return $url;
        }
    }
    return null;
}

function pd_metafield_is_true(?array $mf): bool {
    if (!is_array($mf)) {
        return false;
    }
    $value = $mf['value'] ?? null;
    return ($value === true) || ((string)$value === 'true') || ((string)$value === '1');
}

/** Normalize draft order GraphQL shape so order builders can reuse it. */
function pd_normalize_commerce_entity(array $entity): array {
    // DraftOrder uses note2; Order uses note.
    if (!isset($entity['note']) && isset($entity['note2'])) {
        $entity['note'] = $entity['note2'];
    }
    if (!isset($entity['totalShippingPriceSet']) && isset($entity['shippingLine']['originalPriceSet'])) {
        $entity['totalShippingPriceSet'] = $entity['shippingLine']['originalPriceSet'];
    }
    return $entity;
}

/**
 * If this Shopify order was created from a draft that already generated PandaDoc, return that DB row.
 */
function pd_order_blocked_by_draft_generation(int $orderId): ?array {
    if ($orderId <= 0) {
        return null;
    }
    foreach (pd_db_list_draft_rows_with_documents() as $row) {
        if (!is_array($row)) {
            continue;
        }
        $draftId = (int)($row['shopify_draft_order_id'] ?? 0);
        if ($draftId <= 0) {
            continue;
        }
        $linkedOrderId = shopify_draft_order_linked_order_id($draftId);
        if ($linkedOrderId === $orderId) {
            return $row;
        }
    }
    return null;
}

function pd_sync_order_metafields_from_draft_row(int $orderId, array $draftRow): void {
    $docId = trim((string)($draftRow['pandadoc_document_id'] ?? ''));
    $status = trim((string)($draftRow['status'] ?? 'sent'));
    if ($docId === '') {
        return;
    }
    $metafields = [
        ['namespace' => 'custom', 'key' => 'document_id', 'type' => 'single_line_text_field', 'value' => $docId],
        ['namespace' => 'custom', 'key' => 'document_view_url', 'type' => 'single_line_text_field', 'value' => 'https://app.pandadoc.com/a/#/documents/' . $docId],
        ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => $status],
        ['namespace' => 'custom', 'key' => 'document_generate', 'type' => 'boolean', 'value' => false],
        ['namespace' => 'custom', 'key' => 'document_generate_locked', 'type' => 'boolean', 'value' => true],
    ];
    try {
        shopify_set_order_metafields($orderId, $metafields);
    } catch (Throwable $e) {
        pd_log('pd_sync_order_metafields_from_draft_row failed', ['order_id' => $orderId, 'error' => $e->getMessage()]);
    }
}

function pd_build_fields_from_order(array $order): array {
    // Shopify order (GraphQL minimal) → PandaDoc fields for Template 1.
    $shipping = $order['shippingAddress'] ?? [];
    $billing = $order['billingAddress'] ?? [];
    $cust = $order['customer'] ?? [];

    // Core recipient info (existing behaviour)
    $fullName =
        trim((string)($shipping['name'] ?? '')) ?:
        trim((string)(($cust['firstName'] ?? '') . ' ' . ($cust['lastName'] ?? ''))) ?:
        trim((string)($billing['name'] ?? ''));

    $email = (string)($order['email'] ?? ($cust['email'] ?? ''));

    $addr = $shipping ?: $billing;
    $addressLine = trim((string)($addr['address1'] ?? ''));
    if (!empty($addr['address2'])) {
        $addressLine .= ($addressLine ? ', ' : '') . trim((string)$addr['address2']);
    }
    $city = trim((string)($addr['city'] ?? ''));
    $province = trim((string)($addr['province'] ?? ''));
    $zip = trim((string)($addr['zip'] ?? ''));
    $country = trim((string)($addr['country'] ?? ''));

    // New Template-1 variables you requested
    $billAddr = $billing ?: $shipping;
    $shipAddr = $shipping ?: $billing;

    $billAddressLine = trim((string)($billAddr['address1'] ?? ''));
    if (!empty($billAddr['address2'])) {
        $billAddressLine .= ($billAddressLine ? ', ' : '') . trim((string)$billAddr['address2']);
    }
    $billCity = trim((string)($billAddr['city'] ?? ''));
    $billProvince = trim((string)($billAddr['province'] ?? ''));
    $billZip = trim((string)($billAddr['zip'] ?? ''));
    $billCountry = trim((string)($billAddr['country'] ?? ''));
    $billPhone = trim((string)($billAddr['phone'] ?? ''));
    $billName = trim((string)($billAddr['name'] ?? ''));

    $shipAddressLine = trim((string)($shipAddr['address1'] ?? ''));
    if (!empty($shipAddr['address2'])) {
        $shipAddressLine .= ($shipAddressLine ? ', ' : '') . trim((string)$shipAddr['address2']);
    }
    $shipCity = trim((string)($shipAddr['city'] ?? ''));
    $shipProvince = trim((string)($shipAddr['province'] ?? ''));
    $shipZip = trim((string)($shipAddr['zip'] ?? ''));
    $shipCountry = trim((string)($shipAddr['country'] ?? ''));
    $shipPhone = trim((string)($shipAddr['phone'] ?? ''));
    $shipName = trim((string)($shipAddr['name'] ?? ''));

    $subtotalRaw = (string)($order['subtotalPriceSet']['shopMoney']['amount'] ?? '0.00');
    $totalRaw = (string)($order['totalPriceSet']['shopMoney']['amount'] ?? '0.00');

    $customerPhone = trim((string)($cust['phone'] ?? ''));
    if ($customerPhone === '') {
        $customerPhone = $billPhone !== '' ? $billPhone : $shipPhone;
    }
    if ($customerPhone === '') {
        $customerPhone = trim((string)($order['phone'] ?? ''));
    }

    $orderNote = trim((string)($order['note'] ?? ''));
    $orderNote = trim((string)preg_replace('/\s+/u', ' ', $orderNote));

    return [
        // Existing fields (already used in template)
        'OrderName' => ['value' => (string)($order['name'] ?? '')],
        'RecipientName' => ['value' => $fullName],
        'RecipientEmail' => ['value' => $email],
        'RecipientAddress' => ['value' => trim($addressLine)],
        'RecipientCity' => ['value' => $city],
        'RecipientProvince' => ['value' => $province],
        'RecipientZip' => ['value' => $zip],
        'RecipientCountry' => ['value' => $country],

        // New Template 1 merge fields (names must match your PandaDoc template variables)
        'Bill To Address' => ['value' => trim($billAddressLine . ($billCity ? ', ' . $billCity : '') . ($billProvince ? ', ' . $billProvince : '') . ($billZip ? ' ' . $billZip : '') . ($billCountry ? ', ' . $billCountry : ''))],
        'Bill to Email' => ['value' => $email],
        'Bill to Name' => ['value' => $billName !== '' ? $billName : $shipName],
        'Bill to Phone' => ['value' => $billPhone !== '' ? $billPhone : $shipPhone],

        'extension' => ['value' => $shipZip !== '' ? $shipZip : $billZip],
        'notes' => ['value' => $orderNote],
        // Remove leading "#" from Shopify order name for the token
        'Order Number' => ['value' => ltrim((string)($order['name'] ?? ''), '#')],
        'SALES PERSON' => ['value' => 'Asher'],
        'Ship To Address' => ['value' => trim($shipAddressLine . ($shipCity ? ', ' . $shipCity : '') . ($shipProvince ? ', ' . $shipProvince : '') . ($shipZip ? ' ' . $shipZip : '') . ($shipCountry ? ', ' . $shipCountry : ''))],
        'Ship to Call' => ['value' => $shipPhone],
        // Updated mapping: Ship to Email = shipping email (order email), Ship to Name = shipping name
        'Ship to Email' => ['value' => $email],
        'Ship to Name' => ['value' => $shipName],
        'Customer Phone' => ['value' => $customerPhone],
        'Sub Total' => ['value' => pd_money_fmt_usd((float)$subtotalRaw)],
        'Total' => ['value' => pd_money_fmt_usd((float)$totalRaw)],
    ];

    $imageVisibility = pd_build_image_visibility_fields_from_slots(
        pd_build_product_image_slots_from_order($order, pd_default_product_image_block_names())
    );
    if (!empty($imageVisibility)) {
        $fields = array_merge($fields, $imageVisibility);
    }

    return $fields;
}

function pd_primary_recipient_from_order(array $order): array {
    $cust = $order['customer'] ?? [];
    $shipping = $order['shippingAddress'] ?? [];
    $billing = $order['billingAddress'] ?? [];
    $email = (string)($order['email'] ?? ($cust['email'] ?? ''));
    $first = (string)($shipping['firstName'] ?? $billing['firstName'] ?? $cust['firstName'] ?? '');
    $last  = (string)($shipping['lastName'] ?? $billing['lastName'] ?? $cust['lastName'] ?? '');
    if (trim($first . $last) === '' && !empty($shipping['name'])) {
        $parts = preg_split('/\s+/', trim((string)$shipping['name']));
        $first = $parts[0] ?? '';
        $last = count($parts) > 1 ? implode(' ', array_slice($parts, 1)) : '';
    }
    return [
        'email' => $email,
        'first_name' => $first,
        'last_name' => $last,
        'role' => 'Client',
    ];
}

function pd_run_generate_for_order(int $shopifyOrderId, ?string $drivePdfUrlOverride = null): array {
    return pd_run_generate_entity('order', $shopifyOrderId, $drivePdfUrlOverride);
}

function pd_run_generate_for_draft_order(int $shopifyDraftOrderId, ?string $drivePdfUrlOverride = null): array {
    return pd_run_generate_entity('draft_order', $shopifyDraftOrderId, $drivePdfUrlOverride);
}

function pd_run_generate_entity(string $entityType, int $entityId, ?string $drivePdfUrlOverride = null): array {
    $isDraft = ($entityType === 'draft_order');
    $logKey = $isDraft ? 'draft_order_id' : 'order_id';

    pd_log('pd_run_generate_entity called', [$logKey => $entityId, 'entity_type' => $entityType, 'at' => basename(__FILE__) . ':' . __LINE__]);
    if (function_exists('shopify_webhook_log')) {
        shopify_webhook_log('pd_run_generate_entity started', ['entity_type' => $entityType, $logKey => $entityId]);
    }
    pd_db_ensure_tables();
    $pdo = pd_db();

    $setMetafields = static function (array $metafields) use ($isDraft, $entityId): void {
        if ($isDraft) {
            shopify_set_draft_order_metafields($entityId, $metafields);
            return;
        }
        shopify_set_order_metafields($entityId, $metafields);
    };
    $updateRow = static function (array $fields) use ($isDraft, $entityId): void {
        if ($isDraft) {
            pd_db_update_draft_row($entityId, $fields);
            return;
        }
        pd_db_update_order($entityId, $fields);
    };

    // Idempotency + single-flight per order/draft
    $pdo->beginTransaction();
    try {
        $row = $isDraft ? pd_db_get_or_create_draft_row($entityId) : pd_db_get_or_create_order_row($entityId);
        pd_log('pd_run_generate_entity row fetched', [$logKey => $entityId, 'row' => $row]);
        $status = (string)($row['status'] ?? 'idle');
        $existingDocId = (string)($row['pandadoc_document_id'] ?? '');
        if ($existingDocId !== '' && in_array($status, ['building', 'sent', 'viewed', 'completed'], true)) {
            $pdo->commit();
            return ['success' => true, 'already' => true, 'pandadoc_document_id' => $existingDocId, 'status' => $status, 'entity_type' => $entityType];
        }
        $updateRow(['status' => 'queued', 'last_error' => null]);
        $pdo->commit();
    } catch (Throwable $e) {
        try {
            $pdo->rollBack();
        } catch (Throwable $rb) {
            // Ignore "no active transaction" (e.g. already committed) so we still report the original error
        }
        $errMsg = 'DB error: ' . $e->getMessage();
        pd_log('pd_run_generate_entity idempotency failed', [$logKey => $entityId, 'error' => $errMsg]);
        return ['success' => false, 'error' => $errMsg, 'entity_type' => $entityType];
    }

    $updateRow(['last_api_step' => pd_step_string('started')]);
    pd_log('pd_run_generate_entity started', [$logKey => $entityId, 'entity_type' => $entityType]);

    try {
        $setMetafields([
            ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'queued'],
        ]);

        $updateRow(['last_api_step' => pd_step_string('fetch_order')]);
        $order = $isDraft
            ? pd_normalize_commerce_entity(shopify_get_draft_order_minimal_or_throw($entityId))
            : shopify_get_order_minimal_or_throw($entityId);
        pd_write_order_rows_txt(
            $entityId,
            pd_build_rows_from_order($order),
            $isDraft ? 'draft_order' : 'order'
        );
        $templates = pd_templates();
        if ($templates['doc1'] === '' || $templates['doc3'] === '') {
            $msg = 'Missing PandaDoc template UUIDs. Set env PANDADOC_DOC1_TEMPLATE_UUID and PANDADOC_DOC3_TEMPLATE_UUID.';
            $updateRow(['status' => 'error', 'last_error' => $msg]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => $msg, 'entity_type' => $entityType];
        }

        $driveUrl = $drivePdfUrlOverride;
        if (!$driveUrl) {
            $driveUrl = pd_extract_drive_pdf_url_from_entity($order);
        }
        if (!$driveUrl) {
            $driveUrl = pd_default_drive_pdf_url();
        }
        $driveUrl = trim((string)$driveUrl);
        if ($driveUrl === '') {
            $ownerLabel = $isDraft ? 'draft order' : 'order';
            $msg = 'Missing Google Drive PDF URL. Add ' . $ownerLabel . ' metafield custom.drive_pdf_url (Google Drive link), or set PANDADOC_DRIVE_PDF_URL in config as fallback.';
            $updateRow(['status' => 'error', 'last_error' => $msg]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => $msg, 'entity_type' => $entityType];
        }

        $updateRow(['status' => 'building', 'drive_pdf_url' => $driveUrl, 'last_error' => null]);
        $setMetafields([
            ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'building'],
        ]);

        $recipient = pd_primary_recipient_from_order($order);
        if (empty($recipient['email'])) {
            $msg = 'No recipient email (entity.email/customer.email).';
            $updateRow(['status' => 'error', 'last_error' => $msg]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => $msg, 'entity_type' => $entityType];
        }

        $entityLabel = $isDraft ? 'Draft order' : 'Order';
        $docName = $entityLabel . ' ' . ((string)($order['name'] ?? ('#' . $entityId))) . ' - Signature';
        $fields = pd_build_fields_from_order($order);
        $pricingTables = pd_build_pricing_tables_from_order($order);

        $updateRow(['last_api_step' => pd_step_string('create_document')]);
        pd_log('PandaDoc step: create_document', [$logKey => $entityId, 'entity_type' => $entityType, 'template' => $templates['doc1'], 'name' => $docName]);
        $create = pandadoc_create_from_template($templates['doc1'], $docName, [$recipient], $fields, $pricingTables);
        $updateRow(['last_api_response' => json_encode($create)]);
        if (!$create['success']) {
            $msg = 'PandaDoc create-from-template failed';
            $updateRow(['status' => 'error', 'last_error' => json_encode($create)]);
            pd_log('PandaDoc create_document FAILED', [$logKey => $entityId, 'response' => $create]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => $msg, 'details' => $create, 'entity_type' => $entityType];
        }
        $docId = (string)$create['id'];
        pd_log('PandaDoc create_document OK', [$logKey => $entityId, 'doc_id' => $docId]);
        $updateRow(['pandadoc_document_id' => $docId]);
        $docViewUrl = 'https://app.pandadoc.com/a/#/documents/' . $docId;
        $setMetafields([
            ['namespace' => 'custom', 'key' => 'document_id', 'type' => 'single_line_text_field', 'value' => $docId],
            ['namespace' => 'custom', 'key' => 'document_view_url', 'type' => 'single_line_text_field', 'value' => $docViewUrl],
        ]);

        $docStatus = pandadoc_wait_for_document_status($docId, ['document.draft'], 45);
        if ($docStatus !== 'document.draft') {
            $msg = 'PandaDoc document not ready (status: ' . $docStatus . '). Wait for document.draft before adding sections.';
            $updateRow(['status' => 'error', 'last_error' => $msg]);
            pd_log('PandaDoc document not draft before add_section', [$logKey => $entityId, 'doc_id' => $docId, 'status' => $docStatus]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => $msg, 'entity_type' => $entityType];
        }

        // Populate the "Quote 1" block with the order's line items + summary.
        // Non-fatal: a failure here won't abort the document.
        $quoteApply = pd_apply_quote_from_order($docId, $order);
        $updateRow(['last_api_response' => json_encode($quoteApply)]);
        if (empty($quoteApply['ok'])) {
            pd_log('PandaDoc quote apply failed (continuing)', [$logKey => $entityId, 'doc_id' => $docId, 'result' => $quoteApply]);
        } else {
            pd_log('PandaDoc quote apply OK', [$logKey => $entityId, 'doc_id' => $docId, 'quote_id' => $quoteApply['quote_id'] ?? '', 'items' => $quoteApply['items'] ?? 0]);
        }

        // Populate Image 2 / Image 3 (etc.) from line item custom_image_url or product image.
        try {
            $imageApply = pd_apply_product_images_from_order($docId, $order);
        } catch (Throwable $imageEx) {
            $imageApply = ['ok' => false, 'reason' => 'exception', 'error' => $imageEx->getMessage()];
        }
        if (empty($imageApply['ok'])) {
            pd_log('PandaDoc product images apply failed or skipped (continuing)', [$logKey => $entityId, 'doc_id' => $docId, 'result' => $imageApply]);
        } else {
            pd_log('PandaDoc product images apply OK', [$logKey => $entityId, 'doc_id' => $docId, 'applied' => $imageApply['applied'] ?? 0, 'block_names' => $imageApply['block_names'] ?? []]);
        }

        $updateRow(['last_api_step' => pd_step_string('add_section_from_url')]);
        $directDriveUrl = drive_share_to_direct_download($driveUrl);
        $drivePreflight = drive_preflight_pdf_url($directDriveUrl);
        pd_log('PandaDoc step: add_section_from_url', [
            $logKey => $entityId,
            'doc_id' => $docId,
            'drive_url_original' => $driveUrl,
            'drive_url_direct' => $directDriveUrl,
            'drive_preflight' => $drivePreflight,
        ]);
        if (function_exists('shopify_webhook_log')) {
            shopify_webhook_log('Drive PDF preflight', [
                $logKey => $entityId,
                'drive_url_original' => $driveUrl,
                'drive_url_direct' => $directDriveUrl,
                'preflight_ok' => $drivePreflight['ok'] ?? false,
                'preflight_error' => $drivePreflight['error'] ?? null,
            ]);
        }
        if (empty($drivePreflight['ok'])) {
            $msg = 'Google Drive PDF is not accessible as a PDF: ' . (string)($drivePreflight['error'] ?? 'unknown')
                . '. Share the file as Anyone with the link can view, use a real PDF (not a placeholder URL), then retry.';
            $updateRow(['status' => 'error', 'last_error' => $msg]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => $msg, 'drive_preflight' => $drivePreflight, 'entity_type' => $entityType];
        }
        $sec2 = pandadoc_create_section_from_url($docId, $directDriveUrl, 'Order Drawing');
        $updateRow(['last_api_response' => json_encode($sec2)]);
        if (!$sec2['success']) {
            $updateRow(['status' => 'error', 'last_error' => json_encode($sec2)]);
            pd_log('PandaDoc add_section_from_url FAILED', [$logKey => $entityId, 'response' => $sec2]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => 'Failed to add Drive PDF section', 'details' => $sec2, 'entity_type' => $entityType];
        }
        $wait2 = pandadoc_wait_section_processed($docId, (string)$sec2['upload_id'], 90);
        if (!$wait2['success']) {
            $updateRow(['status' => 'error', 'last_error' => json_encode($wait2)]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            $errDetail = is_array($wait2['error'] ?? null) ? json_encode($wait2['error']) : (string)($wait2['error'] ?? '');
            $msg = 'Drive PDF section processing failed. PandaDoc could not read the PDF from Google Drive'
                . ($errDetail !== '' ? ': ' . $errDetail : '')
                . '. Ensure the file is a valid PDF shared as Anyone with the link can view.';
            pd_log('PandaDoc add_section_from_url processing FAILED', [$logKey => $entityId, 'response' => $wait2, 'drive_url' => $directDriveUrl]);
            return ['success' => false, 'error' => $msg, 'details' => $wait2, 'entity_type' => $entityType];
        }

        $updateRow(['last_api_step' => pd_step_string('add_section_from_template')]);
        pd_log('PandaDoc step: add_section_from_template', [$logKey => $entityId, 'doc_id' => $docId]);
        $sec3 = pandadoc_create_section_from_template($docId, $templates['doc3'], 'Terms & Signature', [$recipient]);
        $updateRow(['last_api_response' => json_encode($sec3)]);
        if (!$sec3['success']) {
            $updateRow(['status' => 'error', 'last_error' => json_encode($sec3)]);
            pd_log('PandaDoc add_section_from_template FAILED', [$logKey => $entityId, 'response' => $sec3]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => 'Failed to add Terms section', 'details' => $sec3, 'entity_type' => $entityType];
        }
        $wait3 = pandadoc_wait_section_processed($docId, (string)$sec3['upload_id'], 90);
        if (!$wait3['success']) {
            $updateRow(['status' => 'error', 'last_error' => json_encode($wait3)]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => 'Terms section processing failed', 'details' => $wait3, 'entity_type' => $entityType];
        }

        $st = pandadoc_wait_for_document_status($docId, ['document.draft'], 45);
        if ($st !== 'document.draft') {
            pd_log('Document not draft before send', ['docId' => $docId, 'status' => $st]);
        }

        $updateRow(['last_api_step' => pd_step_string('send_document')]);
        pd_log('PandaDoc step: send_document', [$logKey => $entityId, 'doc_id' => $docId]);
        $send = pandadoc_send_document(
            $docId,
            'Please sign: ' . ((string)($order['name'] ?? ($entityLabel . ' #' . $entityId))),
            'Hi ' . ($recipient['first_name'] ?: 'there') . ', please review and sign the document.',
            true
        );
        $updateRow(['last_api_response' => json_encode($send)]);
        if (!$send['success']) {
            $updateRow(['status' => 'error', 'last_error' => json_encode($send)]);
            pd_log('PandaDoc send_document FAILED', [$logKey => $entityId, 'response' => $send]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => 'Failed to send PandaDoc document', 'details' => $send, 'entity_type' => $entityType];
        }

        $updateRow(['last_api_step' => pd_step_string('create_document_session')]);
        pd_log('PandaDoc step: create_document_session', [$logKey => $entityId, 'doc_id' => $docId]);
        $session = pandadoc_create_document_session($docId, (string) $recipient['email'], null);
        $updateRow(['last_api_response' => json_encode($session)]);
        if (!$session['success'] || empty($session['id'])) {
            $msg = $isDraft
                ? 'PandaDoc signing session failed (document was sent)'
                : 'PandaDoc signing session failed (document was sent; no HubSpot sign URL)';
            $updateRow(['status' => 'error', 'last_error' => json_encode($session)]);
            pd_log('PandaDoc create_document_session FAILED', [$logKey => $entityId, 'response' => $session]);
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
            return ['success' => false, 'error' => $msg, 'details' => $session, 'pandadoc_document_id' => $docId, 'entity_type' => $entityType];
        }
        $signUrl = pandadoc_recipient_sign_url((string) $session['id']);

        $hs = null;
        $lastErr = null;
        if (!$isDraft) {
            $hs = pd_hubspot_on_pandadoc_sign_url_ready($entityId, $signUrl);
            if (!empty($hs['ok'])) {
                pd_log('HubSpot pandadoc_sign_url OK', ['order_id' => $entityId, 'hubspot_order_id' => $hs['order_id'] ?? null]);
            } elseif (!empty($hs['skipped'])) {
                pd_log('HubSpot pandadoc_sign_url skipped', ['order_id' => $entityId, 'hubspot' => $hs]);
            } else {
                pd_log('HubSpot pandadoc_sign_url patch failed', ['order_id' => $entityId, 'hubspot' => $hs, 'sign_url' => $signUrl]);
            }
            if (empty($hs['ok']) && empty($hs['skipped'])) {
                $lastErr = json_encode($hs);
            }
        } else {
            pd_log('HubSpot pandadoc_sign_url skipped for draft order', ['draft_order_id' => $entityId]);
        }

        $updateRow([
            'status' => 'sent',
            'last_error' => $lastErr,
            'last_api_step' => null,
            'last_api_response' => null,
        ]);
        $setMetafields([
            ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'sent'],
            ['namespace' => 'custom', 'key' => 'document_generate_locked', 'type' => 'boolean', 'value' => true],
        ]);

        $result = [
            'success' => true,
            'pandadoc_document_id' => $docId,
            'status' => 'sent',
            'pandadoc_sign_url' => $signUrl,
            'entity_type' => $entityType,
        ];
        if (!$isDraft) {
            $result['hubspot_sign_url_sync'] = $hs;
        }
        return $result;
    } catch (Throwable $e) {
        $errMsg = $e->getMessage();
        if ($e instanceof \ShopifyApiException) {
            $errMsg = 'Shopify API: ' . $errMsg;
        }
        $updateRow([
            'status' => 'error',
            'last_error' => $errMsg,
            'last_api_step' => $e instanceof \ShopifyApiException ? pd_step_string('fetch_order') : pd_step_string('exception'),
            'last_api_response' => json_encode(['exception' => $errMsg, 'file' => $e->getFile(), 'line' => $e->getLine()]),
        ]);
        pd_log('pd_run_generate_entity failed', [$logKey => $entityId, 'entity_type' => $entityType, 'error' => $errMsg]);
        try {
            $setMetafields([
                ['namespace' => 'custom', 'key' => 'document_status', 'type' => 'single_line_text_field', 'value' => 'error'],
            ]);
        } catch (Throwable $ignored) {
        }
        return ['success' => false, 'error' => $errMsg, 'entity_type' => $entityType];
    }
}

