<?php

declare(strict_types=1);

namespace CloudXM\NFe\Services;

use CloudXM\SDK\Support\HasLogger;
use WC_Order;
use CloudXM\NFe\Persistence\NfeEmissionsRepository;
use CloudXM\NFe\Config\Configuration;
use CloudXM\NFe\Services\NFeHistoryService;
use CloudXM\NFe\Services\NFeFileStorageService;
use CloudXM\NFe\DTOs\CancelInvoiceRequest;
use CloudXM\NFe\DTOs\InvoiceResource;

/**
 * Handles NFe invoice lifecycle for WooCommerce orders.
 *
 * UPDATED FOR V2: Uses OrderMapperV2, v2 API methods, and InvoiceResource DTOs.
 *
 * =========================================================================
 * ARCHITECTURE: SINGLE SOURCE OF TRUTH — EMISSIONS TABLE
 * =========================================================================
 *
 * The `wp_nfe_emissions` table is the authoritative source of truth for all
 * NFe state. Order meta (`_cloudxm_nfe_*`) is a write-through cache only.
 *
 * WHY: Order meta can be deleted or cleared during lifecycle transitions
 * (e.g. cancelScheduledInvoice deletes _cloudxm_nfe_invoice_id to reset the
 * order to "no invoice" state). If any read path relies solely on order meta,
 * it will see stale or missing data and produce incorrect UI or logic.
 *
 * RULES — enforce these on every future change:
 *
 *   READ  → Always use findCurrentEmission() to resolve the current invoice
 *            state. Never read _cloudxm_nfe_invoice_id / _cloudxm_nfe_status /
 *            _cloudxm_nfe_emission_status from order meta as the primary source.
 *            Order meta may be used as a fallback only when no emission record
 *            exists (i.e. the order has never had an NFe).
 *
 *   WRITE → Always write to BOTH the emissions table AND order meta so that:
 *            - The emissions table stays authoritative and queryable.
 *            - Order meta stays in sync for WooCommerce queries (wc_get_orders
 *              by meta_key) and for external integrations that read order meta.
 *
 *   DISPLAY STATE → buildMetaBoxData() derives displayState exclusively from
 *            $emissionStatus (emissions table). It must never fall back to
 *            _cloudxm_nfe_emission_status from order meta for this decision.
 *
 *   REFRESH / CANCEL → refreshStatus() and cancelInvoice() resolve invoice_id
 *            via findCurrentEmission() before calling the API. They must not
 *            bail early just because _cloudxm_nfe_invoice_id is empty in order
 *            meta — the emissions table may still have the current invoice_id.
 *
 *   CAN ISSUE → canIssue() checks the emissions table via findCurrentEmission().
 *            It must not read _cloudxm_nfe_invoice_id from order meta, which
 *            may have been cleared even though an active emission exists.
 *
 * LIFECYCLE EXAMPLE (issue → cancel → reissue):
 *   1. issueInvoice()        → creates emission row (status=issued, invoice_id=A)
 *                               writes _cloudxm_nfe_invoice_id=A to order meta
 *   2. cancelInvoice()       → updates emission row (status=cancelled)
 *                               writes _cloudxm_nfe_status=cancelled to order meta
 *   3. cancelScheduledInvoice() → updates emission row (status=cancelled)
 *                               DELETES _cloudxm_nfe_invoice_id from order meta
 *                               (order meta now empty, emissions table still has record)
 *   4. issueInvoice() again  → creates new emission row (status=issued, invoice_id=B)
 *                               writes _cloudxm_nfe_invoice_id=B to order meta
 *   At step 3, any read that relies only on order meta sees "no invoice".
 *   findCurrentEmission() correctly returns the cancelled row from the table.
 * =========================================================================
 */
class OrderInvoiceLifecycleService
{
    use HasLogger;

    public function __construct(
        private Configuration $config,
        private ApiClient $apiClient,
        private OrderMapperV2 $orderMapper,
        private NfeEmissionsRepository $emissionRepo,
        private NFeHistoryService $historyService,
        private NFeFileStorageService $fileStorageService
    ) {}

    public function getEmissionRepo(): NfeEmissionsRepository
    {
        return $this->emissionRepo;
    }

    /**
     * Update the auto_status on the order and persist.
     */
    public function setAutoStatus(int $orderId, string $status): void
    {
        $order = wc_get_order($orderId);
        if (!$order) {
            return;
        }
        $order->update_meta_data(Configuration::META_AUTO_STATUS, $status);
        $order->save();
    }

    public function buildMetaBoxData(WC_Order $order): array
    {
        $invoiceId = (string) $order->get_meta('_cloudxm_nfe_invoice_id');
        $status = (string) $order->get_meta('_cloudxm_nfe_status');
        $issuanceDate = (string) $order->get_meta('_cloudxm_nfe_issuance_date');

        $allowedActionsJson = $order->get_meta('_cloudxm_nfe_allowed_actions');
        $allowedActions = $allowedActionsJson !== '' ? json_decode($allowedActionsJson, true) : [];

        $emission = $this->findCurrentEmission($order);
        $emissionStatus = $emission['status'] ?? '';
        $emissionId = isset($emission['id']) ? (int) $emission['id'] : 0;
        $scheduledAt = '';

        // Emissions table is the authoritative source of truth.
        // Order meta is a write-through cache that may be stale or cleared.
        // Override the order meta values with emission table data when available.
        // The order meta fallback (right side of ??) only applies when no emission
        // record exists at all (i.e. the order has never had an NFe issued).
        if ($emission && $emissionId > 0) {
            $invoiceId = $emission['invoice_id'] ?? $invoiceId;
            $status = $emission['status'] ?? $status;
            $issuanceDate = $emission['emission_date'] ?? $issuanceDate;
            $allowedActionsJson = $emission['allowed_actions'] ?? $allowedActionsJson;
            $allowedActions = $allowedActionsJson !== '' ? json_decode($allowedActionsJson, true) : [];
        }

        // Check if invoice is scheduled — emissions table only, no order meta fallback.
        // Do NOT add || $orderMetaStatus === 'scheduled' here; see class docblock.
        $isScheduled = in_array($emissionStatus, ['scheduled', 'async_pending'], true);

        // Get scheduled date from various sources
        if ($isScheduled) {
            // First try internal meta field
            $scheduledAt = (string) $order->get_meta('_cloudxm_nfe_scheduled_at');

            // If empty, try external schedule field
            if ($scheduledAt === '' && $this->config->isSchedulingConfigured()) {
                $scheduleFieldKey = $this->config->getScheduleDateField();
                $scheduledAt = (string) $order->get_meta($scheduleFieldKey);
            }

            // If still empty, use emission created_at as fallback
            if ($scheduledAt === '' && isset($emission['created_at'])) {
                $scheduledAt = $emission['created_at'];
            }

            // Debug logging
            $this->log()->debug('nfe.scheduled_date_retrieval', [
                'order_id' => $order->get_id(),
                'emission_status' => $emissionStatus,
                'is_scheduled' => $isScheduled,
                'scheduled_at' => $scheduledAt,
                'internal_meta' => $order->get_meta('_cloudxm_nfe_scheduled_at'),
                'external_field' => $this->config->isSchedulingConfigured() ? $order->get_meta($this->config->getScheduleDateField()) : 'not_configured',
            ]);
        }

        $fallbackNotice = '';
        if ($order->get_meta('_cloudxm_nfe_fallback_refresh')) {
            $fallbackNotice = __('NFe emitida via verificação de fallback', 'woocommerce-cloudxm-nfe');
            $order->delete_meta_data('_cloudxm_nfe_fallback_refresh');
            $order->save();
        }

        // Scheduling information
        $scheduleFieldConfigured = $this->config->isSchedulingConfigured();
        $scheduledAtFormatted = '';
        $timeUntilSchedule = '';
        $canReschedule = false;
        $scheduleFieldChanged = false;
        $newScheduledAt = '';
        $scheduleFieldValue = '';
        $scheduleFieldValueFormatted = '';

        if ($scheduleFieldConfigured) {
            // Get the schedule field value (external systems write to this)
            $scheduleFieldKey = $this->config->getScheduleDateField();
            $scheduleFieldValue = $order->get_meta($scheduleFieldKey);

            // Format the schedule field value for display
            if ($scheduleFieldValue) {
                try {
                    $fieldDate = new \DateTime($scheduleFieldValue, new \DateTimeZone($this->config->getScheduleTimezone()));
                    $scheduleFieldValueFormatted = wp_date(get_option('date_format') . ' ' . get_option('time_format'), $fieldDate->getTimestamp());
                } catch (\Exception $e) {
                    // Invalid date format
                }
            }

            // Check if scheduled date field changed
            if ($scheduleFieldValue && $scheduledAt !== '') {
                try {
                    $fieldDate = new \DateTime($scheduleFieldValue, new \DateTimeZone($this->config->getScheduleTimezone()));
                    $emissionDate = new \DateTime($scheduledAt, new \DateTimeZone($this->config->getScheduleTimezone()));
                    $diff = abs($fieldDate->getTimestamp() - $emissionDate->getTimestamp());
                    if ($diff > 60) {
                        $scheduleFieldChanged = true;
                        $newScheduledAt = wp_date(get_option('date_format') . ' ' . get_option('time_format'), $fieldDate->getTimestamp());
                    }
                } catch (\Exception $e) {
                    // Invalid date, ignore
                }
            }
        }

        // Format scheduled at and calculate time remaining
        if ($scheduledAt !== '') {
            try {
                $scheduledDateTime = new \DateTime($scheduledAt, new \DateTimeZone($this->config->getScheduleTimezone()));
                $scheduledAtFormatted = wp_date(get_option('date_format') . ' ' . get_option('time_format'), $scheduledDateTime->getTimestamp());

                $now = new \DateTime();
                if ($scheduledDateTime > $now) {
                    $interval = $now->diff($scheduledDateTime);
                    $timeUntilSchedule = $this->formatTimeInterval($interval);
                }
            } catch (\Exception $e) {
                // Invalid date format
            }
        }

        // Can reschedule if invoice is scheduled but not yet issued.
        // Emissions table is authoritative — no order meta fallback needed.
        $canReschedule = in_array($emissionStatus, ['scheduled', 'async_pending'], true);

        // Calculate display state - single source of truth for UI rendering
        // Priority: cancelled > scheduled > issued > no_invoice
        // Scheduled must come before issued because async/scheduled responses
        // return invoice_id immediately but the invoice isn't issued yet
        $isScheduledStatus = in_array($emissionStatus, ['scheduled', 'async_pending'], true);

        $displayState = match(true) {
            // If cancelled (from emission table), show cancelled state
            $emissionStatus === 'cancelled' => 'cancelled',

            // If scheduled/async_pending, show scheduled state (BEFORE issued check,
            // because async responses return invoice_id immediately)
            $isScheduledStatus => 'scheduled',

            // If we have an invoice ID, it's issued
            !empty($invoiceId) => 'issued',

            // Default: no invoice yet
            default => 'no_invoice',
        };

        return [
            'order_id' => $order->get_id(),
            'invoice_id' => $invoiceId,
            'access_key' => $emission['access_key'] ?? (string) $order->get_meta('_cloudxm_nfe_access_key'),
            'nfe_number' => $emission['invoice_number'] ?? (string) $order->get_meta('_cloudxm_nfe_number'),
            'nfse_series' => $emission['invoice_series'] ?? (string) $order->get_meta('_cloudxm_nfe_series'),
            'protocol_number' => $emission['protocol_number'] ?? (string) $order->get_meta('_cloudxm_nfe_protocol'),
            'dps_number' => $emission['dps_number'] ?? (string) $order->get_meta('_cloudxm_nfe_dps_number'),
            'status' => $status,
            'status_label' => $this->getStatusLabel($status),
            'issuance_date' => $issuanceDate,
            'issuance_date_formatted' => $this->formatIssuanceDate($issuanceDate),
            'error_message' => (string) $order->get_meta('_cloudxm_nfe_error'),
            'can_issue' => $this->canIssue($order),
            'can_reissue' => $invoiceId !== '' && in_array($status, ['cancelled', 'failed'], true),
            'can_cancel' => $status === 'issued' && $this->canCancel($issuanceDate),
            'can_force_unlock' => $invoiceId !== '' || $this->config->isDebugMode(),
            'allowed_actions' => $allowedActions,
            'emission_status' => $emissionStatus,
            // NOTE: order_meta_emission_status intentionally omitted — emissions table is
            // authoritative. Do not re-add an order meta fallback here; see class docblock.
            'scheduled_at' => $scheduledAt,
            'fallback_notice' => $fallbackNotice,
            'display_state' => $displayState,
            'environment' => $this->config->getEnvironment(),
            // Scheduling fields
            'emission_id' => $emissionId,
            'schedule_field_configured' => $scheduleFieldConfigured,
            'schedule_field_value' => $scheduleFieldValue,
            'schedule_field_value_formatted' => $scheduleFieldValueFormatted,
            'schedule_field_key' => $this->config->getScheduleDateField(),
            'scheduled_at_raw' => $scheduledAt,
            'scheduled_at_formatted' => $scheduledAtFormatted,
            'scheduled_timezone' => $emission['scheduled_timezone'] ?? $this->config->getScheduleTimezone(),
            'time_until_schedule' => $timeUntilSchedule,
            'can_reschedule' => $canReschedule,
            'schedule_field_changed' => $scheduleFieldChanged,
            'new_scheduled_at' => $newScheduledAt,
            'schedule_was_cancelled' => (bool) $order->get_meta('_cloudxm_nfe_schedule_was_cancelled'),
            'schedule_cancelled_date_formatted' => (function() use ($order) {
                $cancelledDate = $order->get_meta('_cloudxm_nfe_schedule_cancelled_date');
                if (empty($cancelledDate)) {
                    return '';
                }
                try {
                    $date = new \DateTime($cancelledDate, new \DateTimeZone($this->config->getScheduleTimezone()));
                    return wp_date(get_option('date_format') . ' ' . get_option('time_format'), $date->getTimestamp());
                } catch (\Exception $e) {
                    return '';
                }
            })(),
            'cancelled_invoice_history' => $this->getCancelledInvoiceHistory($order),
            'cancellation_date_formatted' => (function() use ($order, $emission) {
                $cancelledAt = (string) $order->get_meta('_cloudxm_nfe_cancelled_at');
                if ($cancelledAt === '') {
                    $cancelledAt = (string) $order->get_meta('_cloudxm_nfe_cancellation_date');
                }
                if ($cancelledAt === '' && $emission && !empty($emission['cancellation_date'])) {
                    $cancelledAt = (string) $emission['cancellation_date'];
                }
                if ($cancelledAt === '') {
                    return '';
                }
                try {
                    // Dates are stored as UTC wall time (Y-m-d H:i:s).
                    // Parse as UTC so wp_date() converts to the WP timezone correctly.
                    $date = new \DateTime($cancelledAt, new \DateTimeZone('UTC'));
                    return wp_date(get_option('date_format') . ' ' . get_option('time_format'), $date->getTimestamp());
                } catch (\Exception $e) {
                    return $cancelledAt;
                }
            })(),
            'cancellation_reason' => (string) $order->get_meta('_cloudxm_nfe_cancellation_reason'),

            // Auto-issue state machine (CLO-11)
            'auto_status' => $order->get_meta(Configuration::META_AUTO_STATUS),
        ];
    }

