<?php

/**
 * @package QuickStockForWooCommerce
 * @license GPL-2.0-or-later (https://www.gnu.org/licenses/gpl-2.0.html)
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
/**
 * Erweiterte Filter: Definition (welche Spalten sind filterbar),
 * Sanitisierung und Persistenz – exakt nach dem Muster von
 * QSFW_Columns::get_status_filter() ($_REQUEST → user_meta → leer).
 *
 * Der Status-Filter selbst läuft weiterhin über
 * QSFW_Columns::get_status_filter() und ist NICHT Teil von 'adv'.
 */
class QSFW_Filters {
    const META = 'qsfw_adv_filters';

    /** Per-Spalten-Wertfilter (Excel-Style), getrennt von den adv-Filtern. */
    const COLF_META = 'qsfw_col_filters';

    /**
     * Sentinel-Wert innerhalb von ['in'], der „leere Felder" matcht
     * (Excel-Style „(Leere)"). Wird im Dropdown nur angeboten, wenn die
     * Spalte tatsächlich leere Werte enthält, und im Filter-SQL als
     * NOT EXISTS / IS NULL aufgelöst.
     */
    const EMPTY_TOKEN = '__qsfw_empty__';

    /**
     * Transient-Präfix: signalisiert QSFW_Presets::process_apply(), dass der
     * nächste (durch PRG erzeugte, saubere) GET-Aufruf der Rückprall einer
     * In-Page-Aktion ist – und NICHT als frischer Seitenaufruf das
     * Standard-Preset neu anwenden (und damit Spalten/Ansicht überschreiben) soll.
     */
    const SKIP_APPLY_PREFIX = 'qsfw_skip_apply_';

    /** Normalisiert den Modus auf 'all' | 'reorder' (Default: aktueller Tab). */
    private static function norm_mode( $mode ) {
        if ( $mode === null ) {
            $mode = QSFW_Columns::get_mode();
        }
        return ( $mode === 'reorder' ? 'reorder' : 'all' );
    }

    /** Per-Modus-Meta-Key für erweiterte Filter. */
    private static function adv_key( $mode ) {
        return self::META . '_' . self::norm_mode( $mode );
    }

    /** Per-Modus-Meta-Key für Spalten-Wertfilter. */
    private static function colf_key( $mode ) {
        return self::COLF_META . '_' . self::norm_mode( $mode );
    }

    /**
     * Einmalige, verlustfreie Migration der früher GLOBAL gespeicherten Filter
     * auf die neuen Per-Modus-Keys. Der Altwert wird in BEIDE Tabs übernommen
     * (danach divergieren sie pro Tab) und der Legacy-Key entfernt.
     */
    private static function migrate_legacy() {
        static $done = false;
        if ( $done ) {
            return;
        }
        $done = true;
        $uid = get_current_user_id();
        if ( !$uid ) {
            return;
        }
        foreach ( array(self::META, self::COLF_META) as $base ) {
            $legacy = get_user_meta( $uid, $base, true );
            if ( is_array( $legacy ) && !empty( $legacy ) ) {
                foreach ( array('all', 'reorder') as $m ) {
                    $key = $base . '_' . $m;
                    if ( !is_array( get_user_meta( $uid, $key, true ) ) ) {
                        update_user_meta( $uid, $key, $legacy );
                    }
                }
            }
            delete_user_meta( $uid, $base );
        }
    }

    /** Enum-Optionen je Spalte. */
    private static function enum_options( string $col ) : array {
        switch ( $col ) {
            case 'stock_status':
                return array(
                    'instock'     => __( 'In stock', 'quickstock-for-woocommerce' ),
                    'outofstock'  => __( 'Out of stock', 'quickstock-for-woocommerce' ),
                    'onbackorder' => __( 'On backorder', 'quickstock-for-woocommerce' ),
                );
            case 'backorders':
                return array(
                    'no'     => __( 'Do not allow', 'quickstock-for-woocommerce' ),
                    'notify' => __( 'Allow, but notify customer', 'quickstock-for-woocommerce' ),
                    'yes'    => __( 'Allow', 'quickstock-for-woocommerce' ),
                );
            case 'manage_stock':
                return array(
                    'yes' => __( 'Yes', 'quickstock-for-woocommerce' ),
                    'no'  => __( 'No', 'quickstock-for-woocommerce' ),
                );
        }
        return array();
    }

