<?php
/* Copyright (C) 2026 Eduard Wisch <data@data-it-solution.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 */

/**
 * \file    class/datanorm.class.php
 * \ingroup importzugferd
 * \brief   Class for Datanorm article database operations
 */

require_once DOL_DOCUMENT_ROOT.'/core/class/commonobject.class.php';

/**
 * Class Datanorm
 * Manages Datanorm articles in database
 */
class Datanorm extends CommonObject
{
    /**
     * @var string ID to identify managed object
     */
    public $element = 'datanorm';

    /**
     * @var string Name of table without prefix
     */
    public $table_element = 'importzugferd_datanorm';

    /**
     * @var int Does object support multicompany
     */
    public $ismultientitymanaged = 1;

    /**
     * @var int Supplier ID
     */
    public $fk_soc;

    /**
     * @var string Article number
     */
    public $article_number;

    /**
     * @var string Short text 1
     */
    public $short_text1;

    /**
     * @var string Short text 2
     */
    public $short_text2;

    /**
     * @var string Long text
     */
    public $long_text;

    /**
     * @var string EAN/GTIN
     */
    public $ean;

    /**
     * @var string Manufacturer article number
     */
    public $manufacturer_ref;

    /**
     * @var string Manufacturer name
     */
    public $manufacturer_name;

    /**
     * @var string Unit code
     */
    public $unit_code;

    /**
     * @var float Price
     */
    public $price = 0;

    /**
     * @var int Price unit (actual quantity: 1, 10, 100, or 1000)
     */
    public $price_unit = 1;

    /**
     * @var int Price unit code (original Datanorm PE code: 0, 1, 2, or 3)
     */
    public $price_unit_code = 0;

    /**
     * @var int Price type (1=gross/Brutto, 2=net/Netto)
     */
    public $price_type = 1;

    /**
     * @var int VPE - Verpackungseinheit (packaging quantity from B-record)
     */
    public $vpe;

    /**
     * @var float Metal surcharge (Metallzuschlag/Kupferzuschlag) for cables
     */
    public $metal_surcharge = 0;

    /**
     * @var string Discount group
     */
    public $discount_group;

    /**
     * @var string Product group
     */
    public $product_group;

    /**
     * @var string Alternative unit
     */
    public $alt_unit;

    /**
     * @var float Alternative unit factor
     */
    public $alt_unit_factor = 1;

    /**
     * @var float Weight in kg
     */
    public $weight;

    /**
     * @var string Matchcode
     */
    public $matchcode;

    /**
     * @var string Datanorm version
     */
    public $datanorm_version;

    /**
     * @var string Action code (N=New, A=Update, L=Delete)
     */
    public $action_code = 'N';

    /**
     * @var string Import date
     */
    public $import_date;

    /**
     * @var int Active flag
     */
    public $active = 1;

    /**
     * @var string Date creation
     */
    public $date_creation;

    /**
     * @var int User creator
     */
    public $fk_user_creat;

    /**
     * @var int User modifier
     */
    public $fk_user_modif;

    /**
     * Constructor
     *
     * @param DoliDB $db Database handler
     */
    public function __construct($db)
    {
        $this->db = $db;
    }

