<?php

/**
 * @package QuickStockForWooCommerce
 * @license GPL-2.0-or-later (https://www.gnu.org/licenses/gpl-2.0.html)
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
class QSFW_Ajax {
    public static function init() {
        add_action( 'wp_ajax_qsfw_update_value', array(__CLASS__, 'update_value') );
        add_action( 'wp_ajax_qsfw_reset_checks', array(__CLASS__, 'reset_checks') );
        add_action( 'wp_ajax_qsfw_search_products', array(__CLASS__, 'search_products') );
        add_action( 'wp_ajax_qsfw_bulk_apply', array(__CLASS__, 'bulk_apply') );
        add_action( 'wp_ajax_qsfw_bulk_export', array(__CLASS__, 'bulk_export') );
        add_action( 'wp_ajax_qsfw_log_export', array(__CLASS__, 'log_export') );
        add_action( 'wp_ajax_qsfw_set_thumbnail', array(__CLASS__, 'set_thumbnail') );
        add_action( 'wp_ajax_qsfw_column_values', array(__CLASS__, 'column_values') );
    }

    /**
     * Distinct-Werte einer Spalte für das Excel-Style Wertfilter-Dropdown
     * im Spaltenkopf. Optional per Teilstring (?q=) serverseitig durchsucht.
     */
    public static function column_values() {
        check_ajax_referer( 'qsfw_stock_nonce', 'nonce' );
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Permission denied.', 'quickstock-for-woocommerce' ),
            ) );
        }
        $col = ( isset( $_POST['col'] ) ? sanitize_key( $_POST['col'] ) : '' );
        if ( $col === '' || !isset( QSFW_Filters::value_filterable()[$col] ) ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid column.', 'quickstock-for-woocommerce' ),
            ) );
        }
        $search = ( isset( $_POST['q'] ) ? sanitize_text_field( wp_unslash( $_POST['q'] ) ) : '' );
        $mode = ( isset( $_POST['mode'] ) ? sanitize_text_field( wp_unslash( $_POST['mode'] ) ) : 'all' );
        $res = QSFW_Data::distinct_column_values(
            $col,
            $search,
            $mode,
            500
        );
        wp_send_json_success( $res );
    }

    public static function set_thumbnail() {
        check_ajax_referer( 'qsfw_stock_nonce', 'nonce' );
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Permission denied.', 'quickstock-for-woocommerce' ),
            ) );
        }
        $pid = ( isset( $_POST['product_id'] ) ? intval( $_POST['product_id'] ) : 0 );
        $aid = ( isset( $_POST['attachment_id'] ) ? intval( $_POST['attachment_id'] ) : 0 );
        if ( $pid <= 0 ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid product ID.', 'quickstock-for-woocommerce' ),
            ) );
        }
        if ( !current_user_can( 'edit_post', $pid ) ) {
            wp_send_json_error( array(
                'message' => __( 'Permission denied.', 'quickstock-for-woocommerce' ),
            ) );
        }
        $post = get_post( $pid );
        if ( !$post ) {
            wp_send_json_error( array(
                'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
            ) );
        }
        if ( $aid > 0 ) {
            $att = get_post( $aid );
            if ( !$att || $att->post_type !== 'attachment' ) {
                wp_send_json_error( array(
                    'message' => __( 'Invalid image ID.', 'quickstock-for-woocommerce' ),
                ) );
            }
            set_post_thumbnail( $pid, $aid );
            $url = wp_get_attachment_image_url( $aid, array(40, 40) );
        } else {
            delete_post_thumbnail( $pid );
            $url = '';
        }
        wp_send_json_success( array(
            'url' => ( $url ? $url : '' ),
        ) );
    }

    public static function update_value() {
        check_ajax_referer( 'qsfw_stock_nonce', 'nonce' );
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error();
        }
        $pid = ( isset( $_POST['product_id'] ) ? intval( $_POST['product_id'] ) : 0 );
        $type = ( isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '' );
        // Array-Werte (value[]=…) abweisen: der Wert ist immer skalar, ein Array
        // liefe sonst in trim()/intval() und wuerfe unter PHP 8 einen Fatal.
        if ( isset( $_POST['value'] ) && !is_scalar( $_POST['value'] ) ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid value.', 'quickstock-for-woocommerce' ),
            ) );
        }
        // Newlines bewahren (Textarea/mehrzeilige Custom Fields); URL-Felder
        // direkt mit esc_url_raw() verarbeiten, damit %20 erhalten bleibt.
        $val = ( isset( $_POST['value'] ) ? ( self::is_url_field( $type ) ? esc_url_raw( wp_unslash( $_POST['value'] ) ) : sanitize_textarea_field( wp_unslash( $_POST['value'] ) ) ) : '' );
        // 'check' ist Plugin-intern (Erledigt-Haken), kein Produkt-Update.
        if ( $type === 'check' ) {
            $on = intval( $val ) === 1;
            self::mutate_stock_checks( function ( $c ) use($pid, $on) {
                if ( $on ) {
                    $c[$pid] = time();
                } else {
                    unset($c[$pid]);
                }
                return $c;
            } );
            wp_send_json_success();
        }
        if ( $pid <= 0 ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid product ID.', 'quickstock-for-woocommerce' ),
            ) );
        }
        $res = self::apply_field( $pid, $type, $val );
        if ( !$res['ok'] ) {
            wp_send_json_error( array(
                'message' => $res['message'],
            ) );
        }
        // Optional: Erledigt-Markierung aufheben, wenn der Lagerbestand
        // erhöht wurde (per allgemeiner Einstellung steuerbar).
        $resp = array();
        if ( $type === 'stock' && get_option( 'qsfw_done_reset_on_restock', 0 ) ) {
            $old_n = ( isset( $res['old_stock'] ) && $res['old_stock'] !== '' && $res['old_stock'] !== null ? (float) $res['old_stock'] : null );
            $new_n = (float) intval( $val );
            if ( $old_n !== null && $new_n > $old_n ) {
                $c = get_option( 'qsfw_stock_checks', array() );
                if ( isset( $c[$pid] ) ) {
                    self::mutate_stock_checks( function ( $c ) use($pid) {
                        unset($c[$pid]);
                        return $c;
                    } );
                    $resp['uncheck'] = true;
                }
            }
        }
        wp_send_json_success( $resp );
    }

    /**
     * Atomares Lesen-Ändern-Schreiben der Erledigt-Markierungen
     * (`qsfw_stock_checks`).
     *
     * Jeder Haken-Klick im Reorder-Tab feuert einen eigenen, NEBENLÄUFIGEN
     * AJAX-Request ab. Ohne Sperre liest jeder Request denselben DB-Stand,
     * ändert einen Eintrag und schreibt das GANZE Array zurück → der zuletzt
     * schreibende Request überschreibt die Änderungen der anderen (lost update).
     * Genau das erzeugte das „Haken kommen nach dem Refresh wieder"-Verhalten.
     *
     * Lösung: ein MySQL-Named-Lock serialisiert die nebenläufigen Requests,
     * und der Options-Cache wird vorher verworfen, damit garantiert der frische
     * DB-Stand gelesen wird (nicht ein veralteter Cache-Wert aus diesem Prozess).
     *
     * @param callable $mutator function(array $checks): array — gibt das neue Array zurück.
     */
    private static function mutate_stock_checks( $mutator ) {
        global $wpdb;
        // Prozess-/site-spezifischer Lock-Name (mehrere Sites auf einem DB-Server).
        $lock = 'qsfw_stock_checks_' . md5( DB_NAME . '|' . $wpdb->prefix );
        $lock = substr( $lock, 0, 64 );
        // MySQL-Lock-Namen sind auf 64 Zeichen begrenzt.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- MySQL-Named-Lock ist eine Synchronisationsprimitive und kein cachebarer Datenabruf
        $got = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', $lock, 5 ) );
        // Cache verwerfen → get_option liest frisch aus der DB.
        wp_cache_delete( 'qsfw_stock_checks', 'options' );
        $checks = get_option( 'qsfw_stock_checks', array() );
        if ( !is_array( $checks ) ) {
            $checks = array();
        }
        $checks = call_user_func( $mutator, $checks );
        update_option( 'qsfw_stock_checks', $checks, false );
        if ( $got ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- der zuvor erworbene MySQL-Named-Lock muss im selben Request direkt freigegeben werden
            $wpdb->query( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock ) );
        }
    }

    /**
     * Ist $key ein Custom-Field vom Typ „url"? Entscheidet, ob der Rohwert beim
     * Einlesen mit esc_url_raw statt sanitize_textarea_field behandelt wird
     * (sonst frisst _sanitize_text_fields die %20-Kodierung). Nur Pro — im
     * Free-Build gibt es keine Custom Fields, der Guard liefert dann false und
     * der Wert faellt auf sanitize_textarea_field zurueck.
     */
    private static function is_url_field( $key ) {
        if ( strpos( (string) $key, 'cf_' ) !== 0 ) {
            return false;
        }
        return false;
    }

    /**
     * Speichert genau einen Feldwert für ein Produkt. Geteilt von Einzel-
     * (update_value) und Massenbearbeitung (bulk_apply) — damit beide Wege
     * garantiert identisch über die WC-CRUD / Taxonomien / postmeta speichern.
     * Macht KEIN wp_send_json.
     *
     * @return array ['ok' => bool, 'message' => string, 'old_stock' => mixed|null]
     */
    public static function apply_field( $pid, $type, $val ) {
        $pid = intval( $pid );
        if ( $pid <= 0 ) {
            return array(
                'ok'      => false,
                'message' => __( 'Invalid product ID.', 'quickstock-for-woocommerce' ),
            );
        }
        // === Erledigt-Markierung (kein WC-Produkt-Update nötig) ===
        if ( $type === 'check' || $type === 'uncheck' ) {
            $set = $type === 'check';
            self::mutate_stock_checks( function ( $c ) use($pid, $set) {
                if ( $set ) {
                    $c[$pid] = time();
                } else {
                    unset($c[$pid]);
                }
                return $c;
            } );
            return array(
                'ok'      => true,
                'message' => '',
            );
        }
        // === Echte WooCommerce-Produktfelder → über die WC-CRUD speichern ===
        // WooCommerce hält dabei selbst die Lookup-Tabelle, den Cache und
        // abhängige Werte (Preis ↔ Aktionspreis, Lager ↔ Lagerstatus) konsistent.
        $crud_types = array(
            'stock',
            'limit',
            'price',
            'sale',
            'sale_from',
            'sale_to',
            'sku',
            'manage_stock',
            'stock_status',
            'backorders'
        );
        if ( in_array( $type, $crud_types, true ) ) {
            $product = wc_get_product( $pid );
            if ( !$product ) {
                return array(
                    'ok'      => false,
                    'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
                );
            }
            $old_stock = null;
            try {
                switch ( $type ) {
                    case 'stock':
                        $old_stock = $product->get_stock_quantity();
                        $product->set_stock_quantity( intval( $val ) );
                        break;
                    case 'limit':
                        $product->set_low_stock_amount( ( $val === '' ? '' : intval( $val ) ) );
                        break;
                    case 'price':
                        $product->set_regular_price( wc_format_decimal( $val ) );
                        break;
                    case 'sale':
                        $product->set_sale_price( ( $val === '' ? '' : wc_format_decimal( $val ) ) );
                        break;
                    case 'sale_from':
                        $product->set_date_on_sale_from( ( $val ? strtotime( $val . ' 00:00:00' ) : '' ) );
                        break;
                    case 'sale_to':
                        $product->set_date_on_sale_to( ( $val ? strtotime( $val . ' 23:59:59' ) : '' ) );
                        break;
                    case 'sku':
                        $product->set_sku( sanitize_text_field( $val ) );
                        break;
                    case 'manage_stock':
                        $product->set_manage_stock( $val === '1' || $val === 'yes' );
                        break;
                    case 'stock_status':
                        if ( in_array( $val, array('instock', 'outofstock', 'onbackorder'), true ) ) {
                            $product->set_stock_status( $val );
                        }
                        break;
                    case 'backorders':
                        if ( in_array( $val, array('no', 'notify', 'yes'), true ) ) {
                            $product->set_backorders( $val );
                        }
                        break;
                }
                $product->save();
            } catch ( WC_Data_Exception $e ) {
                // z.B. doppelte SKU → WooCommerce verweigert das Speichern.
                return array(
                    'ok'      => false,
                    'message' => $e->getMessage(),
                );
            }
            // Bestandsänderung in den Verlauf schreiben (Inline- + Massenbearbeitung).
            if ( $type === 'stock' ) {
                QSFW_Log::log_stock_change(
                    $product,
                    $old_stock,
                    intval( $val ),
                    'manual'
                );
            }
            return array(
                'ok'        => true,
                'message'   => '',
                'old_stock' => $old_stock,
            );
        }
        // === Nachbestell-Felder (QuickStock-eigen, kein WC-CRUD) ===
        // restock_target: Zielbestand, auf den aufgefüllt werden soll.
        // order_qty:      gewünschte Bestellmenge (über Tage „sammelbar").
        // Beide als nicht-negative Ganzzahl; leer/0 → Meta entfernen.
        if ( $type === 'restock_target' || $type === 'order_qty' ) {
            $meta_key = ( $type === 'restock_target' ? '_qsfw_restock_target' : '_qsfw_order_qty' );
            $n = ( $val === '' ? 0 : max( 0, intval( $val ) ) );
            if ( $n === 0 ) {
                delete_post_meta( $pid, $meta_key );
            } else {
                update_post_meta( $pid, $meta_key, $n );
            }
            return array(
                'ok'      => true,
                'message' => '',
            );
        }
        // === Eigene Felder / Taxonomien / WP-Post-Felder ===
        // Diese haben keinen WC-CRUD-Weg und bleiben bei den passenden
        // WordPress-Standardfunktionen.
        switch ( $type ) {
            case 'title':
                $post = get_post( $pid );
                if ( $post && $post->post_type === 'product' && trim( $val ) !== '' ) {
                    wp_update_post( array(
                        'ID'         => $pid,
                        'post_title' => sanitize_text_field( $val ),
                    ) );
                }
                break;
            case 'shipping_class':
                $tid = intval( $val );
                wp_set_object_terms( $pid, ( $tid > 0 ? array($tid) : array() ), 'product_shipping_class' );
                break;
            case 'permalink':
                $slug = sanitize_title( $val );
                if ( $slug !== '' ) {
                    wp_update_post( array(
                        'ID'        => $pid,
                        'post_name' => $slug,
                    ) );
                }
                break;
            case 'post_status':
                if ( in_array( $val, array(
                    'publish',
                    'draft',
                    'private',
                    'pending'
                ), true ) ) {
                    wp_update_post( array(
                        'ID'          => $pid,
                        'post_status' => $val,
                    ) );
                }
                break;
            case 'global_unique_id':
                update_post_meta( $pid, '_global_unique_id', sanitize_text_field( $val ) );
                break;
            case 'excerpt':
                wp_update_post( array(
                    'ID'           => $pid,
                    'post_excerpt' => sanitize_textarea_field( $val ),
                ) );
                break;
            case 'product_cat_add':
                $tid = intval( $val );
                if ( $tid > 0 ) {
                    wp_set_object_terms(
                        $pid,
                        array($tid),
                        'product_cat',
                        true
                    );
                }
                break;
            case 'product_cat_remove':
                $tid = intval( $val );
                if ( $tid > 0 ) {
                    wp_remove_object_terms( $pid, array($tid), 'product_cat' );
                }
                break;
            default:
                /* translators: %s is the unknown field type identifier */
                $message = sprintf( __( 'Unknown field type: %s', 'quickstock-for-woocommerce' ), $type );
                return array(
                    'ok'      => false,
                    'message' => $message,
                );
        }
        // Diese Felder umgehen die WC-CRUD (Taxonomien, Custom-Felder,
        // Post-Felder) → Produkt-Cache leeren, damit der Shop sofort stimmt.
        if ( function_exists( 'wc_delete_product_transients' ) ) {
            wc_delete_product_transients( $pid );
        }
        return array(
            'ok'      => true,
            'message' => '',
        );
    }

    public static function bulk_apply() {
        check_ajax_referer( 'qsfw_stock_nonce', 'nonce' );
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error( array(
                'message' => __( 'Permission denied.', 'quickstock-for-woocommerce' ),
            ) );
        }
        $ids = ( isset( $_POST['ids'] ) ? array_map( 'intval', (array) $_POST['ids'] ) : array() );
        $ids = array_filter( $ids, function ( $i ) {
            return $i > 0;
        } );
        $op = ( isset( $_POST['op'] ) ? sanitize_text_field( wp_unslash( $_POST['op'] ) ) : '' );
        // Array-Werte abweisen (siehe update_value): schuetzt trim()/intval()
        // stromabwaerts vor einem PHP-8-TypeError.
        if ( isset( $_POST['value'] ) && !is_scalar( $_POST['value'] ) || isset( $_POST['value2'] ) && !is_scalar( $_POST['value2'] ) ) {
            wp_send_json_error( array(
                'message' => __( 'Invalid value.', 'quickstock-for-woocommerce' ),
            ) );
        }
        // Newlines bewahren; URL-Custom-Fields direkt mit esc_url_raw()
        // verarbeiten, damit %20 erhalten bleibt.
        $val = ( isset( $_POST['value'] ) ? ( self::is_url_field( $op ) ? esc_url_raw( wp_unslash( $_POST['value'] ) ) : sanitize_textarea_field( wp_unslash( $_POST['value'] ) ) ) : '' );
        $v2 = ( isset( $_POST['value2'] ) ? sanitize_textarea_field( wp_unslash( $_POST['value2'] ) ) : '' );
        $sub = ( isset( $_POST['submode'] ) ? sanitize_text_field( wp_unslash( $_POST['submode'] ) ) : '' );
        $opts = ( isset( $_POST['opts'] ) && is_array( $_POST['opts'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['opts'] ) ) : array() );
        if ( empty( $ids ) ) {
            wp_send_json_error( array(
                'message' => __( 'No products selected.', 'quickstock-for-woocommerce' ),
            ) );
        }
        if ( $op === '' ) {
            wp_send_json_error( array(
                'message' => __( 'No action selected.', 'quickstock-for-woocommerce' ),
            ) );
        }
        $processed = 0;
        $skipped = array();
        foreach ( $ids as $pid ) {
            $r = self::bulk_apply_one(
                $pid,
                $op,
                $val,
                $v2,
                $sub,
                $opts
            );
            if ( $r['ok'] ) {
                $processed++;
            } else {
                $skipped[] = array(
                    'id'     => $pid,
                    'reason' => $r['message'],
                );
            }
        }
        wp_send_json_success( array(
            'processed' => $processed,
            'skipped'   => $skipped,
        ) );
    }

    /**
     * Übersetzt eine Bulk-Operation in eine oder mehrere apply_field()-Aufrufe
     * für ein einzelnes Produkt. Sonderfälle (relativ, %, skippen) hier.
     */
    private static function bulk_apply_one(
        $pid,
        $op,
        $val,
        $v2,
        $sub,
        $opts = array()
    ) {
        switch ( $op ) {
            case 'stock_set':
            case 'stock_inc':
                $product = wc_get_product( $pid );
                if ( !$product ) {
                    return array(
                        'ok'      => false,
                        'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
                    );
                }
                if ( !$product->get_manage_stock() ) {
                    return array(
                        'ok'      => false,
                        'message' => __( 'Stock management off', 'quickstock-for-woocommerce' ),
                    );
                }
                if ( $op === 'stock_inc' ) {
                    $cur = (int) $product->get_stock_quantity();
                    $new = $cur + (int) $val;
                } else {
                    $new = (int) $val;
                }
                return self::apply_field( $pid, 'stock', $new );
            case 'manage_stock_on':
                return self::apply_field( $pid, 'manage_stock', '1' );
            case 'manage_stock_off':
                return self::apply_field( $pid, 'manage_stock', '0' );
            case 'stock_status':
                return self::apply_field( $pid, 'stock_status', $val );
            case 'backorders':
                return self::apply_field( $pid, 'backorders', $val );
            case 'post_status':
                return self::apply_field( $pid, 'post_status', $val );
            case 'shipping_class':
                return self::apply_field( $pid, 'shipping_class', $val );
            case 'cat_add':
                return self::apply_field( $pid, 'product_cat_add', $val );
            case 'cat_remove':
                return self::apply_field( $pid, 'product_cat_remove', $val );
            case 'limit_set':
                return self::apply_field( $pid, 'limit', $val );
            case 'restock_set':
                return self::apply_field( $pid, 'restock_target', $val );
            case 'sale_clear':
                $product = wc_get_product( $pid );
                if ( !$product ) {
                    return array(
                        'ok'      => false,
                        'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
                    );
                }
                try {
                    $product->set_sale_price( '' );
                    $product->set_date_on_sale_from( '' );
                    $product->set_date_on_sale_to( '' );
                    $product->save();
                } catch ( WC_Data_Exception $e ) {
                    return array(
                        'ok'      => false,
                        'message' => $e->getMessage(),
                    );
                }
                return array(
                    'ok'      => true,
                    'message' => '',
                );
            case 'sale_dates_clear':
                $product = wc_get_product( $pid );
                if ( !$product ) {
                    return array(
                        'ok'      => false,
                        'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
                    );
                }
                try {
                    $product->set_date_on_sale_from( '' );
                    $product->set_date_on_sale_to( '' );
                    $product->save();
                } catch ( WC_Data_Exception $e ) {
                    return array(
                        'ok'      => false,
                        'message' => $e->getMessage(),
                    );
                }
                return array(
                    'ok'      => true,
                    'message' => '',
                );
            case 'sale_set':
                if ( empty( $opts['overwrite'] ) ) {
                    $product = wc_get_product( $pid );
                    if ( !$product ) {
                        return array(
                            'ok'      => false,
                            'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
                        );
                    }
                    $existing = $product->get_sale_price();
                    if ( $existing !== '' && $existing !== null ) {
                        return array(
                            'ok'      => false,
                            'message' => __( 'Sale price already set', 'quickstock-for-woocommerce' ),
                        );
                    }
                }
                if ( $sub === 'pct' ) {
                    $product = wc_get_product( $pid );
                    if ( !$product ) {
                        return array(
                            'ok'      => false,
                            'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
                        );
                    }
                    $reg = (float) $product->get_regular_price();
                    if ( $reg <= 0 ) {
                        return array(
                            'ok'      => false,
                            'message' => __( 'No regular price', 'quickstock-for-woocommerce' ),
                        );
                    }
                    $pct = (float) str_replace( ',', '.', $val );
                    $sale = self::round_price( $reg * (1 - $pct / 100), $v2 );
                    return self::apply_field( $pid, 'sale', $sale );
                }
                return self::apply_field( $pid, 'sale', $val );
            case 'sale_dates':
                if ( !empty( $opts['only_onsale'] ) ) {
                    $product = wc_get_product( $pid );
                    if ( !$product ) {
                        return array(
                            'ok'      => false,
                            'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
                        );
                    }
                    $existing = $product->get_sale_price();
                    if ( $existing === '' || $existing === null ) {
                        return array(
                            'ok'      => false,
                            'message' => __( 'No sale price set', 'quickstock-for-woocommerce' ),
                        );
                    }
                }
                $r1 = self::apply_field( $pid, 'sale_from', $val );
                if ( !$r1['ok'] ) {
                    return $r1;
                }
                return self::apply_field( $pid, 'sale_to', $v2 );
            case 'price_set':
                return self::apply_field( $pid, 'price', $val );
            case 'trash':
                $post = get_post( $pid );
                if ( !$post ) {
                    return array(
                        'ok'      => false,
                        'message' => __( 'Product not found.', 'quickstock-for-woocommerce' ),
                    );
                }
                if ( $post->post_status === 'trash' ) {
                    return array(
                        'ok'      => true,
                        'message' => '',
                    );
                }
                if ( !current_user_can( 'delete_post', $pid ) ) {
                    return array(
                        'ok'      => false,
                        'message' => __( 'Permission denied.', 'quickstock-for-woocommerce' ),
                    );
                }
                $r = wp_trash_post( $pid );
                if ( !$r ) {
                    return array(
                        'ok'      => false,
                        'message' => __( 'Move to trash failed.', 'quickstock-for-woocommerce' ),
                    );
                }
                return array(
                    'ok'      => true,
                    'message' => '',
                );
            case 'check_set':
                return self::apply_field( $pid, 'check', '' );
            case 'check_unset':
                return self::apply_field( $pid, 'uncheck', '' );
            default:
                /* translators: %s is the unknown bulk action identifier */
                $message = sprintf( __( 'Unknown action: %s', 'quickstock-for-woocommerce' ), $op );
                return array(
                    'ok'      => false,
                    'message' => $message,
                );
        }
    }

    /**
     * CSV-Export der ausgewählten Produkte (sichtbare Spalten, Excel/DE:
     * UTF-8-BOM + Semikolon). Per verstecktem Formular-POST aufgerufen,
     * damit der Browser einen Datei-Download erhält.
     */
    public static function bulk_export() {
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_die( esc_html__( 'Permission denied.', 'quickstock-for-woocommerce' ) );
        }
        check_admin_referer( 'qsfw_bulk_export' );
        $ids = ( isset( $_POST['ids'] ) ? sanitize_text_field( wp_unslash( $_POST['ids'] ) ) : '' );
        $ids = array_filter( array_map( 'intval', explode( ',', $ids ) ), function ( $i ) {
            return $i > 0;
        } );
        if ( empty( $ids ) ) {
            wp_die( esc_html__( 'No products selected.', 'quickstock-for-woocommerce' ) );
        }
        $cols = QSFW_Columns::get_visible();
        $all_cols = QSFW_Columns::get_all();
        // Spaltenreihenfolge wie in der Tabelle (aktiver "Alle Produkte"-Modus),
        // ohne die rein funktionalen Spalten (Auswahl/Erledigt/Bild).
        $active = QSFW_Columns::get_active( 'all' );
        $export_cols = array();
        foreach ( $active as $key => $cfg ) {
            if ( in_array( $key, array('bulk_select', 'check', 'image'), true ) ) {
                continue;
            }
            $export_cols[$key] = $cfg;
        }
        $rows = QSFW_Data::get_stock_data(
            gmdate( 'Y-m-d', strtotime( '-3650 days' ) ),
            'title_asc',
            'all',
            QSFW_Columns::get_status_filter(),
            0,
            $cols,
            array(),
            // keine adv-Filter
            false,
            // keine Eltern-Gruppierung
            array()
        );
        $by_id = array();
        foreach ( $rows as $r ) {
            $by_id[(int) $r->ID] = $r;
        }
        // CSV-Einstellungen aus den allgemeinen Plugin-Settings
        $csv_delim_key = QSFW_Settings_Page::get_general_setting( 'csv_delimiter', ';' );
        $csv_delimiter = ( $csv_delim_key === 'tab' ? "\t" : $csv_delim_key );
        $csv_bom = (int) QSFW_Settings_Page::get_general_setting( 'csv_bom', 1 );
        $csv_date_fmt = QSFW_Settings_Page::get_general_setting( 'csv_date_format', 'Y-m-d' );
        $filename_date = ( $csv_date_fmt === 'd.m.Y' ? current_time( 'd.m.Y' ) : current_time( 'Y-m-d' ) );
        nocache_headers();
        header( 'Content-Type: text/csv; charset=utf-8' );
        header( 'Content-Disposition: attachment; filename="stock-export-' . $filename_date . '.csv"' );
        $out = fopen( 'php://output', 'w' );
        if ( $csv_bom ) {
            echo "﻿";
        }
        $header = array('ID', 'SKU');
        foreach ( $export_cols as $key => $cfg ) {
            if ( in_array( $key, array('sku'), true ) ) {
                continue;
            }
            $header[] = wp_strip_all_tags( str_replace( '<br>', ' ', $cfg['label'] ) );
        }
        fputcsv(
            $out,
            $header,
            $csv_delimiter,
            '"',
            '\\'
        );
        foreach ( $ids as $pid ) {
            if ( !isset( $by_id[$pid] ) ) {
                continue;
            }
            $p = $by_id[$pid];
            $line = array($pid, ( isset( $p->sku ) ? $p->sku : '' ));
            foreach ( $export_cols as $key => $cfg ) {
                if ( $key === 'sku' ) {
                    continue;
                }
                $line[] = self::export_cell_value( $p, $key, $csv_date_fmt );
            }
            fputcsv(
                $out,
                $line,
                $csv_delimiter,
                '"',
                '\\'
            );
        }
        exit;
    }

    /** Rohwert einer Spalte für den CSV-Export (ohne HTML/Inputs). */
    private static function export_cell_value( $p, $key, $date_fmt = 'Y-m-d' ) {
        switch ( $key ) {
            case 'title':
                return ( isset( $p->post_title ) ? $p->post_title : '' );
            case 'stock':
                return ( isset( $p->stock_qty ) ? $p->stock_qty : '' );
            case 'limit':
                return ( isset( $p->threshold ) ? $p->threshold : '' );
            case 'restock_target':
                return ( isset( $p->restock_target ) && $p->restock_target !== '' ? (int) $p->restock_target : '' );
            case 'order_qty':
                return ( isset( $p->order_qty ) && $p->order_qty !== '' ? (int) $p->order_qty : '' );
            case 'price_reg':
                return ( isset( $p->regular_price ) ? $p->regular_price : '' );
            case 'price_sale':
                return ( isset( $p->sale_price ) ? $p->sale_price : '' );
            case 'manage_stock':
                return ( isset( $p->manage_stock ) && $p->manage_stock === 'yes' ? __( 'yes', 'quickstock-for-woocommerce' ) : __( 'no', 'quickstock-for-woocommerce' ) );
            case 'stock_status':
                return ( isset( $p->stock_status ) ? $p->stock_status : '' );
            case 'backorders':
                return ( isset( $p->backorders ) ? $p->backorders : '' );
            case 'post_status':
                return ( isset( $p->post_status_val ) ? $p->post_status_val : '' );
            case 'global_unique_id':
                return ( isset( $p->global_unique_id ) ? $p->global_unique_id : '' );
            case 'permalink':
                return ( isset( $p->post_name ) ? $p->post_name : '' );
            case 'excerpt':
                return ( isset( $p->post_excerpt ) ? wp_strip_all_tags( $p->post_excerpt ) : '' );
            case 'content':
                return ( isset( $p->post_content ) ? wp_strip_all_tags( $p->post_content ) : '' );
            case 'product_cat':
                return QSFW_Data::get_product_categories( $p->ID );
            case 'shipping_class':
                return self::term_name( ( isset( $p->shipping_class_id ) ? $p->shipping_class_id : 0 ), 'product_shipping_class' );
            case 'sale_from':
            case 'sale_to':
                $ts = ( isset( $p->{$key} ) ? (int) $p->{$key} : 0 );
                if ( !$ts ) {
                    return '';
                }
                if ( $date_fmt === 'locale' ) {
                    return date_i18n( get_option( 'date_format' ), $ts );
                }
                return gmdate( $date_fmt, $ts );
        }
        // Custom Field (Slug ist der dynamische Spaltenschlüssel)
        if ( isset( $p->{$key} ) ) {
            return ( is_scalar( $p->{$key} ) ? (string) $p->{$key} : '' );
        }
        return '';
    }

    /**
     * CSV-Export des Bestands-Verlaufs (berücksichtigt die aktiven Filter).
     * Per nonce-gesichertem GET-Link aufgerufen → Browser-Download.
     */
    public static function log_export() {
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_die( esc_html__( 'Permission denied.', 'quickstock-for-woocommerce' ) );
        }
        check_admin_referer( 'qsfw_log_export' );
        $res = QSFW_Log::query( array(
            'q'      => ( isset( $_GET['log_q'] ) ? sanitize_text_field( wp_unslash( $_GET['log_q'] ) ) : '' ),
            'from'   => ( isset( $_GET['log_from'] ) ? sanitize_text_field( wp_unslash( $_GET['log_from'] ) ) : '' ),
            'to'     => ( isset( $_GET['log_to'] ) ? sanitize_text_field( wp_unslash( $_GET['log_to'] ) ) : '' ),
            'source' => ( isset( $_GET['log_source'] ) ? sanitize_key( wp_unslash( $_GET['log_source'] ) ) : '' ),
            'limit'  => false,
        ) );
        $csv_delim_key = QSFW_Settings_Page::get_general_setting( 'csv_delimiter', ';' );
        $csv_delimiter = ( $csv_delim_key === 'tab' ? "\t" : $csv_delim_key );
        $csv_bom = (int) QSFW_Settings_Page::get_general_setting( 'csv_bom', 1 );
        nocache_headers();
        header( 'Content-Type: text/csv; charset=utf-8' );
        header( 'Content-Disposition: attachment; filename="stock-log-' . current_time( 'Y-m-d' ) . '.csv"' );
        $out = fopen( 'php://output', 'w' );
        if ( $csv_bom ) {
            echo "﻿";
        }
        fputcsv(
            $out,
            array(
                __( 'Date', 'quickstock-for-woocommerce' ),
                __( 'Product ID', 'quickstock-for-woocommerce' ),
                __( 'Variation ID', 'quickstock-for-woocommerce' ),
                __( 'Product', 'quickstock-for-woocommerce' ),
                __( 'Before', 'quickstock-for-woocommerce' ),
                __( 'After', 'quickstock-for-woocommerce' ),
                __( 'Change', 'quickstock-for-woocommerce' ),
                __( 'Source', 'quickstock-for-woocommerce' ),
                __( 'Order', 'quickstock-for-woocommerce' ),
                __( 'User', 'quickstock-for-woocommerce' )
            ),
            $csv_delimiter,
            '"',
            '\\'
        );
        $source_labels = array(
            'manual' => __( 'Manual', 'quickstock-for-woocommerce' ),
            'order'  => __( 'Order', 'quickstock-for-woocommerce' ),
            'po'     => __( 'Goods receipt', 'quickstock-for-woocommerce' ),
        );
        foreach ( $res['rows'] as $r ) {
            fputcsv(
                $out,
                array(
                    $r->created_at,
                    $r->product_id,
                    ( $r->variation_id ? $r->variation_id : '' ),
                    $r->product_name,
                    ( $r->qty_from === null ? '' : $r->qty_from ),
                    ( $r->qty_to === null ? '' : $r->qty_to ),
                    ( $r->delta === null ? '' : (( $r->delta > 0 ? '+' . $r->delta : $r->delta )) ),
                    ( isset( $source_labels[$r->source] ) ? $source_labels[$r->source] : $r->source ),
                    ( $r->order_id ? $r->order_id : '' ),
                    self::log_user_name( $r->user_id )
                ),
                $csv_delimiter,
                '"',
                '\\'
            );
        }
        exit;
    }

    /** Anzeigename für eine User-ID im Verlauf (0 = System). */
    public static function log_user_name( $user_id ) {
        $user_id = (int) $user_id;
        if ( $user_id <= 0 ) {
            return __( 'System', 'quickstock-for-woocommerce' );
        }
        $u = get_userdata( $user_id );
        return ( $u ? $u->display_name : '#' . $user_id );
    }

    public static function reset_checks() {
        check_ajax_referer( 'qsfw_stock_nonce', 'nonce' );
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error();
        }
        delete_option( 'qsfw_stock_checks' );
        wp_send_json_success();
    }

    public static function search_products() {
        check_ajax_referer( 'qsfw_stock_nonce', 'nonce' );
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            wp_send_json_error();
        }
        $query = sanitize_text_field( wp_unslash( $_POST['query'] ?? '' ) );
        $date = sanitize_text_field( wp_unslash( $_POST['date'] ?? wp_date( 'Y-m-d', strtotime( '-14 days' ) ) ) );
        $sort = sanitize_text_field( wp_unslash( $_POST['sort'] ?? 'open_first' ) );
        $mode = sanitize_text_field( wp_unslash( $_POST['mode'] ?? 'reorder' ) );
        if ( !in_array( $mode, array('reorder', 'all'), true ) ) {
            $mode = 'reorder';
        }
        $sql_sort = ( $sort === 'open_first' ? 'stock_asc' : $sort );
        $cat = ( isset( $_POST['category'] ) ? intval( $_POST['category'] ) : 0 );
        $cols = QSFW_Columns::get_visible();
        $results = QSFW_Data::get_stock_data(
            $date,
            $sql_sort,
            $mode,
            QSFW_Columns::get_status_filter(),
            $cat,
            $cols,
            QSFW_Filters::get_active( $mode )
        );
        $checks = get_option( 'qsfw_stock_checks', array() );
        if ( $sort === 'open_first' ) {
            $offen = array();
            $erledigt = array();
            foreach ( $results as $r ) {
                if ( isset( $checks[$r->ID] ) ) {
                    $erledigt[] = $r;
                } else {
                    $offen[] = $r;
                }
            }
            $results = array_merge( $offen, $erledigt );
        }
        $filtered = array();
        if ( $query !== '' ) {
            $terms = preg_split(
                '/\\s+/',
                $query,
                -1,
                PREG_SPLIT_NO_EMPTY
            );
            foreach ( $results as $r ) {
                if ( $r->sku && mb_strtolower( trim( $query ) ) === mb_strtolower( trim( $r->sku ) ) ) {
                    $filtered[] = $r;
                    continue;
                }
                $title_lower = mb_strtolower( $r->post_title );
                $all_match = true;
                foreach ( $terms as $term ) {
                    if ( mb_stripos( $title_lower, $term ) === false ) {
                        $all_match = false;
                        break;
                    }
                }
                if ( $all_match ) {
                    $filtered[] = $r;
                }
            }
        } else {
            $filtered = $results;
        }
        $html = '';
        $active_cols = QSFW_Columns::get_active( $mode );
        if ( empty( $filtered ) ) {
            $colspan = count( $active_cols );
            /* translators: %s is the user's search query */
            $html = '<tr><td colspan="' . $colspan . '" style="text-align:center;padding:30px;color:#999;">' . sprintf( esc_html__( 'No results for “%s”', 'quickstock-for-woocommerce' ), esc_html( $query ) ) . '</td></tr>';
        } else {
            foreach ( $filtered as $p ) {
                $html .= QSFW_Renderer::render_row( $p, $checks, $active_cols );
            }
        }
        wp_send_json_success( array(
            'html'  => $html,
            'count' => count( $filtered ),
            'total' => count( $results ),
        ) );
    }

    /** Preis runden: 'exact' = 2 Dezimal, sonst auf 0.05 bzw. 0.10. */
    private static function round_price( $value, $mode ) {
        if ( $mode === '0.05' ) {
            return round( round( $value / 0.05 ) * 0.05, 2 );
        }
        if ( $mode === '0.1' || $mode === '0.10' ) {
            return round( round( $value / 0.1 ) * 0.1, 2 );
        }
        return round( $value, 2 );
    }

    /** Term-Name aus term_id für den CSV-Export (leer wenn nicht gesetzt). */
    private static function term_name( $term_id, $taxonomy ) {
        $term_id = (int) $term_id;
        if ( $term_id <= 0 ) {
            return '';
        }
        $term = get_term( $term_id, $taxonomy );
        // CSV ist kein HTML: Entities dekodieren, damit z.B. "Vaporizer &amp; Zubehör"
        // als "Vaporizer & Zubehör" in der Zelle landet (WP speichert "&" als "&amp;").
        return ( $term && !is_wp_error( $term ) ? html_entity_decode( $term->name, ENT_QUOTES, 'UTF-8' ) : '' );
    }

}

QSFW_Ajax::init();