<?php

namespace BexioConnectorByHelvy;

/**
 * WooCommerce to Bexio conversion / parsing
 *
 * @since 0.1.0
 */

defined('ABSPATH') || exit;

class WCToBexio
{
    use Singleton;

    public function __construct() {}

    /**
     * Syncs a WooCommerce Order to bexio and create the related delivery note (in draft).
     * 
     * @param Order $order WooCommerce Order resource
     * @param boolean $force_sync Whether to force the sync or not
     * @return array|WP_Error Bexio order and invoice resources
     */
    public function sync_order($order, $force_sync = false)
    {
        // Pre-sync checks
        if ($order->get_total() == 0 && !get_option(AdminAPISettings::$prefix . 'sync_zero_orders')) {
            $err_msg = "Cannot create invoice for free order (total=0)";
            return new \WP_Error('SYNC_BEXIO_ORDER', $err_msg, array(
                'status_code' => 412, // Precondition Failed
            ));
        }
        if ($this->is_order_already_synced($order)) return;
        if (!$this->is_order_sync_authorized_based_on_payment_method($order) && !$force_sync) {
            return;
        }

        // Check if the order status is in the list of statuses to sync
        $_sync_statuses = get_option(AdminAPISettings::$prefix . 'sync_orders_statuses');
        if (!$_sync_statuses || (is_array($_sync_statuses) && !in_array($order->get_status(), $_sync_statuses))) {
            return ["status" => "SKIPPED", "reason" => "Order status not authorized for creating bexio order (status: " . $order->get_status() . ")"];
        }

        // Customer sync
        $contacts = $this->find_or_upsert_contact_from_order($order);
        if ($contacts instanceof \WP_Error) {
            return $contacts;
        }
        
        $contact = $contacts["contact"];
        $subcontact = $contacts["subcontact"];

        // Customer's address(es) sync
        $billing_address = $this->upsert_contact_additional_address_from_order($contact['id'], $order, "billing");
        if ($billing_address instanceof \WP_Error) {
            return $billing_address;
        }

        $shipping_address_different_from_billing = ($order->get_shipping_address_1() && ($order->get_billing_address_1() != $order->get_shipping_address_1()))
            || ($order->get_shipping_postcode() && ($order->get_billing_postcode() != $order->get_shipping_postcode()))
            || ($order->get_shipping_city() && ($order->get_billing_city() != $order->get_shipping_city()));
        $shipping_address = $shipping_address_different_from_billing
            ? $this->upsert_contact_additional_address_from_order($contact['id'], $order, "shipping")
            : null; // = Use customers' billing address (same as shipping)
        if ($shipping_address instanceof \WP_Error) {
            return $shipping_address;
        }

        $is_tax_exempt = count($order->get_taxes()) == 0;
        $bexio_line_items = $this->woocommerce_line_items_to_bexio_positions($order, $is_tax_exempt);
        if ($bexio_line_items instanceof \WP_Error) {
            return $bexio_line_items;
        }
        $bexio_fees_lines = $this->woocommerce_fees_to_bexio_positions($order, $is_tax_exempt);
        if ($bexio_fees_lines instanceof \WP_Error) {
            return $bexio_fees_lines;
        }
        $bexio_shipping_lines = $this->woocommerce_shipping_to_bexio_positions($order, $is_tax_exempt);
        if ($bexio_shipping_lines instanceof \WP_Error) {
            return $bexio_shipping_lines;
        }
        $bexio_discount_lines = $this->woocommerce_discount_to_bexio_position($order);
        if ($bexio_discount_lines instanceof \WP_Error) {
            return $bexio_discount_lines;
        }

        // Retrieve Bexio's currency ID or create it if it doesn't exist yet in Bexio
        $bexio_currencies = API::instance()->get_currencies();
        if ($bexio_currencies instanceof \WP_Error) {
            return $bexio_currencies;
        }
        $payment_currency = strtoupper($order->get_currency());
        $currency_id = null;
        foreach ($bexio_currencies as $currency) {
            if ($payment_currency == strtoupper($currency["name"])) {
                $currency_id = $currency["id"];
                break;
            }
        }
        if (!$currency_id) {
            $bexio_currency = API::instance()->create_currency($payment_currency);
            if ($bexio_currency instanceof \WP_Error) {
                return $bexio_currency;
            }
            $currency_id = $bexio_currency["id"];
        }

        $payment_gateway = $order->get_payment_method();
        $bexio_bank_account_id = $this->get_bank_account_from_payment_gateway($payment_gateway, $payment_currency);

        $custom_header_text = $this->parse_header_text($order);
        $custom_footer_text = $this->parse_footer_text($order);

        $use_bexio_taxes = get_option(AdminAPISettings::$prefix . 'use_bexio_products_taxes');
        $prices_include_tax = wc_prices_include_tax() || $use_bexio_taxes;
        $is_valid_from = $this->iso_date_to_bexio_locale_date($order->get_date_created()->__toString());

        // Create order in Bexio
        $bexio_order_body = [
            "title" => $this->generate_bexio_title_from_order($order),
            "is_valid_from" => $is_valid_from,
            "contact_id" => $contact["id"],
            "contact_sub_id" => $subcontact ? $subcontact["id"] : null,
            "user_id" => get_option(AdminAPISettings::$prefix . "bexio_default_user"),
            "pr_project_id" => null,
            "language_id" => get_option(AdminAPISettings::$prefix . "bexio_default_language"),
            "bank_account_id" => $bexio_bank_account_id,
            "payment_type_id" => null,
            "currency_id" => $currency_id,
            "header" => $custom_header_text,
            "footer" => $custom_footer_text,
            "mwst_type" => 0,
            "mwst_is_net" => !$prices_include_tax,
            "show_position_taxes" => false,
            "api_reference" => $order->get_id(),
            "template_slug" => get_option(AdminAPISettings::$prefix . "bexio_default_document_template")
                ? get_option(AdminAPISettings::$prefix . "bexio_default_document_template")
                : null,
            "positions" => [
                ...$bexio_line_items,
                ...$bexio_fees_lines,
                ...$bexio_shipping_lines,
                ...$bexio_discount_lines,
            ],
        ];

        // Handle custom - non-default - billing/shipping addresses
        if (!empty($billing_address["id"])) {
            $bexio_order_body["contact_address_id"] = $billing_address["id"];
        }
        if ($shipping_address_different_from_billing && !empty($shipping_address["id"])) {
            $bexio_order_body["delivery_address_type"] = 1; // 0 (default) = use billing address; 1 = use another address
            $bexio_order_body["delivery_address_id"] = $shipping_address["id"];
        }

        $bexio_order = API::instance()->create_order($bexio_order_body);
        if ($bexio_order instanceof \WP_Error) {
            return $bexio_order;
        }

        // Ensure we are working with the latest order data (patch for when the order is created in "new_order" hook)
        $order = wc_get_order($order->get_id());
        
        // Add metafields to order
        update_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_order_id', $bexio_order["id"]);
        update_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_order_number', $bexio_order["document_nr"]);

        return ["bexio_order" => $bexio_order];
    }