    /**
     * Create object into database
     *
     * @param User $user User that creates
     * @return int <0 if KO, Id of created object if OK
     */
    public function create($user)
    {
        global $conf;

        $this->entity = $conf->entity;

        if (empty($this->date_creation)) {
            $this->date_creation = dol_now();
        }
        if (empty($this->import_date)) {
            $this->import_date = dol_now();
        }

        $this->fk_user_creat = $user->id;

        // Set active=0 if action_code is L (deleted article)
        if ($this->action_code === 'L') {
            $this->active = 0;
        }

        $sql = "INSERT INTO " . MAIN_DB_PREFIX . $this->table_element . " (";
        $sql .= "fk_soc, article_number, short_text1, short_text2, long_text,";
        $sql .= "ean, manufacturer_ref, manufacturer_name, unit_code,";
        $sql .= "price, price_unit, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
        $sql .= "alt_unit, alt_unit_factor, weight, matchcode,";
        $sql .= "datanorm_version, action_code, import_date, active, date_creation, fk_user_creat, entity";
        $sql .= ") VALUES (";
        $sql .= (int) $this->fk_soc . ",";
        $sql .= "'" . $this->db->escape($this->article_number) . "',";
        $sql .= "'" . $this->db->escape($this->short_text1) . "',";
        $sql .= "'" . $this->db->escape($this->short_text2) . "',";
        $sql .= "'" . $this->db->escape($this->long_text) . "',";
        $sql .= "'" . $this->db->escape($this->ean) . "',";
        $sql .= "'" . $this->db->escape($this->manufacturer_ref) . "',";
        $sql .= "'" . $this->db->escape($this->manufacturer_name) . "',";
        $sql .= "'" . $this->db->escape($this->unit_code) . "',";
        $sql .= (float) $this->price . ",";
        $sql .= (int) $this->price_unit . ",";
        $sql .= (int) $this->price_unit_code . ",";
        $sql .= (int) $this->price_type . ",";
        $sql .= (float) $this->metal_surcharge . ",";
        $sql .= ($this->vpe !== null ? (int) $this->vpe : 'NULL') . ",";
        $sql .= "'" . $this->db->escape($this->discount_group) . "',";
        $sql .= "'" . $this->db->escape($this->product_group) . "',";
        $sql .= "'" . $this->db->escape($this->alt_unit) . "',";
        $sql .= (float) $this->alt_unit_factor . ",";
        $sql .= ($this->weight !== null ? (float) $this->weight : 'NULL') . ",";
        $sql .= "'" . $this->db->escape($this->matchcode) . "',";
        $sql .= "'" . $this->db->escape($this->datanorm_version) . "',";
        $sql .= "'" . $this->db->escape($this->action_code) . "',";
        $sql .= "'" . $this->db->escape($this->db->idate($this->import_date)) . "',";
        $sql .= (int) $this->active . ",";
        $sql .= "'" . $this->db->escape($this->db->idate($this->date_creation)) . "',";
        $sql .= (int) $this->fk_user_creat . ",";
        $sql .= (int) $this->entity;
        $sql .= ")";

        dol_syslog(get_class($this) . "::create", LOG_DEBUG);
        $resql = $this->db->query($sql);

        if (!$resql) {
            $this->error = $this->db->lasterror();
            return -1;
        }

        $this->id = $this->db->last_insert_id(MAIN_DB_PREFIX . $this->table_element);
        return $this->id;
    }

    /**
     * Create or update article (upsert)
     *
     * @param User $user User that creates/modifies
     * @return int <0 if KO, Id of object if OK
     */
    public function createOrUpdate($user)
    {
        // Check if article exists
        $existing = $this->fetchByArticleNumber($this->fk_soc, $this->article_number);

        if ($existing > 0) {
            return $this->update($user);
        } else {
            return $this->create($user);
        }
    }

    /**
     * Load object in memory from database
     *
     * @param int $id Id object
     * @return int <0 if KO, 0 if not found, >0 if OK
     */
    public function fetch($id)
    {
        $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2, long_text,";
        $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
        $sql .= " price, price_unit, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
        $sql .= " alt_unit, alt_unit_factor, weight, matchcode,";
        $sql .= " datanorm_version, action_code, import_date, active, date_creation, tms, fk_user_creat, fk_user_modif, entity";
        $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
        $sql .= " WHERE rowid = " . (int) $id;

        dol_syslog(get_class($this) . "::fetch", LOG_DEBUG);
        $resql = $this->db->query($sql);

        if ($resql) {
            if ($this->db->num_rows($resql)) {
                $obj = $this->db->fetch_object($resql);
                $this->setFromObject($obj);
                $this->db->free($resql);
                return 1;
            } else {
                $this->db->free($resql);
                return 0;
            }
        } else {
            $this->error = $this->db->lasterror();
            return -1;
        }
    }

    /**
     * Fetch by supplier and article number
     *
     * @param int $fk_soc Supplier ID
     * @param string $article_number Article number
     * @return int <0 if KO, 0 if not found, >0 if OK
     */
    public function fetchByArticleNumber($fk_soc, $article_number)
    {
        global $conf;

        $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2, long_text,";
        $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
        $sql .= " price, price_unit, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group,";
        $sql .= " alt_unit, alt_unit_factor, weight, matchcode,";
        $sql .= " datanorm_version, action_code, import_date, active, date_creation, tms, fk_user_creat, fk_user_modif, entity";
        $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
        $sql .= " WHERE fk_soc = " . (int) $fk_soc;
        $sql .= " AND article_number = '" . $this->db->escape($article_number) . "'";
        $sql .= " AND entity = " . (int) $conf->entity;

        dol_syslog(get_class($this) . "::fetchByArticleNumber", LOG_DEBUG);
        $resql = $this->db->query($sql);

        if ($resql) {
            if ($this->db->num_rows($resql)) {
                $obj = $this->db->fetch_object($resql);
                $this->setFromObject($obj);
                $this->db->free($resql);
                return 1;
            } else {
                $this->db->free($resql);
                return 0;
            }
        } else {
            $this->error = $this->db->lasterror();
            return -1;
        }
    }