    /**
     * Liefert die im aktuellen Shop filterbaren Felder, gegated über
     * QSFW_Columns::get_enabled() und (bei Taxonomien) taxonomy_exists().
     * Key = Spalten-Key, Wert = ['col','label','group','kind', ...].
     */
    public static function available() : array {
        $enabled = QSFW_Columns::get_enabled();
        $all = QSFW_Columns::get_all();
        $out = array();
        // Eingebaute Spalten → Filter-Art
        $builtin = array(
            'price_reg'        => 'num_range',
            'price_sale'       => 'num_range',
            'stock'            => 'num_range',
            'limit'            => 'num_range',
            'sale_from'        => 'date_range',
            'sale_to'          => 'date_range',
            'stock_status'     => 'enum',
            'backorders'       => 'enum',
            'manage_stock'     => 'enum',
            'global_unique_id' => 'text',
        );
        foreach ( $builtin as $col => $kind ) {
            if ( !in_array( $col, $enabled, true ) || !isset( $all[$col] ) ) {
                continue;
            }
            $entry = array(
                'col'   => $col,
                'label' => $all[$col]['label'],
                'group' => $all[$col]['group'],
                'kind'  => $kind,
            );
            if ( $kind === 'enum' ) {
                $entry['options'] = self::enum_options( $col );
            }
            $out[$col] = $entry;
        }
        // Produktkategorien (Mehrfachauswahl mit Include/Exclude). Immer
        // verfügbar – unabhängig davon, ob die "Kategorien"-Spalte aktiviert ist.
        if ( taxonomy_exists( 'product_cat' ) ) {
            $out['product_cat'] = array(
                'col'   => 'product_cat',
                'label' => __( 'Categories', 'quickstock-for-woocommerce' ),
                'group' => 'taxonomy',
                'kind'  => 'terms_inc_exc',
                'tax'   => 'product_cat',
                'terms' => self::term_list( 'product_cat' ),
            );
        }
        // Taxonomien (nur enabled + existierend)
        $tax_map = QSFW_Columns::taxonomy_bulk_map();
        foreach ( array('shipping_class') as $col ) {
            if ( !in_array( $col, $enabled, true ) || !isset( $all[$col] ) || !isset( $tax_map[$col] ) ) {
                continue;
            }
            $tax = $tax_map[$col]['tax'];
            if ( !taxonomy_exists( $tax ) ) {
                continue;
            }
            $out[$col] = array(
                'col'   => $col,
                'label' => $all[$col]['label'],
                'group' => $all[$col]['group'],
                'kind'  => 'terms',
                'tax'   => $tax,
                'terms' => self::term_list( $tax ),
            );
        }
        return $out;
    }

