<?php

declare(strict_types=1);

namespace BadamSoft\WooProductExporter\Exporter;

use BadamSoft\WooProductExporter\Core\Plugin;
use BadamSoft\WooProductExporter\Exceptions\StopIteration;
use BadamSoft\WooProductExporter\Exporter\Formats\AbstractStreamWriter;
use BadamSoft\WooProductExporter\Exporter\Formats\Contracts\WriterInterface;
use BadamSoft\WooProductExporter\Exporter\Formats\FormatManager;
use BadamSoft\WooProductExporter\Exporter\Meta\MetaFieldDetector;
use BadamSoft\WooProductExporter\Exporter\Queries\QueryBuilder;
use BadamSoft\WooProductExporter\Filters\FilterManager;
use BadamSoft\WooProductExporter\Helpers\ChunkQuery;
use BadamSoft\WooProductExporter\Helpers\Logger;
use BadamSoft\WooProductExporter\Helpers\RunLogFormatter;
use BadamSoft\WooProductExporter\Helpers\Utils;
use BadamSoft\WooProductExporter\Scheduled\ChunkProcessor;
use BadamSoft\WooProductExporter\Scheduler\ScheduledExportManager;
use BadamSoft\WooProductExporter\Templates\TemplateManager;
use WC_Product;
use ZipArchive;
use Throwable;
use WP_Post;
use WP_Term;
use WP_User;

class Exporter {
    private const OPTION_LAST_SELECTION = 'prodexfo_last_fields';
    private const OPTION_LAST_FORMAT    = 'prodexfo_last_format';

    public const PREVIEW_ROW_LIMIT = 20;

    private Plugin $plugin;
    private FilterManager $filterManager;
    private QueryBuilder $queryBuilder;
    private ChunkProcessor $chunkProcessor;
    private FormatManager $formatManager;
    private TemplateManager $templateManager;
    private ScheduledExportManager $scheduledManager;
    private MetaFieldDetector $metaFieldDetector;

    /**
     * Cached field definitions.
     *
     * @var array<string, array<string, mixed>>
     */
    private array $fieldDefinitions = [];

    /**
     * Cached attribute catalog for current request.
     *
     * @var array<string, string>|null
     */
    private ?array $attributeCatalog = null;

    /**
     * Cached detected ACF and meta descriptors.
     *
     * @var array{acf: array<int, array<string, mixed>>, meta: array<int, array<string, mixed>>}|null
     */
    private ?array $detectedMetaFieldCache = null;

    /**
     * Track whether we should collect product images for ZIP packaging.
     */
    private bool $collectImagesZip = false;

    /**
     * Cached ACF root field values per product.
     *
     * @var array<int, array<string, mixed>>
     */
    private array $acfValueCache = [];

    /**
     * @var array<string, bool> Collected absolute image paths keyed by path.
     */
    private array $zipImagePaths = [];

    /**
     * @var array<string, array<int>> Map of image path to product IDs referencing it.
     */
    private array $zipImageSources = [];

    /**
     * @var array<int, string> Cached SKU values per product for ZIP naming.
     */
    private array $zipImageSkuCache = [];

    /**
     * @var array<string, int> Counters per SKU to ensure sequential filenames.
     */
    private array $zipImageSkuCounters = [];

    /**
     * Exporter constructor.
     *
     * @param Plugin                $plugin             Plugin instance.
     * @param FilterManager         $filter_manager     Filters manager.
     * @param QueryBuilder          $query_builder      Product query builder.
     * @param ChunkProcessor        $chunk_processor    Chunked product processor.
     * @param FormatManager         $format_manager     Output format manager.
     * @param TemplateManager       $template_manager   Template manager.
     * @param ScheduledExportManager $scheduled_manager Scheduled export manager.
     * @param MetaFieldDetector     $meta_field_detector Meta field detector.
     */
    public function __construct( Plugin $plugin, FilterManager $filter_manager, QueryBuilder $query_builder, ChunkProcessor $chunk_processor, FormatManager $format_manager, TemplateManager $template_manager, ScheduledExportManager $scheduled_manager, MetaFieldDetector $meta_field_detector ) {
        $this->plugin            = $plugin;
        $this->filterManager     = $filter_manager;
        $this->queryBuilder      = $query_builder;
        $this->chunkProcessor    = $chunk_processor;
        $this->formatManager     = $format_manager;
        $this->templateManager   = $template_manager;
        $this->scheduledManager  = $scheduled_manager;
        $this->metaFieldDetector = $meta_field_detector;
    }