    /**
     * Set object properties from database object
     *
     * @param object $obj Database row object
     */
    protected function setFromObject($obj)
    {
        $this->id = $obj->rowid;
        $this->fk_soc = $obj->fk_soc;
        $this->article_number = $obj->article_number;
        $this->short_text1 = $obj->short_text1;
        $this->short_text2 = $obj->short_text2;
        $this->long_text = $obj->long_text;
        $this->ean = $obj->ean;
        $this->manufacturer_ref = $obj->manufacturer_ref;
        $this->manufacturer_name = $obj->manufacturer_name;
        $this->unit_code = $obj->unit_code;
        $this->price = $obj->price;
        $this->price_unit = $obj->price_unit;
        $this->price_unit_code = $obj->price_unit_code ?? 0;
        $this->price_type = $obj->price_type ?? 1;
        $this->metal_surcharge = $obj->metal_surcharge ?? 0;
        $this->vpe = $obj->vpe;
        $this->discount_group = $obj->discount_group;
        $this->product_group = $obj->product_group;
        $this->alt_unit = $obj->alt_unit;
        $this->alt_unit_factor = $obj->alt_unit_factor;
        $this->weight = $obj->weight;
        $this->matchcode = $obj->matchcode;
        $this->datanorm_version = $obj->datanorm_version;
        $this->action_code = $obj->action_code ?? 'N';
        $this->import_date = $this->db->jdate($obj->import_date);
        $this->active = $obj->active;
        $this->date_creation = $this->db->jdate($obj->date_creation);
        $this->tms = $this->db->jdate($obj->tms);
        $this->fk_user_creat = $obj->fk_user_creat;
        $this->fk_user_modif = $obj->fk_user_modif;
        $this->entity = $obj->entity;
    }

    /**
     * Update object in database
     *
     * @param User $user User that modifies
     * @return int <0 if KO, >0 if OK
     */
    public function update($user)
    {
        $this->fk_user_modif = $user->id;
        $this->import_date = dol_now();

        // Set active=0 if action_code is L (deleted article)
        if ($this->action_code === 'L') {
            $this->active = 0;
        }

        $sql = "UPDATE " . MAIN_DB_PREFIX . $this->table_element . " SET";
        $sql .= " short_text1 = '" . $this->db->escape($this->short_text1) . "',";
        $sql .= " short_text2 = '" . $this->db->escape($this->short_text2) . "',";
        $sql .= " long_text = '" . $this->db->escape($this->long_text) . "',";
        $sql .= " ean = '" . $this->db->escape($this->ean) . "',";
        $sql .= " manufacturer_ref = '" . $this->db->escape($this->manufacturer_ref) . "',";
        $sql .= " manufacturer_name = '" . $this->db->escape($this->manufacturer_name) . "',";
        $sql .= " unit_code = '" . $this->db->escape($this->unit_code) . "',";
        $sql .= " price = " . (float) $this->price . ",";
        $sql .= " price_unit = " . (int) $this->price_unit . ",";
        $sql .= " price_unit_code = " . (int) $this->price_unit_code . ",";
        $sql .= " price_type = " . (int) $this->price_type . ",";
        $sql .= " metal_surcharge = " . (float) $this->metal_surcharge . ",";
        $sql .= " vpe = " . ($this->vpe !== null ? (int) $this->vpe : 'NULL') . ",";
        $sql .= " discount_group = '" . $this->db->escape($this->discount_group) . "',";
        $sql .= " product_group = '" . $this->db->escape($this->product_group) . "',";
        $sql .= " alt_unit = '" . $this->db->escape($this->alt_unit) . "',";
        $sql .= " alt_unit_factor = " . (float) $this->alt_unit_factor . ",";
        $sql .= " weight = " . ($this->weight !== null ? (float) $this->weight : 'NULL') . ",";
        $sql .= " matchcode = '" . $this->db->escape($this->matchcode) . "',";
        $sql .= " datanorm_version = '" . $this->db->escape($this->datanorm_version) . "',";
        $sql .= " action_code = '" . $this->db->escape($this->action_code) . "',";
        $sql .= " import_date = '" . $this->db->escape($this->db->idate($this->import_date)) . "',";
        $sql .= " active = " . (int) $this->active . ",";
        $sql .= " fk_user_modif = " . (int) $this->fk_user_modif;
        $sql .= " WHERE rowid = " . (int) $this->id;

        dol_syslog(get_class($this) . "::update", LOG_DEBUG);
        $resql = $this->db->query($sql);

        if (!$resql) {
            $this->error = $this->db->lasterror();
            return -1;
        }

        return 1;
    }

    /**
     * Delete object from database
     *
     * @param User $user User that deletes
     * @return int <0 if KO, >0 if OK
     */
    public function delete($user)
    {
        $sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element;
        $sql .= " WHERE rowid = " . (int) $this->id;

        dol_syslog(get_class($this) . "::delete", LOG_DEBUG);
        $resql = $this->db->query($sql);

        if (!$resql) {
            $this->error = $this->db->lasterror();
            return -1;
        }

        return 1;
    }