    /**
     * Spalten, die das Excel-Style Wertfilter-Icon im Spaltenkopf bekommen.
     * Liefert col => Quellbeschreibung, mit der QSFW_Data sowohl die
     * Distinct-Werte (AJAX) als auch die Filter-WHERE-Klausel baut.
     *
     * type:
     *   'post' (+expr)        – Spalte aus der posts-Tabelle (p.post_title …)
     *   'meta' (+meta_key)    – eingebautes Postmeta (_sku, _regular_price …)
     *   'cf_meta' (+meta_key) – Custom Field (Postmeta-Quelle)
     *   'tax' (+tax)          – Taxonomie (Werteliste = Terms, Filter per term_id)
     */
    public static function value_filterable() : array {
        $all = QSFW_Columns::get_all();
        $enabled = QSFW_Columns::get_enabled();
        $out = array();
        $label = function ( $col ) use($all) {
            return ( isset( $all[$col]['label'] ) ? $all[$col]['label'] : $col );
        };
        // Immer verfügbare Kern-Textspalten (fix, daher nicht in get_enabled()).
        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Feste Filterdefinition, keine Datenbankabfrage.
        $out['sku'] = array(
            'type'     => 'meta',
            'meta_key' => '_sku',
            'label'    => $label( 'sku' ),
        );
        $out['title'] = array(
            'type'  => 'post',
            'expr'  => 'p.post_title',
            'label' => $label( 'title' ),
        );
        // Eingebaute Spalten (nur wenn aktiviert). Datumsspalten werden bewusst
        // ausgelassen – ihre Rohwerte sind Unix-Timestamps und für eine
        // Werteliste unbrauchbar (dafür gibt es den Datums-Bereichsfilter).
        $meta_cols = array(
            'price_reg'        => '_regular_price',
            'price_sale'       => '_sale_price',
            'stock'            => '_stock',
            'limit'            => '_low_stock_amount',
            'stock_status'     => '_stock_status',
            'backorders'       => '_backorders',
            'manage_stock'     => '_manage_stock',
            'global_unique_id' => '_global_unique_id',
        );
        foreach ( $meta_cols as $col => $mk ) {
            if ( in_array( $col, $enabled, true ) && isset( $all[$col] ) ) {
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Aufbau der internen Filter-Whitelist, keine Datenbankabfrage.
                $out[$col] = array(
                    'type'     => 'meta',
                    'meta_key' => $mk,
                    'label'    => $label( $col ),
                );
            }
        }
        $post_cols = array(
            'permalink'   => 'p.post_name',
            'post_status' => 'p.post_status',
            'excerpt'     => 'p.post_excerpt',
            'content'     => 'p.post_content',
        );
        foreach ( $post_cols as $col => $expr ) {
            if ( in_array( $col, $enabled, true ) && isset( $all[$col] ) ) {
                $out[$col] = array(
                    'type'  => 'post',
                    'expr'  => $expr,
                    'label' => $label( $col ),
                );
            }
        }
        // Taxonomien (Werteliste = Terms).
        if ( taxonomy_exists( 'product_cat' ) ) {
            $out['product_cat'] = array(
                'type'  => 'tax',
                'tax'   => 'product_cat',
                'label' => __( 'Categories', 'quickstock-for-woocommerce' ),
            );
        }
        $tax_map = QSFW_Columns::taxonomy_bulk_map();
        foreach ( array('shipping_class') as $col ) {
            if ( !in_array( $col, $enabled, true ) || !isset( $tax_map[$col] ) ) {
                continue;
            }
            $tax = $tax_map[$col]['tax'];
            if ( !taxonomy_exists( $tax ) ) {
                continue;
            }
            $out[$col] = array(
                'type'  => 'tax',
                'tax'   => $tax,
                'label' => $label( $col ),
            );
        }
        return $out;
    }

    /**
     * Spalten-Wertfilter werden per POST gesendet (nicht als GET-URL), damit
     * auch sehr viele/lange Werte (z. B. Händlerlinks) nicht am URL-Längenlimit
     * scheitern (nginx „414 Request-URI Too Large"). Dieser Handler läuft via
     * admin_init VOR jeglichem Output: er lässt get_col_filters() den Zustand
     * sanitisiert in user_meta speichern und leitet anschließend auf eine
     * saubere URL ohne colf-Parameter um (Post/Redirect/Get) – verhindert auch
     * das „Formular erneut senden?"-Problem beim Neuladen.
     */
    /**
     * Markiert den unmittelbar folgenden GET als PRG-Rückprall einer In-Page-Aktion.
     *
     * Ein Redirect entfernt genau die Parameter, an denen
     * QSFW_Presets::is_fresh_entry() einen frischen Seitenaufruf erkennt — der
     * Rückprall SIEHT deshalb aus wie ein frischer Aufruf. Ohne diesen Marker
     * würde QSFW_Presets::process_apply() das Standard-Preset neu anwenden und
     * genau den Zustand überschreiben, der eben gespeichert wurde.
     *
     * Regel: Jeder Handler, der Ansichts-State schreibt und danach umleitet,
     * muss das hier aufrufen. Ausnahme sind Aktionen, die schon vorher nicht in
     * is_fresh_entry() standen (z. B. set_default_preset) – dort würde der
     * Marker das bestehende Verhalten ändern.
     */
    public static function mark_prg_bounce() : void {
        set_transient( self::SKIP_APPLY_PREFIX . get_current_user_id(), 1, 30 );
    }

