<?php
/**
 * @package QuickStockForWooCommerce
 * @license GPL-2.0-or-later (https://www.gnu.org/licenses/gpl-2.0.html)
 */

if (!defined('ABSPATH')) exit;

/**
 * Bestands-Verlauf / Log.
 *
 * Schreibt jede Lagerbestands-Änderung in eine eigene Tabelle, damit
 * nachvollziehbar bleibt, wann welcher Bestand wie verändert wurde.
 * Quellen: 'manual' (Inline-/Massenbearbeitung im Plugin) und
 * 'order' (Bestellungen — Abbau bzw. Rückbuchung durch WooCommerce).
 */
class QSFW_Log {

    const TABLE        = 'qsfw_stock_log';
    const CRON_HOOK    = 'qsfw_log_cleanup';
    const DB_VERSION   = '1';
    const DB_OPT       = 'qsfw_log_db_version';

    /** Vollständiger Tabellenname inkl. WP-Präfix. */
    public static function table_name() {
        global $wpdb;
        return $wpdb->prefix . self::TABLE;
    }

    /* ===================================================================
     * SCHEMA / CRON
     * =================================================================== */

    /** Legt die Log-Tabelle an bzw. aktualisiert ihr Schema (dbDelta). */
    public static function create_table() {
        global $wpdb;
        $table   = self::table_name();
        $charset = $wpdb->get_charset_collate();

        $sql = "CREATE TABLE $table (
  id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  product_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
  variation_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
  product_name VARCHAR(255) NOT NULL DEFAULT '',
  qty_from VARCHAR(20) DEFAULT NULL,
  qty_to VARCHAR(20) DEFAULT NULL,
  delta INT(11) DEFAULT NULL,
  source VARCHAR(20) NOT NULL DEFAULT 'manual',
  order_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
  user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
  created_at DATETIME NOT NULL,
  PRIMARY KEY  (id),
  KEY product_id (product_id),
  KEY created_at (created_at),
  KEY source (source)
) $charset;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
        update_option(self::DB_OPT, self::DB_VERSION);
    }

    /**
     * Stellt sicher, dass Tabelle und Aufräum-Cron existieren. Wird bei
     * jedem Admin-Aufruf günstig geprüft (nur Options-Lookup), damit das
     * Feature auch bei bereits aktivem Plugin nachträglich greift.
     */
    public static function maybe_install() {
        if (get_option(self::DB_OPT) !== self::DB_VERSION) {
            self::create_table();
        }
        if (!wp_next_scheduled(self::CRON_HOOK)) {
            wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK);
        }
    }

    /** Cron-Callback: entfernt Einträge älter als die Aufbewahrungsdauer. */
    public static function cleanup() {
        $days = (int) QSFW_Settings_Page::get_general_setting('log_retention_days', 90);
        if ($days <= 0) return; // 0 = unbegrenzt aufbewahren

        global $wpdb;
        $cutoff = current_datetime()->modify('-' . $days . ' days')->format('Y-m-d H:i:s');
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Cleanup muss direkt wirken; Grenzwert wird ueber %s gebunden
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$wpdb->prefix}qsfw_stock_log WHERE created_at < %s",
            $cutoff
        ));
    }

    /* ===================================================================
     * SCHREIBEN
     * =================================================================== */

    /** Ist das Logging in den Einstellungen aktiviert? */
    public static function is_enabled() {
        return (int) QSFW_Settings_Page::get_general_setting('log_enabled', 1) === 1;
    }

    /**
     * Schreibt einen Log-Eintrag. Tut nichts, wenn das Logging aus ist.
     *
     * @param array $args product_id, variation_id, product_name,
     *                    qty_from, qty_to, source, order_id, user_id
     */
    public static function add($args) {
        if (!self::is_enabled()) return;

        global $wpdb;
        $d = array(
            'product_id'   => 0,
            'variation_id' => 0,
            'product_name' => '',
            'qty_from'     => null,
            'qty_to'       => null,
            'source'       => 'manual',
            'order_id'     => 0,
            'user_id'      => null,
        );
        $a = array_merge($d, $args);

        if ($a['user_id'] === null) {
            $a['user_id'] = get_current_user_id();
        }

        $from = (is_numeric($a['qty_from'])) ? (string) $a['qty_from'] : null;
        $to   = (is_numeric($a['qty_to']))   ? (string) $a['qty_to']   : null;
        $delta = ($from !== null && $to !== null) ? ((int) $to - (int) $from) : null;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- bewusster Append in die eigene Protokolltabelle; alle Werte werden ueber Formatangaben gebunden
        $wpdb->insert(
            self::table_name(),
            array(
                'product_id'   => (int) $a['product_id'],
                'variation_id' => (int) $a['variation_id'],
                'product_name' => mb_substr((string) $a['product_name'], 0, 255),
                'qty_from'     => $from,
                'qty_to'       => $to,
                'delta'        => $delta,
                'source'       => sanitize_key($a['source']),
                'order_id'     => (int) $a['order_id'],
                'user_id'      => (int) $a['user_id'],
                'created_at'   => current_time('mysql'),
            ),
            array('%d', '%d', '%s', '%s', '%s', '%d', '%s', '%d', '%d', '%s')
        );
    }

    /**
     * Bestandsänderung eines Produkts/einer Variation protokollieren.
     * Loggt nur, wenn sich der Wert tatsächlich geändert hat.
     *
     * @param WC_Product $product   Produkt- oder Variations-Objekt
     * @param mixed      $old_stock Bestand vorher
     * @param mixed      $new_stock Bestand nachher
     * @param string     $source    'manual' | 'order'
     * @param int        $order_id  Bestell-ID (nur bei source 'order')
     */
    public static function log_stock_change($product, $old_stock, $new_stock, $source = 'manual', $order_id = 0) {
        if (!$product) return;

        // Keine echte Änderung → nichts loggen.
        $o = is_numeric($old_stock) ? (int) $old_stock : null;
        $n = is_numeric($new_stock) ? (int) $new_stock : null;
        if ($o === $n) return;

        $is_var = $product->is_type('variation');

        self::add(array(
            'product_id'   => $is_var ? $product->get_parent_id() : $product->get_id(),
            'variation_id' => $is_var ? $product->get_id() : 0,
            'product_name' => $product->get_name(),
            'qty_from'     => $old_stock,
            'qty_to'       => $new_stock,
            'source'       => $source,
            'order_id'     => (int) $order_id,
            'user_id'      => ($source === 'order') ? 0 : null,
        ));
    }

    /* ===================================================================
     * WOOCOMMERCE-BESTELL-HOOKS
     * =================================================================== */

    /** Bestandsabbau durch eine Bestellung. */
    public static function on_order_reduce($item, $change, $order) {
        if (!self::is_enabled() || !is_array($change)) return;
        $product = is_callable(array($item, 'get_product')) ? $item->get_product() : null;
        if (!$product) return;

        self::log_stock_change(
            $product,
            isset($change['from']) ? $change['from'] : null,
            isset($change['to'])   ? $change['to']   : null,
            'order',
            is_object($order) ? $order->get_id() : 0
        );
    }

    /** Bestands-Rückbuchung (Storno/Erstattung) durch eine Bestellung. */
    public static function on_order_restore($item, $item_stock_reduced, $order, $new_stock = null) {
        if (!self::is_enabled()) return;
        $product = is_callable(array($item, 'get_product')) ? $item->get_product() : null;
        if (!$product) return;

        $to   = is_numeric($new_stock) ? (int) $new_stock : null;
        $from = ($to !== null) ? $to - (int) $item_stock_reduced : null;

        self::log_stock_change(
            $product,
            $from,
            $to,
            'order',
            is_object($order) ? $order->get_id() : 0
        );
    }

    /* ===================================================================
     * LESEN
     * =================================================================== */

    /**
     * Liefert Log-Einträge mit Filtern + Paginierung.
     *
     * @param array $args q, from, to, source, per_page, paged, limit
     * @return array ['rows' => object[], 'total' => int]
     */
    /** Whitelist sortierbarer Verlauf-Spalten: Schlüssel => SQL-Spalte + Typ. */
    public static function sortable_columns() {
        return array(
            'date'    => array('col' => 'created_at',   'type' => 'date'),
            'product' => array('col' => 'product_name', 'type' => 'text'),
            'qfrom'   => array('col' => 'qty_from',     'type' => 'num'),
            'qto'     => array('col' => 'qty_to',       'type' => 'num'),
            'delta'   => array('col' => 'delta',        'type' => 'num'),
            'source'  => array('col' => 'source',       'type' => 'text'),
            'user'    => array('col' => 'user_id',      'type' => 'num'),
        );
    }

    /**
     * Zerlegt einen Sortierwert "logcol__<key>__<asc|desc>".
     * @return array|null  ['key' => ..., 'dir' => 'asc'|'desc'] oder null.
     */
    public static function parse_sort($value) {
        if (!is_string($value) || strpos($value, 'logcol__') !== 0) return null;
        $rest = substr($value, 8);
        $pos  = strrpos($rest, '__');
        if ($pos === false) return null;
        $key = substr($rest, 0, $pos);
        $dir = substr($rest, $pos + 2);
        if ($dir !== 'asc' && $dir !== 'desc') return null;
        $cols = self::sortable_columns();
        if (!isset($cols[$key])) return null;
        return array('key' => $key, 'dir' => $dir);
    }

    public static function query($args = array()) {
        global $wpdb;
        $d = array(
            'q'        => '',
            'from'     => '',
            'to'       => '',
            'source'   => '',
            'sort'     => '',
            'per_page' => 50,
            'paged'    => 1,
            'limit'    => true,
        );
        $a = array_merge($d, $args);

        $where  = array();
        $params = array();

        if ($a['q'] !== '') {
            $where[]  = '(product_name LIKE %s OR product_id = %d OR variation_id = %d OR order_id = %d)';
            $params[] = '%' . $wpdb->esc_like($a['q']) . '%';
            $params[] = (int) $a['q'];
            $params[] = (int) $a['q'];
            $params[] = (int) $a['q'];
        }
        if ($a['from'] !== '') {
            $where[]  = 'created_at >= %s';
            $params[] = $a['from'] . ' 00:00:00';
        }
        if ($a['to'] !== '') {
            $where[]  = 'created_at <= %s';
            $params[] = $a['to'] . ' 23:59:59';
        }
        if ($a['source'] !== '') {
            $where[]  = 'source = %s';
            $params[] = $a['source'];
        }

        $where_sql = $where ? (' WHERE ' . implode(' AND ', $where)) : '';

        // Gesamtzahl (für Paginierung). Query als Variable aus festen Fragmenten
        // bauen, nur bei gesetztem Filter über prepare() binden, dann ein einziger
        // get_var()-Aufruf (so zählt die DirectQuery-Warnung nicht doppelt).
        $count_sql = "SELECT COUNT(*) FROM {$wpdb->prefix}qsfw_stock_log" . $where_sql;
        if ($params) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- WHERE aus festen Fragmenten, alle Werte über %s/%d gebunden
            $count_sql = $wpdb->prepare($count_sql, ...$params);
        }
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Query oben aus festen Fragmenten gebaut und gebunden; Zählung muss aktuell sein
        $total = (int) $wpdb->get_var($count_sql);

        // Sortierung: Spalte aus der Whitelist über %i-Identifier gebunden,
        // Richtung als auf ASC|DESC begrenztes Literal (nicht über %i/%s möglich).
        $order_col = 'created_at';
        $order_dir = 'DESC';
        $parsed    = self::parse_sort($a['sort']);
        if ($parsed) {
            $cols      = self::sortable_columns();
            $order_col = $cols[$parsed['key']]['col'];
            $order_dir = ($parsed['dir'] === 'asc') ? 'ASC' : 'DESC';
        }

        // Datenzeilen. ORDER BY-Spalte als %i-Argument nach den WHERE-Werten,
        // danach optional LIMIT/OFFSET als %d.
        $row_params   = $params;
        $row_params[] = $order_col;
        $sql          = "SELECT * FROM {$wpdb->prefix}qsfw_stock_log" . $where_sql . ' ORDER BY %i ' . $order_dir . ', id DESC';
        if ($a['limit']) {
            $per_page     = max(1, (int) $a['per_page']);
            $offset       = max(0, ((int) $a['paged'] - 1) * $per_page);
            $sql         .= ' LIMIT %d OFFSET %d';
            $row_params[] = $per_page;
            $row_params[] = $offset;
        }
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- WHERE aus festen Fragmenten, Sortierspalte über %i und alle Werte über %s/%d gebunden; Richtung auf ASC|DESC begrenzt; Protokoll muss aktuell sein
        $rows = $wpdb->get_results($wpdb->prepare($sql, ...$row_params));

        return array('rows' => $rows, 'total' => $total);
    }

    /** Anzeigename einer Quelle. */
    public static function source_label($source) {
        switch ($source) {
            case 'order':  return '🛒 ' . __('Order', 'quickstock-for-woocommerce');
            case 'manual': return '✏️ ' . __('Manual', 'quickstock-for-woocommerce');
            case 'po':     return '📦 ' . __('Goods receipt', 'quickstock-for-woocommerce');
        }
        return esc_html($source);
    }
}