    /**
     * Delete all articles for a supplier
     *
     * @param User $user User that deletes
     * @param int $fk_soc Supplier ID
     * @return int <0 if KO, number of deleted rows if OK
     */
    public function deleteAllBySupplier($user, $fk_soc)
    {
        global $conf;

        $sql = "DELETE FROM " . MAIN_DB_PREFIX . $this->table_element;
        $sql .= " WHERE fk_soc = " . (int) $fk_soc;
        $sql .= " AND entity = " . (int) $conf->entity;

        dol_syslog(get_class($this) . "::deleteAllBySupplier", LOG_DEBUG);
        $resql = $this->db->query($sql);

        if (!$resql) {
            $this->error = $this->db->lasterror();
            return -1;
        }

        return $this->db->affected_rows($resql);
    }

    /**
     * Search articles by article number (exact or partial)
     *
     * @param string $article_number Article number to search
     * @param int $fk_soc Supplier ID (0 = all suppliers)
     * @param bool $searchAll Search all suppliers if not found in specified
     * @param int $limit Maximum results
     * @return array Array of matching articles
     */
    public function searchByArticleNumber($article_number, $fk_soc = 0, $searchAll = false, $limit = 50)
    {
        global $conf;

        $results = array();
        $foundEan = '';
        $foundManufacturerRef = '';
        $foundIds = array(); // Track found IDs to avoid duplicates

        // First try exact match with specified supplier
        if ($fk_soc > 0) {
            $result = $this->fetchByArticleNumber($fk_soc, $article_number);
            if ($result > 0) {
                $results[] = $this->toArray();
                $foundIds[$this->id] = true;
                // Store EAN from Datanorm
                $foundEan = $this->ean;

                // If Datanorm has no EAN, try to get it from supplier price (barcode field)
                if (empty($foundEan)) {
                    $sqlEan = "SELECT barcode FROM " . MAIN_DB_PREFIX . "product_fournisseur_price";
                    $sqlEan .= " WHERE fk_soc = " . (int)$fk_soc;
                    $sqlEan .= " AND ref_fourn = '" . $this->db->escape($article_number) . "'";
                    $sqlEan .= " AND barcode IS NOT NULL AND barcode != ''";
                    $sqlEan .= " LIMIT 1";
                    $resEan = $this->db->query($sqlEan);
                    if ($resEan && $this->db->num_rows($resEan) > 0) {
                        $objEan = $this->db->fetch_object($resEan);
                        $foundEan = $objEan->barcode;
                    }
                }

                // If not searching all catalogs, return immediately
                if (!$searchAll) {
                    return $results;
                }
            }
        }

        // If searchAll is enabled and we found article with EAN,
        // search other catalogs using EAN ONLY (cross-catalog search)
        // Note: Artikelnummern-Vergleich macht keinen Sinn über Kataloge hinweg
        if ($searchAll && $fk_soc > 0 && !empty($foundEan)) {
            $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
            $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
            $sql .= " price, price_unit, discount_group, product_group, matchcode";
            $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
            $sql .= " WHERE ean = '" . $this->db->escape($foundEan) . "'";
            $sql .= " AND active = 1";
            $sql .= " AND entity = " . (int) $conf->entity;
            $sql .= " AND fk_soc != " . (int) $fk_soc; // Exclude already found supplier

            $sql .= " ORDER BY price ASC"; // Show cheapest alternatives first
            $sql .= " LIMIT " . (int) ($limit - count($results));

            $resql = $this->db->query($sql);
            if ($resql) {
                while ($obj = $this->db->fetch_object($resql)) {
                    if (!isset($foundIds[$obj->rowid])) {
                        $results[] = array(
                            'id' => $obj->rowid,
                            'fk_soc' => $obj->fk_soc,
                            'article_number' => $obj->article_number,
                            'short_text1' => $obj->short_text1,
                            'short_text2' => $obj->short_text2,
                            'ean' => $obj->ean,
                            'manufacturer_ref' => $obj->manufacturer_ref,
                            'manufacturer_name' => $obj->manufacturer_name,
                            'unit_code' => $obj->unit_code,
                            'price' => $obj->price,
                            'price_unit' => $obj->price_unit,
                            'discount_group' => $obj->discount_group,
                            'product_group' => $obj->product_group,
                            'matchcode' => $obj->matchcode,
                        );
                        $foundIds[$obj->rowid] = true;
                    }
                }
                $this->db->free($resql);
            }

            // If we found results via cross-catalog search, return them
            if (!empty($results)) {
                return $results;
            }
        }

        // Fallback: Search by EXACT article number match for the specified supplier only
        // No LIKE search - cross-catalog comparisons only work via EAN
        if ($fk_soc > 0 && empty($results)) {
            $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
            $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
            $sql .= " price, price_unit, discount_group, product_group, matchcode";
            $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
            $sql .= " WHERE article_number = '" . $this->db->escape($article_number) . "'";
            $sql .= " AND fk_soc = " . (int) $fk_soc;
            $sql .= " AND active = 1";
            $sql .= " AND entity = " . (int) $conf->entity;
            $sql .= " LIMIT 1";

            $resql = $this->db->query($sql);
            if ($resql) {
                while ($obj = $this->db->fetch_object($resql)) {
                    if (!isset($foundIds[$obj->rowid])) {
                        $results[] = array(
                            'id' => $obj->rowid,
                            'fk_soc' => $obj->fk_soc,
                            'article_number' => $obj->article_number,
                            'short_text1' => $obj->short_text1,
                            'short_text2' => $obj->short_text2,
                            'ean' => $obj->ean,
                            'manufacturer_ref' => $obj->manufacturer_ref,
                            'manufacturer_name' => $obj->manufacturer_name,
                            'unit_code' => $obj->unit_code,
                            'price' => $obj->price,
                            'price_unit' => $obj->price_unit,
                            'discount_group' => $obj->discount_group,
                            'product_group' => $obj->product_group,
                            'matchcode' => $obj->matchcode,
                        );
                        $foundIds[$obj->rowid] = true;
                    }
                }
                $this->db->free($resql);
            }
        }

        return $results;
    }