    /**
     * Get cancelled invoice history
     *
     * Reads from order meta _cloudxm_nfe_cancelled_invoice_history, then enriches
     * dates from the emissions table (authoritative) when the invoice_id is available.
     * This corrects any stale/incorrectly-stored dates in the order meta history.
     */
    private function getCancelledInvoiceHistory(WC_Order $order): array
    {
        $historyJson = $order->get_meta('_cloudxm_nfe_cancelled_invoice_history');
        if (empty($historyJson)) {
            return [];
        }

        $history = json_decode($historyJson, true);
        if (!is_array($history)) {
            return [];
        }

        // Enrich from emissions table when possible — the table has authoritative UTC dates
        // and corrects any stale values that may have been stored in order meta history.
        // IMPORTANT: Only overwrite issuance_date if the emission was actually issued
        // (has a non-null issued_at or status=issued/cancelled with an access_key).
        // Scheduled emissions store emission_date = scheduling time, not issuance time —
        // using that would show the wrong date.
        if (!empty($history['invoice_id'])) {
            $emissionRecord = $this->emissionRepo->findByInvoiceId($history['invoice_id']);
            if ($emissionRecord) {
                $wasIssued = !empty($emissionRecord['access_key']);
                if ($wasIssued && !empty($emissionRecord['emission_date'])) {
                    $history['issuance_date'] = $emissionRecord['emission_date'];
                }
                if (!empty($emissionRecord['cancellation_date'])) {
                    $history['cancellation_date'] = $emissionRecord['cancellation_date'];
                }
                if (!empty($emissionRecord['cancellation_reason'])) {
                    $history['cancellation_reason'] = $emissionRecord['cancellation_reason'];
                }
                if (!empty($emissionRecord['invoice_number'])) {
                    $history['nfe_number'] = $emissionRecord['invoice_number'];
                }
                if (!empty($emissionRecord['access_key'])) {
                    $history['access_key'] = $emissionRecord['access_key'];
                }
            }
        }

        // Format dates for display.
        // new \DateTime() respects timezone offsets in the string, falling back to
        // the explicit UTC parameter only when no offset is present (plain Y-m-d H:i:s).
        if (!empty($history['issuance_date'])) {
            try {
                $date = new \DateTime($history['issuance_date'], new \DateTimeZone('UTC'));
                $history['issuance_date_formatted'] = wp_date(get_option('date_format') . ' ' . get_option('time_format'), $date->getTimestamp());
            } catch (\Exception $e) {
                $history['issuance_date_formatted'] = $history['issuance_date'];
            }
        }

        if (!empty($history['cancellation_date'])) {
            try {
                $date = new \DateTime($history['cancellation_date'], new \DateTimeZone('UTC'));
                $history['cancellation_date_formatted'] = wp_date(get_option('date_format') . ' ' . get_option('time_format'), $date->getTimestamp());
            } catch (\Exception $e) {
                $history['cancellation_date_formatted'] = $history['cancellation_date'];
            }
        }

        return $history;
    }