    /**
     * Creates the invoice in bexio corresponding to the given WooCommerce order (requires the bexio order to have been already created).
     * 
     * @param int $order_id WooCommerce Order ID
     * @param boolean $force_sync Whether to force the sync or not
     * @return array|WP_Error Bexio order and invoice resources
     */
    public function sync_order_create_invoice($order_id)
    {
        $order = wc_get_order($order_id);
        if (!$order) {
            return new \WP_Error('SYNC_BEXIO_DELIVERY', "Order not found", array(
                'status_code' => 404, // Not Found
            ));
        }

        // Check if invoice already exists in our mapping table
        global $wpdb;
        $table_name = $wpdb->prefix . 'bexio_invoice_mappings';
        
        // Create table if it doesn't exist
        $this->ensure_invoice_mappings_table_exists();
        
        // Check for existing mapping
        $existing_mapping = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$table_name} WHERE order_id = %d",
            $order_id
        ));
        
        if ($existing_mapping) {
            _bcbh_log("⚠️ Invoice mapping already exists for order {$order_id} (Bexio ID: {$existing_mapping->bexio_invoice_id})");
            return new \WP_Error('DUPLICATE_INVOICE', "Invoice already exists in mapping table", array(
                'status_code' => 409, // Conflict
                'bexio_invoice_id' => $existing_mapping->bexio_invoice_id
            ));
        }

        // Check if invoice already exists in meta
        $existing_invoice_id = get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_invoice_id', true);
        if ($existing_invoice_id) {
            _bcbh_log("⏭️ Invoice already exists in meta (ID: {$existing_invoice_id}), skipping creation");
            
            // Store in mapping table for future reference
            $wpdb->insert(
                $table_name,
                array(
                    'order_id' => $order_id,
                    'bexio_invoice_id' => $existing_invoice_id,
                    'created_at' => current_time('mysql'),
                    'source' => 'meta'
                ),
                array('%d', '%d', '%s', '%s')
            );
            
            return array(
                'bexio_invoice' => array(
                    'id' => $existing_invoice_id
                )
            );
        }

        // Check if the order status is in the list of statuses allowed to create invoice
        $_sync_statuses = get_option(AdminAPISettings::$prefix . 'sync_invoices_statuses');
        if (!$_sync_statuses || (is_array($_sync_statuses) && !in_array($order->get_status(), $_sync_statuses))) {
            return ["status" => "SKIPPED", "reason" => "Order status not authorized for creating invoice (status: " . $order->get_status() . ")"];
        }

        $bexio_order_id = get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_order_id', true);
        if (!$bexio_order_id) {
            return new \WP_Error('SYNC_BEXIO_DELIVERY', "No bexio order associated with this WooCommerce order", array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        // Fetch the order from bexio first
        $bexio_order = API::instance()->get_order_by_id($bexio_order_id);

        $bexio_invoice = API::instance()->create_invoice_from_order($bexio_order["id"]);
        if ($bexio_invoice instanceof \WP_Error) {
            return $bexio_invoice;
        }

        $custom_header_text = $this->parse_header_text($order);
        $custom_footer_text = $this->parse_footer_text($order);

        // Set invoice date to match order's date if not already the case
        if ($bexio_order["is_valid_from"] != $bexio_invoice["is_valid_from"]
            || $bexio_order["user_id"] != $bexio_invoice["user_id"]
            || get_option(AdminAPISettings::$prefix . "bexio_default_document_template")
            || $custom_header_text || $custom_footer_text) {

            $is_valid_from = $this->iso_date_to_bexio_locale_date($order->get_date_created()->__toString());

            $updated_invoice = API::instance()->update_invoice($bexio_invoice["id"], [
                "is_valid_from" => $is_valid_from,
                "user_id" => get_option(AdminAPISettings::$prefix . "bexio_default_user"),
                "template_slug" => get_option(AdminAPISettings::$prefix . "bexio_default_document_template")
                    ? get_option(AdminAPISettings::$prefix . "bexio_default_document_template")
                    : null,
                "header" => $custom_header_text ? $custom_header_text : $bexio_invoice["header"],
                "footer" => $custom_footer_text ? $custom_footer_text : $bexio_invoice["footer"],
            ]);
            if ($updated_invoice instanceof \WP_Error) {
                return $updated_invoice;
            }
        }

        // Add metafields to order
        update_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_invoice_id', $bexio_invoice["id"]);
        update_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_invoice_number', $bexio_invoice["document_nr"]);

        // Store in mapping table
        $wpdb->insert(
            $table_name,
            array(
                'order_id' => $order_id,
                'bexio_invoice_id' => $bexio_invoice["id"],
                'created_at' => current_time('mysql'),
                'source' => 'api'
            ),
            array('%d', '%d', '%s', '%s')
        );

        return ["bexio_invoice" => $bexio_invoice];
    }

    /**
     * Ensure the invoice mappings table exists
     */
    private function ensure_invoice_mappings_table_exists() {
        global $wpdb;
        $table_name = $wpdb->prefix . 'bexio_invoice_mappings';
        
        if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") != $table_name) {
            $charset_collate = $wpdb->get_charset_collate();
            
            $sql = "CREATE TABLE {$table_name} (
                id bigint(20) NOT NULL AUTO_INCREMENT,
                order_id bigint(20) NOT NULL,
                bexio_invoice_id bigint(20) NOT NULL,
                created_at datetime NOT NULL,
                source varchar(50) NOT NULL,
                PRIMARY KEY  (id),
                UNIQUE KEY order_id (order_id),
                KEY bexio_invoice_id (bexio_invoice_id)
            ) $charset_collate;";
            
            require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
            dbDelta($sql);
            
            _bcbh_log("Created invoice mappings table");
        }
    }

    /**
     * Issue the invoice in bexio corresponding to the given WooCommerce order (requires the bexio invoice to have been already created).
     * 
     * @param int $order_id WooCommerce Order ID
     * @param boolean $force_sync Whether to force the sync or not
     * @return array|WP_Error Bexio order and invoice resources
     */
    public function sync_order_issue_invoice($order_id)
    {
        $order = wc_get_order($order_id);
        if (!$order) {
            return new \WP_Error('SYNC_BEXIO_DELIVERY', "Order not found", array(
                'status_code' => 404, // Not Found
            ));
        }

        // Check if the order status is in the list of statuses allowed to issue invoice
        $_sync_statuses = get_option(AdminAPISettings::$prefix . 'sync_issue_invoices_statuses');
        if (!$_sync_statuses || (is_array($_sync_statuses) && !in_array($order->get_status(), $_sync_statuses))) {
            return [ "status" => "SKIPPED", "reason" => "Order status not authorized for issuing invoice (status: " . $order->get_status() . ")" ];
        }

        if ($this->keep_invoice_draft_based_on_payment_method($order)) {
            return [ "status" => "SKIPPED", "reason" => "Invoice should be kept in draft status (payment method: " . $order->get_payment_method() . ")"];
        }

        $bexio_invoice_id = get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_invoice_id', true);
        if (!$bexio_invoice_id) {
            return new \WP_Error('SYNC_BEXIO_DELIVERY', "No bexio invoice associated with this WooCommerce order", array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        $bexio_issue_invoice = API::instance()->issue_invoice($bexio_invoice_id);
        if ($bexio_issue_invoice instanceof \WP_Error) {
            return $bexio_issue_invoice;
        }
    }

    /**
     * Creates the delivery in bexio corresponding to the given WooCommerce order (requires the bexio order to have been already created).
     * 
     * @param int $order_id WooCommerce Order ID
     * @param boolean $force_sync Whether to force the sync or not
     * @return array|WP_Error Bexio order and invoice resources
     */
    public function sync_order_create_delivery($order_id)
    {
        if (!get_option(AdminAPISettings::$prefix . 'auto_create_delivery_notes')) {
            return new \WP_Error('SYNC_BEXIO_INVOICE', "Auto-create delivery notes is disabled", array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        $order = wc_get_order($order_id);
        if (!$order) {
            return new \WP_Error('SYNC_BEXIO_DELIVERY', "Order not found", array(
                'status_code' => 404, // Not Found
            ));
        }

        $bexio_order_id = get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_order_id', true);
        if (!$bexio_order_id) {
            return new \WP_Error('SYNC_BEXIO_DELIVERY', "No bexio order associated with this WooCommerce order", array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        if (get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_delivery_id', true)) {
            return new \WP_Error('SYNC_BEXIO_DELIVERY', "Bexio delivery already created for this WooCommerce order", array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        // Create the delivery for this order
        $bexio_delivery = API::instance()->create_delivery_from_order($bexio_order_id);
        if ($bexio_delivery instanceof \WP_Error) {
            return $bexio_delivery;
        }

        // Add metafields to order
        update_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_delivery_id', $bexio_delivery["id"]);
        update_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_delivery_number', $bexio_delivery["document_nr"]);

        return ["bexio_delivery" => $bexio_delivery];
    }

    /**
     * Parses the footer text for the invoice based on the settings and the order.
     */
    private function parse_footer_text($order)
    {
        $footer_text = get_option(AdminAPISettings::$prefix . "invoice_footer_text");
        if (!$footer_text) return false; // Return false to indicate no footer text

        $footer_text = str_replace("\n", '<br>', $footer_text);
        $footer_text = str_replace('{woocommerce_order_id}', $order->get_id(), $footer_text);
        $footer_text = str_replace('{woocommerce_order_number}', $order->get_id(), $footer_text);
        $footer_text = str_replace('{woocommerce_order_payment_method}', $order->get_payment_method_title(), $footer_text);
        $footer_text = str_replace('{woocommerce_order_customer_notes}', $order->get_customer_note(), $footer_text);
        $footer_text = preg_replace('/\*{2}([^\*\_]+)\*{2}/', '<b>$1</b>', $footer_text);
        $footer_text = preg_replace('/\*{1}([^\*\_]+)\*{1}/', '<i>$1</i>', $footer_text);
        return $footer_text;
    }

    /**
     * Parses the header text for the invoice based on the settings and the order.
     */
    private function parse_header_text($order)
    {
        $header_text = get_option(AdminAPISettings::$prefix . "invoice_header_text");
        if (!$header_text) return false; // Return false to indicate no header text

        $header_text = str_replace("\n", '<br>', $header_text);
        $header_text = str_replace('{woocommerce_order_id}', $order->get_id(), $header_text);
        $header_text = str_replace('{woocommerce_order_number}', $order->get_id(), $header_text);
        $header_text = str_replace('{woocommerce_order_payment_method}', $order->get_payment_method_title(), $header_text);
        $header_text = str_replace('{woocommerce_order_customer_notes}', $order->get_customer_note(), $header_text);
        $header_text = preg_replace('/\*{2}([^\*\_]+)\*{2}/', '<b>$1</b>', $header_text);
        $header_text = preg_replace('/\*{1}([^\*\_]+)\*{1}/', '<i>$1</i>', $header_text);
        return trim($header_text);
    }

    /**
     * Syncs a WooCommerce Product to bexio (upsert).
     * 
     * @param Product $product WooCommerce Product resource
     * @param boolean $force_sync Whether to force the sync or not
     * @return array|WP_Error Bexio product or error
     */
    public function sync_product($product)
    {
        $is_virtual = $product->is_virtual();
        $is_downloadable = $product->is_downloadable();
        $is_physical = !$is_virtual && !$is_downloadable;

        $is_stock = get_option(AdminAPISettings::$prefix . "sync_stocks") == "BEXIO_TO_WOOCOMMERCE";
        
        // Check if product has a 'bexio_product_id' metafield
        $bexio_product_id = $product->get_meta(AdminAPISettings::$prefix . 'bexio_product_id') ?: get_post_meta($product->get_id(), AdminAPISettings::$prefix . 'bexio_product_id', true);

        // Try to find the article in bexio based on its SKU, otherwise based on its "bexio_product_id"
        $bexio_product = null;
        if ($product->get_sku()) {
            $bexio_product = API::instance()->get_article_by_sku($product->get_sku());
        } else if ($bexio_product_id) {
            $bexio_product = API::instance()->get_article_by_id($bexio_product_id);
        }

        $product_name = $product->get_title();
        if ($product instanceof \WC_Product_Variation) {
            $product_name .= ' - ' . wc_get_formatted_variation($product, true, false);
        }

        if ($bexio_product) {
            // If bexio product is found, update it in bexio
            $bexio_product_data = [
                "intern_code" => $product->get_sku(),
                "intern_name" => $product_name,
                "purchase_price" => $product->get_meta('_purchase_price') ?: get_post_meta($product->get_id(), '_purchase_price', true),
                "sale_price" => $product->get_price(),
                "currency_id" => get_option(AdminAPISettings::$prefix . "bexio_default_currency"),
            ];

            // Optional/conditional fields
            if (get_option(AdminAPISettings::$prefix . "sync_products_description")) $bexio_product_data["intern_description"] = $product->get_description();
            if ($product->get_width()) $bexio_product_data["width"] = round($product->get_width() * 10); // Convert cm to mm and round to integer
            if ($product->get_height()) $bexio_product_data["height"] = round($product->get_height() * 10); // Convert cm to mm and round to integer
            if ($product->get_weight()) $bexio_product_data["weight"] = round($product->get_weight() * 1000); // Convert kg to g and round to integer
            
            $bexio_product = API::instance()->update_article($bexio_product["id"], $bexio_product_data);
        } else {
            // Otherwise, create it in bexio
            $bexio_product = API::instance()->create_article([
                "user_id" => get_option(AdminAPISettings::$prefix . "bexio_default_user"),
                "article_type_id" => $is_physical ? 1 : 2, // 1=physical; 2=service
                "contact_id" => null,
                "deliverer_code" => null,
                "deliverer_name" => null,
                "deliverer_description" => null,
                "intern_code" => $product->get_sku(),
                "intern_name" => $product_name,
                "intern_description" => get_option(AdminAPISettings::$prefix . "sync_products_description") ? $product->get_description() : null,
                "intern_description" => null,
                "purchase_price" => $product->get_meta('_purchase_price'),
                "sale_price" => $product->get_price(),
                "purchase_total" => null,
                "sale_total" => null,
                "currency_id" => get_option(AdminAPISettings::$prefix . "bexio_default_currency"),
                "tax_income_id" => null,
                "tax_expense_id" => null,
                "unit_id" => null,
                "is_stock" => $is_stock,
                "stock_id" => null,
                "stock_place_id" => null,
                "stock_nr" => $product->get_stock_quantity(),
                "stock_min_nr" => 0,
                "width" => $product->get_width() ? round($product->get_width() * 10) : null, // Convert cm to mm and round to integer
                "height" => $product->get_height() ? round($product->get_height() * 10) : null, // Convert cm to mm and round to integer
                "weight" => $product->get_weight() ? round($product->get_weight() * 1000) : null, // Convert kg to g and round to integer
                "volume" => null,
                "remarks" => null,
                "delivery_price" => null,
                "article_group_id" => null,
            ]);
        }
        
        if ($bexio_product instanceof \WP_Error) {
            return $bexio_product;
        }

        // Update product's metafield
        update_post_meta($product->get_id(), AdminAPISettings::$prefix . 'bexio_product_id', $bexio_product["id"]);

        return ["bexio_product" => $bexio_product];
    }

    /**
     * Indicates if the payment of the given order should be ignored from sync
     * 
     * @param object $order The WooCommerce order
     * @return boolean True if the payment should be ignored, false otherwise
     */
    public function ignore_payment_sync_based_on_payment_method($order)
    {
        $ignore_payment_methods = get_option(AdminAPISettings::$prefix . 'ignore_payment_sync_for_payment_methods');
        if (empty($ignore_payment_methods) || !is_array($ignore_payment_methods)) return false;
        $order_payment_method = $order->get_payment_method();
        foreach ($ignore_payment_methods as $method) {
            if ($method['paymentMethod'] === $order_payment_method) {
                return true;
            }
        }
        return false;
    }

    /**
     * Indicates if the invoice should stay draft (not issued) based on the payment method
     * 
     * @param object $order The WooCommerce order
     * @return boolean True if the invoice should stay in draft status
     */
    public function keep_invoice_draft_based_on_payment_method($order)
    {
        $ignore_payment_methods = get_option(AdminAPISettings::$prefix . 'ignore_payment_sync_for_payment_methods');
        if (empty($ignore_payment_methods) || !is_array($ignore_payment_methods)) return false;
        $order_payment_method = $order->get_payment_method();
        foreach ($ignore_payment_methods as $method) {
            if ($method['paymentMethod'] === $order_payment_method && $method['keepInvoicesDraft']) {
                return true;
            }
        }
        return false;
    }

    /**
     * Syncs a WooCommerce Order's payment to bexio.
     * 
     * @param Order $order WooCommerce Order resource
     */
    public function sync_order_payment($order, $force_sync = false)
    {
        // Pre-sync checks

        // Check if payments sync is enabled
        if (!get_option(AdminAPISettings::$prefix . 'sync_payments') && !$force_sync) {
            return ["status" => "SKIPPED", "reason" => "sync_payments disabled"];
        }

        // Check if the order is authorized to be synced for this order's payment method
        if ($this->ignore_payment_sync_based_on_payment_method($order)) {
            $err_msg = "Payment sync is disabled for this payment method";
            return new \WP_Error('SYNC_BEXIO_PAYMENT', $err_msg, array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        // Check if the order has been paid
        if (!$order->is_paid()) {
            $err_msg = "Payment sync: order has not been paid yet";
            return new \WP_Error('SYNC_BEXIO_PAYMENT', $err_msg, array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        // 1. Retrieve invoice
        $bexio_invoice_id = get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_invoice_id', true);
        if (!$bexio_invoice_id) {
            return ["status" => "SKIPPED", "reason" => "Payment sync: invoice not yet created for this order"];
        }

        // 2. Check existing payments (skip if some already exist)
        $existing_payments = API::instance()->get_invoice_payments($bexio_invoice_id);
        if ($existing_payments && is_array($existing_payments) && count($existing_payments)) {
            $err_msg = "Payment sync: payment(s) already exist in Bexio for this order";
            return new \WP_Error('SYNC_BEXIO_PAYMENT', $err_msg, array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        // 3. Mark invoice paid by creating the related payment
        $payment_currency = strtoupper($order->get_currency());
        $payment_date = $order->get_date_paid()
            ? $this->iso_date_to_bexio_locale_date($order->get_date_paid())
            : (new \DateTime())->format('Y-m-d');
        
        $bexio_payments = [];

        // Retrieve gift cards (if applicable)
        $giftcards = $order->get_items('gift_card');
        $giftcards_total = 0;
        if ($giftcards) {
            foreach ( $giftcards as $giftcard ) {
                $amount = $giftcard->get_amount();
                if ($amount <= 0) {
                    continue;
                }
                $giftcards_total += $amount;

                $bexio_bank_account_id = $this->get_bank_account_from_payment_gateway("gift_card", $payment_currency);
                $bexio_payments[] = [
                    "date" => $payment_date,
                    "value" => $amount,
                    "bank_account_id" => $bexio_bank_account_id,
                    "payment_service_id" => null,
                ];
            }
        }

        // Retrieve payment currency and gateway
        $payment_gateway = $order->get_payment_method();
        $bexio_bank_account_id = $this->get_bank_account_from_payment_gateway($payment_gateway, $payment_currency);
        $bexio_payments[] = [
            "date" => $payment_date,
            "value" => $order->get_total(),
            "bank_account_id" => $bexio_bank_account_id,
            "payment_service_id" => null,
        ];

        $bexio_payments_res = [];
        foreach ($bexio_payments as $bexio_payment) {
            $bexio_payment_res = API::instance()->create_invoice_payment($bexio_invoice_id, $bexio_payment);
            if ($bexio_payment_res instanceof \WP_Error) {
                return $bexio_payment_res;
            }
            $bexio_payments_res[] = $bexio_payment_res;
        }


        // Ensure we are working with the latest order data (patch for when the order is created in "new_order" hook)
        $order = wc_get_order($order->get_id());

        // Add metafield to order
        $bexio_payments_ids = implode(',', array_column($bexio_payments_res, 'id'));
        update_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_payment_id', $bexio_payments_ids);

        return ["bexio_payment" => $bexio_payments_res];
    }

    /**
     * Syncs a WooCommerce Order's fulfillment to bexio.
     * 
     * @param Order $order WooCommerce Order resource
     */
    public function sync_order_fulfillment($order, $force_sync = false)
    {
        if (get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_fulfillment_id', true)) {
            return new \WP_Error('SYNC_BEXIO_DELIVERY', "Bexio order already fulfilled", array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        $_sync_statuses = get_option(AdminAPISettings::$prefix . 'sync_fulfillments_statuses');
        if (!$_sync_statuses || (is_array($_sync_statuses) && !in_array($order->get_status(), $_sync_statuses))) {
            return ["status" => "SKIPPED", "reason" => "Order status not authorized for marking the bexio order as 'done' (status: " . $order->get_status() . ")"];
        }

        $bexio_order_id = get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_order_id', true);
        if (!$bexio_order_id) {
            $err_msg = "No bexio order associated with this WooCommerce order";
            return new \WP_Error('ISSUE_BEXIO_DELIVERY', $err_msg, array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        $bexio_delivery_id = get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_delivery_id', true);
        if (!$bexio_delivery_id) {
            $err_msg = "No bexio delivery associated with this WooCommerce order";
            return new \WP_Error('ISSUE_BEXIO_DELIVERY', $err_msg, array(
                'status_code' => 412, // Precondition Failed
            ));
        }

        // 2. Issue the delivery (will mark the order as "Done" in bexio)
        $issued_delivery = API::instance()->issue_delivery($bexio_delivery_id);
        if ($issued_delivery instanceof \WP_Error) {
            return $issued_delivery;
        }

        // Ensure we are working with the latest order data (patch for when the order is created in "new_order" hook)
        $order = wc_get_order($order->get_id());

        // Add metafield to order
        update_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_fulfillment_id', $bexio_delivery_id);

        return ["issued_delivery" => $issued_delivery];
    }

    /**
     * Converts an ISO 8601 date into the date format expected by bexio.
     * 
     * @param string $iso_date The ISO 8601 date to convert
     * @return string The Y-m-d formated date expected by bexio
     */
    protected function iso_date_to_bexio_locale_date($iso_date)
    {
        try {
            $date = new \DateTime($iso_date, new \DateTimeZone('UTC'));
            $date->setTimezone(new \DateTimeZone('Europe/Zurich'));
            return $date->format('Y-m-d');
        } catch (\Exception $e) {
            return null;
        }
    }

    /**
     * Get the bexio bank account to be used based on the given payment gateway and payment currency.
     * 
     * @param string $gateway Payment gateway method (usually a "technical" name)
     * @param string $payment_currency Currency used for the order
     * @return array ID of the bexio bank account to use
     */
    protected function get_bank_account_from_payment_gateway($gateway, $payment_currency)
    {
        $bexio_bank_account = get_option(AdminAPISettings::$prefix . "bexio_default_bank_account");
        $custom_payment_methods_to_bank_accounts = get_option(AdminAPISettings::$prefix . "custom_payment_methods_to_bank_accounts");
        if (!empty($custom_payment_methods_to_bank_accounts)) {
            // Check for bank account with currency matching
            $found_in_custom_bank_account_binding_with_currency = null;
            foreach ($custom_payment_methods_to_bank_accounts as $c) {
                if (
                    strtolower($c["paymentMethod"]) === strtolower($gateway) &&
                    strtolower($c["currency"]) === strtolower($payment_currency)
                ) {
                    $found_in_custom_bank_account_binding_with_currency = $c;
                    break;
                }
            }

            if ($found_in_custom_bank_account_binding_with_currency) {
                $bexio_bank_account = $found_in_custom_bank_account_binding_with_currency["bexioBankAccount"];
            } else {
                // Check for bank account with empty currency ("default" mapping)
                $found_in_custom_bank_account_binding = null;
                foreach ($custom_payment_methods_to_bank_accounts as $c) {
                    if (strtolower($c["paymentMethod"]) === strtolower($gateway) && empty($c["currency"])) {
                        $found_in_custom_bank_account_binding = $c;
                        break;
                    }
                }

                if ($found_in_custom_bank_account_binding) {
                    $bexio_bank_account = $found_in_custom_bank_account_binding["bexioBankAccount"];
                }
            }
        }

        return $bexio_bank_account;
    }

    /**
     * Get applicable tax data for shipping in an order.
     *
     * @param WC_Order $order The WooCommerce order.
     * @return array Formatted as WC_Order_Item::get_taxes(), with 'total' and 'subtotal' arrays.
     */
    function get_shipping_taxes_formatted($order) {
        // Ensure the provided object is a valid WooCommerce order
        if (!$order instanceof \WC_Order) {
            return [
                'total' => [],
                'subtotal' => []
            ];
        }

        // Determine the shipping destination from the order
        $destination = [
            'country'  => $order->get_shipping_country(),
            'state'    => $order->get_shipping_state(),
            'postcode' => $order->get_shipping_postcode(),
            'city'     => $order->get_shipping_city(),
        ];

        // Get applicable shipping tax rates using find_shipping_rates()
        $tax_rates = \WC_Tax::find_shipping_rates([
            'country'  => $destination['country'],
            'state'    => $destination['state'],
            'postcode' => $destination['postcode'],
            'city'     => $destination['city'],
        ]);

        // Format the result to match $item->get_taxes() structure
        $taxes = [
            'total' => [],
            'subtotal' => []
        ];

        foreach ($tax_rates as $rate_id => $rate) {
            // Add the tax rate with zero amounts for consistency
            $taxes['total'][$rate_id] = 0;
            $taxes['subtotal'][$rate_id] = 0;
        }

        return $taxes;
    }
	
    /**
     * Retrieves the applicable bexio tax rate based on the given WooCommerce Order Item
     * 
     * @param object $order The WooCommerce order
     * @param object $item The order's item
     * @param boolean $is_tax_exempt Whether the order is tax exempt or not
     * @param array/string $bexio_product bexio product linked to the item (if applicable; required only if "use_bexio_taxes" is enabled). Can also be "shipping", "fees" if referring to a shipping line or a fee line.
     * @return string Applicable tax rate (bexio tax ID)
     */
    protected function get_applicable_tax_by_woocommerce_item($order, $item, $is_tax_exempt = false, $bexio_product = null)
    {
        $use_bexio_taxes = get_option(AdminAPISettings::$prefix . 'use_bexio_products_taxes');
        if ($use_bexio_taxes) {
            if ($bexio_product == "shipping" || $bexio_product == "fees") {
                $shipping_tax_rate = get_option(AdminAPISettings::$prefix . 'use_bexio_products_taxes_shipping_tax');
                if ($shipping_tax_rate) {
                    return $shipping_tax_rate;
                } else {
                    return new \WP_Error("MISSING_TAX_MAPPING", "No bexio tax mapping found for shipping item");
                }
            } else if ($bexio_product && is_array($bexio_product) && !empty($bexio_product["tax_id"])) {
                return $bexio_product["tax_id"];
            } else {
                return new \WP_Error("MISSING_TAX_MAPPING", "No bexio tax mapping found for item: " . $item->get_name());
            }
        } 

        $taxes = $item->get_taxes();

        // If taxes are not given because the item is "free shipping", determine the applicable tax rate on-the-fly
        if ($bexio_product == "shipping" && bccomp($item->get_total(), "0.00", 2) === 0
            && (empty($taxes["total"]) || !is_array($taxes["total"]) || !count($taxes["total"]))) {
            $taxes = $this->get_shipping_taxes_formatted($order);
        }

        $applicable_tax = null;
        if (get_option('woocommerce_calc_taxes') !== 'yes' || $is_tax_exempt || empty($taxes["total"]) || !is_array($taxes["total"]) || !count($taxes["total"])) {
            $applicable_tax = get_option(AdminAPISettings::$prefix . 'bexio_default_exempt_tax');
        } else if (is_array($taxes) && count($taxes)) {
            // Item taxes
            $tax_rate_percentage = null;
            
			if (count($taxes['total']) == 1) {
				// Take first tax listed
                $tax_rate_percentage = \WC_Tax::get_rate_percent_value(key($taxes['total']));
			} else {
                // Otherwise, take the first non-zero tax rate
                foreach ($taxes['total'] as $tax_rate_id => $tax_amount) {
                    if ($tax_amount !== "") {
                        $tax_rate_percentage = \WC_Tax::get_rate_percent_value($tax_rate_id);
                        break;
                    }
                }
            }

            // Retrieve applicable bexio tax
            $applicable_tax = null;
            $custom_vat_mapping = get_option(AdminAPISettings::$prefix . 'custom_vat_mapping');
            if (is_array($custom_vat_mapping) && count($custom_vat_mapping)) {
                // Search applicable tax rate including country
                foreach ($custom_vat_mapping as $vat_mapping) {
                    if (
                        bccomp($tax_rate_percentage, round($vat_mapping["vatRate"], 2), 2) === 0 && isset($vat_mapping["country"]) &&
                        strtoupper($vat_mapping["country"]) == strtoupper($order->get_billing_country())
                    ) {
                        $applicable_tax = $vat_mapping["bexioTax"];
                        break;
                    }
                }

                if (!$applicable_tax) {
                    // Search applicable tax rate non-country specific
                    foreach ($custom_vat_mapping as $vat_mapping) {
                        if (bccomp($tax_rate_percentage, round($vat_mapping["vatRate"], 2), 2) === 0 && empty($vat_mapping["country"])) {
                            $applicable_tax = $vat_mapping["bexioTax"];
                            break;
                        }
                    }
                }
            }

            if (!$applicable_tax) {
                return new \WP_Error("MISSING_TAX_MAPPING", "No bexio tax mapping found for " . ($tax_rate_percentage ?? 'N/A') . "%");
            }
        }

        return $applicable_tax;
    }


    /**
     * Converts WooCommerce's order discount to bexio line.
     * 
     * @param object $order The WooCommerce order
     * @return array Bexio positions (containing the shipping line(s))
     */
    protected function woocommerce_discount_to_bexio_position($order)
    {
        if ($order->get_discount_total()) {
            $text = get_option(AdminAPISettings::$prefix . "invoice_discount_line_text");
            $text = preg_replace('/\*{2}([^\*\_]+)\*{2}/', '<b>$1</b>', $text);
            $text = preg_replace('/\*{1}([^\*\_]+)\*{1}/', '<i>$1</i>', $text);

            return [
                // [
                //     "type" => "KbPositionCustom",
                //     // "amount" => 1,
                //     // "tax_id" => $applicable_tax,
                //     "text" => $text,
                //     // "value" => $order->get_discount_total() * -1,
                //     // "is_percentual" => false,
                //     // "discount_in_percent" => discountInPercent,
                //     // "account_id" => $account_id ? $account_id : null,
                // ]
                [
                    "type" => "KbPositionDiscount",
                    "text" => $text,
                    "value" => wc_prices_include_tax() ? $order->get_discount_total() + $order->get_discount_tax() : $order->get_discount_total(),
                    "is_percentual" => false,
                ]
            ];
        } else {
            return [];
        }
    }

    /**
     * Converts WooCommerce's shipping line to bexio positions.
     * 
     * @param object $order The WooCommerce order
     * @param boolean $is_tax_exempt Whether the order is tax exempt or not
     * @return array Bexio positions (containing the shipping line(s))
     */
    protected function woocommerce_shipping_to_bexio_positions($order, $is_tax_exempt = false)
    {
        $bexio_positions = [];
        foreach ($order->get_items('shipping') as $item) {
            $account_id = get_option(AdminAPISettings::$prefix . 'bexio_default_shipping_income_account');

            $unit_price = wc_prices_include_tax()
                ? wc_format_decimal(((float) $item->get_total() + (float) $item->get_total_tax()) / $item->get_quantity(), wc_get_price_decimals())
                : wc_format_decimal((float) $item->get_total() / $item->get_quantity(), wc_get_price_decimals());

            $text = get_option(AdminAPISettings::$prefix . "invoice_shipping_line_text");
            $text = str_replace('{shipping_line_title}', $item->get_name(), $text);
            $text = preg_replace('/\*{2}([^\*\_]+)\*{2}/', '<b>$1</b>', $text);
            $text = preg_replace('/\*{1}([^\*\_]+)\*{1}/', '<i>$1</i>', $text);

            $applicable_tax = $this->get_applicable_tax_by_woocommerce_item($order, $item, $is_tax_exempt, "shipping");
            if ($applicable_tax instanceof \WP_Error) {
				if (bccomp($item->get_total(), "0.00", 2) === 0) {
                    continue; // Skip shipping line with 0 total
                } else {
                    return $applicable_tax;
                }
            }
            $bexio_positions[] = [
                "type" => "KbPositionCustom",
                "amount" => 1,
                "tax_id" => $applicable_tax,
                "text" => $text,
                "unit_price" => $unit_price,
                "account_id" => $account_id ? $account_id : null,
            ];
        }

        return $bexio_positions;
    }

    /**
     * Converts WooCommerce's order line items to bexio positions.
     * 
     * @param object $order The WooCommerce order
     * @param boolean $is_tax_exempt Whether the order is tax exempt or not
     * @return array Bexio positions
     */
    protected function woocommerce_line_items_to_bexio_positions($order, $is_tax_exempt = false)
    {
        $bexio_positions = [];
        foreach ($order->get_items() as $item_id => $item) {
            try {
                // Debug log - Starting line item processing
                _bcbh_log("Processing line item: Order ID {$order->get_id()}, Item ID {$item_id}, Item Name: {$item->get_name()}");

                $product_variation_id = $item->get_variation_id();
                $product = $product_variation_id
                    ? wc_get_product($product_variation_id)
                    : wc_get_product($item->get_product_id());

                $unit_price = wc_prices_include_tax()
                    ? wc_format_decimal(((float) $item->get_subtotal() + (float) $item->get_subtotal_tax()) / $item->get_quantity(), wc_get_price_decimals())
                    : wc_format_decimal((float) $item->get_subtotal() / $item->get_quantity(), wc_get_price_decimals());

                if (!$product) {
                    // Debug log - Product not found
                    _bcbh_log("Product not found: Order ID {$order->get_id()}, Item ID {$item_id}, Item Name: {$item->get_name()}, Unit Price: {$unit_price}");

                    // Product has been deleted/archived, create a custom position with available order item data
                    $account_id = get_option(AdminAPISettings::$prefix . 'bexio_default_product_income_account');
                    $text = $item->get_name();

                    $applicable_tax = $this->get_applicable_tax_by_woocommerce_item($order, $item, $is_tax_exempt, null);
                    if ($applicable_tax instanceof \WP_Error) {
                        // Debug log - Tax error for deleted product
                        _bcbh_log("Tax error for deleted product: Order ID {$order->get_id()}, Item ID {$item_id}, Error: {$applicable_tax->get_error_message()}");
                        return $applicable_tax;
                    }

                    $bexio_position = [
                        "type" => "KbPositionCustom",
                        "amount" => $item->get_quantity(),
                        "tax_id" => $applicable_tax,
                        "text" => $text,
                        "unit_price" => $unit_price,
                        "account_id" => $account_id ? $account_id : null,
                    ];

                    // Debug log - Created position for deleted product
                    _bcbh_log("Created position for deleted product: Order ID {$order->get_id()}, Item ID {$item_id}, Position: " . wp_json_encode($bexio_position));

                    $bexio_positions[] = $bexio_position;
                    continue;
                }

                $requires_shipping = !$product->is_virtual();
                $account_id = $requires_shipping
                    ? get_option(AdminAPISettings::$prefix . 'bexio_default_product_income_account')
                    : get_option(AdminAPISettings::$prefix . 'bexio_default_service_income_account');

                // Try to match item in bexio
                $bexio_product = null;
                if ($product->get_sku()) {
                    $bexio_product = API::instance()->get_article_by_sku($product->get_sku());
                }

                $text = $this->generate_line_item_text_from_product($bexio_product, $product);
                $applicable_tax = $this->get_applicable_tax_by_woocommerce_item($order, $item, $is_tax_exempt, $bexio_product);

                if ($applicable_tax instanceof \WP_Error) {
                    return $applicable_tax;
                }

                if ($bexio_product) {
                    $bexio_position = [
                        "type" => "KbPositionArticle",
                        "article_id" => $bexio_product["id"],
                        "amount" => $item->get_quantity(),
                        "tax_id" => $applicable_tax,
                        "text" => $text,
                        "unit_price" => $unit_price,
                    ];
                } else {
                    $bexio_position = [
                        "type" => "KbPositionCustom",
                        "amount" => $item->get_quantity(),
                        "tax_id" => $applicable_tax,
                        "text" => $text,
                        "unit_price" => $unit_price,
                        "account_id" => $account_id ? $account_id : null,
                    ];
                }

                $bexio_positions[] = $bexio_position;

            } catch (\Exception $e) {
                // Debug log - Exception during line item processing
                _bcbh_log("Line item exception: Order ID {$order->get_id()}, Item ID {$item_id}, Error: {$e->getMessage()}, Stack trace: {$e->getTraceAsString()}");
                throw $e; // Re-throw to be caught by the outer try-catch
            }
        }

        // Debug log - Finished processing all line items
        _bcbh_log("Line items complete: Order ID {$order->get_id()}, Positions count: " . count($bexio_positions));

        return $bexio_positions;
    }

    /**
     * Converts WooCommerce's order fees to bexio positions.
     * 
     * @param object $order The WooCommerce order
     * @param boolean $is_tax_exempt Whether the order is tax exempt or not
     * @return array Bexio positions
     */
    protected function woocommerce_fees_to_bexio_positions($order, $is_tax_exempt = false)
    {
        $bexio_positions = [];
        foreach ($order->get_fees() as $fee_id => $fee) {

            // Account ID for fees
            $account_id = get_option(AdminAPISettings::$prefix . 'bexio_default_product_income_account');
            $text = $fee->get_name(); // Fee name as the description

            // Fee amount can be contained in the fee amount or the total attributes
            // Usually, amount and total contain the same value. But in some cases, the total excludes taxes while amount includes them.
            $fee_amount = $fee->get_total()
                ? $fee->get_total()
                : $fee->get_amount();

            // Fee amount (including tax if applicable)
            $amount = wc_prices_include_tax()
                ? wc_format_decimal(round(((float) $fee_amount + (float) $fee->get_total_tax()), 5), wc_get_price_decimals())
                : wc_format_decimal(round((float) $fee_amount, 5), wc_get_price_decimals());

            // Get the applicable tax for the fee
            $applicable_tax = $this->get_applicable_tax_by_woocommerce_item($order, $fee, $is_tax_exempt, "fees");
            if ($applicable_tax instanceof \WP_Error) {
                return $applicable_tax;
            }

            // Creating a custom Bexio position for the fee
            $bexio_position = [
                "type" => "KbPositionCustom",
                "amount" => 1, // Fees are usually a single unit
                "tax_id" => $applicable_tax,
                "text" => $text,
                "unit_price" => $amount,
                "account_id" => $account_id ? $account_id : null,
            ];

            $bexio_positions[] = $bexio_position;
        }

        return $bexio_positions;
    }


    /**
     * Generates the line item text of the bexio order/invoice based on the given product
     * 
     * @param object $bexio_product Bexio product corresponding to the given product
     * @param object $wc_product Actual WooCommerce product that is part of the order
     * @return string Generated line item text
     */
    protected function generate_line_item_text_from_product($bexio_product, $wc_product)
    {
        $product_name = $wc_product->get_title();
        if ($wc_product instanceof \WC_Product_Variation) {
            $product_name .= ' - ' . wc_get_formatted_variation($wc_product, true, false);
        }

        $line_item_text = get_option(AdminAPISettings::$prefix . "invoice_line_item_format");
        $line_item_text = str_replace("\n", '<br>', $line_item_text);
        $line_item_text = str_replace('{woocommerce_product_name}', $product_name, $line_item_text);
        $line_item_text = str_replace('{woocommerce_sku}', $wc_product->get_sku(), $line_item_text);
        $line_item_text = str_replace('{woocommerce_product_description}', $wc_product->get_description(), $line_item_text);
        $line_item_text = str_replace('{woocommerce_barcode}', $wc_product->get_meta('_global_unique_id'), $line_item_text);
        $line_item_text = str_replace('{bexio_product_title}', !empty($bexio_product["intern_name"]) ? $bexio_product["intern_name"] : (!empty($wc_product->get_name()) ? $wc_product->get_name() : ""), $line_item_text);
        $line_item_text = str_replace('{bexio_product_description}', !empty($bexio_product["intern_description"]) ? $bexio_product["intern_description"] : "", $line_item_text);
        $line_item_text = str_replace('{bexio_sku}', !empty($bexio_product["intern_code"]) ? $bexio_product["intern_code"] : (!empty($wc_product->get_sku()) ? $wc_product->get_sku() : ""), $line_item_text);
        $line_item_text = preg_replace('/\*{2}([^\*\_]+)\*{2}/', '<b>$1</b>', $line_item_text);
        $line_item_text = preg_replace('/\*{1}([^\*\_]+)\*{1}/', '<i>$1</i>', $line_item_text);

        return $line_item_text;
    }

    /**
     * Generates the bexio order/invoice title based on the given WooCommerce order
     * 
     * @param object $wc_order WooCommerce order
     * @return string Generated line item text
     */
    protected function generate_bexio_title_from_order($wc_order)
    {
        $bexio_title = get_option(AdminAPISettings::$prefix . "invoice_title_format");
        $bexio_title = str_replace('{woocommerce_order_id}', $wc_order->get_id(), $bexio_title);
        $bexio_title = str_replace('{woocommerce_order_number}', $wc_order->get_id(), $bexio_title);
        $bexio_title = str_replace('{woocommerce_order_payment_method}', $wc_order->get_payment_method_title(), $bexio_title);
        $bexio_title = str_replace('{woocommerce_order_title}', $wc_order->get_title(), $bexio_title);
        return $bexio_title;
    }

    /**
     * Checks if the given order is authorized to be synced or not based on the authorized payment methods
     * 
     * @param object $order The WooCommerce order
     * @return boolean
     */
    protected function is_order_sync_authorized_based_on_payment_method($order)
    {
        $payment_methods_option = get_option(AdminAPISettings::$prefix . 'sync_orders_only_for_payment_methods');
        $order_payment_method = $order->get_payment_method();
        
        _bcbh_log("Checking sync authorization for payment method: {$order_payment_method}");
        _bcbh_log("Configured payment methods: " . print_r($payment_methods_option, true));
        
        if ($payment_methods_option) {
            $payment_methods_to_sync = array_map('trim', explode("\n", $payment_methods_option));
            $found_in_payment_methods_to_sync = in_array($order_payment_method, $payment_methods_to_sync);
            
            _bcbh_log("Payment method {$order_payment_method} " . ($found_in_payment_methods_to_sync ? "is" : "is not") . " in allowed list");
            
            if (!$found_in_payment_methods_to_sync) {
                return false;
            }
        }
        return true;
    }

    /**
     * Checks if the given order has already been synced
     * 
     * @param object $order The WooCommerce order
     * @return boolean
     */
    protected function is_order_already_synced($order)
    {
        return get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_invoice_id', true)
            || get_post_meta($order->get_id(), AdminAPISettings::$prefix . 'bexio_order_id', true)
            || $order->get_meta(AdminAPISettings::$prefix . 'bexio_invoice_id')
            || $order->get_meta(AdminAPISettings::$prefix . 'bexio_order_id');
    }

    /**
     * Retrieves the bexio contact (find / upsert) corresponding to the given order's customer
     * 
     * @param object $order The WooCommerce order
     * @return array|WP_Error Bexio contact corresponding to the order's customer
     */
    protected function find_or_upsert_contact_from_order($order)
    {
        $company_name = $order->get_billing_company();

        // Fetch matching bexio contact(s) by email, otherwise by first and last name
        $customer_email = $order->get_billing_email();
        $customer_first_name = $order->get_billing_first_name();
        $customer_last_name = $order->get_billing_last_name();
        $contacts = API::instance()->get_contacts_by_email_or_name($customer_email, $customer_first_name, $customer_last_name);
        if ($contacts instanceof \WP_Error) {
            return $contacts;
        }

        $person_contact = null;
        $company_contact = null; // Both contacts to fill
        if (count($contacts) > 0) {
            foreach ($contacts as $contact) {
                if ($contact['contact_type_id'] == 2) {
                    $person_contact = $contact;
                }
            }
            foreach ($contacts as $contact) {
                if ($contact['contact_type_id'] == 1) {
                    $company_contact = $contact;
                }
            }
        }

        // If the person contact is found, update it; otherwise, create it
        $person_contact = $this->upsert_customer($order, $person_contact);
        if ($person_contact instanceof \WP_Error) {
            return $person_contact;
        }

        // Handle company contact / information
        if ($company_name) {
            // If available, update the company information and link the company to the order, not the physical person
            if (!$company_contact) {
                // Search contacts owning this person (i.e. their company)
                $company_relations = API::instance()->search_contact_relations([["field" => "contact_sub_id", "value" => $person_contact['id'], "criteria" => "="]]);
                if ($company_relations instanceof \WP_Error) {
                    return $company_relations;
                }
                if (is_array($company_relations) && count($company_relations)) {
                    foreach ($company_relations as $company_relation) {
                        $contact = API::instance()->get_contact_by_id($company_relation['contact_id']);
                        if ($contact instanceof \WP_Error) {
                            return $contact;
                        }
                        if (strtolower($contact['name_1']) == strtolower($company_name)) {
                            $company_contact = $contact;
                            break;
                        }
                    }
                }

                // Otherwise, search companyContact directly by name & city (not name only to prevent match on homonyms)
                if (!$company_contact) {
                    $company_contacts = API::instance()->get_contacts_companies_by_name_and_postcode($company_name, $order->get_billing_postcode());
                    if ($company_contacts instanceof \WP_Error) {
                        return $company_contacts;
                    }
                    if (count($company_contacts)) {
                        $company_contact = $company_contacts[0];
                        // In this case, create the relation between the person and the company in bexio
                        $contact_relation = API::instance()->create_contact_relation($company_contact['id'], $person_contact['id']);
                        if ($contact_relation instanceof \WP_Error) {
                            return $contact_relation;
                        }
                    }
                }
            }

            // If found, update company's contact information; otherwise, create the company contact and the relation with the existing customer
            $company_contact = $this->upsert_customer($order, $company_contact, $person_contact['id']);
            if ($company_contact instanceof \WP_Error) {
                return $company_contact;
            }
        }

        if ($company_contact) {
            // The customer is a company, if a "person contact" exist for this company, use it as subcontact
            return [ "contact" => $company_contact, "subcontact" => $person_contact ];
        } else {
            // The customer is a private person
            return [ "contact" => $person_contact, "subcontact" => null ];
        }
    }

    /**
     * Upsert the customer of the given WooCommerce order.
     * If the bexio contact is empty, the contact is created in bexio.
     * Otherwise, the given bexio contact is updated based on the order's customer information.
     * 
     * @param object $order The WooCommerce order
     * @param object $bexio_contact The contact corresponding to the order's customer, if already found in bexio contacts
     * @param string $is_company_owning_contact_id Provide a value if the given contact is a company that owns the given contact ID (for binding)
     * @return array Upserted bexio contact
     */
    protected function upsert_customer($order, $bexio_contact, $is_company_owning_contact_id = null)
    {
        $company_name = $order->get_billing_company();

        $bexio_address_fields = $this->extract_bexio_address_from_woo($order, 'billing', false);

        if ($bexio_contact) {
            $contact_data = []; // Potential customer's data to update

            // Update contact's default (billing) address if it has changed (compare street_name, house_number, etc.)
            $address_changed = false;
            foreach (['street_name', 'house_number', 'address_addition', 'postcode', 'city', 'country_id'] as $field) {
                if (!isset($bexio_contact[$field]) || $bexio_contact[$field] !== $bexio_address_fields[$field]) {
                    $address_changed = true;
                    break;
                }
            }
            if ($address_changed) {
                foreach (['street_name', 'house_number', 'address_addition', 'postcode', 'city', 'country_id'] as $field) {
                    $contact_data[$field] = $bexio_address_fields[$field];
                }
            }

            // Update phone only if none is set in bexio (fix phone for companies, mobile phone for persons)
            if ($order->get_billing_phone()) {
                if ($company_name && !$bexio_contact["phone_fixed"]) {
                    $contact_data["phone_fixed"] = $order->get_billing_phone();
                } else if (!$company_name && !$bexio_contact["phone_mobile"]) {
                    $contact_data["phone_mobile"] = $order->get_billing_phone();
                }
            }

            // Update Bexio customer (if some data has changed)
            if (count($contact_data) > 0) {
                // Remove deprecated 'address' field if present
                if (isset($contact_data['address'])) {
                    unset($contact_data['address']);
                }
                if (isset($bexio_contact['address'])) {
                    unset($bexio_contact['address']);
                }
                $bexio_contact = API::instance()->update_contact_by_id($bexio_contact['id'], $contact_data);
                if ($bexio_contact instanceof \WP_Error) {
                    return $bexio_contact;
                }
            }
        } else {
            if ($is_company_owning_contact_id) {
                // Create a company
                $contact_object = array_merge([
                    "contact_type_id" => 1,
                    "name_1" => $company_name,
                    "mail" => $order->get_billing_email(),
                    "phone_fixed" => $order->get_billing_phone(),
                    "phone_fixed_second" => null,
                    "phone_mobile" => null,
                    "fax" => null,
                    "url" => null,
                    "skype_name" => null,
                    "remarks" => null,
                    "language_id" => null,
                    "contact_group_ids" => get_option(AdminAPISettings::$prefix . 'bexio_default_contacts_group'),
                    "user_id" => get_option(AdminAPISettings::$prefix . 'bexio_default_user'),
                    "owner_id" => get_option(AdminAPISettings::$prefix . 'bexio_default_user'),
                ], $bexio_address_fields);
                $bexio_contact = API::instance()->create_contact($contact_object);
                if (!empty($bexio_contact['id'])) {
                    API::instance()->create_contact_relation($bexio_contact['id'], $is_company_owning_contact_id);
                }
            } else {
                // Create a physical person
                $bexio_contact = API::instance()->create_contact(array_merge([
                    "contact_type_id" => 2,
                    "name_1" => $order->get_billing_last_name(),
                    "name_2" => $order->get_billing_first_name(),
                    "salutation_id" => null,
                    "salutation_form" => null,
                    "titel_id" => null,
                    "birthday" => null,
                    "mail" => $order->get_billing_email(),
                    "phone_fixed" => null,
                    "phone_fixed_second" => null,
                    "phone_mobile" => $order->get_billing_phone(),
                    "fax" => null,
                    "url" => null,
                    "skype_name" => null,
                    "remarks" => null,
                    "language_id" => null,
                    "contact_group_ids" => get_option(AdminAPISettings::$prefix . 'bexio_default_contacts_group'),
                    "contact_branch_ids" => null,
                    "user_id" => get_option(AdminAPISettings::$prefix . 'bexio_default_user'),
                    "owner_id" => get_option(AdminAPISettings::$prefix . 'bexio_default_user'),
                ], $bexio_address_fields));
            }
        }

        return $bexio_contact;
    }

    /**
     * Extracts a Bexio-compatible address array from a WooCommerce order.
     *
     * @param object $order WooCommerce order object
     * @param string $address_type 'billing' or 'shipping'
     * @param bool $include_name Whether to include the 'name' field
     * @return array|WP_Error
     */
    protected function extract_bexio_address_from_woo($order, $address_type = 'billing', $include_name = true) {
        $split_street_and_number = function($address_line) {
            if (!is_string($address_line) || trim($address_line) === '') {
                return ['', ''];
            }
            if (preg_match('/^(.+?)\s+(\d+\w*)$/u', trim($address_line), $matches)) {
                return [trim($matches[1]), trim($matches[2])];
            } else {
                return [trim($address_line), ''];
            }
        };

        $address_1 = $address_type === 'shipping' ? $order->get_shipping_address_1() : $order->get_billing_address_1();
        $address_2 = $address_type === 'shipping' ? $order->get_shipping_address_2() : $order->get_billing_address_2();
        list($street_name, $house_number) = $split_street_and_number($address_1);
        $address_addition = (!empty($address_2) && is_string($address_2)) ? $address_2 : null;

        $country_code = $address_type === 'shipping' ? $order->get_shipping_country() : $order->get_billing_country();
        $country = API::instance()->find_or_create_country_by_country_code($country_code);
        if ($country instanceof \WP_Error) {
            return $country;
        }

        $postcode = $address_type === 'shipping' ? $order->get_shipping_postcode() : $order->get_billing_postcode();
        $city = $address_type === 'shipping' ? $order->get_shipping_city() : $order->get_billing_city();

        $address = [
            'street_name' => $street_name,
            'house_number' => $house_number,
            'address_addition' => $address_addition,
            'postcode' => $postcode ? $postcode : '0000',
            'city' => $city,
            'country_id' => $country['id'],
        ];

        if ($include_name) {
            $name = $address_type === 'shipping'
                ? ($order->get_shipping_company()
                    ? $order->get_shipping_company() . "\n" . $order->get_formatted_shipping_full_name()
                    : $order->get_formatted_shipping_full_name())
                : ($order->get_billing_company()
                    ? $order->get_billing_company() . "\n" . $order->get_formatted_billing_full_name()
                    : $order->get_formatted_billing_full_name());
            $address['name'] = $name;
        }

        return $address;
    }

    /**
     * Converts an order's address (billing or shipping) to bexio address format
     * 
     * @param object $order The WooCommerce order resource
     * @param string $address_type The type of address to convert (billing or shipping, defaults to billing)
     * @return array Bexio-structured address
     */
    protected function order_address_to_bexio_address($order, $address_type = "billing")
    {
        return $this->extract_bexio_address_from_woo($order, $address_type, true);
    }

    /**
     * Updates the bexio contact with the given order's address
     * 
     * @param string $contact_id Bexio contact ID
     * @param object $order The WooCommerce order resource
     * @param string $address_type Whether the address is "billing" or "shipping"
     * @return array Created or updated additional adress
     */
    protected function upsert_contact_additional_address_from_order($contact_id, $order, $address_type)
    {
        $address_subject = "WooCommerce " . $address_type . " address";
        $additional_addresses = API::instance()->get_contact_additional_addresses_by_subject($contact_id, $address_subject);
        $existing_address_id = count($additional_addresses) ? $additional_addresses[0]['id'] : 0;

        $bexio_address_obj = $this->order_address_to_bexio_address($order, $address_type);
        if ($bexio_address_obj instanceof \WP_Error) {
            return $bexio_address_obj;
        }

        $bexio_address_obj['subject'] = $address_subject;
        $bexio_address_obj['description'] = $address_subject . " of the customer - Last update " . (new \DateTime())->format('c');

        return $existing_address_id
            ? API::instance()->update_contact_additional_address($contact_id, $existing_address_id, $bexio_address_obj)
            : API::instance()->create_contact_additional_addresses($contact_id, $bexio_address_obj);
    }

    /**
     * Cloning is forbidden.
     */
    public function __clone()
    {
        _doing_it_wrong(__FUNCTION__, __('Cheatin&#8217; huh?'), BEXIO_CONNECTOR_BY_HELVY_VERSION);
    }
}