    /**
     * Convert object to array
     *
     * @return array Object as array
     */
    public function toArray()
    {
        return array(
            'id' => $this->id,
            'fk_soc' => $this->fk_soc,
            'article_number' => $this->article_number,
            'short_text1' => $this->short_text1,
            'short_text2' => $this->short_text2,
            'long_text' => $this->long_text,
            'ean' => $this->ean,
            'manufacturer_ref' => $this->manufacturer_ref,
            'manufacturer_name' => $this->manufacturer_name,
            'unit_code' => $this->unit_code,
            'price' => $this->price,
            'price_unit' => $this->price_unit,
            'discount_group' => $this->discount_group,
            'product_group' => $this->product_group,
            'alt_unit' => $this->alt_unit,
            'alt_unit_factor' => $this->alt_unit_factor,
            'weight' => $this->weight,
            'matchcode' => $this->matchcode,
            'datanorm_version' => $this->datanorm_version,
            'import_date' => $this->import_date,
            'active' => $this->active,
        );
    }

    /**
     * Count articles for a supplier
     *
     * @param int $fk_soc Supplier ID
     * @return int Count
     */
    public function countBySupplier($fk_soc)
    {
        global $conf;

        $sql = "SELECT COUNT(*) as nb FROM " . MAIN_DB_PREFIX . $this->table_element;
        $sql .= " WHERE fk_soc = " . (int) $fk_soc;
        $sql .= " AND entity = " . (int) $conf->entity;

        $resql = $this->db->query($sql);
        if ($resql) {
            $obj = $this->db->fetch_object($resql);
            return (int) $obj->nb;
        }

        return 0;
    }

    /**
     * Get all suppliers with Datanorm data
     *
     * @return array Array of suppliers with article counts
     */
    public function getSuppliersWithData()
    {
        global $conf;

        $suppliers = array();

        $sql = "SELECT d.fk_soc, s.nom as supplier_name, COUNT(*) as article_count,";
        $sql .= " MAX(d.import_date) as last_import";
        $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element . " as d";
        $sql .= " LEFT JOIN " . MAIN_DB_PREFIX . "societe as s ON s.rowid = d.fk_soc";
        $sql .= " WHERE d.entity = " . (int) $conf->entity;
        $sql .= " GROUP BY d.fk_soc, s.nom";
        $sql .= " ORDER BY s.nom";

        $resql = $this->db->query($sql);
        if ($resql) {
            while ($obj = $this->db->fetch_object($resql)) {
                $suppliers[] = array(
                    'fk_soc' => $obj->fk_soc,
                    'name' => $obj->supplier_name,
                    'article_count' => $obj->article_count,
                    'last_import' => $this->db->jdate($obj->last_import),
                );
            }
            $this->db->free($resql);
        }

        return $suppliers;
    }