    /**
     * Issue NFe invoice (v2 API)
     *
     * Uses OrderMapperV2 to create IssueInvoiceRequest DTO
     * Calls ApiClient::emitInvoiceV2() and handles InvoiceResource response
     * Stores v2 fields: processing_strategy, allowed_actions, lifecycle timestamps
     *
     * IDEMPOTENCY: Uses emission attempt counter to generate unique idempotency keys
     * - First emission: attempt_1 (stored in order meta)
     * - After cancel + reissue: attempt_2 (incremented on cancellation)
     * - Retry after timeout: Same attempt (idempotent - server returns existing invoice)
     */
    public function issueInvoice(int $orderId): array
    {
        $order = wc_get_order($orderId);
        if (!$order instanceof WC_Order) {
            return ['success' => false, 'message' => __('Order not found.', 'woocommerce-cloudxm-nfe')];
        }

        if (!$this->canIssue($order)) {
            return ['success' => false, 'message' => __('An active NFe already exists for this order.', 'woocommerce-cloudxm-nfe')];
        }

        $existingInvoiceId = (string) $order->get_meta('_cloudxm_nfe_invoice_id');
        $existingStatus = (string) $order->get_meta('_cloudxm_nfe_status');
        $isReissue = $existingInvoiceId !== '' && $existingStatus === 'cancelled';

        // Check if there's a scheduled invoice - cancel it first to avoid duplicates
        $existingEmission = $this->emissionRepo->findByOrderId($orderId, false);
        $hasScheduledInvoice = $existingEmission && in_array($existingEmission['status'] ?? '', ['scheduled', 'async_pending'], true);

        if ($hasScheduledInvoice && !empty($existingEmission['invoice_id'])) {
            $this->log()->info('nfe.issue.cancelling_schedule', [
                'order_id' => $orderId,
                'scheduled_invoice_id' => $existingEmission['invoice_id'],
            ]);

            try {
                // Cancel the scheduled invoice on the server
                $cancelRequest = new CancelInvoiceRequest('Issuing immediately instead of scheduled');
                $this->apiClient->cancelInvoiceV2($existingEmission['invoice_id'], $cancelRequest);

                // Delete the emission record
                $this->emissionRepo->delete((int) $existingEmission['id']);

                // Clear scheduling meta
                $this->clearSchedulingMeta($order);
            } catch (\Throwable $e) {
                $this->log()->warning('nfe.issue.cancel_schedule_failed', [
                    'order_id' => $orderId,
                    'error' => $e->getMessage(),
                ]);
                // Continue anyway - the scheduled invoice might not exist on server
            }
        }

        // If this is a re-issue after cancellation, clear the old invoice data first
        if ($isReissue) {
            $this->log()->info('nfe.issue.clearing_cancelled', [
                'order_id' => $orderId,
                'cancelled_invoice_id' => $existingInvoiceId,
            ]);

            // Clear all invoice-related meta data
            $order->update_meta_data('_cloudxm_nfe_invoice_id', '');
            $order->update_meta_data('_cloudxm_nfe_status', '');
            $order->update_meta_data('_cloudxm_nfe_emission_status', '');
            $order->update_meta_data('_cloudxm_nfe_access_key', '');
            $order->update_meta_data('_cloudxm_nfe_number', '');
            $order->update_meta_data('_cloudxm_nfe_series', '');
            $order->update_meta_data('_cloudxm_nfe_protocol', '');
            $order->update_meta_data('_cloudxm_nfe_issuance_date', '');
            $order->update_meta_data('_cloudxm_nfe_protocol_number', '');
            $order->update_meta_data('_cloudxm_nfe_dps_number', '');
            $order->update_meta_data('_cloudxm_nfe_error', '');
            $order->update_meta_data('_cloudxm_nfe_allowed_actions', '');
            $order->update_meta_data('_cloudxm_nfe_processing_strategy', '');
            $order->update_meta_data('_cloudxm_nfe_cancellation_date', '');
            $order->update_meta_data('_cloudxm_nfe_cancellation_reason', '');
            $order->save();
        }

        // Get or increment emission attempt counter
        // CRITICAL: Read directly from database to bypass WordPress object cache
        // which can cause stale emission attempt values after force unlock
        global $wpdb;
        $emissionAttempt = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_nfe_emission_attempt'",
            $orderId
        ));
        if ($emissionAttempt < 1) {
            $emissionAttempt = 1;
        }

        try {
            // Use OrderMapperV2 to create IssueInvoiceRequest DTO with emission attempt
            // CRITICAL: Pass 'immediate' to ensure server processes now, not schedule
            $request = $this->orderMapper->mapOrderToInvoiceRequest(
                $order,
                $emissionAttempt,
                'immediate'  // Force immediate processing, not scheduled
            );

            // Call v2 API method
            $invoice = $this->apiClient->emitInvoiceV2($request);

            // Validate invoice ID
            if (empty($invoice->id)) {
                return ['success' => false, 'message' => __('Invalid API response: missing invoice ID.', 'woocommerce-cloudxm-nfe')];
            }

            // Handle synchronous failures — API returns 200 with status "failed" and error_message
            $normalizedStatus = $this->normalizeStatus($invoice->status);
            if ($normalizedStatus === 'failed') {
                $errorMessage = $invoice->error_message ?? __('Unknown error', 'woocommerce-cloudxm-nfe');

                $this->log()->warning('nfe.issue.sync_failed', [
                    'order_id' => $orderId,
                    'invoice_id' => $invoice->id,
                    'error' => $errorMessage,
                ]);

                // Save failed emission record so the error is visible in the invoices list
                $emissionData = [
                    'order_id' => $orderId,
                    'invoice_id' => $invoice->id,
                    'status' => 'failed',
                    'emission_date' => null,
                    'error_message' => $errorMessage,
                    'environment' => $this->config->getEnvironment(),
                    'total_value' => (float) $order->get_total(),
                    'processing_strategy' => $invoice->processing_strategy,
                    'allowed_actions' => !empty($invoice->allowed_actions) ? json_encode($invoice->allowed_actions) : null,
                    'created_at' => $invoice->lifecycle?->created_at,
                ];

                $existingEmission = $this->emissionRepo->findByOrderId($orderId, false);
                if ($existingEmission) {
                    $this->emissionRepo->update((int) $existingEmission['id'], $emissionData);
                } else {
                    $this->emissionRepo->create($emissionData);
                }

                // Store error in order meta so the meta box displays it
                $order->update_meta_data('_cloudxm_nfe_invoice_id', $invoice->id);
                $order->update_meta_data('_cloudxm_nfe_status', 'failed');
                $order->update_meta_data('_cloudxm_nfe_emission_status', 'failed');
                $order->update_meta_data('_cloudxm_nfe_error', $errorMessage);
                if ($invoice->processing_strategy !== null) {
                    $order->update_meta_data('_cloudxm_nfe_processing_strategy', $invoice->processing_strategy);
                }
                if (!empty($invoice->allowed_actions)) {
                    $order->update_meta_data('_cloudxm_nfe_allowed_actions', json_encode($invoice->allowed_actions));
                }
                $order->save();

                $order->add_order_note(
                    sprintf(__('NFe falhou: %s', 'woocommerce-cloudxm-nfe'), $errorMessage)
                );

                return ['success' => false, 'message' => $errorMessage];
            }

            // Prepare emission date.
            // API dates are ISO 8601 with timezone offset (e.g. 2026-04-22T21:19:30-03:00).
            // When offset is present, PHP uses it (ignoring the explicit param).
            // When offset is absent, the explicit param is used as fallback.
            // Parse as WP timezone so that dates without offset are treated as local time.
            $emissionDate = $invoice->lifecycle?->issued_at ?? $invoice->lifecycle?->created_at ?? null;
            if ($emissionDate && strtotime($emissionDate) !== false) {
                $dateTime = new \DateTime($emissionDate, new \DateTimeZone($this->config->getScheduleTimezone()));
                $emissionDate = $dateTime->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
            } else {
                $emissionDate = current_time('mysql', true); // UTC
            }

            // Find existing emission by invoice_id for update (not order_id)
            $existingEmission = $isReissue
                ? null
                : $this->emissionRepo->findByOrderId($orderId, false);
            
            // Prepare emission data with v2 fields
            $emissionData = [
                'invoice_id' => $invoice->id,
                'access_key' => $invoice->nfse?->access_key ?? '',
                'status' => $this->normalizeStatus($invoice->status),
                'emission_date' => $emissionDate,
                'invoice_number' => $invoice->nfse?->number ?? '',
                'invoice_series' => $invoice->nfse?->series ?? '',
                'dps_number' => $invoice->dps_number ?? '',
                'protocol_number' => $invoice->nfse?->protocol ?? '',
                'authorization_code' => '',
                'environment' => $this->config->getEnvironment(),
                'total_value' => (float) $order->get_total(),
                'processing_strategy' => $invoice->processing_strategy,
                'allowed_actions' => !empty($invoice->allowed_actions) ? json_encode($invoice->allowed_actions) : null,
                'created_at' => $invoice->lifecycle?->created_at,
            ];

            if ($isReissue && $existingInvoiceId !== '') {
                $previousEmission = $this->emissionRepo->findByInvoiceId($existingInvoiceId);
                if ($previousEmission) {
                    $emissionData['replacement_for_id'] = (int) $previousEmission['id'];
                }
            }
            
            if ($existingEmission && !$isReissue) {
                $emissionId = (int) $existingEmission['id'];
                $success = $this->emissionRepo->update($emissionId, $emissionData);
            } else {
                $emissionData['order_id'] = $orderId;
                $emissionId = $this->emissionRepo->create($emissionData);
                $success = $emissionId !== false;
            }

            if (!$success) {
                $this->log()->error('nfe.emission.save_failed', [
                    'order_id' => $orderId,
                    'invoice_id' => $invoice->id,
                    'existing_emission' => $existingEmission ? $existingEmission['id'] : null,
                ]);

                return [
                    'success' => false,
                    'message' => __('Failed to save emission record.', 'woocommerce-cloudxm-nfe'),
                ];
            }

            // Archive previous invoice data only after the replacement emission row exists.
            if ($isReissue) {
                $order->update_meta_data('_cloudxm_nfe_previous_invoice_id', $existingInvoiceId);
                $order->update_meta_data('_cloudxm_nfe_previous_access_key', (string) $order->get_meta('_cloudxm_nfe_access_key'));
                $order->update_meta_data('_cloudxm_nfe_previous_number', (string) $order->get_meta('_cloudxm_nfe_number'));
                $order->update_meta_data('_cloudxm_nfe_previous_cancelled_at', current_time('mysql'));
            }

            $normalizedStatus = $this->normalizeStatus($invoice->status);

            // Store emission attempt counter for idempotency
            $order->update_meta_data('_nfe_emission_attempt', $emissionAttempt);

            // Store v2 fields in order meta
            $order->update_meta_data('_cloudxm_nfe_invoice_id', $invoice->id);
            $order->update_meta_data('_cloudxm_nfe_status', $normalizedStatus);
            $order->update_meta_data('_cloudxm_nfe_emission_status', $normalizedStatus);

            // Store v2-specific fields
            if ($invoice->processing_strategy !== null) {
                $order->update_meta_data('_cloudxm_nfe_processing_strategy', $invoice->processing_strategy);
            }
            if (!empty($invoice->allowed_actions)) {
                $order->update_meta_data('_cloudxm_nfe_allowed_actions', json_encode($invoice->allowed_actions));
            } else {
                $order->delete_meta_data('_cloudxm_nfe_allowed_actions');
            }

            // Store lifecycle timestamps
            if ($invoice->lifecycle !== null) {
                if ($invoice->lifecycle->created_at !== null) {
                    $order->update_meta_data('_cloudxm_nfe_created_at', $invoice->lifecycle->created_at);
                }
                if ($invoice->lifecycle->issued_at !== null) {
                    $order->update_meta_data('_cloudxm_nfe_issuance_date', $invoice->lifecycle->issued_at);
                }
            }

            // Store NFSe data
            if ($invoice->nfse !== null) {
                if ($invoice->nfse->access_key !== null) {
                    $order->update_meta_data('_cloudxm_nfe_access_key', $invoice->nfse->access_key);
                }
                if ($invoice->nfse->number !== null) {
                    $order->update_meta_data('_cloudxm_nfe_number', $invoice->nfse->number);
                }
                if ($invoice->nfse->protocol !== null) {
                    $order->update_meta_data('_cloudxm_nfe_protocol', $invoice->nfse->protocol);
                }
                if ($invoice->nfse->verification_code !== null) {
                    $order->update_meta_data('_cloudxm_nfe_verification_code', $invoice->nfse->verification_code);
                }
            }

            // Store DPS number
            if ($invoice->dps_number !== null) {
                $order->update_meta_data('_cloudxm_nfe_dps_number', $invoice->dps_number);
            }

            $this->clearSchedulingMeta($order);
            $order->delete_meta_data('_cloudxm_nfe_error');
            $order->save();

            // Log to wp_nfe_history table using v2-aware method
            $this->historyService->logInvoiceEvent(
                $emissionId,
                NFeHistoryService::EVENT_TYPE_EMISSION,
                $invoice,
                [
                    'order_id' => $orderId,
                    'is_reissue' => $isReissue,
                ]
            );

            $this->log()->info('nfe.emission.saved', [
                'order_id' => $orderId,
                'emission_id' => $emissionId,
                'invoice_id' => $invoice->id,
                'dps_number' => $invoice->dps_number,
                'action' => $existingEmission ? 'updated' : 'created',
                'processing_strategy' => $invoice->processing_strategy,
                'allowed_actions' => $invoice->allowed_actions,
            ]);

            // Automatically download and store PDF and XML files
            $this->downloadAndStoreInvoiceFiles(
                $invoice->id,
                $invoice->nfse?->access_key ?? '',
                $emissionDate
            );

            $this->setAutoStatus($orderId, Configuration::AUTO_ISSUED);

            return [
                'success' => true,
                'message' => $isReissue
                    ? __('NFe re-issued successfully.', 'woocommerce-cloudxm-nfe')
                    : __('NFe issued successfully.', 'woocommerce-cloudxm-nfe'),
            ];
        } catch (\Throwable $e) {
            $this->log()->error('nfe.order.issue_failed', [
                'order_id' => $orderId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            // Check if this is an HTTP 409 Conflict (invoice was cancelled)
            // The server returns 409 when an idempotency key matches a cancelled invoice
            $errorMessage = $e->getMessage();
            $isConflictError = stripos($errorMessage, 'conflict') !== false
                || stripos($errorMessage, '409') !== false
                || stripos($errorMessage, 'cancelled') !== false;

            if ($isConflictError) {
                // Increment emission attempt counter for next try
                // CRITICAL: Write directly to database to bypass WordPress object cache
                $newAttempt = $emissionAttempt + 1;
                global $wpdb;
                $wpdb->update(
                    $wpdb->postmeta,
                    ['meta_value' => $newAttempt],
                    ['post_id' => $orderId, 'meta_key' => '_nfe_emission_attempt'],
                    ['%d'],
                    ['%d', '%s']
                );

                $this->log()->info('nfe.order.idempotency_conflict', [
                    'order_id' => $orderId,
                    'previous_attempt' => $emissionAttempt,
                    'new_attempt' => $newAttempt,
                    'idempotency_key' => $request->idempotency_key ?? 'not_set',
                ]);

                return [
                    'success' => false,
                    'message' => sprintf(
                        __('This invoice was previously cancelled. A new emission attempt has been prepared. Please try again to issue the invoice.', 'woocommerce-cloudxm-nfe')
                    ),
                    'is_idempotency_conflict' => true,
                    'new_attempt' => $newAttempt,
                ];
            }

            // Regular error handling
            $order->update_meta_data('_cloudxm_nfe_error', $errorMessage);
            $order->save();

            return ['success' => false, 'message' => $errorMessage];
        }
    }

    /**
     * Schedule NFe invoice for future emission (v2 API)
     *
     * Creates a scheduled invoice that will be automatically emitted at the specified date
     */
    public function scheduleInvoice(int $orderId, string $scheduledAt): array
    {
        $order = wc_get_order($orderId);
        if (!$order instanceof WC_Order) {
            return ['success' => false, 'message' => __('Order not found.', 'woocommerce-cloudxm-nfe')];
        }

        // Check if order can be scheduled
        $terminal_statuses = ['cancelled', 'refunded', 'failed'];
        if (in_array($order->get_status(), $terminal_statuses)) {
            return ['success' => false, 'message' => __('Cannot schedule NFe for cancelled, refunded, or failed orders.', 'woocommerce-cloudxm-nfe')];
        }

        // Check if there's already a scheduled invoice for this order
        $existingEmission = $this->emissionRepo->findByOrderId($orderId, false);
        if ($existingEmission && in_array($existingEmission['status'], ['scheduled', 'async_pending'], true)) {
            return ['success' => false, 'message' => __('This order already has a scheduled NFe.', 'woocommerce-cloudxm-nfe')];
        }

        // Check if there's a cancelled invoice - clear it before scheduling a new one
        $invoiceId = (string) $order->get_meta('_cloudxm_nfe_invoice_id');
        $invoiceStatus = (string) $order->get_meta('_cloudxm_nfe_status');
        if ($invoiceId !== '' && $invoiceStatus === 'cancelled') {
            $this->log()->info('nfe.schedule.clearing_cancelled', [
                'order_id' => $orderId,
                'cancelled_invoice_id' => $invoiceId,
            ]);

            // Clear the cancelled invoice data from order meta
            $order->update_meta_data('_cloudxm_nfe_invoice_id', '');
            $order->update_meta_data('_cloudxm_nfe_status', '');
            $order->update_meta_data('_cloudxm_nfe_emission_status', '');
            $order->update_meta_data('_cloudxm_nfe_access_key', '');
            $order->update_meta_data('_cloudxm_nfe_number', '');
            $order->update_meta_data('_cloudxm_nfe_series', '');
            $order->update_meta_data('_cloudxm_nfe_protocol', '');
            $order->update_meta_data('_cloudxm_nfe_issuance_date', '');
            $order->update_meta_data('_cloudxm_nfe_protocol_number', '');
            $order->update_meta_data('_cloudxm_nfe_dps_number', '');
            $order->update_meta_data('_cloudxm_nfe_error', '');
            $order->update_meta_data('_cloudxm_nfe_allowed_actions', '');
            $order->update_meta_data('_cloudxm_nfe_processing_strategy', '');
            $order->update_meta_data('_cloudxm_nfe_cancellation_date', '');
            $order->update_meta_data('_cloudxm_nfe_cancellation_reason', '');
            $order->save();

            $this->log()->info('nfe.schedule.cancelled_cleared', [
                'order_id' => $orderId,
                'new_emission_attempt' => $emissionAttempt ?? 1,
            ]);
        }

        // Get or increment emission attempt for idempotency
        // CRITICAL: Read directly from database to bypass WordPress object cache
        // which can cause stale emission attempt values after force unlock
        global $wpdb;
        $emissionAttempt = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_nfe_emission_attempt'",
            $orderId
        ));
        if ($emissionAttempt < 1) {
            $emissionAttempt = 1;
        }

        try {
            // Use OrderMapperV2 to create IssueInvoiceRequest DTO with emission attempt
            // CRITICAL: Pass 'scheduled' mode with scheduled_at and timezone
            $timezone = get_option('timezone_string') ?: 'America/Sao_Paulo';
            $request = $this->orderMapper->mapOrderToInvoiceRequest(
                $order,
                $emissionAttempt,
                'scheduled',  // Explicitly set to scheduled mode
                $scheduledAt,
                $timezone
            );

            // Call v2 API method with scheduled date
            $invoice = $this->apiClient->emitInvoiceV2($request);

            // Validate invoice ID
            if (empty($invoice->id)) {
                return ['success' => false, 'message' => __('Invalid API response: missing invoice ID.', 'woocommerce-cloudxm-nfe')];
            }

            // Prepare emission data
            $emissionStatus = $this->normalizeStatus($invoice->status ?? 'scheduled');
            $emissionData = [
                'order_id' => $orderId,
                'invoice_id' => $invoice->id,
                'access_key' => $invoice->nfse?->access_key,
                'invoice_number' => $invoice->nfse?->number,
                'invoice_series' => $invoice->nfse?->series,
                'protocol_number' => $invoice->nfse?->protocol,
                'status' => $emissionStatus,
                'processing_strategy' => $invoice->processing_strategy,
                'allowed_actions' => !empty($invoice->allowed_actions) ? json_encode($invoice->allowed_actions) : null,
                'scheduled_for' => $invoice->lifecycle?->scheduled_for,
                'scheduled_for_local' => $invoice->lifecycle?->scheduled_for_local,
                'scheduled_timezone' => $invoice->lifecycle?->scheduled_timezone,
                'total_value' => (float) $order->get_total(),
                'environment' => $this->config->getEnvironment(),
                'emission_date' => null,
            ];

            // Check for existing emission with this invoice_id (idempotency check)
            $existingEmission = $this->emissionRepo->findByInvoiceId($invoice->id);

            if ($existingEmission) {
                // Update existing emission record (API returned same invoice due to idempotency)
                $emissionId = (int) $existingEmission['id'];
                $success = $this->emissionRepo->update($emissionId, $emissionData);
            } else {
                // Create new emission record
                $emissionData['order_id'] = $orderId;
                $emissionId = $this->emissionRepo->create($emissionData);
                $success = $emissionId !== false;
            }

            if (!$success) {
                return ['success' => false, 'message' => __('Failed to save emission record.', 'woocommerce-cloudxm-nfe')];
            }

            // Update order meta with invoice info
            $order->update_meta_data('_cloudxm_nfe_invoice_id', $invoice->id);
            $order->update_meta_data('_cloudxm_nfe_status', $emissionStatus);
            $order->update_meta_data('_cloudxm_nfe_emission_status', $emissionStatus);
            $order->update_meta_data('_cloudxm_nfe_processing_strategy', $invoice->processing_strategy);
            $order->update_meta_data('_cloudxm_nfe_allowed_actions', !empty($invoice->allowed_actions) ? json_encode($invoice->allowed_actions) : null);
            $order->update_meta_data('_cloudxm_nfe_scheduled_at', $invoice->lifecycle?->scheduled_for_local ?? $scheduledAt);
            $order->delete_meta_data('_cloudxm_nfe_error');
            $order->delete_meta_data('_cloudxm_nfe_schedule_was_cancelled');
            $order->delete_meta_data('_cloudxm_nfe_schedule_cancelled_date');
            $order->save();

            $this->log()->info('nfe.schedule.success', [
                'order_id' => $orderId,
                'emission_id' => $emissionId,
                'invoice_id' => $invoice->id,
                'scheduled_for' => $scheduledAt,
                'status' => $emissionStatus,
            ]);

            return [
                'success' => true,
                'invoice_id' => $invoice->id,
                'emission_id' => $emissionId,
                'status' => $emissionStatus,
                'scheduled_for' => $invoice->lifecycle->scheduled_for_local ?? $scheduledAt,
            ];

        } catch (\Exception $e) {
            // Log error with full details
            $this->log()->error('nfe.schedule.failed', [
                'order_id' => $orderId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            // Return the actual error message so user can see what's wrong
            return [
                'success' => false,
                'message' => sprintf(
                    __('Failed to schedule NFe: %s', 'woocommerce-cloudxm-nfe'),
                    $e->getMessage()
                )
            ];
        }
    }

    /**
     * Reschedule NFe invoice to a new date
     *
     * Updates an existing scheduled invoice to a new date/time
     */
    public function rescheduleInvoice(int $orderId, string $scheduledAt, ?string $reason = null): array
    {
        $order = wc_get_order($orderId);
        if (!$order instanceof WC_Order) {
            return ['success' => false, 'message' => __('Order not found.', 'woocommerce-cloudxm-nfe')];
        }

        $invoiceId = (string) $order->get_meta('_cloudxm_nfe_invoice_id');
        $currentEmission = $this->findCurrentEmission($order);
        if ($invoiceId === '' && $currentEmission) {
            $invoiceId = (string) ($currentEmission['invoice_id'] ?? '');
        }

        if ($invoiceId === '') {
            return ['success' => false, 'message' => __('No NFe scheduled for this order.', 'woocommerce-cloudxm-nfe')];
        }

        // Get current status
        $currentStatus = (string) $order->get_meta('_cloudxm_nfe_status');
        if ($currentStatus === '' && $currentEmission) {
            $currentStatus = (string) ($currentEmission['status'] ?? '');
        }
        if (!in_array($currentStatus, ['scheduled', 'async_pending'], true)) {
            return ['success' => false, 'message' => __('Only scheduled invoices can be rescheduled.', 'woocommerce-cloudxm-nfe')];
        }

        try {
            $timezone = get_option('timezone_string') ?: 'America/Sao_Paulo';

            // Call API to reschedule
            $result = $this->apiClient->rescheduleInvoice($invoiceId, $scheduledAt, $timezone, $reason);

            if (isset($result['error']) || isset($result['message'])) {
                $this->log()->error('nfe.reschedule.api_error', [
                    'order_id' => $orderId,
                    'invoice_id' => $invoiceId,
                    'error' => $result['error'] ?? $result['message'],
                ]);
                return ['success' => false, 'message' => $result['error'] ?? $result['message'] ?? __('Failed to reschedule NFe.', 'woocommerce-cloudxm-nfe')];
            }

            // Update emission record
            $existingEmission = $this->emissionRepo->findByInvoiceId($invoiceId);
            if ($existingEmission) {
                $emissionId = (int) $existingEmission['id'];
                $this->emissionRepo->update($emissionId, [
                    'scheduled_for' => $scheduledAt,
                    'scheduled_for_local' => $scheduledAt,
                    'scheduled_timezone' => $timezone,
                ]);
            }

            // Update order meta
            $order->update_meta_data('_cloudxm_nfe_status', 'scheduled');
            $order->update_meta_data('_cloudxm_nfe_emission_status', 'scheduled');
            $order->update_meta_data('_cloudxm_nfe_scheduled_at', $scheduledAt);
            $order->save();

            $this->log()->info('nfe.reschedule.success', [
                'order_id' => $orderId,
                'invoice_id' => $invoiceId,
                'scheduled_for' => $scheduledAt,
                'timezone' => $timezone,
            ]);

            return [
                'success' => true,
                'invoice_id' => $invoiceId,
                'scheduled_for' => $scheduledAt,
            ];

        } catch (\Exception $e) {
            $this->log()->error('nfe.reschedule.failed', [
                'order_id' => $orderId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            return ['success' => false, 'message' => __('Failed to reschedule NFe.', 'woocommerce-cloudxm-nfe')];
        }
    }

    /**
     * Cancel NFe invoice (v2 API)
     * 
     * Uses CancelInvoiceRequest DTO and ApiClient::cancelInvoiceV2()
     * Handles InvoiceResource response with v2 fields
     */
    public function cancelInvoice(int $orderId, string $reason): array
    {
        $order = wc_get_order($orderId);
        if (!$order instanceof WC_Order) {
            return ['success' => false, 'message' => __('Order not found.', 'woocommerce-cloudxm-nfe')];
        }

        // Resolve invoice_id from emissions table first (authoritative), fall back to order meta.
        // IMPORTANT: Do NOT read _cloudxm_nfe_invoice_id from order meta as the primary source.
        // Order meta may be empty or stale; the emissions table always has the current record.
        $emission = $this->findCurrentEmission($order);
        $invoiceId = $emission['invoice_id'] ?? (string) $order->get_meta('_cloudxm_nfe_invoice_id');

        if ($invoiceId === '') {
            return ['success' => false, 'message' => __('No NFe issued for this order.', 'woocommerce-cloudxm-nfe')];
        }

        try {
            // Create CancelInvoiceRequest DTO
            $cancelRequest = new CancelInvoiceRequest($reason);
            
            // ATOMIC OPERATION: Call v2 API first, only update local state if successful
            $invoice = $this->apiClient->cancelInvoiceV2($invoiceId, $cancelRequest);

            // API call succeeded - now update local state atomically
            $order->update_meta_data('_cloudxm_nfe_status', 'cancelled');
            $order->update_meta_data('_cloudxm_nfe_emission_status', 'cancelled');

            // Save cancelled invoice history — prefer emission table data over order meta
            // since order meta may have been cleared in a prior lifecycle step.
            $cancelledInvoiceData = [
                'invoice_id' => $invoiceId,
                'access_key' => $emission['access_key'] ?? (string) $order->get_meta('_cloudxm_nfe_access_key'),
                'nfe_number' => $emission['invoice_number'] ?? (string) $order->get_meta('_cloudxm_nfe_number'),
                'protocol' => $emission['protocol_number'] ?? (string) $order->get_meta('_cloudxm_nfe_protocol'),
                'issuance_date' => $emission['emission_date'] ?? (string) $order->get_meta('_cloudxm_nfe_issuance_date'),
                'cancellation_date' => current_time('mysql', true), // UTC — parsed as UTC in getCancelledInvoiceHistory()
                'cancellation_reason' => $reason,
            ];
            $order->update_meta_data('_cloudxm_nfe_cancelled_invoice_history', json_encode($cancelledInvoiceData));

            // Increment emission attempt counter so reissue gets a new idempotency key
            // This prevents the reissue from conflicting with the cancelled invoice
            $currentAttempt = (int) $order->get_meta('_nfe_emission_attempt', true);
            if ($currentAttempt < 1) {
                $currentAttempt = 1;
            }
            $order->update_meta_data('_nfe_emission_attempt', $currentAttempt + 1);

            $this->log()->info('nfe.cancellation.incremented_attempt', [
                'order_id' => $orderId,
                'previous_attempt' => $currentAttempt,
                'new_attempt' => $currentAttempt + 1,
            ]);
            
            // Store v2 cancellation timestamp
            if ($invoice->lifecycle?->cancelled_at !== null) {
                $order->update_meta_data('_cloudxm_nfe_cancelled_at', $invoice->lifecycle->cancelled_at);
            } else {
                $order->update_meta_data('_cloudxm_nfe_cancelled_at', current_time('mysql', true));
            }
            
            // Update v2-specific fields
            if ($invoice->processing_strategy !== null) {
                $order->update_meta_data('_cloudxm_nfe_processing_strategy', $invoice->processing_strategy);
            }
            if (!empty($invoice->allowed_actions)) {
                $order->update_meta_data('_cloudxm_nfe_allowed_actions', json_encode($invoice->allowed_actions));
            }
            
            $order->save();

            // Find emission record by invoice_id (not order_id) to update the correct row
            $emission = $this->emissionRepo->findByInvoiceId($invoiceId);
            if ($emission && isset($emission['id'])) {
                $emissionId = (int) $emission['id'];

                // Prepare cancellation date.
                // API dates are ISO 8601 with timezone offset (e.g. 2026-04-22T21:19:55-03:00).
                // When offset is present, PHP uses it (ignoring the explicit param).
                // When offset is absent, the explicit param is used as fallback.
                // Parse as WP timezone so that dates without offset are treated as local time.
                $cancellationDate = $invoice->lifecycle?->cancelled_at ?? null;
                if ($cancellationDate && strtotime($cancellationDate) !== false) {
                    $dateTime = new \DateTime($cancellationDate, new \DateTimeZone($this->config->getScheduleTimezone()));
                    $cancellationDate = $dateTime->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
                } else {
                    $cancellationDate = current_time('mysql', true); // UTC
                }
                
                $this->emissionRepo->update($emissionId, [
                    'status' => $invoice->status ?? 'cancelled',
                    'cancellation_date' => $cancellationDate,
                    'cancellation_reason' => $reason,
                    'processing_strategy' => $invoice->processing_strategy,
                    'allowed_actions' => !empty($invoice->allowed_actions) ? json_encode($invoice->allowed_actions) : null,
                ]);

                $this->historyService->logInvoiceEvent(
                    $emissionId,
                    NFeHistoryService::EVENT_TYPE_CANCELLATION,
                    $invoice,
                    [
                        'order_id' => $orderId,
                        'reason' => $reason,
                    ]
                );

                $this->log()->info('nfe.cancellation.saved', [
                    'order_id' => $orderId,
                    'emission_id' => $emissionId,
                    'invoice_id' => $invoiceId,
                    'reason' => $reason,
                    'processing_strategy' => $invoice->processing_strategy,
                    'allowed_actions' => $invoice->allowed_actions,
                ]);
                
                // Download updated XML/PDF files (includes cancellation event).
                // Use the original emission date so the file path matches what was stored at issuance.
                // Delete stale cached files first so the cancelled versions are fetched fresh.
                $accessKey = $invoice->nfse?->access_key ?? (string) $order->get_meta('_cloudxm_nfe_access_key');
                $originalEmissionDate = (string) $order->get_meta('_cloudxm_nfe_issuance_date');
                if ($originalEmissionDate === '') {
                    $originalEmissionDate = current_time('mysql');
                }
                if ($accessKey !== '') {
                    try {
                        $emissionDateTime = new \DateTime($originalEmissionDate);
                        $this->fileStorageService->deleteCachedFiles($accessKey, $emissionDateTime);
                    } catch (\Exception $e) {
                        $this->log()->warning('nfe.cancellation.cache_delete_failed', [
                            'invoice_id' => $invoiceId,
                            'error' => $e->getMessage(),
                        ]);
                    }
                    $this->downloadAndStoreInvoiceFiles(
                        $invoiceId,
                        $accessKey,
                        $originalEmissionDate
                    );
                }
            } else {
                $this->log()->warning('nfe.cancellation.emission_not_found', [
                    'order_id' => $orderId,
                    'invoice_id' => $invoiceId,
                ]);
            }

            return ['success' => true, 'message' => __('NFe cancelled successfully.', 'woocommerce-cloudxm-nfe')];
        } catch (\Throwable $e) {
            // API call or local update failed
            $this->log()->error('nfe.cancellation.failed', [
                'order_id' => $orderId,
                'invoice_id' => $invoiceId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
            
            return [
                'success' => false,
                'message' => sprintf(
                    __('Failed to cancel NFe: %s', 'woocommerce-cloudxm-nfe'),
                    $e->getMessage()
                )
            ];
        }
    }

    /**
     * Refresh invoice status from API (v2 API)
     * 
     * Uses ApiClient::getInvoiceV2() and handles InvoiceResource response
     * Updates order meta and emissions table with v2 fields
     */
    public function refreshStatus(int $orderId): array
    {
        $order = wc_get_order($orderId);
        if (!$order instanceof WC_Order) {
            return ['success' => false, 'message' => __('Order not found.', 'woocommerce-cloudxm-nfe')];
        }

        // Resolve invoice_id from emissions table first (authoritative), fall back to order meta.
        // IMPORTANT: Do NOT read _cloudxm_nfe_invoice_id from order meta as the primary source.
        // cancelScheduledInvoice() deletes that meta key, so order meta may be empty even when
        // the emissions table has a valid invoice_id for this order.
        $emission = $this->findCurrentEmission($order);
        $invoiceId = $emission['invoice_id'] ?? (string) $order->get_meta('_cloudxm_nfe_invoice_id');

        if ($invoiceId === '') {
            return ['success' => false, 'message' => __('No invoice found for this order.', 'woocommerce-cloudxm-nfe')];
        }

        try {
            // Call v2 API method
            $invoice = $this->apiClient->getInvoiceV2($invoiceId);

            // Update order meta with v2 fields
            if ($invoice->status !== null) {
                $order->update_meta_data('_cloudxm_nfe_status', $this->normalizeStatus($invoice->status));
            }
            
            // Update v2-specific fields
            if ($invoice->processing_strategy !== null) {
                $order->update_meta_data('_cloudxm_nfe_processing_strategy', $invoice->processing_strategy);
            }
            if (!empty($invoice->allowed_actions)) {
                $order->update_meta_data('_cloudxm_nfe_allowed_actions', json_encode($invoice->allowed_actions));
            }
            
            // Update lifecycle timestamps
            if ($invoice->lifecycle !== null) {
                if ($invoice->lifecycle->created_at !== null) {
                    $order->update_meta_data('_cloudxm_nfe_created_at', $invoice->lifecycle->created_at);
                }
                if ($invoice->lifecycle->issued_at !== null) {
                    $order->update_meta_data('_cloudxm_nfe_issuance_date', $invoice->lifecycle->issued_at);
                }
                if ($invoice->lifecycle->cancelled_at !== null) {
                    $order->update_meta_data('_cloudxm_nfe_cancelled_at', $invoice->lifecycle->cancelled_at);
                }
            }
            
            // Update NFSe data
            if ($invoice->nfse !== null) {
                if ($invoice->nfse->number !== null) {
                    $order->update_meta_data('_cloudxm_nfe_number', $invoice->nfse->number);
                }
                if ($invoice->nfse->access_key !== null) {
                    $order->update_meta_data('_cloudxm_nfe_access_key', $invoice->nfse->access_key);
                }
                if ($invoice->nfse->protocol !== null) {
                    $order->update_meta_data('_cloudxm_nfe_protocol', $invoice->nfse->protocol);
                }
                if ($invoice->nfse->verification_code !== null) {
                    $order->update_meta_data('_cloudxm_nfe_verification_code', $invoice->nfse->verification_code);
                }
            }
            
            // Update DPS number
            if ($invoice->dps_number !== null) {
                $order->update_meta_data('_cloudxm_nfe_dps_number', $invoice->dps_number);
            }
            
            $order->save();

            // Find emission record by invoice_id (not order_id) for precision
            $emission = $this->emissionRepo->findByInvoiceId($invoiceId);
            if ($emission && $invoice->status !== null && $invoice->status !== $emission['status']) {
                $emissionId = (int) $emission['id'];
                
                // Prepare update data with v2 fields
                $updateData = [
                    'status' => $invoice->status,
                    'processing_strategy' => $invoice->processing_strategy,
                    'allowed_actions' => !empty($invoice->allowed_actions) ? json_encode($invoice->allowed_actions) : null,
                ];

                // Update lifecycle timestamp if available
                if ($invoice->lifecycle?->created_at !== null) {
                    $createdAt = $invoice->lifecycle->created_at;
                    if (strtotime($createdAt) !== false) {
                        $dateTime = new \DateTime($createdAt, new \DateTimeZone($this->config->getScheduleTimezone()));
                        $updateData['created_at'] = $dateTime->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
                    }
                }

                // Update scheduled fields if available
                if ($invoice->lifecycle?->scheduled_for !== null) {
                    $updateData['scheduled_for'] = $invoice->lifecycle->scheduled_for;
                }
                if ($invoice->lifecycle?->scheduled_for_local !== null) {
                    $updateData['scheduled_for_local'] = $invoice->lifecycle->scheduled_for_local;
                }
                if ($invoice->lifecycle?->scheduled_timezone !== null) {
                    $updateData['scheduled_timezone'] = $invoice->lifecycle->scheduled_timezone;
                }
                
                $this->emissionRepo->update($emissionId, $updateData);

                // Log status change to history using v2-aware method
                $this->historyService->logStatusChange(
                    $emissionId,
                    $emission['status'],
                    $invoice->status,
                    $invoice
                );

                $this->log()->info('nfe.status_updated', [
                    'order_id' => $orderId,
                    'emission_id' => $emissionId,
                    'invoice_id' => $invoiceId,
                    'old_status' => $emission['status'],
                    'new_status' => $invoice->status,
                    'processing_strategy' => $invoice->processing_strategy,
                    'allowed_actions' => $invoice->allowed_actions,
                ]);
            }

            $statusLabel = $invoice->status !== null ? $this->getStatusLabel($invoice->status) : __('updated', 'woocommerce-cloudxm-nfe');
            return ['success' => true, 'message' => sprintf(__('SEFIN status: %s', 'woocommerce-cloudxm-nfe'), $statusLabel)];
        } catch (\Throwable $e) {
            $this->log()->error('nfe.status_refresh_failed', [
                'order_id' => $orderId,
                'invoice_id' => $invoiceId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
            
            return [
                'success' => false,
                'message' => sprintf(
                    __('Failed to refresh status: %s', 'woocommerce-cloudxm-nfe'),
                    $e->getMessage()
                )
            ];
        }
    }

    /**
     * Cancel a scheduled NFe invoice
     *
     * Cancels a scheduled invoice on the server and updates local state.
     * This is different from cancelInvoice() which is for issued invoices.
     *
     * @param int $orderId Order ID
     * @param string $reason Reason for cancellation
     * @return array Result with success status and message
     */
    public function cancelScheduledInvoice(int $orderId, string $reason): array
    {
        $order = wc_get_order($orderId);
        if (!$order instanceof WC_Order) {
            return ['success' => false, 'message' => __('Order not found.', 'woocommerce-cloudxm-nfe')];
        }

        // Get invoice ID from order meta or emissions table
        $invoiceId = (string) $order->get_meta('_cloudxm_nfe_invoice_id');
        $emission = $this->emissionRepo->findByOrderId($orderId);

        if ($invoiceId === '' && $emission && !empty($emission['invoice_id'])) {
            $invoiceId = (string) $emission['invoice_id'];
        }

        if ($invoiceId === '') {
            return ['success' => false, 'message' => __('No scheduled invoice found for this order.', 'woocommerce-cloudxm-nfe')];
        }

        try {
            // Call API to cancel the scheduled invoice
            $cancelRequest = new CancelInvoiceRequest($reason);
            $invoice = $this->apiClient->cancelInvoiceV2($invoiceId, $cancelRequest);

            // Get the scheduled date before cancelling for display
            $scheduledAt = (string) $order->get_meta('_cloudxm_nfe_scheduled_at');
            if ($scheduledAt === '' && $emission && !empty($emission['scheduled_for_local'])) {
                $scheduledAt = (string) $emission['scheduled_for_local'];
            }

            // Save cancelled schedule info for history display
            if ($scheduledAt !== '') {
                $order->update_meta_data('_cloudxm_nfe_schedule_was_cancelled', '1');
                $order->update_meta_data('_cloudxm_nfe_schedule_cancelled_date', $scheduledAt);
            }

            // CRITICAL: Clear ALL invoice data from order meta to return to initial state (no_invoice)
            // This ensures the order meta doesn't have stale 'scheduled' status that would
            // cause the wrong buttons to show (like "Cancelar Agendamento" for issued invoices)
            $order->delete_meta_data('_cloudxm_nfe_invoice_id');
            $order->delete_meta_data('_cloudxm_nfe_status');
            $order->delete_meta_data('_cloudxm_nfe_emission_status');
            $order->delete_meta_data('_cloudxm_nfe_access_key');
            $order->delete_meta_data('_cloudxm_nfe_number');
            $order->delete_meta_data('_cloudxm_nfe_series');
            $order->delete_meta_data('_cloudxm_nfe_protocol');
            $order->delete_meta_data('_cloudxm_nfe_issuance_date');
            $order->delete_meta_data('_cloudxm_nfe_protocol_number');
            $order->delete_meta_data('_cloudxm_nfe_dps_number');
            $order->delete_meta_data('_cloudxm_nfe_error');
            $order->delete_meta_data('_cloudxm_nfe_allowed_actions');
            $order->delete_meta_data('_cloudxm_nfe_processing_strategy');
            $order->delete_meta_data('_cloudxm_nfe_cancelled_at');
            $order->delete_meta_data('_cloudxm_nfe_cancellation_date');
            $order->delete_meta_data('_cloudxm_nfe_cancellation_reason');

            // Clear scheduling-related fields
            $this->clearSchedulingMeta($order);
            $order->save();

            // CRITICAL: UPDATE emission record to 'cancelled' status instead of deleting it
            // This preserves invoice history and maintains proper idempotency tracking
            if ($emission && isset($emission['id'])) {
                $emissionId = (int) $emission['id'];

                // Prepare cancellation date.
                // API dates are ISO 8601 with timezone offset (e.g. 2026-04-22T21:19:55-03:00).
                // When offset is present, PHP uses it (ignoring the explicit param).
                // When offset is absent, the explicit param is used as fallback.
                // Parse as WP timezone so that dates without offset are treated as local time.
                $cancellationDate = $invoice->lifecycle?->cancelled_at ?? null;
                if ($cancellationDate && strtotime($cancellationDate) !== false) {
                    $dateTime = new \DateTime($cancellationDate, new \DateTimeZone($this->config->getScheduleTimezone()));
                    $cancellationDate = $dateTime->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
                } else {
                    $cancellationDate = current_time('mysql', true); // UTC
                }

                // Update emission record to cancelled (NOT delete!)
                $this->emissionRepo->update($emissionId, [
                    'status' => $invoice->status ?? 'cancelled',
                    'cancellation_date' => $cancellationDate,
                    'cancellation_reason' => $reason,
                    'processing_strategy' => $invoice->processing_strategy,
                    'allowed_actions' => !empty($invoice->allowed_actions) ? json_encode($invoice->allowed_actions) : null,
                ]);

                // Log to history
                $this->historyService->logEvent(
                    $emissionId,
                    'schedule_cancelled',
                    [
                        'order_id' => $orderId,
                        'invoice_id' => $invoiceId,
                        'reason' => $reason,
                        'scheduled_for' => $scheduledAt,
                    ]
                );

                $this->log()->info('nfe.schedule_cancelled.success', [
                    'order_id' => $orderId,
                    'emission_id' => $emissionId,
                    'invoice_id' => $invoiceId,
                    'reason' => $reason,
                ]);
            }

            // Flush cache to ensure fresh data on next load
            clean_post_cache($orderId);

            return ['success' => true, 'message' => __('Scheduled invoice cancelled successfully.', 'woocommerce-cloudxm-nfe')];
        } catch (\Throwable $e) {
            $this->log()->error('nfe.schedule_cancelled.failed', [
                'order_id' => $orderId,
                'invoice_id' => $invoiceId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            return [
                'success' => false,
                'message' => sprintf(
                    __('Failed to cancel scheduled invoice: %s', 'woocommerce-cloudxm-nfe'),
                    $e->getMessage()
                )
            ];
        }
    }

    /**
     * Force unlock/clear NFe data from order
     *
     * Use this when the invoice is stuck or the server has lost the record.
     * This does NOT cancel the invoice on the server - it only clears local state.
     *
     * This method returns the order to its initial state (no_invoice) by clearing
     * ALL NFe-related data including active fields, archived fields, and emission records.
     *
     * @param int $orderId Order ID
     * @param string $reason Reason for force unlock
     * @return array Result with success status and message
     */
    public function forceUnlock(int $orderId, string $reason): array
    {
        try {
            $order = wc_get_order($orderId);
            if (!$order instanceof WC_Order) {
                return ['success' => false, 'message' => __('Order not found.', 'woocommerce-cloudxm-nfe')];
            }

            $invoiceId = (string) $order->get_meta('_cloudxm_nfe_invoice_id');

            // Log the force unlock action (log even if invoice_id is empty)
            $this->log()->warning('nfe.force_unlock', [
                'order_id' => $orderId,
                'invoice_id' => $invoiceId ?: 'none',
                'reason' => $reason,
                'previous_status' => (string) $order->get_meta('_cloudxm_nfe_status'),
            ]);

            // Check for emission record (may exist even without invoice_id for scheduled invoices)
            $emission = $this->emissionRepo->findByOrderId($orderId);
            if ($emission && !empty($emission['invoice_id'])) {
                $invoiceId = $emission['invoice_id'];
            }

            // Check if invoice is scheduled and cancel it on the server first
            $isScheduled = $emission && in_array($emission['status'] ?? '', ['scheduled', 'async_pending'], true);

            if ($isScheduled && $invoiceId !== '') {
                try {
                    // Cancel the scheduled invoice on the server to prevent 409 conflicts
                    $cancelRequest = new CancelInvoiceRequest('Force unlocked: ' . $reason);
                    $this->apiClient->cancelInvoiceV2($invoiceId, $cancelRequest);
                    $this->log()->info('nfe.force_unlock.cancelled_scheduled', [
                        'order_id' => $orderId,
                        'invoice_id' => $invoiceId,
                    ]);
                } catch (\Throwable $e) {
                    // Log but don't fail - the scheduled invoice might not exist on server
                    $this->log()->warning('nfe.force_unlock.cancel_failed', [
                        'order_id' => $orderId,
                        'invoice_id' => $invoiceId,
                        'error' => $e->getMessage(),
                    ]);
                }
            }

            // Clear ALL active NFe data - fields that buildMetaBoxData() uses
            $order->delete_meta_data('_cloudxm_nfe_invoice_id');
            $order->delete_meta_data('_cloudxm_nfe_status');
            $order->delete_meta_data('_cloudxm_nfe_emission_status');
            $order->delete_meta_data('_cloudxm_nfe_access_key');
            $order->delete_meta_data('_cloudxm_nfe_number');
            $order->delete_meta_data('_cloudxm_nfe_dps_number');
            $order->delete_meta_data('_cloudxm_nfe_issuance_date');
            $order->delete_meta_data('_cloudxm_nfe_protocol');
            $order->delete_meta_data('_cloudxm_nfe_verification_code');
            $order->delete_meta_data('_cloudxm_nfe_error');
            $order->delete_meta_data('_cloudxm_nfe_processing_strategy');
            $order->delete_meta_data('_cloudxm_nfe_allowed_actions');
            $order->delete_meta_data('_cloudxm_nfe_created_at');
            $order->delete_meta_data('_cloudxm_nfe_cancelled_at');

            // Clear scheduling-related fields to prevent scheduled UI from showing
            $order->delete_meta_data('_cloudxm_nfe_scheduled_at');
            $order->delete_meta_data('_cloudxm_nfe_schedule_was_cancelled');
            $order->delete_meta_data('_cloudxm_nfe_schedule_cancelled_date');
            $scheduleFieldKey = $this->config->getScheduleDateField();
            if ($scheduleFieldKey !== '') {
                $order->delete_meta_data($scheduleFieldKey);
            }

            // Clear archived NFe display data to prevent cancelled UI from showing
            // CRITICAL: This ensures the order returns to initial state (no_invoice)
            $order->delete_meta_data('_cloudxm_nfe_archived_invoice_id');
            $order->delete_meta_data('_cloudxm_nfe_archived_status');
            $order->delete_meta_data('_cloudxm_nfe_archived_access_key');
            $order->delete_meta_data('_cloudxm_nfe_cancelled_invoice_id');
            $order->delete_meta_data('_cloudxm_nfe_cancelled_status');
            $order->delete_meta_data('_cloudxm_nfe_cancelled_at');
            $order->delete_meta_data('_cloudxm_nfe_cancelled_reason');
            $order->delete_meta_data('_cloudxm_nfe_cancellation_date');
            $order->delete_meta_data('_cloudxm_nfe_cancelled_invoice_history');

            // CRITICAL: Clear force unlock fields themselves to prevent permanent 'no_invoice' state
            // If we don't clear these, the order will always show as 'no_invoice' even after issuing new invoice
            $order->delete_meta_data('_cloudxm_nfe_force_unlocked_at');
            $order->delete_meta_data('_cloudxm_nfe_force_unlock_reason');

            $order->save();

            // CRITICAL: Flush WordPress object cache to ensure WC_Order doesn't return stale cached data
            // Without this, buildMetaBoxData() might read old cached values even after deletion
            clean_post_cache($orderId);

            // DELETE ALL emission records for this order (not just the latest one)
            // This prevents unique key conflicts on (order_id, status) when re-issuing
            if ($emission && isset($emission['id'])) {
                $emissionId = (int) $emission['id'];

                // Log to history BEFORE deleting
                $this->historyService->logEvent(
                    $emissionId,
                    'force_unlock',
                    [
                        'order_id' => $orderId,
                        'invoice_id' => $invoiceId ?: 'none',
                        'reason' => $reason,
                        'previous_status' => $emission['status'] ?? 'unknown',
                        'was_scheduled' => $isScheduled,
                    ]
                );
            }

            $this->emissionRepo->deleteAllByOrderId($orderId);

            // CRITICAL: Increment emission attempt counter so the next issue
            // generates a NEW idempotency key (different from the scheduled one)
            $currentAttempt = (int) $order->get_meta('_nfe_emission_attempt', true);
            if ($currentAttempt < 1) {
                $currentAttempt = 1;
            }
            $order->update_meta_data('_nfe_emission_attempt', $currentAttempt + 1);
            $order->save();

            $this->log()->info('nfe.force_unlock.success', [
                'order_id' => $orderId,
                'invoice_id' => $invoiceId ?: 'none',
            ]);

            return [
                'success' => true,
                'message' => __('NFe data cleared. You can now re-issue the invoice.', 'woocommerce-cloudxm-nfe')
            ];
        } catch (\Throwable $e) {
            $this->log()->error('nfe.force_unlock.failed', [
                'order_id' => $orderId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            return [
                'success' => false,
                'message' => sprintf(
                    __('Failed to force unlock: %s', 'woocommerce-cloudxm-nfe'),
                    $e->getMessage()
                )
            ];
        }
    }

    private function canIssue(WC_Order $order): bool
    {
        $emission = $this->findCurrentEmission($order);
        $emissionStatus = $emission['status'] ?? '';
        $invoiceId = $emission['invoice_id'] ?? '';

        // No emission record at all — can issue (ignore stale order meta)
        if ($invoiceId === '' && $emissionStatus === '') {
            return true;
        }

        // Cancelled or failed — can re-issue
        if (in_array($emissionStatus, ['cancelled', 'failed'], true)) {
            return true;
        }

        // Check if order is in terminal WC status (only for non-cancelled invoices)
        $terminalStatuses = ['cancelled', 'refunded', 'failed'];
        if (in_array($order->get_status(), $terminalStatuses, true)) {
            return false;
        }

        // Scheduled/async_pending — allow re-issuing (user can cancel schedule and issue immediately)
        $reissuableStatuses = array_merge($terminalStatuses, ['scheduled', 'async_pending']);
        if ($emission && !in_array($emissionStatus, $reissuableStatuses, true)) {
            return false;
        }

        // Default: cannot issue if invoice exists and is not cancelled/failed/scheduled
        return false;
    }

    private function canCancel(string $issuanceDate): bool
    {
        if ($issuanceDate === '') {
            return false;
        }

        $limitDays = absint((string) get_option('cloudxm_nfe_cancel_days_limit', '5'));
        $issuanceTimestamp = strtotime($issuanceDate);
        if ($issuanceTimestamp === false) {
            return false;
        }

        $daysElapsed = (current_time('timestamp') - $issuanceTimestamp) / DAY_IN_SECONDS;
        return $daysElapsed <= $limitDays;
    }

    private function normalizeStatus(?string $status): string
    {
        if ($status === null || $status === '') {
            return 'processing';
        }

        $valid = ['processing', 'issued', 'authorized', 'cancelled', 'failed', 'draft'];
        $valid[] = 'scheduled';
        $valid[] = 'async_pending';
        if (in_array($status, $valid, true)) {
            return $status;
        }

        return 'failed';
    }

    private function getStatusLabel(string $status): string
    {
        return match ($status) {
            'draft' => __('Draft', 'woocommerce-cloudxm-nfe'),
            'issued' => __('Issued', 'woocommerce-cloudxm-nfe'),
            'authorized' => __('Authorized', 'woocommerce-cloudxm-nfe'),
            'processing' => __('Processing', 'woocommerce-cloudxm-nfe'),
            'cancelled' => __('Cancelled', 'woocommerce-cloudxm-nfe'),
            'failed' => __('Failed', 'woocommerce-cloudxm-nfe'),
            'scheduled' => __('Agendado', 'woocommerce-cloudxm-nfe'),
            'async_pending' => __('Aguardando Emissão', 'woocommerce-cloudxm-nfe'),
            default => $status,
        };
    }

    private function formatIssuanceDate(string $issuanceDate): string
    {
        if ($issuanceDate === '') {
            return '';
        }

        // Handles both UTC wall time (Y-m-d H:i:s) from emissions table
        // and ISO 8601 with offset (e.g. 2026-04-22T03:27:00-03:00) from order meta.
        // new \DateTime() respects timezone offsets in the string, falling back to
        // the explicit UTC parameter only when no offset is present.
        try {
            $date = new \DateTime($issuanceDate, new \DateTimeZone('UTC'));
            return wp_date(get_option('date_format') . ' ' . get_option('time_format'), $date->getTimestamp());
        } catch (\Exception $e) {
            return $issuanceDate;
        }
    }

    /**
     * Download and store PDF and XML files automatically after issuance
     *
     * Note: Files may not be immediately available after issuance.
     * The NFE server needs time to process and store them.
     * This method will attempt download with retries but won't fail the issuance if files aren't ready.
     *
     * @param string $invoiceId Invoice ID from NFE server
     * @param string $accessKey NFe access key
     * @param string $emissionDate Emission date in MySQL format
     */
    private function downloadAndStoreInvoiceFiles(string $invoiceId, string $accessKey, string $emissionDate): void
    {
        try {
            $emissionDateTime = new \DateTime($emissionDate);

            $this->log()->info('nfe.files_download_started', [
                'invoice_id' => $invoiceId,
                'access_key' => $accessKey,
            ]);

            $paths = $this->fileStorageService->downloadAndStoreAll($invoiceId, $accessKey, $emissionDateTime);

            $this->log()->info('nfe.files_download_completed', [
                'invoice_id' => $invoiceId,
                'access_key' => $accessKey,
                'paths' => $paths,
            ]);
        } catch (\Exception $e) {
            // Don't fail the entire process if file download fails
            // Files can be downloaded later via the download buttons
            $this->log()->warning('nfe.files_download_failed', [
                'invoice_id' => $invoiceId,
                'access_key' => $accessKey,
                'error' => $e->getMessage(),
                'note' => 'Files can be downloaded manually via the UI',
            ]);
        }
    }

    /**
     * Format a DateInterval as human-readable time remaining
     *
     * @param \DateInterval $interval
     * @return string
     */
    private function formatTimeInterval(\DateInterval $interval): string
    {
        $parts = [];

        if ($interval->y > 0) {
            $parts[] = sprintf(_n('%d year', '%d years', $interval->y, 'woocommerce-cloudxm-nfe'), $interval->y);
        }
        if ($interval->m > 0) {
            $parts[] = sprintf(_n('%d month', '%d months', $interval->m, 'woocommerce-cloudxm-nfe'), $interval->m);
        }
        if ($interval->d > 0) {
            $parts[] = sprintf(_n('%d day', '%d days', $interval->d, 'woocommerce-cloudxm-nfe'), $interval->d);
        }
        if ($interval->h > 0) {
            $parts[] = sprintf(_n('%d hour', '%d hours', $interval->h, 'woocommerce-cloudxm-nfe'), $interval->h);
        }
        if ($interval->i > 0) {
            $parts[] = sprintf(_n('%d minute', '%d minutes', $interval->i, 'woocommerce-cloudxm-nfe'), $interval->i);
        }

        if (empty($parts)) {
            return __('less than a minute', 'woocommerce-cloudxm-nfe');
        }

        // Show only the 2 most significant parts
        return implode(', ', array_slice($parts, 0, 2));
    }

    /**
     * Resolve the current emission record for an order.
     *
     * This is the single entry point for reading NFe state. All methods that
     * need to know the current invoice_id, status, or any other emission field
     * MUST call this instead of reading order meta directly.
     *
     * Strategy:
     *   1. Fast path  — look up by _cloudxm_nfe_invoice_id from order meta.
     *                   This works for the common case where meta is in sync.
     *   2. Authoritative path — fall back to findByOrderId() (latest row by id).
     *                   This is the correct path when order meta has been cleared,
     *                   e.g. after cancelScheduledInvoice() deletes invoice_id meta.
     *
     * Do NOT bypass this method by reading order meta directly in lifecycle logic.
     */
    private function findCurrentEmission(WC_Order $order): ?array
    {
        // Fast path: order meta is in sync — use it to avoid a second DB query.
        $invoiceId = (string) $order->get_meta('_cloudxm_nfe_invoice_id');
        if ($invoiceId !== '') {
            $emission = $this->emissionRepo->findByInvoiceId($invoiceId);
            if ($emission) {
                return $emission;
            }
        }

        // Authoritative fallback: order meta may have been cleared (e.g. after
        // cancelScheduledInvoice deletes _cloudxm_nfe_invoice_id). The emissions
        // table always has the current record regardless of order meta state.
        return $this->emissionRepo->findByOrderId($order->get_id());
    }

    private function clearSchedulingMeta(WC_Order $order): void
    {
        $order->delete_meta_data('_cloudxm_nfe_scheduled_at');
        $order->delete_meta_data('_cloudxm_nfe_schedule_was_cancelled');
        $order->delete_meta_data('_cloudxm_nfe_schedule_cancelled_date');

        $scheduleFieldKey = $this->config->getScheduleDateField();
        if ($scheduleFieldKey !== '') {
            $order->delete_meta_data($scheduleFieldKey);
        }
    }

    /**
     * Sync invoice data from API to order meta
     *
     * This method is called when invoice data is updated externally (e.g., from Faturas screen)
     * to ensure order meta stays in sync with the actual invoice state on the server.
     *
     * @param int $orderId Order ID
     * @param string $invoiceId Invoice ID from server
     * @return void
     */
    public function recoverInvoice(int $orderId, string $series, int $dpsNumber): array
    {
        try {
            $order = wc_get_order($orderId);
            if (!$order instanceof WC_Order) {
                return ['success' => false, 'message' => __('Order not found.', 'woocommerce-cloudxm-nfe')];
            }

            $result = $this->apiClient->recoverInvoice([
                'series' => $series,
                'dps_number' => $dpsNumber,
                'environment' => $this->config->getServerEnvironment(),
            ]);

            $error = $result['error'] ?? $result['body']['error'] ?? '';
            if (!empty($error)) {
                return ['success' => false, 'message' => $error];
            }

            $invoiceId = $result['id'] ?? '';
            if ($invoiceId === '') {
                return ['success' => false, 'message' => __('Recovery returned no invoice ID.', 'woocommerce-cloudxm-nfe')];
            }

            $invoice = $this->apiClient->getInvoiceV2($invoiceId);

            if (!$invoice || empty($invoice->id)) {
                return ['success' => false, 'message' => __('Failed to fetch recovered invoice data.', 'woocommerce-cloudxm-nfe')];
            }

            $emissionDate = $invoice->lifecycle?->issued_at ?? $invoice->lifecycle?->created_at ?? null;
            if ($emissionDate && strtotime($emissionDate) !== false) {
                $dateTime = new \DateTime($emissionDate, new \DateTimeZone($this->config->getScheduleTimezone()));
                $emissionDate = $dateTime->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
            } else {
                $emissionDate = current_time('mysql', true);
            }

            $existingEmission = $this->emissionRepo->findByOrderId($orderId, false);

            $emissionData = [
                'invoice_id' => $invoice->id,
                'access_key' => $invoice->nfse?->access_key ?? '',
                'status' => $this->normalizeStatus($invoice->status),
                'emission_date' => $emissionDate,
                'invoice_number' => $invoice->nfse?->number ?? '',
                'invoice_series' => $invoice->nfse?->series ?? '',
                'dps_number' => $invoice->dps_number ?? (string) $dpsNumber,
                'protocol_number' => $invoice->nfse?->protocol ?? '',
                'authorization_code' => '',
                'environment' => $this->config->getEnvironment(),
                'total_value' => (float) $order->get_total(),
                'processing_strategy' => $invoice->processing_strategy,
                'allowed_actions' => !empty($invoice->allowed_actions) ? json_encode($invoice->allowed_actions) : null,
                'created_at' => $invoice->lifecycle?->created_at,
            ];

            if ($existingEmission) {
                $emissionId = (int) $existingEmission['id'];
                $success = $this->emissionRepo->update($emissionId, $emissionData);
            } else {
                $emissionData['order_id'] = $orderId;
                $emissionId = $this->emissionRepo->create($emissionData);
                $success = $emissionId !== false;
            }

            if (!$success) {
                $this->log()->error('nfe.recovery.save_failed', [
                    'order_id' => $orderId,
                    'invoice_id' => $invoice->id,
                ]);
                return ['success' => false, 'message' => __('Failed to save emission record.', 'woocommerce-cloudxm-nfe')];
            }

            $normalizedStatus = $this->normalizeStatus($invoice->status);

            $order->update_meta_data('_cloudxm_nfe_invoice_id', $invoice->id);
            $order->update_meta_data('_cloudxm_nfe_status', $normalizedStatus);
            $order->update_meta_data('_cloudxm_nfe_emission_status', $normalizedStatus);

            if ($invoice->processing_strategy !== null) {
                $order->update_meta_data('_cloudxm_nfe_processing_strategy', $invoice->processing_strategy);
            }
            if (!empty($invoice->allowed_actions)) {
                $order->update_meta_data('_cloudxm_nfe_allowed_actions', json_encode($invoice->allowed_actions));
            }

            if ($invoice->lifecycle !== null) {
                if ($invoice->lifecycle->created_at !== null) {
                    $order->update_meta_data('_cloudxm_nfe_created_at', $invoice->lifecycle->created_at);
                }
                if ($invoice->lifecycle->issued_at !== null) {
                    $order->update_meta_data('_cloudxm_nfe_issuance_date', $invoice->lifecycle->issued_at);
                }
            }

            if ($invoice->nfse !== null) {
                if ($invoice->nfse->access_key !== null) {
                    $order->update_meta_data('_cloudxm_nfe_access_key', $invoice->nfse->access_key);
                }
                if ($invoice->nfse->number !== null) {
                    $order->update_meta_data('_cloudxm_nfe_number', $invoice->nfse->number);
                }
                if ($invoice->nfse->series !== null) {
                    $order->update_meta_data('_cloudxm_nfe_series', $invoice->nfse->series);
                }
                if ($invoice->nfse->protocol !== null) {
                    $order->update_meta_data('_cloudxm_nfe_protocol', $invoice->nfse->protocol);
                }
            }

            if ($invoice->dps_number !== null) {
                $order->update_meta_data('_cloudxm_nfe_dps_number', $invoice->dps_number);
            }

            $this->clearSchedulingMeta($order);
            $order->delete_meta_data('_cloudxm_nfe_error');
            $order->save();

            $this->historyService->logInvoiceEvent(
                $emissionId,
                NFeHistoryService::EVENT_TYPE_EMISSION,
                $invoice,
                ['order_id' => $orderId, 'recovered' => true]
            );

            $this->log()->info('nfe.recovery_success', [
                'order_id' => $orderId,
                'invoice_id' => $invoice->id,
                'series' => $series,
                'dps_number' => $dpsNumber,
                'status' => $normalizedStatus,
            ]);

            return ['success' => true, 'invoice_id' => $invoice->id];
        } catch (\Throwable $e) {
            $this->log()->error('nfe.recovery_failed', [
                'order_id' => $orderId,
                'series' => $series,
                'dps_number' => $dpsNumber,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
            return ['success' => false, 'message' => $e->getMessage()];
        }
    }

    public function syncInvoiceToOrderMeta(int $orderId, string $invoiceId): void
    {
        try {
            $order = wc_get_order($orderId);
            if (!$order instanceof WC_Order) {
                $this->log()->warning('nfe.sync_order_not_found', [
                    'order_id' => $orderId,
                    'invoice_id' => $invoiceId,
                ]);
                return;
            }

            // Fetch current invoice data from API
            $invoice = $this->apiClient->getInvoiceV2($invoiceId);

            if (!$invoice || empty($invoice->id)) {
                $this->log()->warning('nfe.sync_invoice_not_found', [
                    'order_id' => $orderId,
                    'invoice_id' => $invoiceId,
                ]);
                return;
            }

            // Prepare emission date.
            // API dates are ISO 8601 with timezone offset (e.g. 2026-04-22T21:19:30-03:00).
            // When offset is present, PHP uses it (ignoring the explicit param).
            // When offset is absent, the explicit param is used as fallback.
            // Parse as WP timezone so that dates without offset are treated as local time.
            $emissionDate = $invoice->lifecycle?->issued_at ?? $invoice->lifecycle?->created_at ?? null;
            if ($emissionDate && strtotime($emissionDate) !== false) {
                $dateTime = new \DateTime($emissionDate, new \DateTimeZone($this->config->getScheduleTimezone()));
                $emissionDate = $dateTime->setTimezone(new \DateTimeZone('UTC'))->format('Y-m-d H:i:s');
            } else {
                $emissionDate = current_time('mysql', true); // UTC
            }

            // Normalize status
            $status = $this->normalizeStatus($invoice->status ?? 'processing');

            // Update order meta with current invoice data
            $order->update_meta_data('_cloudxm_nfe_invoice_id', $invoice->id);
            $order->update_meta_data('_cloudxm_nfe_status', $status);
            $order->update_meta_data('_cloudxm_nfe_emission_status', $status);
            $order->update_meta_data('_cloudxm_nfe_issuance_date', $emissionDate);

            // Update NFe details if available
            if (!empty($invoice->nfse?->access_key)) {
                $order->update_meta_data('_cloudxm_nfe_access_key', $invoice->nfse->access_key);
            }
            if (!empty($invoice->nfse?->number)) {
                $order->update_meta_data('_cloudxm_nfe_number', $invoice->nfse->number);
            }
            if (!empty($invoice->nfse?->series)) {
                $order->update_meta_data('_cloudxm_nfe_series', $invoice->nfse->series);
            }
            if (!empty($invoice->nfse?->protocol)) {
                $order->update_meta_data('_cloudxm_nfe_protocol', $invoice->nfse->protocol);
            }
            if (!empty($invoice->dps_number)) {
                $order->update_meta_data('_cloudxm_nfe_dps_number', $invoice->dps_number);
            }

            // Update v2-specific fields
            if ($invoice->processing_strategy !== null) {
                $order->update_meta_data('_cloudxm_nfe_processing_strategy', $invoice->processing_strategy);
            }
            if (!empty($invoice->allowed_actions)) {
                $order->update_meta_data('_cloudxm_nfe_allowed_actions', json_encode($invoice->allowed_actions));
            }

            // Update cancellation data if cancelled
            if ($status === 'cancelled') {
                if ($invoice->lifecycle?->cancelled_at !== null) {
                    $order->update_meta_data('_cloudxm_nfe_cancelled_at', $invoice->lifecycle->cancelled_at);
                }
            }

            $order->save();

            $this->log()->info('nfe.sync_success', [
                'order_id' => $orderId,
                'invoice_id' => $invoiceId,
                'status' => $status,
            ]);
        } catch (\Throwable $e) {
            $this->log()->error('nfe.sync_failed', [
                'order_id' => $orderId,
                'invoice_id' => $invoiceId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
        }
    }
}