    /**
     * Sanitize a field key received from request or storage.
     *
     * @param mixed $value Raw field key value.
     */
    public function sanitize_field_key( $value ): string { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
        return Utils::sanitize_field_key( $value );
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    public function get_field_definitions(): array {
        if ( ! empty( $this->fieldDefinitions ) ) {
            return $this->fieldDefinitions;
        }

        $fields = [
            'product_id'        => [
                'label' => __( 'Product ID', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'general',
            ],
            'product_name'      => [
                'label' => __( 'Product Name', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'general',
            ],
            'product_slug'      => [
                'label' => __( 'Slug', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'general',
            ],
            'product_sku'       => [
                'label' => __( 'SKU', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'general',
            ],
            'product_type'      => [
                'label' => __( 'Product Type', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'general',
            ],
            'product_permalink' => [
                'label' => __( 'Product URL', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'general',
            ],
            'regular_price'     => [
                'label' => __( 'Regular Price', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'pricing',
            ],
            'sale_price'        => [
                'label' => __( 'Sale Price', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'pricing',
            ],
            'current_price'     => [
                'label' => __( 'Current Price', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'pricing',
            ],
            'stock_status'      => [
                'label' => __( 'Stock Status', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'inventory',
            ],
            'stock_quantity'    => [
                'label' => __( 'Stock Quantity', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'inventory',
            ],
            'total_sales'       => [
                'label' => __( 'Total Sales', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'inventory',
            ],
            'short_description' => [
                'label' => __( 'Short Description', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'content',
            ],
            'full_description'  => [
                'label' => __( 'Full Description', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'content',
            ],
            'product_categories' => [
                'label' => __( 'Categories', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'taxonomy',
            ],
            'product_brand'      => [
                'label' => __( 'Brands', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'taxonomy',
            ],
            'product_tags'       => [
                'label' => __( 'Tags', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'taxonomy',
            ],
            'featured_image'     => [
                'label' => __( 'Featured Image URL', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'media',
            ],
            'gallery_images'     => [
                'label' => __( 'Gallery Image URLs', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'media',
            ],
            'all_images'         => [
                'label' => __( 'All Image URLs', 'badamsoft-product-exporter-for-woocommerce' ),
                'group' => 'media',
            ],
        ];

        foreach ( $this->getAttributeDefinitions() as $attribute_key => $definition ) {
            $fields[ $attribute_key ] = $definition;
        }

        foreach ( $this->getDetectedMetaFieldDefinitions() as $meta_key => $definition ) {
            $fields[ $meta_key ] = $definition;
        }

        /**
         * Filters the field definitions for the WooCommerce CSV exporter.
         *
         * @param array<string, array<string, mixed>> $fields Field definitions.
         */
        $this->fieldDefinitions = apply_filters( 'prodexfo_fields', $fields );

        return $this->fieldDefinitions;
    }

    /**
     * Ensure the configured filename uses the writer's preferred extension.
     *
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    private function prepareFilenameForWriter( array $settings, WriterInterface $writer ): array {
        $filename  = isset( $settings['filename'] ) ? (string) $settings['filename'] : '';
        $filename  = trim( $filename );
        $extension = trim( $writer->getFileExtension() );

        if ( '' === $extension ) {
            return $settings;
        }

        $timestamp = \wp_date( 'Y-m-d-H-i-s' );

        if ( '' === $filename ) {
            $filename = sprintf( 'wc-products-export-%s', $timestamp );
        }

        $resolved_filename = str_replace( '{{date}}', $timestamp, $filename );
        $resolved_filename = sanitize_file_name( $resolved_filename );

        if ( '' === $resolved_filename ) {
            $resolved_filename = sprintf( 'wc-products-export-%s', $timestamp );
        }

        $normalized_extension = strtolower( $extension );
        $final_filename       = $resolved_filename;

        $current_extension = strtolower( pathinfo( $resolved_filename, PATHINFO_EXTENSION ) );

        if ( '' !== $current_extension ) {
            if ( $current_extension !== $normalized_extension ) {
                $final_filename = substr( $resolved_filename, 0, - ( strlen( $current_extension ) + 1 ) ) . '.' . $normalized_extension;
            }
        } else {
            $final_filename = $resolved_filename . '.' . $normalized_extension;
        }

        $settings['_resolved_filename'] = $final_filename;
        $settings['filename']           = $final_filename;

        return $settings;
    }

    /**
     * @return array<int, string>
     */
    public function get_default_fields(): array {
        return [
            'product_id',
            'product_name',
            'product_sku',
            'product_permalink',
            'product_categories',
            'featured_image',
            'gallery_images',
            'current_price',
            'regular_price',
            'sale_price',
            'short_description',
            'full_description',
        ];
    }

    /**
     * @return array<int, string>
     */
    public function get_last_selected_fields(): array {
        $saved = get_option( self::OPTION_LAST_SELECTION, [] );

        return is_array( $saved ) ? $saved : [];
    }

    public function remember_selected_fields( array $fields ): void {
        update_option( self::OPTION_LAST_SELECTION, $fields );
    }

    public function remember_selected_format( string $format ): void {
        update_option( self::OPTION_LAST_FORMAT, $format );
    }

    public function get_last_selected_format(): string {
        $saved = get_option( self::OPTION_LAST_FORMAT, '' );

        return $this->formatManager->sanitize_format_key( is_string( $saved ) ? $saved : '' );
    }

    /**
     * @return array<string, string>
     */
    public function get_format_options(): array {
        return $this->formatManager->get_format_options();
    }

    public function get_default_format_key(): string {
        return $this->formatManager->get_default_writer_key();
    }

    /**
     * Sanitize field list coming from a request.
     *
     * @param array<string, mixed> $request
     * @return array<int, string>
     */
    public function sanitize_fields_from_request( array $request ): array {
        $raw_fields      = isset( $request['fields'] ) ? (array) $request['fields'] : [];
        $selected_fields = array_filter( array_map( [ $this, 'sanitize_field_key' ], $raw_fields ) );
        $available       = array_keys( $this->get_field_definitions() );

        return array_values( array_intersect( $selected_fields, $available ) );
    }

    /**
     * Sanitize filters coming from a request.
     *
     * @param array<string, mixed> $request
     * @return array<string, mixed>
     */
    public function sanitize_filters_from_request( array $request ): array {
        return $this->filterManager->sanitize_filters_from_request( $request );
    }

    /**
     * Sanitize export format key coming from a request.
     *
     * @param array<string, mixed> $request
     */
    public function sanitize_format_from_request( array $request ): string {
        $raw = isset( $request['export_format'] ) ? wp_unslash( (string) $request['export_format'] ) : '';

        return $this->formatManager->sanitize_format_key( $raw );
    }

    /**
     * Sanitize export settings coming from a request.
     *
     * @param array<string, mixed> $request
     * @return array<string, mixed>
     */
    public function sanitize_settings_from_request( array $request ): array {
        $settings = [
            'delimiter' => isset( $request['export_delimiter'] ) ? wp_unslash( (string) $request['export_delimiter'] ) : ',',
            'encoding'  => isset( $request['export_encoding'] ) ? wp_unslash( (string) $request['export_encoding'] ) : 'UTF-8',
            'filename'  => isset( $request['export_filename'] ) ? wp_unslash( (string) $request['export_filename'] ) : 'wc-products-export-{{date}}',
            'attach_images_zip' => ! empty( $request['export_attach_images_zip'] ) ? 1 : 0,
        ];

        if ( isset( $request['export_field_labels'] ) ) {
            $raw = wp_unslash( $request['export_field_labels'] );
            $decoded = null;

            if ( is_string( $raw ) ) {
                $decoded = json_decode( $raw, true );
            } elseif ( is_array( $raw ) ) {
                $decoded = $raw;
            }

            if ( is_array( $decoded ) ) {
                $settings['field_labels'] = $decoded;
            }
        }

        return $this->templateManager->sanitize_settings_payload( $settings );
    }

    /**
     * Persist last-used filters for convenience.
     *
     * @param array<string, mixed> $filters
     */
    public function remember_filters( array $filters ): void {
        $this->filterManager->remember_filters( $filters );
    }

    /**
     * @return array<string, int>
     */
    public function get_saved_filters(): array {
        return $this->filterManager->get_saved_filters();
    }

    /**
     * Estimate how many rows an export would generate (products + variations).
     *
     * @param array<string, mixed> $filters
     */
    public function estimate_row_count( array $filters, int $limit = 0 ): int {
        $limit = $limit > 0 ? $limit : null;

        return $this->iterateProducts(
            [ 'product_id' ],
            $filters,
            static function ( array $row ): void {
                unset( $row );
            },
            $limit
        );
    }

    /**
     * Run an export and stream the result to the browser.
     *
     * @param array<int, string>   $fields
     * @param array<string, mixed> $filters
     * @param string               $format_key
     * @param array<string, mixed> $settings
     */
    public function export( array $fields, array $filters, string $format_key, array $settings = [] ): void {
        $result = $this->performExport( $fields, $filters, $format_key, $settings );

        $run_meta = $this->record_run( $fields, $filters, $format_key, $result );

        do_action( 'prodexfo_export_completed', $fields, $filters, $run_meta );

        exit;
    }

    /**
     * Export data into a file and return result meta.
     *
     * @param array<int, string> $fields
     * @param array<string, mixed> $filters
     * @param string $format_key
     * @param array<string, mixed> $settings
     * @param string $file_path Absolute path to write into.
     * @return array<string, mixed>
     */
    public function export_to_file( array $fields, array $filters, string $format_key, array $settings, string $file_path ): array {
        $settings['_destination'] = 'file';
        $settings['_file_path']   = $file_path;

        return $this->performExport( $fields, $filters, $format_key, $settings );
    }

    /**
     * @param array<int, string> $fields
     * @param array<string, mixed> $filters
     * @param string $format_key
     * @param array<string, mixed> $settings
     * @return array<string, mixed>
     */
    private function performExport( array $fields, array $filters, string $format_key, array $settings ): array {
        $definitions = $this->get_field_definitions();
        $format      = $this->formatManager->sanitize_format_key( $format_key );
        $writer      = $this->formatManager->get_writer( $format );

        if ( ! $writer ) {
            wp_die( esc_html__( 'Selected export format is unavailable.', 'badamsoft-product-exporter-for-woocommerce' ) );
        }

        $this->remember_selected_format( $format );

        $internal_overrides = [];
        $destination        = 'browser';

        foreach ( [ '_destination', '_file_path' ] as $internal_key ) {
            if ( array_key_exists( $internal_key, $settings ) ) {
                $internal_overrides[ $internal_key ] = $settings[ $internal_key ];
                unset( $settings[ $internal_key ] );
            }
        }

        if ( isset( $internal_overrides['_destination'] ) ) {
            $destination = (string) $internal_overrides['_destination'];
        }

        $user_settings = ! empty( $settings )
            ? $this->templateManager->sanitize_settings_payload( $settings )
            : $this->get_saved_settings();

        $custom_labels = $user_settings['field_labels'] ?? [];
        if ( is_array( $custom_labels ) && ! empty( $custom_labels ) ) {
            foreach ( $custom_labels as $field_key => $label ) {
                if ( ! is_string( $field_key ) || ! is_string( $label ) ) {
                    continue;
                }

                $field_key = $this->sanitize_field_key( $field_key );
                $label     = trim( $label );

                if ( '' === $field_key || '' === $label ) {
                    continue;
                }

                if ( ! isset( $definitions[ $field_key ] ) || ! is_array( $definitions[ $field_key ] ) ) {
                    $definitions[ $field_key ] = [];
                }

                $definitions[ $field_key ]['label'] = $label;
            }
        }

        $prepared_settings = $this->prepareFilenameForWriter( $user_settings, $writer );

        $this->remember_settings( $user_settings );

        $effective_settings = array_merge( $prepared_settings, $internal_overrides );

        $this->collectImagesZip     = ! empty( $user_settings['attach_images_zip'] );
        $this->zipImagePaths        = [];
        $this->zipImageSources      = [];
        $this->zipImageSkuCache    = [];
        $this->zipImageSkuCounters = [];
        $this->acfValueCache        = [];

        $writer->open( $fields, $definitions, $effective_settings );

        $row_count = $this->iterateProducts(
            $fields,
            $filters,
            function ( array $row ) use ( $writer ): void {
                $writer->writeRow( $row );
            }
        );

        $writer->close();

        $zip_meta          = null;
        $image_file_paths  = array_keys( $this->zipImagePaths );
        $image_file_sources = $this->zipImageSources;

        if ( $this->collectImagesZip ) {
            $export_file_path = isset( $internal_overrides['_file_path'] ) ? (string) $internal_overrides['_file_path'] : null;
            $zip_meta         = $this->generateImagesZip( $export_file_path, $destination );
        }

        $this->collectImagesZip = false;

        $result = [
            'rows'         => $row_count,
            'generated_at' => current_time( 'mysql' ),
            'site'         => home_url(),
            'format'       => $format,
            'settings'     => $user_settings,
            'file_path'    => $writer instanceof AbstractStreamWriter ? $writer->get_file_path() : null,
        ];

        if ( $zip_meta ) {
            $result['images_zip']      = $zip_meta;
            $result['images_zip_path'] = $zip_meta['path'];
            $result['images_zip_url']  = $zip_meta['url'];
            $result['images_zip_size'] = $zip_meta['size'];
            $result['images_zip_files'] = array_map( 'basename', array_keys( $this->zipImagePaths ) );
        }

        if ( ! empty( $image_file_paths ) ) {
            $result['image_file_paths'] = array_values( $image_file_paths );
        }

        if ( ! empty( $image_file_sources ) ) {
            $result['image_file_sources'] = $image_file_sources;
        }

        $this->zipImagePaths        = [];
        $this->zipImageSources      = [];
        $this->zipImageSkuCache    = [];
        $this->zipImageSkuCounters = [];

        return $result;
    }

    /**
     * Resolve a final filename for given settings and format.
     *
     * @param array<string, mixed> $settings
     */
    public function resolve_filename_for_format( array $settings, string $format_key ): string {
        $sanitized_settings = $this->templateManager->sanitize_settings_payload( $settings );
        $format             = $this->formatManager->sanitize_format_key( $format_key );
        $writer             = $this->formatManager->get_writer( $format );

        if ( ! $writer ) {
            $writer = $this->formatManager->get_default_writer();

            if ( ! $writer ) {
                return sanitize_file_name( sprintf( 'wc-products-export-%s.csv', \wp_date( 'Y-m-d-H-i-s' ) ) );
            }
        }

        $prepared = $this->prepareFilenameForWriter( $sanitized_settings, $writer );

        return isset( $prepared['filename'] ) && '' !== $prepared['filename']
            ? $prepared['filename']
            : sanitize_file_name( sprintf( 'wc-products-export-%s.%s', \wp_date( 'Y-m-d-H-i-s' ), $writer->getFileExtension() ) );
    }

    /**
     * @param array<int, string>      $fields
     * @param array<string, mixed>    $filters
     * @param string                  $format
     * @param array<string, mixed>    $result
     * @return array<string, mixed>
     */
    public function record_run( array $fields, array $filters, string $format, array $result, string $template_id = '', string $run_type = 'manual', int $task_id = 0, array $actions = [] ): array {
        return $this->persistRun( $fields, $filters, $format, $result, $template_id, $run_type, $task_id, $actions );
    }

    private function persistRun( array $fields, array $filters, string $format, array $result, string $template_id, string $run_type, int $task_id, array $actions ): array {
        $file_path  = isset( $result['file_path'] ) ? (string) $result['file_path'] : null;
        $zip_path   = isset( $result['images_zip_path'] ) ? (string) $result['images_zip_path'] : null;
        $zip_size   = isset( $result['images_zip_size'] ) ? (int) $result['images_zip_size'] : 0;
        $format_key = strtoupper( sanitize_key( $format ) );

        $sanitized_fields = array_values(
            array_filter(
                array_map( [ $this, 'sanitize_field_key' ], $fields ),
                static function ( $field ): bool {
                    return '' !== $field;
                }
            )
        );

        if ( empty( $sanitized_fields ) ) {
            $sanitized_fields = $this->get_default_fields();
        }

        $normalized_filters = is_array( $filters ) ? $filters : [];

        $raw_settings       = isset( $result['settings'] ) && is_array( $result['settings'] )
            ? $result['settings']
            : $this->get_saved_settings();
        $sanitized_settings = $this->templateManager->sanitize_settings_payload( $raw_settings );

        $rows_exported = (int) ( $result['rows'] ?? 0 );

        $run_payload = [
            'task_id'         => $task_id,
            'template_id'     => $template_id,
            'run_type'        => $run_type,
            'status'          => 'success',
            'rows_exported'   => $rows_exported,
            'file_path'       => $file_path,
            'fields_json'     => wp_json_encode( $sanitized_fields ),
            'filters_json'    => wp_json_encode( $normalized_filters ),
            'settings_json'   => wp_json_encode( $sanitized_settings ),
            'file_format'     => $format_key,
            'images_zip_path' => $zip_path,
            'images_zip_size' => $zip_size,
            'actions'         => $actions,
            'log'             => RunLogFormatter::formatSuccessLog(
                [
                    'rows'           => $rows_exported,
                    'file_path'      => $file_path,
                    'file_url'       => $result['file_path'] ?? null,
                    'images_zip_path'=> $zip_path,
                    'images_zip_url' => $result['images_zip_path'] ?? null,
                    'file_format'    => $format_key,
                ]
            ),
        ];

        $run_id = $this->scheduledManager->record_run( $run_payload );

        $result['run_id']       = $run_id;
        $result['run_type']     = $run_type;
        $result['task_id']      = $task_id;
        $result['template_id']  = $template_id;
        $result['actions']      = $actions;
        $result['status']       = 'success';
        $result['fields']       = $sanitized_fields;
        $result['filters']      = $normalized_filters;
        $result['settings']     = $sanitized_settings;

        if ( $file_path ) {
            $result['file_path'] = $this->normalizeUrlFromPath( $file_path );
        }

        if ( $zip_path ) {
            $result['images_zip_url'] = $this->normalizeUrlFromPath( $zip_path );
            $result['images_zip_path'] = $result['images_zip_url'];
        }

        return $result;
    }

    private function normalizeUrlFromPath( string $path ): string {
        if ( preg_match( '#^https?://#i', $path ) ) {
            return $path;
        }

        $uploads = wp_upload_dir();

        if ( empty( $uploads['basedir'] ) || empty( $uploads['baseurl'] ) ) {
            return $path;
        }

        if ( 0 === strpos( $path, $uploads['basedir'] ) ) {
            $relative = ltrim( str_replace( $uploads['basedir'], '', $path ), '/\\' );

            return trailingslashit( $uploads['baseurl'] ) . str_replace( DIRECTORY_SEPARATOR, '/', $relative );
        }

        return $path;
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function get_category_choices(): array {
        return $this->filterManager->get_category_choices();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function get_brand_choices(): array {
        return $this->filterManager->get_brand_choices();
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    public function get_tag_choices(): array {
        return $this->filterManager->get_tag_choices();
    }

    /**
     * Get the detected brand taxonomy, if any.
     */
    public function get_brand_taxonomy(): ?string {
        return $this->filterManager->get_brand_taxonomy();
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    public function get_condition_field_map(): array {
        return $this->filterManager->get_condition_field_map();
    }

    /**
     * Get last saved export settings.
     *
     * @return array<string, mixed>
     */
    public function get_saved_settings(): array {
        return $this->templateManager->get_last_settings();
    }

    /**
     * Remember last used export settings.
     *
     * @param array<string, mixed> $settings
     */
    public function remember_settings( array $settings ): void {
        $this->templateManager->remember_last_settings( $settings );
    }

    /**
     * Expose template manager for external integrations.
     */
    public function get_template_manager(): TemplateManager {
        return $this->templateManager;
    }

    /**
     * @param resource           $output CSV handle.
     * @param array<int, string> $fields Field keys.
     * @param array<string, int> $filters Active filters.
     */
    private function iterateProducts( array $fields, array $filters, callable $callback, ?int $row_limit = null ): int {
        $query_args = $this->queryBuilder->build_product_query_args( $filters, ChunkQuery::DEFAULT_PER_PAGE );
        $row_count  = 0;

        $stock_filters      = $filters['stock'] ?? [];
        $has_stock_filters  = $this->hasStockFilters( $stock_filters );

        try {
            $this->chunkProcessor->process(
                function ( WC_Product $product ) use ( $fields, &$row_count, $callback, $has_stock_filters, $stock_filters, $row_limit ): void {
                $should_export_parent = ! $has_stock_filters || $this->passesStockFilters( $product, $stock_filters );

                if ( $should_export_parent ) {
                    if ( $this->collectImagesZip ) {
                        $this->collectProductImages( $product );
                    }

                    $row = $this->buildRow( $product, $fields );
                    $callback( $row );
                    $row_count++;

                    if ( null !== $row_limit && $row_count >= $row_limit ) {
                        throw new StopIteration();
                    }
                }

                if ( ! $product->is_type( 'variable' ) ) {
                    return;
                }

                foreach ( $product->get_children() as $variation_id ) {
                    $variation = wc_get_product( $variation_id );

                    if ( ! $variation ) {
                        continue;
                    }

                    if ( ! in_array( $variation->get_status(), [ 'publish', 'private' ], true ) ) {
                        continue;
                    }

                    if ( $has_stock_filters && ! $this->passesStockFilters( $variation, $stock_filters ) ) {
                        continue;
                    }

                    if ( $this->collectImagesZip ) {
                        $this->collectProductImages( $variation );
                    }

                    $variation_row = $this->buildRow( $variation, $fields );
                    $callback( $variation_row );
                    $row_count++;

                    if ( null !== $row_limit && $row_count >= $row_limit ) {
                        throw new StopIteration();
                    }
                }
            },
            $query_args
            );
        } catch ( StopIteration $exception ) {
            // Reached the configured row limit; exit early.
        }

        return $row_count;
    }

    /**
     * @param array<int, string>   $fields
     * @param array<string, mixed> $filters
     * @return array<string, mixed>
     */
    public function get_preview_data( array $fields, array $filters, int $limit = self::PREVIEW_ROW_LIMIT, array $settings = [] ): array {
        $limit = max( 1, $limit );
        $rows  = [];

        try {
            $this->iterateProducts(
                $fields,
                $filters,
                static function ( array $row ) use ( &$rows ): void {
                    $rows[] = $row;
                },
                $limit
            );
        } catch ( StopIteration $exception ) {
            // Reached the configured preview limit, continue.
        }

        $definitions = $this->get_field_definitions();

        $sanitized_settings = ! empty( $settings )
            ? $this->templateManager->sanitize_settings_payload( $settings )
            : [];

        $custom_labels = $sanitized_settings['field_labels'] ?? [];
        if ( is_array( $custom_labels ) && ! empty( $custom_labels ) ) {
            foreach ( $custom_labels as $field_key => $label ) {
                if ( ! is_string( $field_key ) || ! is_string( $label ) ) {
                    continue;
                }

                $field_key = $this->sanitize_field_key( $field_key );
                $label     = trim( $label );

                if ( '' === $field_key || '' === $label ) {
                    continue;
                }

                if ( ! isset( $definitions[ $field_key ] ) || ! is_array( $definitions[ $field_key ] ) ) {
                    $definitions[ $field_key ] = [];
                }

                $definitions[ $field_key ]['label'] = $label;
            }
        }
        $columns     = [];

        foreach ( $fields as $field_key ) {
            $definition = $definitions[ $field_key ] ?? [];

            $columns[] = [
                'key'     => $field_key,
                'label'   => isset( $definition['label'] ) ? (string) $definition['label'] : $field_key,
                'private' => ! empty( $definition['private'] ),
                'group'   => isset( $definition['group'] ) ? (string) $definition['group'] : '',
            ];
        }

        return [
            'columns' => $columns,
            'rows'    => $rows,
            'count'   => count( $rows ),
            'limit'   => $limit,
        ];
    }

    private function hasStockFilters( array $stock_filters ): bool {
        return (
            isset( $stock_filters['min'] ) && null !== $stock_filters['min']
        ) || (
            isset( $stock_filters['max'] ) && null !== $stock_filters['max']
        ) || ! empty( $stock_filters['only_in_stock'] ) || ! empty( $stock_filters['only_zero'] );
    }

    private function passesStockFilters( WC_Product $product, array $stock_filters ): bool {
        if ( ! $this->hasStockFilters( $stock_filters ) ) {
            return true;
        }

        $quantity = $product->get_stock_quantity();
        $stock    = is_numeric( $quantity ) ? (float) $quantity : null;
        $status   = $product->get_stock_status();

        $min = $stock_filters['min'] ?? null;
        $max = $stock_filters['max'] ?? null;

        if ( null !== $min ) {
            if ( null === $stock || $stock < (float) $min ) {
                return false;
            }
        }

        if ( null !== $max ) {
            if ( null === $stock || $stock > (float) $max ) {
                return false;
            }
        }

        $only_zero     = ! empty( $stock_filters['only_zero'] );
        $only_in_stock = ! empty( $stock_filters['only_in_stock'] ) && ! $only_zero;

        if ( $only_in_stock && 'instock' !== $status ) {
            return false;
        }

        if ( $only_in_stock && null !== $stock && $stock <= 0 ) {
            return false;
        }

        if ( $only_zero ) {
            if ( null !== $stock ) {
                return $stock <= 0;
            }

            return 'outofstock' === $status;
        }

        return true;
    }

    /**
     * @param array<int, string> $fields
     * @return array<string, string>
     */
    private function buildRow( WC_Product $product, array $fields ): array {
        $row = [];

        foreach ( $fields as $field_key ) {
            $row[ $field_key ] = Utils::prepare_csv_value( $this->getFieldValue( $product, $field_key ) );
        }

        return $row;
    }

    private function getFieldValue( WC_Product $product, string $field_key ) {
        $definitions = $this->get_field_definitions();
        $definition  = $definitions[ $field_key ] ?? null;

        if ( ! $definition ) {
            return '';
        }

        switch ( $field_key ) {
            case 'product_id':
                return $product->get_id();
            case 'product_sku':
                return $product->get_sku();
            case 'product_name':
                return $product->get_name();
            case 'product_slug':
                return $product->get_slug();
            case 'product_type':
                return $product->get_type();
            case 'regular_price':
                return $product->get_regular_price();
            case 'sale_price':
                return $product->get_sale_price();
            case 'current_price':
                return $product->get_price();
            case 'stock_status':
                return $product->get_stock_status();
            case 'stock_quantity':
                $qty = $product->get_stock_quantity();
                return null === $qty ? '' : (string) $qty;
            case 'short_description':
                return wp_strip_all_tags( $product->get_short_description() );
            case 'full_description':
                return wp_strip_all_tags( $product->get_description() );
            case 'product_permalink':
                return get_permalink( $product->get_id() );
            case 'featured_image':
                $image_id = (int) $product->get_image_id();

                return $this->getImageUrl( $image_id > 0 ? $image_id : null );
            case 'gallery_images':
                return $this->getGalleryUrls( $product );
            case 'all_images':
                return $this->getAllImageUrls( $product );
            case 'product_categories':
                return $this->getTermList( $product->get_id(), 'product_cat' );
            case 'product_tags':
                return $this->getTermList( $product->get_id(), 'product_tag' );
            case 'product_brand':
                return $this->getBrandNames( $product );
            case 'total_sales':
                return get_post_meta( $product->get_id(), 'total_sales', true );
        }

        if ( isset( $definition['type'] ) ) {
            if ( 'attribute' === $definition['type'] ) {
                return $this->getAttributeValue( $product, $definition['attribute_name'] );
            }

            if ( 'acf' === $definition['type'] ) {
                return $this->getAcfFieldValue( $product, $definition );
            }

            if ( 'meta' === $definition['type'] ) {
                return $this->getMetaFieldValue( $product, $definition );
            }
        }

        return apply_filters( 'prodexfo_field_value', '', $field_key, $product, $definition );
    }

    private function getImageUrl( ?int $attachment_id ): string {
        if ( empty( $attachment_id ) ) {
            return '';
        }

        $url = wp_get_attachment_url( $attachment_id );

        return $url ? $url : '';
    }

    private function getGalleryUrls( WC_Product $product ): string {
        $urls = $this->getGalleryUrlList( $product );

        if ( empty( $urls ) ) {
            return '';
        }

        return implode( ', ', $urls );
    }

    /**
     * @return array<int, string>
     */
    private function getGalleryUrlList( WC_Product $product ): array {
        $gallery_ids = $product->get_gallery_image_ids();

        if ( empty( $gallery_ids ) ) {
            return [];
        }

        $urls = array_filter(
            array_map(
                static function ( $attachment_id ) {
                    $url = wp_get_attachment_url( $attachment_id );

                    return $url ? $url : null;
                },
                $gallery_ids
            )
        );

        return array_values( array_unique( $urls ) );
    }

    private function getAllImageUrls( WC_Product $product ): string {
        $urls = [];

        $featured = $this->getImageUrl( (int) $product->get_image_id() ?: null );

        if ( '' !== $featured ) {
            $urls[] = $featured;
        }

        $gallery_urls = $this->getGalleryUrlList( $product );

        if ( ! empty( $gallery_urls ) ) {
            $urls = array_merge( $urls, $gallery_urls );
        }

        if ( empty( $urls ) ) {
            return '';
        }

        $urls       = array_values( array_unique( $urls ) );
        $separator  = $this->getAllImagesSeparator();

        return implode( $separator, $urls );
    }

    private function getAllImagesSeparator(): string {
        $separator = apply_filters( 'prodexfo_all_images_separator', ', ' );

        if ( ! is_string( $separator ) || '' === $separator ) {
            return ', ';
        }

        return $separator;
    }

    private function collectProductImages( WC_Product $product ): void {
        $attachment_ids = [];

        $featured_id = (int) $product->get_image_id();

        if ( $featured_id > 0 ) {
            $attachment_ids[] = $featured_id;
        }

        if ( method_exists( $product, 'get_gallery_image_ids' ) ) {
            foreach ( (array) $product->get_gallery_image_ids() as $gallery_id ) {
                $gallery_id = (int) $gallery_id;

                if ( $gallery_id > 0 ) {
                    $attachment_ids[] = $gallery_id;
                }
            }
        }

        if ( $product->is_type( 'variation' ) ) {
            $parent_id = (int) $product->get_parent_id();

            if ( $parent_id > 0 ) {
                $main_product = wc_get_product( $parent_id );

                if ( $main_product ) {
                    $parent_featured = (int) $main_product->get_image_id();

                    if ( $parent_featured > 0 ) {
                        $attachment_ids[] = $parent_featured;
                    }

                    if ( method_exists( $main_product, 'get_gallery_image_ids' ) ) {
                        foreach ( (array) $main_product->get_gallery_image_ids() as $gallery_id ) {
                            $gallery_id = (int) $gallery_id;

                            if ( $gallery_id > 0 ) {
                                $attachment_ids[] = $gallery_id;
                            }
                        }
                    }
                }
            }
        }

        if ( empty( $attachment_ids ) ) {
            return;
        }

        $attachment_ids = array_values( array_unique( $attachment_ids ) );
        $product_id     = (int) $product->get_id();

        foreach ( $attachment_ids as $attachment_id ) {
            $file_path = get_attached_file( $attachment_id );

            if ( ! $file_path || ! file_exists( $file_path ) ) {
                continue;
            }

            $this->zipImagePaths[ $file_path ] = true;

            if ( ! isset( $this->zipImageSources[ $file_path ] ) ) {
                $this->zipImageSources[ $file_path ] = [];
            }

            if ( $product_id > 0 ) {
                $this->zipImageSources[ $file_path ][] = $product_id;
            }
        }
    }

    private function generateImagesZip( ?string $export_file_path, string $destination ): ?array {
        if ( empty( $this->zipImagePaths ) ) {
            return null;
        }

        if ( ! class_exists( ZipArchive::class ) ) {
            Logger::error( 'ZipArchive extension is not available. Skipping image ZIP generation.' );

            return null;
        }

        $uploads = wp_upload_dir();

        if ( ! empty( $uploads['error'] ) ) {
            Logger::error( 'Uploads directory is not accessible. Skipping image ZIP generation.', [ 'error' => $uploads['error'] ] );

            return null;
        }

        if ( 'file' === $destination && ! empty( $export_file_path ) ) {
            $directory = dirname( $export_file_path );
            $base_name = pathinfo( $export_file_path, PATHINFO_FILENAME );
        } else {
            $directory = trailingslashit( $uploads['basedir'] ) . 'wc-pce/manual/';
            $base_name = 'manual-export-' . \wp_date( 'Ymd-His' );
        }

        if ( ! wp_mkdir_p( $directory ) ) {
            Logger::error( 'Unable to prepare directory for image ZIP.', [ 'directory' => $directory ] );

            return null;
        }

        $zip_filename = sanitize_file_name( $base_name . '-images.zip' );
        $zip_path     = trailingslashit( $directory ) . $zip_filename;

        if ( file_exists( $zip_path ) ) {
            wp_delete_file( $zip_path );
        }

        $archive = new ZipArchive();
        $opened  = $archive->open( $zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE );

        if ( true !== $opened ) {
            Logger::error( 'Unable to create ZIP archive for images.', [ 'path' => $zip_path, 'code' => $opened ] );

            return null;
        }

        $used_names = [];

        foreach ( array_keys( $this->zipImagePaths ) as $path ) {
            if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
                continue;
            }

            $entry_name = $this->generateUniqueZipEntryName( basename( $path ), $used_names, $path );
            $archive->addFile( $path, 'images/' . $entry_name );
        }

        if ( 0 === $archive->numFiles ) {
            $archive->close();
            wp_delete_file( $zip_path );

            return null;
        }

        $archive->close();

        $url = $this->convertPathToUrl( $zip_path );

        return [
            'path'     => $zip_path,
            'url'      => $url,
            'size'     => file_exists( $zip_path ) ? (int) filesize( $zip_path ) : 0,
            'filename' => $zip_filename,
        ];
    }

    /**
     * @param array<string, int> $used
     */
    private function generateUniqueZipEntryName( string $base_name, array &$used, string $path ): string {
        $extension = pathinfo( $path, PATHINFO_EXTENSION );

        if ( '' === $extension ) {
            $extension = pathinfo( $base_name, PATHINFO_EXTENSION );
        }

        $extension = $extension ? strtolower( (string) $extension ) : '';

        if ( '' === $extension ) {
            $extension = 'jpg';
        }

        $product_ids = $this->zipImageSources[ $path ] ?? [];
        $sku         = $this->resolveZipEntrySku( $product_ids );
        $counter     = $this->zipImageSkuCounters[ $sku ] ?? 0;

        do {
            $counter++;
            $candidate = $sku . '-' . $counter . ( $extension ? '.' . $extension : '' );
        } while ( isset( $used[ $candidate ] ) );

        $this->zipImageSkuCounters[ $sku ] = $counter;
        $used[ $candidate ]                = 1;

        return $candidate;
    }

    /**
     * @param array<int> $product_ids
     */
    private function resolveZipEntrySku( array $product_ids ): string {
        if ( ! empty( $product_ids ) ) {
            foreach ( $product_ids as $product_id ) {
                $sku = $this->getProductSkuForZip( (int) $product_id );

                if ( '' !== $sku ) {
                    return $sku;
                }
            }
        }

        return 'image';
    }

    private function getProductSkuForZip( int $product_id ): string {
        if ( isset( $this->zipImageSkuCache[ $product_id ] ) ) {
            return $this->zipImageSkuCache[ $product_id ];
        }

        $sku_value = '';
        $product   = wc_get_product( $product_id );

        if ( $product ) {
            $sku_value = (string) $product->get_sku();

            if ( '' === $sku_value && $product->is_type( 'variation' ) ) {
                $parent_id = (int) $product->get_parent_id();

                if ( $parent_id > 0 ) {
                    $parent = wc_get_product( $parent_id );

                    if ( $parent ) {
                        $sku_value = (string) $parent->get_sku();
                    }
                }
            }
        }

        if ( '' === $sku_value ) {
            $sku_value = 'product-' . $product_id;
        }

        $normalized = $this->normalizeSkuForZip( $sku_value, $product_id );

        $this->zipImageSkuCache[ $product_id ] = $normalized;

        return $normalized;
    }

    private function normalizeSkuForZip( string $value, int $product_id ): string {
        $value = trim( $value );

        if ( '' === $value ) {
            $value = 'product-' . $product_id;
        }

        $normalized = sanitize_title( $value );

        if ( '' === $normalized ) {
            $normalized = 'product-' . $product_id;
        }

        return $normalized;
    }

    private function convertPathToUrl( string $path ): string {
        $uploads = wp_upload_dir();

        if ( empty( $uploads['basedir'] ) || empty( $uploads['baseurl'] ) ) {
            return '';
        }

        $basedir = wp_normalize_path( $uploads['basedir'] );
        $path    = wp_normalize_path( $path );

        if ( 0 !== strpos( $path, $basedir ) ) {
            return '';
        }

        $relative = ltrim( substr( $path, strlen( $basedir ) ), '/\\' );

        return trailingslashit( $uploads['baseurl'] ) . str_replace( '\\', '/', $relative );
    }

    private function getTermList( int $product_id, string $taxonomy ): string {
        $terms = get_the_terms( $product_id, $taxonomy );

        if ( empty( $terms ) || is_wp_error( $terms ) ) {
            return '';
        }

        $names = wp_list_pluck( $terms, 'name' );

        return implode( ', ', $names );
    }

    private function getBrandNames( WC_Product $product ): string {
        $taxonomy = $this->get_brand_taxonomy();

        if ( ! $taxonomy ) {
            return '';
        }

        return $this->getTermList( $product->get_id(), $taxonomy );
    }

    private function getAttributeValue( WC_Product $product, string $attribute_name ): string {
        if ( $product->is_type( 'variation' ) ) {
            $variation_value = $this->getVariationAttributeValue( $product, $attribute_name );

            if ( '' !== $variation_value ) {
                return $variation_value;
            }
        }

        $attributes = $product->get_attributes();

        foreach ( $attributes as $attribute ) {
            if ( ! is_object( $attribute ) || ! method_exists( $attribute, 'get_name' ) ) {
                continue;
            }

            $name = $attribute->get_name();

            if ( $name !== $attribute_name ) {
                continue;
            }

            if ( $attribute->is_taxonomy() ) {
                $values = wc_get_product_terms(
                    $product->get_id(),
                    $attribute->get_name(),
                    [ 'fields' => 'names' ]
                );
            } else {
                $values = $attribute->get_options();
            }

            $values = array_filter( array_map( 'trim', (array) $values ) );

            return implode( ', ', $values );
        }

        return '';
    }

    private function getAcfFieldValue( WC_Product $product, array $definition ) {
        $path = $definition['acf_path'] ?? '';

        if ( '' === $path ) {
            return '';
        }

        $root = $definition['acf_root'] ?? '';
        $type = $definition['acf_type'] ?? '';

        $value_tree = $this->loadAcfValueTree( $product, $root ?: $path );

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

        $relative_path = $root && 0 === strpos( $path, $root )
            ? ltrim( substr( $path, strlen( $root ) ), '.' )
            : $path;

        if ( '' === $relative_path ) {
            $value = $value_tree;
        } else {
            $value = $this->extractValueFromPath( $value_tree, $relative_path );
        }

        return $this->formatAcfValue( $value, $type, $definition, $product );
    }

    private function getMetaFieldValue( WC_Product $product, array $definition ) {
        $meta_key = isset( $definition['meta_key'] ) ? (string) $definition['meta_key'] : '';

        if ( '' === $meta_key ) {
            return '';
        }

        $value = get_post_meta( $product->get_id(), $meta_key, true );
        $formatted = is_array( $value ) || is_object( $value )
            ? $this->flattenComplexValue( $value )
            : (string) $value;

        return apply_filters( 'prodexfo_format_meta_value', $formatted, $value, $definition, $product, $this );
    }

    private function loadAcfValueTree( WC_Product $product, string $root_path ) {
        $product_id = (int) $product->get_id();

        if ( isset( $this->acfValueCache[ $product_id ][ $root_path ] ) ) {
            return $this->acfValueCache[ $product_id ][ $root_path ];
        }

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

        $segments = explode( '.', $root_path );
        $primary  = array_shift( $segments );

        if ( ! $primary ) {
            return null;
        }

        $value = function_exists( 'get_field' ) ? get_field( $primary, $product_id ) : null;

        if ( null === $value ) {
            $value = get_post_meta( $product_id, $primary, true );
        }

        if ( ! empty( $segments ) ) {
            $value = $this->extractValueFromPath( $value, implode( '.', $segments ) );
        }

        if ( ! isset( $this->acfValueCache[ $product_id ] ) ) {
            $this->acfValueCache[ $product_id ] = [];
        }

        $this->acfValueCache[ $product_id ][ $root_path ] = $value;

        return $value;
    }

    private function extractValueFromPath( $value, string $path ) {
        if ( '' === $path ) {
            return $value;
        }

        $segments = explode( '.', $path );

        foreach ( $segments as $segment ) {
            if ( is_array( $value ) && array_key_exists( $segment, $value ) ) {
                $value = $value[ $segment ];
                continue;
            }

            if ( is_object( $value ) && isset( $value->$segment ) ) {
                $value = $value->$segment;
                continue;
            }

            return null;
        }

        return $value;
    }

    private function formatAcfValue( $value, string $type, array $definition, WC_Product $product ) {
        if ( null === $value ) {
            return '';
        }

        if ( in_array( $type, [ 'image', 'file' ], true ) ) {
            $formatted = $this->formatAcfMediaValue( $value );

            return apply_filters( 'prodexfo_format_acf_value', $formatted, $value, $type, $definition, $product, $this );
        }

        if ( in_array( $type, [ 'gallery', 'relationship', 'post_object', 'user', 'taxonomy' ], true ) ) {
            $formatted = $this->formatAcfCollectionValue( $value );

            return apply_filters( 'prodexfo_format_acf_value', $formatted, $value, $type, $definition, $product, $this );
        }

        if ( in_array( $type, [ 'repeater', 'group', 'flexible_content' ], true ) ) {
            $formatted = $this->encodeComplexValue( $value );

            return apply_filters( 'prodexfo_format_acf_value', $formatted, $value, $type, $definition, $product, $this );
        }

        if ( is_array( $value ) || is_object( $value ) ) {
            $formatted = $this->encodeComplexValue( $value );

            return apply_filters( 'prodexfo_format_acf_value', $formatted, $value, $type, $definition, $product, $this );
        }

        if ( is_bool( $value ) ) {
            $formatted = $value ? '1' : '0';

            return apply_filters( 'prodexfo_format_acf_value', $formatted, $value, $type, $definition, $product, $this );
        }

        $formatted = (string) $value;

        return apply_filters( 'prodexfo_format_acf_value', $formatted, $value, $type, $definition, $product, $this );
    }

    private function formatAcfMediaValue( $value ): string {
        if ( is_numeric( $value ) ) {
            $attachment_id = (int) $value;

            if ( $attachment_id > 0 ) {
                $url = wp_get_attachment_url( $attachment_id );

                return $url ? $url : '';
            }
        }

        if ( is_array( $value ) ) {
            if ( isset( $value['url'] ) && is_string( $value['url'] ) ) {
                return $value['url'];
            }

            if ( isset( $value['ID'] ) ) {
                return $this->formatAcfMediaValue( $value['ID'] );
            }
        }

        return '';
    }

    private function formatAcfCollectionValue( $value ): string {
        if ( empty( $value ) ) {
            return '';
        }

        if ( is_array( $value ) ) {
            $collection = [];

            foreach ( $value as $item ) {
                $formatted = $this->formatAcfCollectionItem( $item );

                if ( '' !== $formatted ) {
                    $collection[] = $formatted;
                }
            }

            $collection = array_values( array_unique( array_filter( $collection ) ) );

            return implode( ', ', $collection );
        }

        $formatted = $this->formatAcfCollectionItem( $value );

        return $formatted;
    }

    private function formatAcfCollectionItem( $item ): string {
        if ( is_numeric( $item ) ) {
            $post = get_post( (int) $item );

            if ( $post ) {
                return $post->post_title ?: $post->post_name;
            }

            $user = get_user_by( 'id', (int) $item );

            if ( $user instanceof WP_User ) {
                return $user->display_name ?: $user->user_login;
            }

            $term = get_term( (int) $item );

            if ( $term && ! is_wp_error( $term ) ) {
                return $term->name;
            }

            return (string) $item;
        }

        if ( is_array( $item ) ) {
            if ( isset( $item['post_title'] ) ) {
                return (string) $item['post_title'];
            }

            if ( isset( $item['display_name'] ) ) {
                return (string) $item['display_name'];
            }

            if ( isset( $item['name'] ) ) {
                return (string) $item['name'];
            }

            if ( isset( $item['url'] ) ) {
                return (string) $item['url'];
            }

            return $this->flattenComplexValue( $item );
        }

        if ( $item instanceof WP_Post ) {
            return $item->post_title ?: $item->post_name;
        }

        if ( $item instanceof WP_Term ) {
            return $item->name;
        }

        if ( $item instanceof WP_User ) {
            return $item->display_name ?: $item->user_login;
        }

        if ( is_object( $item ) ) {
            return $this->flattenComplexValue( $item );
        }

        return '' !== (string) $item ? (string) $item : '';
    }

    private function flattenComplexValue( $value ): string {
        $encoded = $this->encodeComplexValue( $value );

        return apply_filters( 'prodexfo_flatten_complex_value', $encoded, $value, $this );
    }

    private function encodeComplexValue( $value ): string {
        if ( null === $value ) {
            return '';
        }

        if ( is_scalar( $value ) ) {
            return (string) $value;
        }

        $normalized = $this->normalizeComplexValue( $value );

        if ( empty( $normalized ) ) {
            return '';
        }

        $encoded = wp_json_encode( $normalized, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );

        return is_string( $encoded ) ? $encoded : '';
    }

    private function normalizeComplexValue( $value ): array {
        if ( is_object( $value ) ) {
            $value = get_object_vars( $value );
        }

        if ( ! is_array( $value ) ) {
            return [];
        }

        $result = [];

        foreach ( $value as $key => $item ) {
            $key = is_int( $key ) ? (string) $key : sanitize_key( (string) $key );

            if ( is_scalar( $item ) || null === $item ) {
                $result[ $key ] = $item;
                continue;
            }

            if ( is_object( $item ) ) {
                $item = get_object_vars( $item );
            }

            if ( is_array( $item ) ) {
                $result[ $key ] = $this->normalizeComplexValue( $item );
                continue;
            }

            $result[ $key ] = (string) $item;
        }

        return $result;
    }

    private function normalizeAttributeIdentifier( string $key ): string {
        return Utils::normalize_attribute_identifier( $key );
    }

    private function getVariationAttributeValue( WC_Product $product, string $attribute_name ): string {
        $attributes = $product->get_attributes();

        foreach ( $attributes as $key => $value ) {
            $normalized = Utils::normalize_attribute_identifier( (string) $key );

            if ( $normalized !== $attribute_name ) {
                continue;
            }

            if ( is_array( $value ) ) {
                $clean = array_filter( array_map( 'wc_clean', $value ) );

                return implode( ', ', $clean );
            }

            $value = (string) $value;

            if ( '' === $value ) {
                return '';
            }

            if ( taxonomy_exists( $attribute_name ) ) {
                $term = get_term_by( 'slug', $value, $attribute_name );

                if ( $term && ! is_wp_error( $term ) ) {
                    return $term->name;
                }
            }

            return wc_clean( $value );
        }

        return '';
    }

    /**
     * @return array<int, array<string, mixed>>
     */
    private function getAttributeDefinitions(): array {
        $definitions = [];

        foreach ( $this->getAttributeCatalog() as $slug => $label ) {
            $definitions[ 'attribute|' . $slug ] = [
                'label'          => sprintf(
                    /* translators: %s attribute label */
                    __( 'Attribute: %s', 'badamsoft-product-exporter-for-woocommerce' ),
                    $label
                ),
                'group'          => 'attributes',
                'type'           => 'attribute',
                'attribute_name' => $slug,
            ];
        }

        return $definitions;
    }

    /**
     * @return array<string, array<string, mixed>>
     */
    private function getDetectedMetaFieldDefinitions(): array {
        $definitions = [];
        $detected    = $this->getDetectedMetaFields();

        foreach ( $detected['acf'] as $descriptor ) {
            $field_key = Utils::sanitize_field_key( $descriptor['field_key'] ?? '' );

            if ( '' === $field_key ) {
                continue;
            }

            $label = isset( $descriptor['label'] ) && '' !== $descriptor['label']
                ? $descriptor['label']
                : ( $descriptor['path'] ?? $field_key );

            $definitions[ $field_key ] = [
                /* translators: %s: field label */
                'label'           => sprintf( __( 'ACF: %s', 'badamsoft-product-exporter-for-woocommerce' ), $label ),
                'group'           => 'acf',
                'type'            => 'acf',
                'acf_path'        => $descriptor['path'] ?? '',
                'acf_root'        => $descriptor['root'] ?? '',
                'acf_type'        => $descriptor['acf_type'] ?? '',
                'acf_group_title' => $descriptor['group_title'] ?? '',
            ];
        }

        foreach ( $detected['meta'] as $descriptor ) {
            $field_key = Utils::sanitize_field_key( $descriptor['field_key'] ?? '' );

            if ( '' === $field_key ) {
                continue;
            }

            $label = isset( $descriptor['label'] ) && '' !== $descriptor['label']
                ? $descriptor['label']
                : Utils::humanize_meta_key( $descriptor['meta_key'] ?? $field_key );

            $definitions[ $field_key ] = [
                /* translators: %s: meta field label */
                'label'    => sprintf( __( 'Meta: %s', 'badamsoft-product-exporter-for-woocommerce' ), $label ),
                'group'    => 'meta',
                'type'     => 'meta',
                'meta_key' => $descriptor['meta_key'] ?? '',
                'is_private' => ! empty( $descriptor['is_private'] ),
            ];
        }

        if ( ! empty( $definitions ) ) {
            uasort(
                $definitions,
                static function ( array $a, array $b ): int {
                    return strcasecmp( (string) ( $a['label'] ?? '' ), (string) ( $b['label'] ?? '' ) );
                }
            );
        }

        return $definitions;
    }

    /**
     * @return array{acf: array<int, array<string, mixed>>, meta: array<int, array<string, mixed>>}
     */
    private function getDetectedMetaFields(): array {
        if ( null === $this->detectedMetaFieldCache ) {
            try {
                $this->detectedMetaFieldCache = $this->metaFieldDetector->detect();
            } catch ( Throwable $exception ) {
                Logger::error( 'Failed to detect ACF/meta fields.', [ 'message' => $exception->getMessage() ] );
                $this->detectedMetaFieldCache = [ 'acf' => [], 'meta' => [] ];
            }
        }

        return $this->detectedMetaFieldCache;
    }

    /**
     * @return array<string, string>
     */
    private function getAttributeCatalog(): array {
        if ( null !== $this->attributeCatalog ) {
            return $this->attributeCatalog;
        }

        $catalog = [];

        if ( function_exists( 'wc_get_attribute_taxonomies' ) ) {
            foreach ( wc_get_attribute_taxonomies() as $taxonomy ) {
                $taxonomy_name = wc_attribute_taxonomy_name( $taxonomy->attribute_name );
                $slug          = $taxonomy_name;
                $label         = $taxonomy->attribute_label ?? '';

                if ( '' === $label && function_exists( 'wc_get_attribute' ) ) {
                    $attribute_object = wc_get_attribute( (int) $taxonomy->attribute_id );

                    if ( $attribute_object && is_object( $attribute_object ) ) {
                        if ( method_exists( $attribute_object, 'get_label' ) ) {
                            $label = $attribute_object->get_label();
                        } elseif ( isset( $attribute_object->attribute_label ) ) {
                            $label = $attribute_object->attribute_label;
                        }
                    }
                }

                if ( '' === $label ) {
                    $label = wc_attribute_label( $taxonomy_name );
                }

                if ( '' === $label ) {
                    $label = Utils::humanize_attribute_label( $taxonomy_name );
                }

                $catalog[ $slug ] = $label;
            }
        }

        foreach ( $this->collectAttributeNamesFromProducts() as $slug => $label ) {
            if ( ! isset( $catalog[ $slug ] ) ) {
                $catalog[ $slug ] = $label;
            }
        }

        uasort(
            $catalog,
            static function ( string $a, string $b ): int {
                return strcasecmp( $a, $b );
            }
        );

        $this->attributeCatalog = $catalog;

        return $this->attributeCatalog;
    }

    /**
     * @return array<string, string>
     */
    private function collectAttributeNamesFromProducts(): array {
        global $wpdb;

        $custom = [];
        $cache_group = 'prodexfo';
        $cache_key   = 'attribute_names_from_products:v1';
        $results     = wp_cache_get( $cache_key, $cache_group );
        if ( false === $results ) {
            $results = $wpdb->get_col( "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_product_attributes'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
            wp_cache_set( $cache_key, $results, $cache_group, HOUR_IN_SECONDS );
        }

        foreach ( $results as $value ) {
            $attributes = maybe_unserialize( $value );

            if ( ! is_array( $attributes ) ) {
                continue;
            }

            foreach ( $attributes as $key => $attribute ) {
                if ( ! is_array( $attribute ) ) {
                    continue;
                }

                if ( ! empty( $attribute['is_taxonomy'] ) ) {
                    $slug  = isset( $attribute['name'] ) && '' !== $attribute['name'] ? (string) $attribute['name'] : (string) $key;
                    $label = '';

                    if ( ! empty( $attribute['id'] ) && function_exists( 'wc_get_attribute' ) ) {
                        $attribute_object = wc_get_attribute( (int) $attribute['id'] );

                        if ( $attribute_object && is_object( $attribute_object ) ) {
                            if ( method_exists( $attribute_object, 'get_label' ) ) {
                                $label = $attribute_object->get_label();
                            } elseif ( isset( $attribute_object->attribute_label ) ) {
                                $label = $attribute_object->attribute_label;
                            }
                        }
                    }

                    if ( '' === $label ) {
                        $label = wc_attribute_label( $slug );
                    }

                    if ( '' === $label && ! empty( $attribute['name'] ) ) {
                        $label = sanitize_text_field( $attribute['name'] );
                    }

                    $label         = $label ?: Utils::humanize_attribute_label( $slug );
                    $custom[ $slug ] = $label;
                    continue;
                }

                $slug  = isset( $attribute['name'] ) && '' !== $attribute['name'] ? (string) $attribute['name'] : (string) $key;
                $label = isset( $attribute['name'] ) ? sanitize_text_field( $attribute['name'] ) : '';

                if ( '' === $slug ) {
                    continue;
                }

                $custom[ $slug ] = $label ?: Utils::humanize_attribute_label( $slug );
            }
        }

        return $custom;
    }

}