    /**
     * Import articles from parser
     *
     * @param User $user User that imports
     * @param int $fk_soc Supplier ID
     * @param DatanormParser $parser Parser with parsed articles
     * @param bool $deleteExisting Delete existing articles before import
     * @return int Number of imported articles, <0 on error
     */
    public function importFromParser($user, $fk_soc, $parser, $deleteExisting = false)
    {
        $this->db->begin();

        // Delete existing if requested
        if ($deleteExisting) {
            $result = $this->deleteAllBySupplier($user, $fk_soc);
            if ($result < 0) {
                $this->db->rollback();
                return -1;
            }
        }

        $count = 0;
        $errors = 0;

        foreach ($parser->getArticles() as $articleData) {
            $article = new Datanorm($this->db);
            $article->fk_soc = $fk_soc;
            $article->article_number = $articleData['article_number'];
            $article->short_text1 = $articleData['short_text1'] ?? '';
            $article->short_text2 = $articleData['short_text2'] ?? '';
            $article->long_text = $articleData['long_text'] ?? '';
            $article->ean = $articleData['ean'] ?? '';
            $article->manufacturer_ref = $articleData['manufacturer_ref'] ?? '';
            $article->manufacturer_name = $articleData['manufacturer_name'] ?? '';
            $article->unit_code = $articleData['unit_code'] ?? '';
            $article->price = $articleData['price'] ?? 0;
            $article->price_unit = $articleData['price_unit'] ?? 1;
            $article->price_unit_code = $articleData['price_unit_code'] ?? 0;
            $article->price_type = $articleData['price_type'] ?? 1;
            $article->metal_surcharge = $articleData['metal_surcharge'] ?? 0;
            $article->vpe = $articleData['vpe'] ?? null;
            $article->discount_group = $articleData['discount_group'] ?? '';
            $article->product_group = $articleData['product_group'] ?? '';
            $article->matchcode = $articleData['matchcode'] ?? '';
            $article->datanorm_version = $parser->version;
            $article->action_code = $articleData['action_code'] ?? 'N';

            $result = $article->createOrUpdate($user);
            if ($result > 0) {
                $count++;
            } else {
                $errors++;
                $this->errors[] = 'Error importing ' . $articleData['article_number'] . ': ' . $article->error;
            }
        }

        if ($errors > 0 && $count == 0) {
            $this->db->rollback();
            $this->error = 'All imports failed';
            return -1;
        }

        $this->db->commit();
        return $count;
    }

    /**
     * Import articles from directory using streaming (for large files)
     * Uses batch inserts to minimize memory usage
     *
     * @param User $user User that imports
     * @param int $fk_soc Supplier ID
     * @param string $directory Directory with Datanorm files
     * @param bool $deleteExisting Delete existing articles before import
     * @return int Number of imported articles, <0 on error
     */
    public function importFromDirectoryStreaming($user, $fk_soc, $directory, $deleteExisting = false)
    {
        global $conf;

        require_once __DIR__ . '/datanormparser.class.php';

        // Delete existing if requested
        if ($deleteExisting) {
            $result = $this->deleteAllBySupplier($user, $fk_soc);
            if ($result < 0) {
                return -1;
            }
        }

        $db = $this->db;
        $importCount = 0;
        $version = '';

        // Create batch callback that inserts articles directly to database
        $batchCallback = function ($articles) use ($db, $user, $fk_soc, &$importCount, &$version, $conf) {
            if (empty($articles)) {
                return;
            }

            // Use multi-row INSERT for better performance
            $values = array();
            $now = $db->idate(dol_now());

            foreach ($articles as $articleData) {
                $vpe = isset($articleData['vpe']) ? (int)$articleData['vpe'] : 'NULL';
                $actionCode = $articleData['action_code'] ?? 'N';
                $active = ($actionCode === 'L') ? 0 : 1; // Set active=0 for deleted articles
                $values[] = sprintf(
                    "(%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %f, %d, %d, %d, %f, %s, '%s', '%s', '%s', '%s', '%s', %d, '%s', %d, '%s', %d)",
                    (int) $fk_soc,
                    $db->escape($articleData['article_number'] ?? ''),
                    $db->escape($articleData['short_text1'] ?? ''),
                    $db->escape($articleData['short_text2'] ?? ''),
                    $db->escape($articleData['long_text'] ?? ''),
                    $db->escape($articleData['ean'] ?? ''),
                    $db->escape($articleData['manufacturer_ref'] ?? ''),
                    $db->escape($articleData['manufacturer_name'] ?? ''),
                    $db->escape($articleData['unit_code'] ?? ''),
                    (float) ($articleData['price'] ?? 0),
                    (int) ($articleData['price_unit'] ?? 1),
                    (int) ($articleData['price_unit_code'] ?? 0),
                    (int) ($articleData['price_type'] ?? 1),
                    (float) ($articleData['metal_surcharge'] ?? 0),
                    $vpe,
                    $db->escape($articleData['discount_group'] ?? ''),
                    $db->escape($articleData['product_group'] ?? ''),
                    $db->escape($articleData['matchcode'] ?? ''),
                    $db->escape($version),
                    $db->escape($actionCode),
                    $active,
                    $now,
                    (int) $user->id,
                    $now,
                    (int) $conf->entity
                );
            }

            if (!empty($values)) {
                // Use INSERT IGNORE to skip duplicates (for the same supplier + article_number)
                $sql = "INSERT INTO " . MAIN_DB_PREFIX . "importzugferd_datanorm ";
                $sql .= "(fk_soc, article_number, short_text1, short_text2, long_text, ";
                $sql .= "ean, manufacturer_ref, manufacturer_name, unit_code, ";
                $sql .= "price, price_unit, price_unit_code, price_type, metal_surcharge, vpe, discount_group, product_group, matchcode, ";
                $sql .= "datanorm_version, action_code, active, import_date, fk_user_creat, date_creation, entity) VALUES ";
                $sql .= implode(", ", $values);

                // For updates of existing articles, use ON DUPLICATE KEY UPDATE
                $sql .= " ON DUPLICATE KEY UPDATE ";
                $sql .= "short_text1 = VALUES(short_text1), ";
                $sql .= "short_text2 = VALUES(short_text2), ";
                $sql .= "long_text = VALUES(long_text), ";
                $sql .= "ean = VALUES(ean), ";
                $sql .= "manufacturer_ref = VALUES(manufacturer_ref), ";
                $sql .= "manufacturer_name = VALUES(manufacturer_name), ";
                $sql .= "unit_code = VALUES(unit_code), ";
                $sql .= "price = VALUES(price), ";
                $sql .= "price_unit = VALUES(price_unit), ";
                $sql .= "price_unit_code = VALUES(price_unit_code), ";
                $sql .= "price_type = VALUES(price_type), ";
                $sql .= "metal_surcharge = VALUES(metal_surcharge), ";
                $sql .= "vpe = VALUES(vpe), ";
                $sql .= "discount_group = VALUES(discount_group), ";
                $sql .= "product_group = VALUES(product_group), ";
                $sql .= "matchcode = VALUES(matchcode), ";
                $sql .= "datanorm_version = VALUES(datanorm_version), ";
                $sql .= "action_code = VALUES(action_code), ";
                $sql .= "active = VALUES(active), ";
                $sql .= "import_date = VALUES(import_date), ";
                $sql .= "fk_user_modif = " . (int) $user->id;

                $resql = $db->query($sql);
                if ($resql) {
                    $importCount += count($values);
                }
            }
        };

        // Parse with streaming enabled
        // The parser now loads prices first, then articles
        $parser = new DatanormParser();
        $parser->enableStreaming($batchCallback, 500);

        // Parse directory - prices are loaded first, then articles with streaming
        $count = $parser->parseDirectory($directory);
        $version = $parser->version;

        if ($count < 0) {
            $this->error = $parser->error;
            return -1;
        }

        // Second pass: Update prices from DATPREIS files
        // Use case-insensitive search for Linux compatibility
        $priceFiles = array();
        $allFiles = glob($directory . '/*');
        foreach ($allFiles as $file) {
            $basename = strtoupper(basename($file));
            if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) {
                $priceFiles[] = $file;
            }
        }
        if (!empty($priceFiles)) {
            foreach ($priceFiles as $file) {
                $this->updatePricesFromFile($fk_soc, $file);
            }
        }