    public static function maybe_handle_colf_post() {
        if ( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? '' ) ) !== 'POST' ) {
            return;
        }
        if ( !isset( $_REQUEST['colf_submitted'] ) ) {
            return;
        }
        if ( !isset( $_GET['page'] ) || $_GET['page'] !== 'quickstock-for-woocommerce' ) {
            return;
        }
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            return;
        }
        check_admin_referer( 'qsfw_view_state' );
        $colf = ( isset( $_POST['colf'] ) && is_array( $_POST['colf'] ) ? map_deep( wp_unslash( $_POST['colf'] ), 'sanitize_textarea_field' ) : array() );
        self::save_col_filters( $colf );
        self::mark_prg_bounce();
        // Auf saubere URL ohne colf umleiten – der Zustand liegt jetzt in
        // user_meta, die reinen Anzeigeparameter bleiben als GET erhalten.
        // stock_sort/per_page stehen NICHT mehr dabei: sie liegen in user_meta
        // und würden als GET-Parameter den nonce-losen Schreibzweig der Getter
        // auslösen.
        $keep = array(
            'page',
            'mode',
            'cat_filter',
            'stock_start_date',
            'hb_q'
        );
        $args = array();
        foreach ( $keep as $k ) {
            if ( isset( $_GET[$k] ) && $_GET[$k] !== '' ) {
                $args[$k] = sanitize_text_field( wp_unslash( $_GET[$k] ) );
            }
        }
        wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) );
        exit;
    }

    /**
     * Setzt ALLE Filter dieses Benutzers zurück: erweiterte Filter (adv),
     * Spalten-Wertfilter (Excel-Style) UND den Status-Filter. Das Löschen der
     * user_meta lässt get_active()/get_col_filters()/get_status_filter() wieder
     * auf ihre Defaults (leer bzw. „alle Status") zurückfallen.
     */
    public static function reset_all( $mode = null ) : void {
        self::migrate_legacy();
        $uid = get_current_user_id();
        delete_user_meta( $uid, self::adv_key( $mode ) );
        delete_user_meta( $uid, self::colf_key( $mode ) );
        delete_user_meta( $uid, 'qsfw_status_filter' );
    }

    /** Nonce-gesicherte URL, die maybe_handle_reset() auslöst. */
    public static function reset_url( string $mode = 'all' ) : string {
        return wp_nonce_url( add_query_arg( array(
            'page'               => 'quickstock-for-woocommerce',
            'mode'               => $mode,
            'qsfw_reset_filters' => '1',
        ), admin_url( 'admin.php' ) ), 'qsfw_reset_filters' );
    }

    /**
     * „Filter zurücksetzen" – löscht erweiterte Filter, Spalten-Wertfilter und
     * Status-Filter und leitet auf eine saubere mode-URL um. Läuft via admin_init
     * VOR jeglichem Output. Funktioniert auch dann, wenn die Liste gerade „keine
     * Produkte gefunden" zeigt (dann fehlen die Spaltenköpfe mitsamt Trichter,
     * sodass der Wertfilter sonst nicht mehr erreichbar wäre).
     */
    public static function maybe_handle_reset() {
        if ( !isset( $_GET['qsfw_reset_filters'] ) ) {
            return;
        }
        if ( !isset( $_GET['page'] ) || $_GET['page'] !== 'quickstock-for-woocommerce' ) {
            return;
        }
        if ( !current_user_can( 'manage_woocommerce' ) ) {
            return;
        }
        check_admin_referer( 'qsfw_reset_filters' );
        $mode = ( isset( $_GET['mode'] ) ? sanitize_text_field( wp_unslash( $_GET['mode'] ) ) : 'all' );
        self::reset_all( $mode );
        self::mark_prg_bounce();
        wp_safe_redirect( add_query_arg( array(
            'page' => 'quickstock-for-woocommerce',
            'mode' => $mode,
        ), admin_url( 'admin.php' ) ) );
        exit;
    }

    /**
     * Speichert die Spalten-Wertfilter eines Formular-Submits.
     * Aufgerufen von maybe_handle_colf_post() (nonce-geprüft).
     */
    public static function save_col_filters( array $colf, $mode = null ) : array {
        self::migrate_legacy();
        $avail = self::value_filterable();
        $clean = self::sanitize_col_filters( $colf, $avail );
        update_user_meta( get_current_user_id(), self::colf_key( $mode ), $clean );
        return $clean;
    }

    /**
     * Aktive Spalten-Wertfilter (reiner Leser).
     *
     * Hier stand ein Schreibzweig auf $_REQUEST['colf_submitted']. Er war auch
     * per GET auslösbar und lief damit an der POST-/Nonce-Prüfung von
     * maybe_handle_colf_post() vorbei. Geschrieben wird jetzt nur noch dort.
     */
    public static function get_col_filters( $mode = null ) : array {
        self::migrate_legacy();
        $avail = self::value_filterable();
        $user_id = get_current_user_id();
        $key = self::colf_key( $mode );
        $saved = get_user_meta( $user_id, $key, true );
        if ( !is_array( $saved ) ) {
            return array();
        }
        return array_intersect_key( $saved, $avail );
    }

    /**
     * Säubert die Spalten-Wertfilter. Pro Spalte genau EINE Form:
     *   ['like' => 'teilstring']  (Vorrang) oder ['in' => ['w1','w2',…]].
     */
    private static function sanitize_col_filters( array $raw, array $avail ) : array {
        $out = array();
        foreach ( $avail as $col => $def ) {
            if ( !isset( $raw[$col] ) || !is_array( $raw[$col] ) ) {
                continue;
            }
            $v = $raw[$col];
            // „Ist nicht leer" – semantischer Filter (Vorrang). Bildet die
            // Auswahl „alle Werte ausser (Leere)" ab, OHNE jeden Einzelwert
            // aufzuzählen. Skaliert damit unabhängig von der Werteliste-Deckelung.
            if ( !empty( $v['nonempty'] ) ) {
                $out[$col] = array(
                    'nonempty' => 1,
                );
                continue;
            }
            if ( isset( $v['like'] ) ) {
                $s = sanitize_text_field( (string) $v['like'] );
                if ( $s !== '' ) {
                    $out[$col] = array(
                        'like' => $s,
                    );
                    continue;
                }
            }
            if ( isset( $v['in'] ) && is_array( $v['in'] ) ) {
                $vals = array();
                foreach ( $v['in'] as $item ) {
                    $s = sanitize_text_field( (string) $item );
                    if ( $s !== '' ) {
                        $vals[] = $s;
                    }
                    if ( count( $vals ) >= 1000 ) {
                        break;
                    }
                }
                $vals = array_values( array_unique( $vals ) );
                if ( !empty( $vals ) ) {
                    $out[$col] = array(
                        'in' => $vals,
                    );
                }
            }
        }
        return $out;
    }

    private static function term_list( string $taxonomy ) : array {
        $out = array();
        $terms = get_terms( array(
            'taxonomy'   => $taxonomy,
            'hide_empty' => false,
        ) );
        if ( !is_wp_error( $terms ) && is_array( $terms ) ) {
            foreach ( $terms as $t ) {
                // Roh-Name aus get_terms() enthält HTML-Entities (z. B. "&amp;").
                // Einmal dekodieren; die Render-Stellen (esc_html im Adv-Filter)
                // escapen anschließend genau einmal → kein doppeltes "&amp;".
                $out[] = array(
                    'id'   => (int) $t->term_id,
                    'name' => html_entity_decode( $t->name, ENT_QUOTES, 'UTF-8' ),
                );
            }
        }
        return $out;
    }

    /**
     * Speichert die erweiterten Filter eines Formular-Submits.
     * Aufgerufen von QSFW_Columns::maybe_handle_view_post() (nonce-geprüft).
     */
    public static function save_adv( array $adv, $mode = null ) : array {
        self::migrate_legacy();
        $avail = self::available();
        $clean = self::sanitize( $adv, $avail );
        update_user_meta( get_current_user_id(), self::adv_key( $mode ), $clean );
        return $clean;
    }

    /**
     * Aktive, sanitisierte Filter aus user_meta (reiner Leser).
     * Leere Teilfilter werden verworfen. Geschrieben wird in save_adv().
     */
    public static function get_active( $mode = null ) : array {
        self::migrate_legacy();
        $avail = self::available();
        $user_id = get_current_user_id();
        $key = self::adv_key( $mode );
        // Reiner Leser: Geschrieben wird in save_adv(), aufgerufen aus
        // QSFW_Columns::maybe_handle_view_post() (nonce-geprüft).
        $saved = get_user_meta( $user_id, $key, true );
        if ( !is_array( $saved ) ) {
            return array();
        }
        // Gegen aktuell verfügbare Felder filtern (Spalte könnte deaktiviert worden sein)
        return array_intersect_key( $saved, $avail );
    }

    private static function sanitize( array $raw, array $avail ) : array {
        $out = array();
        foreach ( $avail as $col => $def ) {
            if ( !isset( $raw[$col] ) ) {
                continue;
            }
            $v = $raw[$col];
            switch ( $def['kind'] ) {
                case 'num_range':
                case 'date_range':
                    if ( !is_array( $v ) ) {
                        break;
                    }
                    $min = ( isset( $v['min'] ) ? trim( (string) $v['min'] ) : '' );
                    $max = ( isset( $v['max'] ) ? trim( (string) $v['max'] ) : '' );
                    if ( $def['kind'] === 'num_range' ) {
                        $min = ( $min === '' ? '' : (string) floatval( $min ) );
                        $max = ( $max === '' ? '' : (string) floatval( $max ) );
                    } else {
                        $min = ( preg_match( '/^\\d{4}-\\d{2}-\\d{2}$/', $min ) ? $min : '' );
                        $max = ( preg_match( '/^\\d{4}-\\d{2}-\\d{2}$/', $max ) ? $max : '' );
                    }
                    if ( $min === '' && $max === '' ) {
                        break;
                    }
                    $r = array();
                    if ( $min !== '' ) {
                        $r['min'] = $min;
                    }
                    if ( $max !== '' ) {
                        $r['max'] = $max;
                    }
                    $out[$col] = $r;
                    break;
                case 'enum':
                    $vals = array_values( array_intersect( array_map( 'strval', (array) $v ), array_keys( $def['options'] ) ) );
                    if ( !empty( $vals ) ) {
                        $out[$col] = $vals;
                    }
                    break;
                case 'terms':
                    $valid = array();
                    foreach ( $def['terms'] as $t ) {
                        $valid[$t['id']] = true;
                    }
                    $ids = array();
                    foreach ( (array) $v as $id ) {
                        $id = intval( $id );
                        if ( $id > 0 && isset( $valid[$id] ) ) {
                            $ids[] = $id;
                        }
                    }
                    if ( !empty( $ids ) ) {
                        $out[$col] = array_values( array_unique( $ids ) );
                    }
                    break;
                case 'terms_inc_exc':
                    if ( !is_array( $v ) ) {
                        break;
                    }
                    $mode = ( isset( $v['mode'] ) && $v['mode'] === 'ex' ? 'ex' : 'in' );
                    $valid = array();
                    foreach ( $def['terms'] as $t ) {
                        $valid[$t['id']] = true;
                    }
                    $ids = array();
                    $raw_ids = ( isset( $v['ids'] ) ? (array) $v['ids'] : array() );
                    foreach ( $raw_ids as $id ) {
                        $id = intval( $id );
                        if ( $id > 0 && isset( $valid[$id] ) ) {
                            $ids[] = $id;
                        }
                    }
                    if ( empty( $ids ) ) {
                        break;
                    }
                    $out[$col] = array(
                        'mode' => $mode,
                        'ids'  => array_values( array_unique( $ids ) ),
                    );
                    break;
                case 'text':
                    $s = sanitize_text_field( ( is_array( $v ) ? '' : (string) $v ) );
                    if ( $s !== '' ) {
                        $out[$col] = $s;
                    }
                    break;
            }
        }
        return $out;
    }

    /** Anzahl gesetzter Filtergruppen (für Badge). Inkl. Status, wenn eingeschränkt. */
    public static function active_count() : int {
        $n = count( self::get_active() ) + count( self::get_col_filters() );
        $status = QSFW_Columns::get_status_filter();
        if ( count( $status ) < 4 ) {
            $n++;
        }
        return $n;
    }

}