        return $importCount;
    }

    /**
     * Update prices from DATPREIS file (streaming)
     * Processes file line by line and updates database directly
     *
     * @param int $fk_soc Supplier ID
     * @param string $file Path to DATPREIS file
     * @return int Number of prices updated
     */
    protected function updatePricesFromFile($fk_soc, $file)
    {
        global $conf;

        $handle = fopen($file, 'r');
        if ($handle === false) {
            return 0;
        }

        $updated = 0;
        $batch = array();
        $batchSize = 500;

        while (($line = fgets($handle)) !== false) {
            $line = rtrim($line, "\r\n");

            // Convert encoding if needed
            if (!mb_check_encoding($line, 'UTF-8')) {
                $line = mb_convert_encoding($line, 'UTF-8', 'ISO-8859-1');
            }

            if (strlen($line) < 10 || strpos($line, ';') === false) {
                continue;
            }

            $parts = explode(';', $line);
            $recordType = trim($parts[0] ?? '');

            // P;A format - multiple articles per line
            // Format: P;A;ArtNr;PreisKz;Preis;PE;Zuschlag;x;x;x;ArtNr2;...
            // Rabattkennzeichen aus DATPREIS (wird gespeichert aber nicht fuer price_unit verwendet)
            if ($recordType === 'P' && isset($parts[1]) && $parts[1] === 'A') {
                $i = 2;
                while ($i < count($parts) - 2) {
                    $articleNumber = trim($parts[$i] ?? '');
                    $priceRaw = trim($parts[$i + 2] ?? '0');
                    $datpreisPeCode = (int)trim($parts[$i + 3] ?? '0'); // Rabattkennzeichen (nicht PE!)
                    $metalSurchargeRaw = trim($parts[$i + 4] ?? '0');
                    $price = (float)$priceRaw / 100; // Convert cents to euros
                    $metalSurcharge = (float)$metalSurchargeRaw / 100; // Convert cents to euros

                    if (!empty($articleNumber) && $price > 0) {
                        $batch[$articleNumber] = array(
                            'price' => $price,
                            'metal_surcharge' => $metalSurcharge,
                            'datpreis_pe_code' => $datpreisPeCode
                        );
                    }

                    $i += 9; // 9 fields per article
                }
            } elseif ($recordType === 'P' || $recordType === '0') {
                // Simple format: P;ArtNr;PreisKz;Preis;PE;...
                $articleNumber = trim($parts[1] ?? '');
                $priceRaw = trim($parts[3] ?? '0');
                $datpreisPeCode = (int)trim($parts[4] ?? '0'); // Rabattkennzeichen (nicht PE!)

                if (strpos($priceRaw, ',') === false && strpos($priceRaw, '.') === false) {
                    $price = (float)$priceRaw / 100;
                } else {
                    $priceRaw = str_replace(',', '.', $priceRaw);
                    $price = (float)$priceRaw;
                }

                if (!empty($articleNumber) && $price > 0) {
                    $batch[$articleNumber] = array(
                        'price' => $price,
                        'metal_surcharge' => 0,
                        'datpreis_pe_code' => $datpreisPeCode
                    );
                }
            }

            // Flush batch when it reaches the limit
            if (count($batch) >= $batchSize) {
                $updated += $this->flushPriceBatch($fk_soc, $batch);
                $batch = array();
            }
        }

        // Flush remaining
        if (!empty($batch)) {
            $updated += $this->flushPriceBatch($fk_soc, $batch);
        }

        fclose($handle);
        return $updated;
    }

    /**
     * Flush price batch to database
     * DATPREIS prices are already given for the A-Satz PE unit - no normalization needed!
     *
     * @param int $fk_soc Supplier ID
     * @param array $prices Array of article_number => array('price' => ..., 'metal_surcharge' => ...)
     * @return int Number of rows updated
     */
    protected function flushPriceBatch($fk_soc, $prices)
    {
        global $conf;

        if (empty($prices)) {
            return 0;
        }

        $updated = 0;

        // Build CASE statements for batch update
        // Note: DATPREIS prices are already for the A-Satz PE unit, no normalization needed
        $priceCases = array();
        $metalCases = array();
        $articleNumbers = array();

        foreach ($prices as $artNum => $priceData) {
            $priceCases[] = "WHEN '" . $this->db->escape($artNum) . "' THEN " . (float)$priceData['price'];
            $metalCases[] = "WHEN '" . $this->db->escape($artNum) . "' THEN " . (float)$priceData['metal_surcharge'];
            $articleNumbers[] = "'" . $this->db->escape($artNum) . "'";
        }

        if (!empty($priceCases)) {
            $sql = "UPDATE " . MAIN_DB_PREFIX . "importzugferd_datanorm SET ";
            $sql .= "price = CASE article_number ";
            $sql .= implode(" ", $priceCases);
            $sql .= " END, ";
            $sql .= "metal_surcharge = CASE article_number ";
            $sql .= implode(" ", $metalCases);
            $sql .= " END ";
            $sql .= "WHERE fk_soc = " . (int)$fk_soc;
            $sql .= " AND entity = " . (int)$conf->entity;
            $sql .= " AND article_number IN (" . implode(",", $articleNumbers) . ")";

            $resql = $this->db->query($sql);
            if ($resql) {
                $updated = $this->db->affected_rows($resql);
            }
        }

        return $updated;
    }

    /**
     * Get full description for product creation
     *
     * @return string Full description
     */
    public function getFullDescription()
    {
        $desc = '';

        if (!empty($this->short_text1)) {
            $desc .= $this->short_text1;
        }
        if (!empty($this->short_text2)) {
            $desc .= ($desc ? "\n" : '') . $this->short_text2;
        }
        if (!empty($this->long_text)) {
            $desc .= ($desc ? "\n\n" : '') . $this->long_text;
        }

        // Add metadata
        $meta = array();
        if (!empty($this->manufacturer_name)) {
            $meta[] = 'Hersteller: ' . $this->manufacturer_name;
        }
        if (!empty($this->manufacturer_ref)) {
            $meta[] = 'Hersteller-Nr: ' . $this->manufacturer_ref;
        }
        if (!empty($this->ean)) {
            $meta[] = 'EAN: ' . $this->ean;
        }
        if (!empty($this->product_group)) {
            $meta[] = 'Warengruppe: ' . $this->product_group;
        }

        if (!empty($meta)) {
            $desc .= ($desc ? "\n\n" : '') . implode("\n", $meta);
        }

        return $desc;
    }

    /**
     * Calculate selling price with markup
     *
     * @param float $markupPercent Markup percentage
     * @return float Selling price
     */
    public function getSellingPrice($markupPercent = 0)
    {
        $basePrice = $this->price;

        // Adjust for price unit
        if ($this->price_unit > 1) {
            $basePrice = $basePrice / $this->price_unit;
        }

        if ($markupPercent > 0) {
            return $basePrice * (1 + $markupPercent / 100);
        }

        return $basePrice;
    }
}
