import sys
import os
import subprocess
import logging
import time
import threading
import json
import uuid
import re
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from functools import partial
from dataclasses import asdict
from core.constants import StockType # Yeni import
try:
    from reportlab.pdfgen import canvas
    from reportlab.lib.units import mm
    from reportlab.graphics.barcode import code128
    from reportlab.pdfbase import pdfdoc
    HAS_REPORTLAB = True
except ImportError:
    HAS_REPORTLAB = False
try:
    import pyautogui
    HAS_PYAUTOGUI = True
except ImportError:
    HAS_PYAUTOGUI = False
from PyQt6.QtWidgets import (
    QCheckBox, QDialog, QVBoxLayout, QGroupBox, QFormLayout, QComboBox,
    QSpinBox, QPushButton, QLabel, QListWidget, QHBoxLayout, QTabWidget, QWidget,
    QMessageBox, QFileDialog, QDialogButtonBox, QLineEdit,
    QTableWidget, QTableWidgetItem, QHeaderView, QFrame
)
from PyQt6.QtGui import QIcon, QDoubleValidator, QColor, QFont
from PyQt6.QtCore import Qt, QTimer
from core.config import (
    SUPPORTED_CURRENCIES,
    logger,
    load_language,
    get_active_language_code,
    get_language_text,
    get_language_aliases,
    get_default_depot_name,
)
from core.tenant_manager import TenantManager
from modules.hardware.thermal_printer import ThermalPrinterManager
from core.db import (
    BaseManager,
    Category,
    DataManager,
    ProductDefinition,
    StockItem,
    Depot,
    BarcodeHistory,
    SYNC_ACTION_ADD,
    SYNC_ACTION_DELETE,
    SYNC_ACTION_UPDATE,
    fetch_row_snapshot_by_id,
    queue_row_sync_event,
)

def _get_ui_lang(parent=None):
    for candidate in (parent, getattr(parent, "main_app", None)):
        lang = getattr(candidate, "lang", None)
        if isinstance(lang, dict):
            return lang
    return load_language(get_active_language_code())

def _set_dialog_button_texts(button_box, lang):
    ok_btn = button_box.button(QDialogButtonBox.StandardButton.Ok)
    if ok_btn:
        ok_btn.setText(lang.get("ok", "OK"))
    cancel_btn = button_box.button(QDialogButtonBox.StandardButton.Cancel)
    if cancel_btn:
        cancel_btn.setText(lang.get("cancel", "Cancel"))
class ProductDefinitionManager(BaseManager):
    """
    Ürün tanımlarını (Product, Ziynet, Scrap) yönetir.
    Veritabanı tiplerini ve işçilik alanlarını garanti altına alır.
    """
    def __init__(self, data_manager: DataManager, stock_type: str):
        self.stock_type_filter = stock_type
        if stock_type == "Product":
            data_key = "product_definitions"
        elif stock_type == "Ziynet":
            data_key = "product_definitions"
        else:  # Scrap
            data_key = "product_definitions"
        super().__init__(data_manager, data_key, ProductDefinition)
    def _stock_type_value(self) -> str:
        try:
            return str(self.stock_type_filter.value)
        except Exception:
            return str(self.stock_type_filter)
    def _custom_map_to_sql(self, item: Any, sql_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """ProductDefinition tablosu için özel eşleştirme."""
        data = asdict(item)
        sql_data['name'] = data.get('product_name')
        if 'product_name' in sql_data: del sql_data['product_name']
        sql_data['barcode_header'] = data.get('barcode', '')
        if 'barcode' in sql_data: del sql_data['barcode']
        category_id = data.get('category_id') or sql_data.get('category_id')
        category_name = (data.get('category') or "").strip()
        if not category_id and category_name:
            res = self.db.fetch_one(
                "SELECT id FROM categories WHERE name=%s AND profile_id=%s LIMIT 1",
                (category_name, self.profile_id),
            )
            if res:
                category_id = res.get("id")
            else:
                new_cat_id = str(uuid.uuid4())
                self.db.execute_non_query(
                    "INSERT INTO categories (id, profile_id, name) VALUES (%s, %s, %s)",
                    (new_cat_id, self.profile_id, category_name),
                )
                category_id = new_cat_id
        sql_data['category_id'] = category_id
        sql_data.pop('category', None)
        return sql_data
    def _custom_map_from_sql(self, row: Dict[str, Any]) -> Optional[Any]:
        def to_float(val, default=0.0):
            try:
                if val is None:
                    return default
                return float(val)
            except Exception:
                return default
        def to_optional_float(val):
            try:
                if val is None:
                    return None
                return float(val)
            except Exception:
                return None
        category_name = get_language_text("default_category_gold", "Altın")
        category_id = row.get("category_id")
        if category_id:
            cat_row = self.db.fetch_one(
                "SELECT name FROM categories WHERE id=%s AND profile_id=%s LIMIT 1",
                (category_id, self.profile_id),
            )
            if cat_row and cat_row.get("name"):
                category_name = cat_row["name"]
        return self.data_class(
            product_name=row.get("name", "") or "",
            category=category_name,
            barcode=row.get("barcode_header", "") or "",
            karat_purity=row.get("karat_purity", "") or "",
            workmanship_fee=to_float(row.get("workmanship_fee")),
            workmanship_currency=row.get("workmanship_currency", "TRY") or "TRY",
            default_purchase_price=to_float(row.get("default_purchase_price")),
            default_sale_price=to_float(row.get("default_sale_price")),
            purchase_currency=row.get("purchase_currency", "TRY") or "TRY",
            sale_currency=row.get("sale_currency", "TRY") or "TRY",
            is_active=bool(row.get("is_active", 1)),
            
            # BURADAKİ ZORLAMAYI KALDIRDIK:
            default_weight_gr=to_float(row.get("default_weight_gr")), 
            
            stock_type=row.get("stock_type", "Product") or "Product",
            id=row.get("id") or str(uuid.uuid4()),
            created_at=str(row.get("created_at") or ""),
            updated_at=str(row.get("updated_at") or ""),
            profile_id=row.get("profile_id") or "",
            category_id=category_id,
        )
    def get_all(self) -> List[ProductDefinition]:
        """Sadece ilgili stock_type'a ait tanımları döndürür."""
        sql = """
            SELECT *
            FROM product_definitions
            WHERE profile_id = %s AND is_deleted = 0 AND stock_type = %s
        """
        rows = self.db.fetch_all(sql, (self.profile_id, self._stock_type_value()))
        results: List[ProductDefinition] = []
        for row in rows:
            obj = self._map_sql_to_dataclass(row)
            if obj:
                results.append(obj)
        return results
    def find_by_id(self, item_id: str, cursor=None) -> Optional[Any]:
        sql = """
            SELECT *
            FROM product_definitions
            WHERE id = %s AND profile_id = %s AND is_deleted = 0 AND stock_type = %s
            LIMIT 1
        """
        if cursor:
            cursor.execute(sql, (item_id, self.profile_id, self._stock_type_value()))
            row = cursor.fetchone()
        else:
            row = self.db.fetch_one(sql, (item_id, self.profile_id, self._stock_type_value()))
        if row:
            return self._map_sql_to_dataclass(row)
        return None
    # modules/stock.py içinde yaklaşık 158. satır civarı

    def find_by_attribute(self, attr_name: str, value: Any) -> Optional[Any]:
        col_name = attr_name
        
        # --- DÜZELTME BAŞLANGICI ---
        if attr_name == "product_name":
            col_name = "name"
        elif attr_name == "barcode":  # <-- BU KISMI EKLEYİN
            col_name = "barcode_header"
        # --- DÜZELTME SONU ---

        sql = f"SELECT * FROM product_definitions WHERE {col_name} = %s AND profile_id = %s AND is_deleted = 0 AND stock_type = %s LIMIT 1"
        
        # ... geri kalan kod aynı ...
        row = self.db.fetch_one(sql, (value, self.profile_id, self._stock_type_value()))
        if row:
            return self._map_sql_to_dataclass(row)
        return None
    def get_product_names(self) -> List[str]:
        return sorted([p.product_name for p in self.get_all() if p.is_active])
    def add_item(self, item: ProductDefinition):
        item.stock_type = self._stock_type_value()
        if not item.workmanship_currency:
            item.workmanship_currency = "TRY"
        super().add_item(item)
class CategoryManager(BaseManager):
    def __init__(self, data_manager: DataManager):
        super().__init__(data_manager, "categories", Category)
        defaults = [
            ("default_category_gold", "Altın"),
            ("default_category_silver", "Gümüş"),
        ]
        for lang_key, fallback in defaults:
            aliases = get_language_aliases(lang_key, fallback)
            if not any(self.find_by_attribute("name", alias) for alias in aliases):
                self.add_item(Category(name=get_language_text(lang_key, fallback)))
    def get_category_names(self) -> List[str]:
        return sorted([c.name for c in self.get_all()])
class DepotManager(BaseManager):
    def __init__(self, data_manager: DataManager):
        super().__init__(data_manager, "depots", Depot)
        depot_aliases = get_language_aliases("default_depot_name", "Merkez")
        if not any(self.find_by_attribute("name", alias) for alias in depot_aliases):
            self.add_item(Depot(name=get_default_depot_name()))
    def get_depot_names(self) -> List[str]:
        return sorted([d.name for d in self.get_all()])
    def get_id_by_name(self, name: str) -> Optional[str]:
        """Depo isminden ID'sini bulur."""
        row = self.db.fetch_one(
            "SELECT id FROM depots WHERE name=%s AND profile_id=%s AND is_deleted=0 LIMIT 1",
            (name, self.profile_id),
        )
        return row.get("id") if row else None
class StockMovementManager(BaseManager):
    """
    Stok hareketlerini (Log) yöneten sınıf.
    FAZ 4: Transaction bütünlüğü için 'cursor' desteği eklendi.
    """
    def __init__(self, data_manager: DataManager):
        from core.db import StockMovement
        super().__init__(data_manager, "stock_movements", StockMovement)
    def add_log(self, item_type, product_name, op_type, qty, source, dest, desc, user="Admin", cursor=None):
        """
        Log ekler (Transaction Uyumlu).
        Args:
            cursor: Eğer bir transaction içindeysek, bu cursor kullanılmalı.
                    Yoksa, işlem bağımsız (auto-commit) çalışır.
        """
        from core.db import StockMovement
        try:
            movement = StockMovement(
                date=datetime.now().isoformat(),
                item_type=item_type,
                product_name=product_name,
                operation_type=op_type,
                quantity=qty,
                source=source,
                destination=dest,
                description=desc,
                user=user
            )
            self.add_item(movement, cursor=cursor)
        except Exception as e:
            logger.error(f"Stok hareketi loglanamadı: {e}")
            raise e
class StockManager(BaseManager):
    """
    Genelleştirilmiş Stok Yönetimi.
    Product, Ziynet ve Scrap tiplerini tek çatı altında yönetir.
    """
    def __init__(self, data_manager: DataManager, stock_type: str = StockType.PRODUCT):
        self.stock_type_filter = stock_type
        super().__init__(data_manager, "stock_items", StockItem)
        self.depot_manager = DepotManager(data_manager)
    def _stock_type_value(self) -> str:
        try:
            return str(self.stock_type_filter.value)
        except Exception:
            return str(self.stock_type_filter)
    def get_all(self) -> List[StockItem]:
        """Sadece ilgili ürün tipine ait stokları (bellekteki) döndürür."""
        sql = """
            SELECT *
            FROM stock_items
            WHERE profile_id = %s AND is_deleted = 0 AND stock_type = %s
        """
        rows = self.db.fetch_all(sql, (self.profile_id, self._stock_type_value()))
        results: List[StockItem] = []
        for row in rows:
            obj = self._map_sql_to_dataclass(row)
            if obj:
                results.append(obj)
        return results
    def get_page(
        self,
        page: int = 1,
        page_size: int = 50,
        filters: Optional[Dict[str, Any]] = None,
    ) -> Tuple[List[StockItem], int]:
        """
        Stok listesini sayfalayarak çeker (SQL seviyesinde stock_type filtresi zorunlu).

        Not: UI tarafında sonradan filtreleme yapılmaz; bu manager hangi stock_type için oluşturulduysa,
        sorgu her zaman o tipe WHERE ile filtrelenir.
        """
        merged_filters = dict(filters or {})
        merged_filters["stock_type"] = self._stock_type_value()
        logger.debug(
            "StockManager.get_page stock_type=%s page=%s page_size=%s filters=%s",
            merged_filters.get("stock_type"),
            page,
            page_size,
            {k: v for k, v in merged_filters.items() if v is not None},
        )
        return super().get_page(page=page, page_size=page_size, filters=merged_filters)
    def _custom_map_to_sql(self, item: Any, sql_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """StockItem tablosu için özel eşleştirme."""
        data = asdict(item)
        sql_data['item_type_name'] = data.get('item_type')
        if 'item_type' in sql_data: del sql_data['item_type']
        depot_name = data.get('depot_name')
        if depot_name:
            res = self.db.fetch_one("SELECT id FROM depots WHERE name=%s AND profile_id=%s",
                                  (depot_name, self.profile_id))
            sql_data['depot_id'] = res['id'] if res else None
        if 'depot_name' in sql_data: del sql_data['depot_name']
        sql_data['stock_type'] = self._stock_type_value()
        return sql_data
    def _custom_map_from_sql(self, row: Dict[str, Any]) -> Optional[Any]:
        """SQL'den StockItem nesnesine dönüşüm."""
        args = {}
        args['item_type'] = row.get('item_type_name')
        args['stock_type'] = row.get('stock_type') or self._stock_type_value()
        args['unique_barcode'] = row.get('unique_barcode')
        args['quantity'] = float(row.get('quantity', 0))
        args['quantity_gr'] = float(row.get('quantity_gr', 0))
        args['purchase_price_base'] = float(row.get('purchase_price_base', 0))
        args['id'] = row.get('id')
        args['created_at'] = str(row.get('created_at'))
        depot_id = row.get('depot_id')
        if depot_id:
            res = self.db.fetch_one(
                "SELECT name FROM depots WHERE id=%s AND profile_id=%s AND is_deleted=0",
                (depot_id, self.profile_id),
            )
            args['depot_name'] = res['name'] if res else "Unknown"
        else:
            args['depot_name'] = get_default_depot_name()
        return self.data_class(**args)
    def find_by_attribute(self, attr_name: str, value: Any) -> Optional[Any]:
        col_name = attr_name
        if attr_name == "item_type":
            col_name = "item_type_name"
        sql = f"SELECT * FROM stock_items WHERE {col_name} = %s AND profile_id = %s AND is_deleted = 0 AND stock_type = %s LIMIT 1"
        row = self.db.fetch_one(sql, (value, self.profile_id, self._stock_type_value()))
        if row:
            return self._map_sql_to_dataclass(row)
        return None
    def get_stock_by_type_and_depot(self, item_type: str, depot_name: str) -> Optional[StockItem]:
        """
        Belirtilen ürün ve depo için TOPLAM stoku hesaplayıp hayali bir StockItem döner.
        Ortalama maliyet, satır bazındaki gerçek fiyatlama birimine göre ağırlıklandırılır:
        - Barkodlu/gramlı satırlarda `quantity_gr`
        - Diğer satırlarda `quantity`
        """
        depot_id = self.depot_manager.get_id_by_name(depot_name)
        if not depot_id:
            return None
        query = """
            SELECT
                COALESCE(SUM(quantity), 0) AS total_qty,
                COALESCE(SUM(quantity_gr), 0) AS total_gram,
                COALESCE(SUM(
                    (CASE WHEN COALESCE(quantity_gr, 0) > 0 THEN quantity_gr ELSE quantity END)
                    * purchase_price_base
                ), 0) AS total_val,
                COALESCE(SUM(
                    CASE WHEN COALESCE(quantity_gr, 0) > 0 THEN quantity_gr ELSE quantity END
                ), 0) AS total_basis_qty
            FROM stock_items
            WHERE item_type_name = %s AND depot_id = %s AND profile_id = %s AND is_deleted = 0 AND stock_type = %s
        """
        res = self.db.fetch_one(query, (item_type, depot_id, self.profile_id, self._stock_type_value()))
        total_qty = float((res or {}).get('total_qty') or 0.0)
        if total_qty > 0:
            total_basis_qty = float((res or {}).get('total_basis_qty') or 0.0)
            total_val = float((res or {}).get('total_val') or 0.0)
            avg_price = (total_val / total_basis_qty) if total_basis_qty > 1e-9 else 0.0
            return StockItem(
                item_type=item_type,
                quantity=total_qty,
                quantity_gr=float((res or {}).get('total_gram') or 0.0),
                depot_name=depot_name,
                purchase_price_base=avg_price
            )
        return None
    def add_or_update_stock(self, item_type: str, quantity: float, purchase_price_base: float, depot_name: str, cursor=None) -> None:
        """
        Stok ekler veya günceller (Atomic Transaction Safe).
        Args:
            item_type (str): Ürün adı (Örn: 'Çeyrek Altın')
            quantity (float): Eklenecek miktar (Adet veya Gram)
            purchase_price_base (float): Baz para birimindeki birim maliyet
            depot_name (str): Hedef depo adı
            cursor: Opsiyonel DB imleci (Transaction bütünlüğü için)
        """
        if quantity <= 0: return
        depot_id = self.depot_manager.get_id_by_name(depot_name)
        if not depot_id:
            logger.error(f"Depo bulunamadı: {depot_name}")
            return
        try:
            if cursor:
                self._execute_add_stock_sql(cursor, depot_id, item_type, quantity, purchase_price_base)
            else:
                with self.db.transaction_scope() as new_cursor:
                    self._execute_add_stock_sql(new_cursor, depot_id, item_type, quantity, purchase_price_base)
            if not cursor:
                self.get_all() # Bellek tazele (Transaction dışıysa)
        except Exception as e:
            logger.error(f"Stok ekleme hatası: {e}")
            raise e
    def _execute_add_stock_sql(self, cursor, depot_id, item_type, quantity, purchase_price_base):
        """
        Yardımcı SQL metod.
        FAZ 7: Atomic Update (quantity = quantity + val) kullanılarak Race Condition önlendi.
        """
        check_sql = """
            SELECT id FROM stock_items
            WHERE profile_id = %s AND depot_id = %s AND item_type_name = %s
            AND stock_type = %s
            AND ABS(purchase_price_base - %s) < 0.0001
            AND (unique_barcode IS NULL OR unique_barcode = '')
            LIMIT 1
            FOR UPDATE
        """
        cursor.execute(check_sql, (self.profile_id, depot_id, item_type, self._stock_type_value(), purchase_price_base))
        existing = cursor.fetchone()
        if existing:
            row_id = str(existing["id"])
            before_row = fetch_row_snapshot_by_id(
                db=self.db,
                table_name="stock_items",
                record_id=row_id,
                profile_id=self.profile_id,
                cursor=cursor,
            )
            # Aynı anahtarda soft-deleted satır varsa tekrar görünür hale getir.
            update_sql = """
                UPDATE stock_items
                SET quantity = quantity + %s,
                    is_deleted = 0,
                    updated_at = NOW()
                WHERE id = %s
            """
            cursor.execute(update_sql, (quantity, row_id))
            after_row = fetch_row_snapshot_by_id(
                db=self.db,
                table_name="stock_items",
                record_id=row_id,
                profile_id=self.profile_id,
                cursor=cursor,
            )
            queue_row_sync_event(
                db=self.db,
                table_name="stock_items",
                profile_id=self.profile_id,
                action_type=SYNC_ACTION_UPDATE,
                record_id=row_id,
                before_row=before_row,
                after_row=after_row,
                cursor=cursor,
                meta={"source": "StockManager._execute_add_stock_sql"},
            )
        else:
            new_id = str(uuid.uuid4())
            insert_sql = """
                INSERT INTO stock_items (id, profile_id, depot_id, stock_type, item_type_name, quantity, purchase_price_base, unique_barcode, created_at)
                VALUES (%s, %s, %s, %s, %s, %s, %s, '', NOW())
            """
            cursor.execute(insert_sql, (new_id, self.profile_id, depot_id, self._stock_type_value(), item_type, quantity, purchase_price_base))
            after_row = fetch_row_snapshot_by_id(
                db=self.db,
                table_name="stock_items",
                record_id=new_id,
                profile_id=self.profile_id,
                cursor=cursor,
            )
            queue_row_sync_event(
                db=self.db,
                table_name="stock_items",
                profile_id=self.profile_id,
                action_type=SYNC_ACTION_ADD,
                record_id=new_id,
                before_row=None,
                after_row=after_row,
                cursor=cursor,
                meta={"source": "StockManager._execute_add_stock_sql"},
            )
    def remove_stock(self, item_type: str, quantity: float, depot_name: str, cursor=None) -> bool:
        """
        Stoktan düşüm yapar (Transaction Uyumlu).
        """
        if quantity <= 0: return False
        depot_id = self.depot_manager.get_id_by_name(depot_name)
        if not depot_id: return False
        try:
            if cursor:
                return self._execute_remove_stock_sql(cursor, depot_id, item_type, quantity)
            else:
                with self.db.transaction_scope() as new_cursor:
                    result = self._execute_remove_stock_sql(new_cursor, depot_id, item_type, quantity)
                if result: self.get_all()
                return result
        except Exception as e:
            logger.error(f"Stok düşme hatası: {e}")
            return False
    def _execute_remove_stock_sql(self, cursor, depot_id, item_type, quantity):
        """
        Stok düşüm işlemi (FIFO).
        FAZ 2 DÜZELTME: Miktarı biten stokları silmek yerine quantity=0 yapar.
        Böylece geçmiş maliyet ve işlem izleri (Traceability) korunur.
        """
        fetch_sql = """
            SELECT id, quantity FROM stock_items
            WHERE profile_id = %s AND depot_id = %s AND item_type_name = %s
            AND stock_type = %s
            AND quantity > 0 AND is_deleted = 0
            ORDER BY created_at ASC
            FOR UPDATE
        """
        cursor.execute(fetch_sql, (self.profile_id, depot_id, item_type, self._stock_type_value()))
        rows = cursor.fetchall()
        total_available = sum(float(r['quantity']) for r in rows)
        if total_available < quantity:
            raise ValueError(f"Yetersiz stok! İstenen: {quantity}, Mevcut: {total_available}")
        remaining = quantity
        for row in rows:
            if remaining <= 0.0001: break
            row_id = str(row["id"])
            before_row = fetch_row_snapshot_by_id(
                db=self.db,
                table_name="stock_items",
                record_id=row_id,
                profile_id=self.profile_id,
                cursor=cursor,
            )
            curr_qty = float(row['quantity'])
            if curr_qty > remaining:
                new_qty = curr_qty - remaining
                cursor.execute("UPDATE stock_items SET quantity = %s WHERE id = %s", (new_qty, row_id))
                remaining = 0
            else:
                cursor.execute("UPDATE stock_items SET quantity = 0 WHERE id = %s", (row_id,))
                remaining -= curr_qty
            after_row = fetch_row_snapshot_by_id(
                db=self.db,
                table_name="stock_items",
                record_id=row_id,
                profile_id=self.profile_id,
                cursor=cursor,
            )
            queue_row_sync_event(
                db=self.db,
                table_name="stock_items",
                profile_id=self.profile_id,
                action_type=SYNC_ACTION_UPDATE,
                record_id=row_id,
                before_row=before_row,
                after_row=after_row,
                cursor=cursor,
                meta={"source": "StockManager._execute_remove_stock_sql"},
            )
        return True
    def transfer_stock_bulk(
        self,
        item_type: str,
        quantity: float,
        source_depot_name: str,
        dest_depot_name: str,
        cursor=None,
    ) -> float:
        """
        Barkodsuz stok transferi (FIFO maliyetle).
        Kaynak depodan düşer, hedef depoya ortalama birim maliyetle ekler.
        Returns:
            float: Transfer edilen stok için hesaplanan birim maliyet.
        """
        if quantity <= 0:
            return 0.0
        source_depot_id = self.depot_manager.get_id_by_name(source_depot_name)
        dest_depot_id = self.depot_manager.get_id_by_name(dest_depot_name)
        if not source_depot_id or not dest_depot_id:
            raise ValueError("Depo bulunamadı.")
        if cursor:
            return self._execute_transfer_stock_sql(cursor, source_depot_id, dest_depot_id, item_type, quantity)
        with self.db.transaction_scope() as new_cursor:
            unit_cost = self._execute_transfer_stock_sql(
                new_cursor, source_depot_id, dest_depot_id, item_type, quantity
            )
        self.get_all()
        return unit_cost
    def _execute_transfer_stock_sql(self, cursor, source_depot_id, dest_depot_id, item_type, quantity) -> float:
        fetch_sql = """
            SELECT id, quantity, purchase_price_base FROM stock_items
            WHERE profile_id = %s AND depot_id = %s AND item_type_name = %s
            AND stock_type = %s
            AND quantity > 0 AND is_deleted = 0
            ORDER BY created_at ASC
            FOR UPDATE
        """
        cursor.execute(fetch_sql, (self.profile_id, source_depot_id, item_type, self._stock_type_value()))
        rows = cursor.fetchall()
        total_available = sum(float(r["quantity"]) for r in rows)
        if total_available < quantity:
            raise ValueError(f"Yetersiz stok! İstenen: {quantity}, Mevcut: {total_available}")
        remaining = quantity
        total_cost = 0.0
        for row in rows:
            if remaining <= 0.0001:
                break
            row_id = str(row["id"])
            before_row = fetch_row_snapshot_by_id(
                db=self.db,
                table_name="stock_items",
                record_id=row_id,
                profile_id=self.profile_id,
                cursor=cursor,
            )
            curr_qty = float(row["quantity"])
            unit_price = float(row.get("purchase_price_base") or 0)
            take = remaining if curr_qty > remaining else curr_qty
            total_cost += take * unit_price
            if curr_qty > remaining:
                new_qty = curr_qty - remaining
                cursor.execute("UPDATE stock_items SET quantity = %s WHERE id = %s", (new_qty, row_id))
                remaining = 0
            else:
                cursor.execute("UPDATE stock_items SET quantity = 0 WHERE id = %s", (row_id,))
                remaining -= curr_qty
            after_row = fetch_row_snapshot_by_id(
                db=self.db,
                table_name="stock_items",
                record_id=row_id,
                profile_id=self.profile_id,
                cursor=cursor,
            )
            queue_row_sync_event(
                db=self.db,
                table_name="stock_items",
                profile_id=self.profile_id,
                action_type=SYNC_ACTION_UPDATE,
                record_id=row_id,
                before_row=before_row,
                after_row=after_row,
                cursor=cursor,
                meta={"source": "StockManager._execute_transfer_stock_sql"},
            )
        unit_cost = total_cost / quantity if quantity > 0 else 0.0
        self._execute_add_stock_sql(cursor, dest_depot_id, item_type, quantity, unit_cost)
        return unit_cost
    def remove_stock_by_barcodes(self, barcodes: List[str], depot_name: str, cursor=None) -> bool:
        """Barkodlu silme (Transaction Uyumlu)."""
        if not barcodes: return False
        depot_id = self.depot_manager.get_id_by_name(depot_name)
        if not depot_id: return False
        placeholders = ', '.join(['%s'] * len(barcodes))
        sql = f"""
            DELETE FROM stock_items
            WHERE profile_id = %s AND depot_id = %s AND stock_type = %s AND unique_barcode IN ({placeholders})
        """
        params = [self.profile_id, depot_id, self._stock_type_value()] + barcodes

        snapshot_sql = f"""
            SELECT *
            FROM stock_items
            WHERE profile_id = %s AND depot_id = %s AND stock_type = %s AND unique_barcode IN ({placeholders})
            FOR UPDATE
        """

        def _queue_deleted_rows(cur, deleted_rows: List[Dict[str, Any]]) -> None:
            for row in deleted_rows:
                row_id = str(row.get("id") or "")
                if not row_id:
                    continue
                queue_row_sync_event(
                    db=self.db,
                    table_name="stock_items",
                    profile_id=self.profile_id,
                    action_type=SYNC_ACTION_DELETE,
                    record_id=row_id,
                    before_row=dict(row),
                    after_row=None,
                    cursor=cur,
                    meta={"source": "StockManager.remove_stock_by_barcodes"},
                )

        try:
            if cursor:
                cursor.execute(snapshot_sql, tuple(params))
                deleted_rows = cursor.fetchall() or []
                cursor.execute(sql, tuple(params))
                deleted_count = int(cursor.rowcount or 0)
                if deleted_count < len(barcodes):
                    raise ValueError(
                        f"Barkod stok düşümü eksik: istenen={len(barcodes)}, düşen={deleted_count}"
                    )
                _queue_deleted_rows(cursor, deleted_rows)
                return True
            else:
                with self.db.transaction_scope() as new_cursor:
                    new_cursor.execute(snapshot_sql, tuple(params))
                    deleted_rows = new_cursor.fetchall() or []
                    new_cursor.execute(sql, tuple(params))
                    deleted_count = int(new_cursor.rowcount or 0)
                    if deleted_count < len(barcodes):
                        raise ValueError(
                            f"Barkod stok düşümü eksik: istenen={len(barcodes)}, düşen={deleted_count}"
                        )
                    _queue_deleted_rows(new_cursor, deleted_rows)
                self.get_all()
                return True
        except Exception as e:
            logger.error(f"Barkod silme hatası: {e}")
            raise e
    def generate_bulk_barcodes(self, main_barcode_prefix: str, quantity: int) -> List[str]:
        """
        Toplu barkod üretimi (Sadece string listesi döndürür, DB'ye yazmaz).
        Mevcut en büyük numarayı bulup üstüne ekler.
        """
        pattern = f"{main_barcode_prefix}/%"
        sql = """
            SELECT unique_barcode FROM stock_items
            WHERE profile_id = %s AND stock_type = %s AND unique_barcode LIKE %s
        """
        rows = self.db.fetch_all(sql, (self.profile_id, self._stock_type_value(), pattern))
        max_num = 0
        for row in rows:
            code = row['unique_barcode']
            try:
                parts = code.split('/')
                if len(parts) > 1 and parts[-1].isdigit():
                    num = int(parts[-1])
                    if num > max_num: max_num = num
            except: pass
        generated_list = []
        for i in range(1, quantity + 1):
            next_num = max_num + i
            generated_list.append(f"{main_barcode_prefix}/{next_num:03d}")
        return generated_list
class BarcodeHistoryManager(BaseManager):
    """
    Barkod tarihçesini (Alış, Satış, Hareketler) yönetir.
    JSON alanını (movements_json) doğru şekilde günceller.
    """
    def __init__(self, data_manager: DataManager):
        super().__init__(data_manager, "barcode_history", BarcodeHistory)
    def _custom_map_to_sql(self, item: Any, sql_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        """BarcodeHistory tablosu için JSON alan eşleştirme."""
        if "movements" in sql_data:
            sql_data["movements_json"] = sql_data.pop("movements") or []
        return sql_data
    def _custom_map_from_sql(self, row: Dict[str, Any]) -> Optional[Any]:
        """SQL'den BarcodeHistory nesnesine dönüşüm."""
        def to_float(val, default=0.0):
            try:
                if val is None:
                    return default
                return float(val)
            except Exception:
                return default
        def fmt_dt(val):
            if not val:
                return ""
            if isinstance(val, datetime):
                return val.strftime("%d.%m.%Y %H:%M")
            return str(val)
        movements: List[Dict[str, Any]] = []
        raw_mov = row.get("movements_json")
        if raw_mov:
            try:
                movements = json.loads(raw_mov) if isinstance(raw_mov, str) else raw_mov
            except Exception:
                movements = []
        return self.data_class(
            barcode=row.get("barcode", "") or "",
            product_type=row.get("product_type", "") or "",
            gram=to_float(row.get("gram")),
            karat=row.get("karat", "") or "",
            buy_date=fmt_dt(row.get("buy_date")),
            buy_from=row.get("buy_from", "") or "",
            buy_price=to_float(row.get("buy_price")),
            buy_method=row.get("buy_method", "") or "",
            is_sold=bool(row.get("is_sold", 0)),
            sell_date=fmt_dt(row.get("sell_date")),
            sell_to=row.get("sell_to", "") or "",
            sell_price=to_float(row.get("sell_price")),
            sell_method=row.get("sell_method", "") or "",
            movements=movements or [],
            id=row.get("id") or str(uuid.uuid4()),
            created_at=str(row.get("created_at") or ""),
            updated_at=str(row.get("updated_at") or ""),
            profile_id=row.get("profile_id") or "",
        )
    def get_history(self, barcode: str, cursor=None) -> Optional[BarcodeHistory]:
        """Barkoda göre tarihçeyi getirir."""
        sql = "SELECT * FROM barcode_history WHERE profile_id = %s AND barcode = %s"
        if cursor:
            cursor.execute(sql, (self.profile_id, barcode))
            row = cursor.fetchone()
        else:
            row = self.db.fetch_one(sql, (self.profile_id, barcode))
        if row:
            return self._map_sql_to_dataclass(row)
        return None
    def _parse_event_datetime(self, value: Any) -> Optional[datetime]:
        """Try to parse datetime from display or ISO formats."""
        if isinstance(value, datetime):
            return value
        if not value:
            return None
        s = str(value).strip()
        if not s:
            return None
        for fmt in ("%d.%m.%Y %H:%M:%S", "%d.%m.%Y %H:%M", "%d.%m.%Y"):
            try:
                dt = datetime.strptime(s, fmt)
                if fmt == "%d.%m.%Y":
                    now = datetime.now()
                    dt = dt.replace(hour=now.hour, minute=now.minute, second=now.second, microsecond=0)
                return dt
            except ValueError:
                pass
        try:
            if "T" in s or ":" in s:
                return datetime.fromisoformat(s.replace("Z", ""))
        except ValueError:
            pass
        return None
    def _format_event_datetime(self, value: Any) -> str:
        dt = self._parse_event_datetime(value) or datetime.now()
        return dt.strftime("%d.%m.%Y %H:%M")
    def _is_sell_movement(self, movement: Dict[str, Any]) -> bool:
        event = str(movement.get("event", "") or "").strip().lower()
        if event == "sell":
            return True
        desc = str(movement.get("desc", "") or "").strip().lower()
        return ("sat" in desc and ("cik" in desc or "çik" in desc))
    def get_sold_entries(self, date_str: Optional[str] = None, depot_name: Optional[str] = None) -> List[Dict[str, Any]]:
        """
        Returns sold barcode entries by reading movement history.
        This keeps older days visible even if barcode later re-enters stock.
        """
        wanted_day = str(date_str or "").strip()
        wanted_depot = str(depot_name or "").strip().lower()
        results: List[Dict[str, Any]] = []
        seen = set()
        for rec in self.get_all():
            rec_movements = rec.movements if isinstance(rec.movements, list) else []
            sale_moves: List[Dict[str, Any]] = [m for m in rec_movements if isinstance(m, dict) and self._is_sell_movement(m)]

            # If current state is unsold and last movement is a sell,
            # this trailing sell usually belongs to a reverted operation.
            if not rec.is_sold and rec_movements:
                last_mv = rec_movements[-1] if isinstance(rec_movements[-1], dict) else {}
                if self._is_sell_movement(last_mv):
                    last_dt = str(last_mv.get("date", "") or "").strip()
                    sale_moves = [m for m in sale_moves if str(m.get("date", "") or "").strip() != last_dt]

            has_sale_moves = len(sale_moves) > 0

            for mv in sale_moves:
                sell_dt_text = str(mv.get("date", "") or "").strip()
                if not sell_dt_text:
                    continue
                sell_day = sell_dt_text.split(" ")[0]
                if wanted_day and sell_day != wanted_day:
                    continue

                raw_depot = str(mv.get("depot") or mv.get("from") or "").strip()
                depot = "" if raw_depot.lower() in {"depo", "stok", "stock"} else raw_depot
                if wanted_depot and depot.lower() != wanted_depot:
                    continue

                entry = {
                    "sell_date": sell_dt_text,
                    "barcode": rec.barcode,
                    "product_type": rec.product_type,
                    "sell_to": str(mv.get("target") or mv.get("to") or rec.sell_to or "").strip(),
                    "sell_method": str(mv.get("method") or rec.sell_method or "").strip(),
                    "depot": depot,
                }
                key = (entry["barcode"], entry["sell_date"], entry["sell_to"], entry["sell_method"], entry["depot"])
                if key in seen:
                    continue
                seen.add(key)
                results.append(entry)

            # Backward compatibility: only for very old rows that do not carry sell movements.
            if (not has_sale_moves) and rec.sell_date:
                sell_day = rec.sell_date.split(" ")[0]
                if wanted_day and sell_day != wanted_day:
                    continue
                key = (rec.barcode, rec.sell_date, rec.sell_to, rec.sell_method, "")
                if key in seen:
                    continue
                if not wanted_depot:
                    seen.add(key)
                    results.append({
                        "sell_date": rec.sell_date,
                        "barcode": rec.barcode,
                        "product_type": rec.product_type,
                        "sell_to": rec.sell_to,
                        "sell_method": rec.sell_method,
                        "depot": "",
                    })

        results.sort(key=lambda x: self._parse_event_datetime(x.get("sell_date")) or datetime.min, reverse=True)
        return results
    def log_movement(self, barcode: str, from_loc: str, to_loc: str, desc: str, user: str = "Admin"):
        """Bir barkod için hareket kaydı ekler."""
        record = self.get_history(barcode)
        if record:
            move = {
                "date": datetime.now().strftime("%d.%m.%Y %H:%M"),
                "from": from_loc,
                "to": to_loc,
                "desc": desc,
                "user": user
            }
            record.movements.append(move)
            self.update_item(record.id, {"movements": record.movements})
    def register_buy(self, barcode, p_type, gram, karat, source, price, method, cursor=None):
        """Alış kaydı (Transaction Uyumlu)."""
        record = self.get_history(barcode, cursor=cursor)
        if not record:
            new_record = BarcodeHistory(
                barcode=barcode, product_type=p_type, gram=float(gram), karat=karat,
                buy_date=datetime.now().strftime("%d.%m.%Y %H:%M"), buy_from=source,
                buy_price=float(price), buy_method=method,
                movements=[{
                    "date": datetime.now().strftime("%d.%m.%Y %H:%M"),
                    "from": source, "to": get_default_depot_name(),
                    "desc": f"İlk Giriş ({method})", "user": "Sistem"
                }]
            )
            self.add_item(new_record, cursor=cursor)
        else:
            updates = {"is_sold": False, "buy_date": datetime.now().strftime("%d.%m.%Y %H:%M")}
            record.movements.append({
                "date": datetime.now().strftime("%d.%m.%Y %H:%M"),
                "from": source, "to": "Depo",
                "desc": f"Tekrar Giriş ({method})", "user": "Sistem"
            })
            updates["movements"] = record.movements
            self.update_item(record.id, updates, cursor=cursor)
    def register_sell(self, barcode, target, price, method, cursor=None, depot: Optional[str] = None, sold_at: Optional[Any] = None, user: str = "Sistem"):
        """Satış kaydı (Transaction Uyumlu)."""
        record = self.get_history(barcode, cursor=cursor)
        if record:
            sell_dt_text = self._format_event_datetime(sold_at)
            source_depot = str(depot or "Depo").strip() or "Depo"
            target_name = str(target or "").strip()
            method_name = str(method or "").strip()
            try:
                sell_price = float(price)
            except Exception:
                sell_price = 0.0

            record.movements.append({
                "date": sell_dt_text,
                "from": source_depot,
                "to": target_name,
                "desc": f"Satış/Çıkış ({method_name})",
                "user": user,
                "event": "sell",
                "depot": source_depot,
                "target": target_name,
                "method": method_name,
                "price": sell_price,
            })
            updates = {
                "is_sold": True,
                "sell_date": sell_dt_text,
                "sell_to": target_name,
                "sell_price": sell_price,
                "sell_method": method_name,
                "movements": record.movements
            }
            self.update_item(record.id, updates, cursor=cursor)

class BulkBarcodeDialog(QDialog):
    """Toplu Barkod Oluşturma ve Yazdırma Penceresi."""
    def __init__(self, product_manager: "ProductDefinitionManager", stock_manager: "StockManager", lang: Dict[str, str], parent=None):
        super().__init__(parent)
        self.product_manager = product_manager
        self.stock_manager = stock_manager
        self.lang = lang
        self.setWindowTitle("Toplu Barkod Basımı")
        self.setMinimumWidth(450)
        self.setMinimumHeight(550)
        self._init_ui()
    def _init_ui(self):
        layout = QVBoxLayout(self)
        form_group = QGroupBox("Barkod Ayarları")
        form_layout = QFormLayout(form_group)
        self.product_combo = QComboBox()
        self.product_combo.setEditable(True)
        products = [p for p in self.product_manager.get_all() if p.barcode_header]
        for p in products:
            self.product_combo.addItem(f"{p.product_name} ({p.barcode_header})", p.barcode_header)
        self.quantity_spin = QSpinBox()
        self.quantity_spin.setRange(1, 1000)
        self.quantity_spin.setValue(10)
        form_layout.addRow("Ürün Seç (Ana Barkodlu):", self.product_combo)
        form_layout.addRow("Basılacak Adet:", self.quantity_spin)
        gen_btn = QPushButton("Barkodları Oluştur / Önizle")
        gen_btn.setDefault(True)
        gen_btn.setAutoDefault(True)
        gen_btn.clicked.connect(self.generate_preview)
        form_layout.addRow(gen_btn)
        if self.product_combo.isEditable() and self.product_combo.lineEdit():
            self.product_combo.lineEdit().returnPressed.connect(self.generate_preview)
        try:
            spin_edit = self.quantity_spin.lineEdit()
        except Exception:
            spin_edit = None
        if spin_edit:
            spin_edit.returnPressed.connect(self.generate_preview)
        layout.addWidget(form_group)
        self.list_widget = QListWidget()
        layout.addWidget(QLabel("Oluşturulacak Barkodlar:"))
        layout.addWidget(self.list_widget)
        btn_layout = QHBoxLayout()
        print_btn = QPushButton("Etiketleri Kaydet (PDF)")
        print_btn.setIcon(QIcon.fromTheme("document-save"))
        print_btn.clicked.connect(lambda: self.print_labels(auto_print=False))
        auto_print_btn = QPushButton("Barkodları Bas (Otomatik)")
        auto_print_btn.setIcon(QIcon.fromTheme("document-print"))
        auto_print_btn.setStyleSheet("background-color: #D32F2F; color: white; font-weight: bold; padding: 10px;")
        auto_print_btn.clicked.connect(lambda: self.print_labels(auto_print=True))
        close_btn = QPushButton("Kapat")
        close_btn.clicked.connect(self.reject)
        btn_layout.addWidget(print_btn)
        btn_layout.addWidget(auto_print_btn)
        btn_layout.addWidget(close_btn)
        layout.addLayout(btn_layout)
    def generate_preview(self):
        main_barcode = self.product_combo.currentData()
        if not main_barcode:
            QMessageBox.warning(self, "Uyarı", "Lütfen barkodu tanımlı bir ürün seçin.")
            return
        qty = self.quantity_spin.value()
        codes = self.stock_manager.generate_bulk_barcodes(main_barcode, qty)
        self.list_widget.clear()
        self.list_widget.addItems(codes)
    def print_labels(self, auto_print=False):
        """Barkodları PDF yapar."""
        if self.list_widget.count() == 0:
            self.generate_preview()
            if self.list_widget.count() == 0: return
        if not HAS_REPORTLAB:
            QMessageBox.critical(self, "Hata", "ReportLab kütüphanesi eksik.")
            return
        if auto_print:
            import tempfile
            filename = os.path.join(tempfile.gettempdir(), f"AutoPrint_Barkod_{datetime.now().strftime('%H%M%S')}.pdf")
        else:
            filename, _ = QFileDialog.getSaveFileName(self, "Etiketleri Kaydet", f"Barkodlar_{datetime.now().strftime('%H%M')}.pdf", "PDF (*.pdf)")
            if not filename: return
        try:
            product_text = self.product_combo.currentText().split('(')[0].strip()
            c = canvas.Canvas(filename, pagesize=(40*mm, 25*mm))
            if auto_print:
                js_code = "this.print({bUI:true,bSilent:false,bShrinkToFit:true});"
                c._doc.Catalog['OpenAction'] = pdfdoc.PDFDictionary({
                    'S': pdfdoc.PDFName('JavaScript'),
                    'JS': pdfdoc.PDFString(js_code)
                })
            for i in range(self.list_widget.count()):
                code = self.list_widget.item(i).text()
                c.setFont("Helvetica-Bold", 6)
                c.drawCentredString(20*mm, 22*mm, product_text[:15])
                bc = code128.Code128(code, barHeight=8*mm, barWidth=0.22*mm)
                bc.drawOn(c, (40*mm - bc.width)/2, 8*mm)
                c.setFont("Helvetica", 6)
                c.drawCentredString(20*mm, 4*mm, code)
                c.showPage()
            c.save()
            if sys.platform == 'win32':
                os.startfile(filename)
            else:
                opener = 'open' if sys.platform == 'darwin' else 'xdg-open'
                subprocess.call([opener, filename])
            if auto_print:
                threading.Thread(target=self._delayed_ctrl_p).start()
            else:
                QMessageBox.information(self, "Başarılı", "Etiketler PDF olarak kaydedildi.")
        except Exception as e:
            logger.error(f"Yazdırma hatası: {e}")
            QMessageBox.critical(self, "Hata", f"İşlem başarısız: {e}")
    def _delayed_ctrl_p(self):
        if not HAS_PYAUTOGUI: return
        time.sleep(3)
        try:
            pyautogui.hotkey('ctrl', 'p')
        except: pass
class ManualStockDialog(QDialog):
    """
    Manuel Stok Giriş/Çıkış Penceresi.
    """
    def __init__(self, mode, stock_type, product_list, depot_list, lang, parent=None):
        super().__init__(parent)
        self.mode = mode  # "In" veya "Out"
        self.lang = lang
        action = "Girişi (Ekleme)" if mode == "In" else "Çıkışı (Düşme)"
        stock_label = "Ürün" if stock_type == "Product" else "Hurda"
        self.setWindowTitle(f"Manuel {stock_label} {action}")
        self.setMinimumWidth(400)
        layout = QFormLayout(self)
        self.depot_combo = QComboBox()
        self.depot_combo.addItems(depot_list)
        layout.addRow("İşlem Deposu:", self.depot_combo)
        self.product_combo = QComboBox()
        self.product_combo.setEditable(True)
        self.product_combo.addItems(product_list)
        layout.addRow(f"{stock_label} Seçimi:", self.product_combo)
        self.qty_entry = QLineEdit()
        self.qty_entry.setValidator(QDoubleValidator(0, 999999999, 3))
        self.qty_entry.setPlaceholderText("0.00")
        qty_label = "Adet/Gram (Kapanmis barkod ise miktar):" if stock_type == "Product" else "Miktar (gr):"
        layout.addRow(qty_label, self.qty_entry)
        price_layout = QHBoxLayout()
        self.price_entry = QLineEdit()
        self.price_entry.setValidator(QDoubleValidator(0, 999999999, 2))
        self.price_entry.setPlaceholderText(self.lang.get("unit_price_ph", "Unit Price"))
        self.currency_combo = QComboBox()
        self.currency_combo.addItems(SUPPORTED_CURRENCIES)
        self.currency_combo.setCurrentText("TRY")
        price_layout.addWidget(self.price_entry)
        price_layout.addWidget(self.currency_combo)
        lbl_price = "Birim Maliyet (Giriş Fiyatı):" if mode == "In" else "Çıkış Değeri (Opsiyonel):"
        layout.addRow(lbl_price, price_layout)
        self.desc_entry = QLineEdit()
        self.desc_entry.setPlaceholderText("Örn: Sayım fazlası, Hediye, Kayıp vb.")
        layout.addRow("Açıklama:", self.desc_entry)
        buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        ok_btn = buttons.button(QDialogButtonBox.StandardButton.Ok)
        if ok_btn:
            ok_btn.setDefault(True)
            ok_btn.setAutoDefault(True)
        if self.product_combo.isEditable() and self.product_combo.lineEdit():
            self.product_combo.lineEdit().returnPressed.connect(self.accept)
        self.qty_entry.returnPressed.connect(self.accept)
        self.price_entry.returnPressed.connect(self.accept)
        self.desc_entry.returnPressed.connect(self.accept)
        layout.addRow(buttons)
    def get_data(self):
        try:
            qty = float(self.qty_entry.text().replace(',', '.') or 0)
            price = float(self.price_entry.text().replace(',', '.') or 0)
        except ValueError:
            qty = 0.0
            price = 0.0
        return {
            "depot": self.depot_combo.currentText(),
            "product": self.product_combo.currentText(),
            "quantity": qty,
            "description": self.desc_entry.text(),
            "price": price,
            "currency": self.currency_combo.currentText()
        }
class BarcodeCreatedDialog(QDialog):
    """Yeni bir barkod oluşturulduğunda gösterilen bilgi penceresi."""
    def __init__(self, barcode_data, parent=None):
        super().__init__(parent)
        self.barcode_data = barcode_data if isinstance(barcode_data, list) else [barcode_data]
        self.settings_manager = getattr(parent, "settings_manager", None)
        self.lang = _get_ui_lang(parent)
        self.setWindowTitle(self.lang.get("generated_barcodes_title", "Generated Barcodes"))
        self.setMinimumWidth(500)
        self.setMinimumHeight(400)
        layout = QVBoxLayout(self)
        info_label = QLabel(
            f"<b>{self.lang.get('generated_barcodes_info', '{count} barcodes created:').format(count=len(self.barcode_data))}</b>"
        )
        layout.addWidget(info_label)
        self.list_widget = QListWidget()
        for code in self.barcode_data:
            self.list_widget.addItem(str(code))
        layout.addWidget(self.list_widget)
        btn_layout = QHBoxLayout()
        print_btn = QPushButton(self.lang.get("print_pdf", "Print (PDF)"))
        print_btn.setIcon(QIcon.fromTheme("document-print"))
        print_btn.clicked.connect(self._print_barcodes)
        btn_layout.addWidget(print_btn)
        thermal_btn = QPushButton(self.lang.get("print_thermal", "Print (Thermal)"))
        thermal_btn.clicked.connect(self._print_thermal)
        btn_layout.addWidget(thermal_btn)
        btn_layout.addStretch()
        ok_btn = QPushButton(self.lang.get("ok", "OK"))
        ok_btn.setDefault(True)
        ok_btn.setAutoDefault(True)
        ok_btn.clicked.connect(self.accept)
        btn_layout.addWidget(ok_btn)
        layout.addLayout(btn_layout)
    def _print_barcodes(self):
        if not HAS_REPORTLAB:
            QMessageBox.critical(self, "Hata", "ReportLab kütüphanesi eksik. Terminalden 'pip install reportlab' komutunu çalıştırın.")
            return

        filename, _ = QFileDialog.getSaveFileName(
            self,
            "Etiketleri Kaydet",
            f"Barkodlar_{datetime.now().strftime('%H%M')}.pdf",
            "PDF (*.pdf)",
        )
        if not filename:
            return

        try:
            from reportlab.pdfgen import canvas
            from reportlab.lib.units import mm
            from reportlab.graphics.barcode import code128

            c = canvas.Canvas(filename, pagesize=(40 * mm, 25 * mm))
            for code in self.barcode_data:
                c.setFont("Helvetica-Bold", 6)
                c.drawCentredString(20 * mm, 22 * mm, "GOLDER") # İstersen buraya mağaza adını çekebiliriz
                bc = code128.Code128(code, barHeight=8 * mm, barWidth=0.22 * mm)
                bc.drawOn(c, (40 * mm - bc.width) / 2, 8 * mm)
                c.setFont("Helvetica", 6)
                c.drawCentredString(20 * mm, 4 * mm, code)
                c.showPage()
            c.save()

            if sys.platform == 'win32':
                os.startfile(filename)
            else:
                subprocess.call(['open' if sys.platform == 'darwin' else 'xdg-open', filename])

        except Exception as e:
            logger.error(f"Barkod basma hatası: {e}")
            QMessageBox.critical(self, "Hata", f"Yazdırma işlemi başarısız: {e}")

    def _print_thermal(self):
        printer_type = "ZPL"
        printer_name = ""
        if self.settings_manager:
            printer_type = str(self.settings_manager.get_setting("printer_type", "ZPL") or "ZPL")
            printer_name = str(self.settings_manager.get_setting("printer_name", "") or "").strip()

        if not printer_name:
            QMessageBox.warning(self, "Uyari", "Termal yazici adi/IP tanimli degil. Ayarlardan yazici bilgisini girin.")
            return

        printer_ip = ""
        if re.match(r"^\\d{1,3}(?:\\.\\d{1,3}){3}$", printer_name):
            printer_ip = printer_name
            printer_name = ""

        manager = ThermalPrinterManager(printer_name=printer_name, printer_ip=printer_ip)
        failed = []
        for code in self.barcode_data:
            ok = manager.print_jewelry_tag(
                product_name="GOLDER",
                barcode_val=str(code),
                karat="",
                weight="",
                price="",
                language=printer_type,
            )
            if not ok:
                failed.append(code)

        if failed:
            QMessageBox.warning(self, "Uyari", f"Bazi barkodlar yazdirilamadi: {len(failed)} adet.")
        else:
            QMessageBox.information(self, "Basarili", "Barkodlar termal yaziciya gonderildi.")
class BarcodeQueryDialog(QDialog):
    """
    Gelişmiş Barkod Yaşam Döngüsü Sorgulama.
    """
    def __init__(self, stock_manager, sale_manager, product_manager, currency_manager, stock_movement_manager, barcode_history_manager, lang, parent=None):
        super().__init__(parent)
        self.barcode_history_manager = barcode_history_manager
        self.setWindowTitle("Barkod Sorgulama")
        self.setMinimumWidth(800)
        self._init_ui()
    def _init_ui(self):
        layout = QVBoxLayout(self)
        search_layout = QHBoxLayout()
        self.barcode_input = QLineEdit()
        self.barcode_input.setPlaceholderText("Barkod okutun...")
        self.barcode_input.returnPressed.connect(self._search)
        search_btn = QPushButton("Sorgula")
        search_btn.setDefault(True)
        search_btn.setAutoDefault(True)
        search_btn.clicked.connect(self._search)
        search_layout.addWidget(self.barcode_input)
        search_layout.addWidget(search_btn)
        layout.addLayout(search_layout)
        self.info_table = QTableWidget(0, 2)
        self.info_table.setHorizontalHeaderLabels(["Özellik", "Değer"])
        self.info_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
        layout.addWidget(self.info_table)
        self.history_table = QTableWidget(0, 4)
        self.history_table.setHorizontalHeaderLabels(["Tarih", "İşlem", "Yer", "Açıklama"])
        self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        layout.addWidget(self.history_table)
    def _search(self):
        barcode = self.barcode_input.text().strip()
        if not barcode: return
        rec = self.barcode_history_manager.get_history(barcode)
        self.info_table.setRowCount(0)
        self.history_table.setRowCount(0)
        if not rec:
            QMessageBox.warning(self, "Bulunamadı", "Kayıt yok.")
            return
        infos = [
            ("Barkod", rec.barcode),
            ("Ürün", rec.product_type),
            ("Gram", f"{rec.gram} gr"),
            ("Ayar", rec.karat),
            ("Durum", "SATILDI" if rec.is_sold else "STOKTA"),
            ("Alış Fiyatı", f"{rec.buy_price:.2f}"),
            ("Satış Fiyatı", f"{rec.sell_price:.2f}" if rec.is_sold else "-")
        ]
        self.info_table.setRowCount(len(infos))
        for r, (k, v) in enumerate(infos):
            self.info_table.setItem(r, 0, QTableWidgetItem(k))
            self.info_table.setItem(r, 1, QTableWidgetItem(str(v)))
        self.history_table.setRowCount(len(rec.movements))
        for r, m in enumerate(rec.movements):
            self.history_table.setItem(r, 0, QTableWidgetItem(m.get('date', '')))
            self.history_table.setItem(r, 1, QTableWidgetItem(m.get('desc', '')))
            self.history_table.setItem(r, 2, QTableWidgetItem(f"{m.get('from','')} -> {m.get('to','')}"))
            self.history_table.setItem(r, 3, QTableWidgetItem(m.get('user', '')))
class BarcodeSelectionDialog(QDialog):
    """Birden fazla ürün aynı barkoda sahipse (Hata durumunda) seçim yaptıran pencere."""
    def __init__(self, items, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Ürün Seçimi")
        self.items = items
        layout = QVBoxLayout(self)
        self.list = QListWidget()
        for i in items:
            self.list.addItem(f"{i.item_type} - {i.depot_name} ({i.quantity})")
        layout.addWidget(self.list)
        btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
        btns.accepted.connect(self.accept)
        btns.rejected.connect(self.reject)
        ok_btn = btns.button(QDialogButtonBox.StandardButton.Ok)
        if ok_btn:
            ok_btn.setDefault(True)
            ok_btn.setAutoDefault(True)
        self.list.itemActivated.connect(lambda _item: self.accept())
        layout.addWidget(btns)
    def get_selected_item(self):
        row = self.list.currentRow()
        if row >= 0: return self.items[row]
        return None
class BarcodeSaleSelectionDialog(QDialog):
    """
    Satış yaparken hangi barkodların satılacağını seçtiren diyalog.
    [AI_DOC]
    Eğer 'use_barcode_system' False ise, ekrana çıkmadan ilk N ürünü otomatik seçer
    ve arka planda onaylayarak kapanır. Kullanıcıya rahatsızlık vermez.
    [/AI_DOC]
    """
    def __init__(self, product_name, required_qty, stock_manager, depot_name, lang, product_manager=None, parent=None):
        super().__init__(parent)
        self.product_name = product_name
        self.required_qty = required_qty
        self.stock_manager = stock_manager
        self.depot_name = depot_name
        self.selected_barcodes = []
        self.use_barcode = True

        if hasattr(parent, 'settings_manager'):
            self.use_barcode = parent.settings_manager.get_setting("use_barcode_system", True)

        self.setWindowTitle(f"Barkod Secimi: {product_name}")
        self.setMinimumWidth(600)
        self.setMinimumHeight(500)

        self._init_ui()
        self._load_data()

        if not self.use_barcode:
            count = 0
            for r in range(self.table.rowCount()):
                if count < self.required_qty:
                    self.table.cellWidget(r, 0).setChecked(True)
                    count += 1
            if count == self.required_qty:
                QTimer.singleShot(0, self.verify_selection)
            else:
                QTimer.singleShot(0, lambda: QMessageBox.warning(self, "Hata", "Yeterli stok bulunamadi."))

    def _init_ui(self):
        layout = QVBoxLayout(self)
        lbl = QLabel(f"Lutfen <b>{self.required_qty} adet</b> '{self.product_name}' seciniz:")
        layout.addWidget(lbl)
        self.table = QTableWidget(0, 3)
        self.table.setHorizontalHeaderLabels(["Sec", "Barkod", "Gramaj"])
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        layout.addWidget(self.table)
        self.lbl_status = QLabel("Secilen: 0")
        layout.addWidget(self.lbl_status)
        btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
        btns.accepted.connect(self.verify_selection)
        btns.rejected.connect(self.reject)
        ok_btn = btns.button(QDialogButtonBox.StandardButton.Ok)
        if ok_btn:
            ok_btn.setDefault(True)
            ok_btn.setAutoDefault(True)
        layout.addWidget(btns)

    def _load_data(self):
        all_items = self.stock_manager.get_all()
        candidates = [
            i for i in all_items
            if i.item_type == self.product_name and i.depot_name == self.depot_name
            and i.quantity > 0 and i.unique_barcode
        ]
        self.table.setRowCount(len(candidates))
        for r, item in enumerate(candidates):
            chk = QCheckBox()
            chk.stateChanged.connect(self._update_count)
            self.table.setCellWidget(r, 0, chk)
            self.table.setItem(r, 1, QTableWidgetItem(item.unique_barcode))
            self.table.setItem(r, 2, QTableWidgetItem(f"{item.quantity_gr:.2f}"))

    def _update_count(self):
        count = 0
        for r in range(self.table.rowCount()):
            chk = self.table.cellWidget(r, 0)
            if chk.isChecked():
                count += 1
        self.lbl_status.setText(f"Secilen: {count} / {self.required_qty}")
        if count > self.required_qty:
            self.lbl_status.setStyleSheet("color: red; font-weight: bold;")
        elif count == self.required_qty:
            self.lbl_status.setStyleSheet("color: green; font-weight: bold;")
        else:
            self.lbl_status.setStyleSheet("color: black;")

    def verify_selection(self):
        selected = []
        for r in range(self.table.rowCount()):
            chk = self.table.cellWidget(r, 0)
            if chk.isChecked():
                barcode = self.table.item(r, 1).text()
                selected.append(barcode)
        if len(selected) != self.required_qty:
            QMessageBox.warning(self, "Hata", f"Tam olarak {self.required_qty} adet secmelisiniz.")
            return
        self.selected_barcodes = selected
        self.accept()

    def get_selected_barcodes(self):
        return self.selected_barcodes
class StockBarcodeListDialog(QDialog):
    """Stoktaki barkodları listeleyen basit dialog."""
    def __init__(self, product_name, depot_name, barcode_list, parent=None):
        super().__init__(parent)
        self.setWindowTitle(f"Barkod Listesi: {product_name}")
        layout = QVBoxLayout(self)
        list_w = QListWidget()
        list_w.addItems(barcode_list)
        layout.addWidget(list_w)
        btn = QPushButton("Kapat")
        btn.setDefault(True)
        btn.setAutoDefault(True)
        btn.clicked.connect(self.accept)
        layout.addWidget(btn)
class AdvancedGoldEntryDialog(QDialog):
    """
    Altin Giris/Cikis icin detayli tablo (Barkod, Gram, Ayar).
    [AI_DOC]
    Barkod sistemi kapaliysa, sutun gizlenir ve arka planda sahte (SYS-) barkod atilir.
    Satis (Out) durumunda eger barkod kullanilmiyorsa DB'den otomatik ceker ve kilitler.
    [/AI_DOC]
    """
    def __init__(self, mode, product_name, quantity, stock_manager, product_manager, parent=None):
        super().__init__(parent)
        self.mode = mode
        self.product_name = product_name
        self.quantity = quantity
        self.stock_manager = stock_manager
        self.product_manager = product_manager
        self.lang = _get_ui_lang(parent)

        self.use_barcode = True
        if hasattr(parent, 'settings_manager'):
            self.use_barcode = parent.settings_manager.get_setting("use_barcode_system", True)

        self.barcode_history_manager = getattr(parent, "barcode_history_manager", None)
        if not self.barcode_history_manager and hasattr(parent, "main_app"):
            self.barcode_history_manager = getattr(parent.main_app, "barcode_history_manager", None)

        self.data_rows = []
        title_key = "stock_entry_details_title" if mode == "In" else "stock_exit_details_title"
        self.setWindowTitle(
            self.lang.get(title_key, "{product} Details").format(product=product_name)
        )
        self.setMinimumWidth(600)
        self.setMinimumHeight(400)

        self._init_ui()

    def _init_ui(self):
        layout = QVBoxLayout(self)
        self.table = QTableWidget(self.quantity, 3)
        self.table.setHorizontalHeaderLabels([
            self.lang.get("barcode", "Barcode"),
            self.lang.get("gram", "Gram"),
            self.lang.get("product_karat", "Karat"),
        ])
        self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
        layout.addWidget(self.table)

        default_karat = ""
        if self.product_manager:
            p = self.product_manager.find_by_attribute('product_name', self.product_name)
            if p:
                default_karat = p.karat_purity

        # Barkod Sistemi kapali ve Satis yapiliyorsa stoktaki urunleri direkt doldur ve kitle
        if self.mode == "Out" and not self.use_barcode:
            available = [i for i in self.stock_manager.get_all() if i.item_type == self.product_name and i.quantity > 0]
            if len(available) >= self.quantity:
                for r in range(self.quantity):
                    item = available[r]
                    self.table.setItem(r, 0, QTableWidgetItem(item.unique_barcode))
                    g_item = QTableWidgetItem(str(item.quantity_gr))
                    g_item.setFlags(g_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                    g_item.setBackground(Qt.GlobalColor.lightGray)
                    self.table.setItem(r, 1, g_item)

                    k_item = QTableWidgetItem(default_karat)
                    k_item.setFlags(k_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                    k_item.setBackground(Qt.GlobalColor.lightGray)
                    self.table.setItem(r, 2, k_item)
        else:
            for r in range(self.quantity):
                self.table.setItem(r, 0, QTableWidgetItem(""))
                gram_item = QTableWidgetItem("0.00")
                karat_item = QTableWidgetItem(default_karat)

                if self.mode == "Out":
                    gram_item.setText("")
                    karat_item.setText("")
                    gram_item.setFlags(gram_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                    karat_item.setFlags(karat_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
                    gram_item.setBackground(Qt.GlobalColor.lightGray)
                    karat_item.setBackground(Qt.GlobalColor.lightGray)

                self.table.setItem(r, 1, gram_item)
                self.table.setItem(r, 2, karat_item)

        self.table.cellChanged.connect(self._on_cell_changed)

        self.tm = TenantManager()
        self.btn_remote_scan = QPushButton(self.lang.get("scan_from_phone", "Scan From Phone"))
        self.btn_remote_scan.setStyleSheet("background-color: #1976D2; color: white; font-weight: bold; padding: 8px;")
        self.btn_remote_scan.clicked.connect(self._start_remote_scan)
        layout.addWidget(self.btn_remote_scan)

        if not self.use_barcode:
            self.table.setColumnHidden(0, True)
            self.btn_remote_scan.hide()

        btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
        btns.accepted.connect(self.verify)
        btns.rejected.connect(self.reject)
        _set_dialog_button_texts(btns, self.lang)
        ok_btn = btns.button(QDialogButtonBox.StandardButton.Ok)
        if ok_btn:
            ok_btn.setDefault(True)
            ok_btn.setAutoDefault(True)
        layout.addWidget(btns)

    def _on_cell_changed(self, row, column):
        if column != 0 or self.mode != "Out" or not self.use_barcode:
            return
        barcode_item = self.table.item(row, 0)
        if not barcode_item:
            return
        barcode = barcode_item.text().strip()
        if not barcode:
            return

        found_item = None
        for stock_row in self.stock_manager.get_all():
            if stock_row.unique_barcode == barcode and stock_row.item_type == self.product_name and float(stock_row.quantity or 0) > 0:
                found_item = stock_row
                break

        gram_item = self.table.item(row, 1)
        karat_item = self.table.item(row, 2)
        if not gram_item or not karat_item:
            return

        self.table.blockSignals(True)
        try:
            if found_item:
                gram_item.setText(f"{float(found_item.quantity_gr or 0):.2f}")
                p_def = self.product_manager.find_by_attribute("product_name", found_item.item_type) if self.product_manager else None
                karat_item.setText(p_def.karat_purity if p_def else "")
            else:
                gram_item.setText("")
                karat_item.setText("")
        finally:
            self.table.blockSignals(False)

    def _start_remote_scan(self):
        if not self.tm.is_web_mobile_enabled():
            message = (
                "Senkronizasyon suresi doldu. Telefon uygulamasi calismayacaktir."
                if self.tm.get_sync_status() == "expired"
                else "Bu lisans planinda telefon ve web erisimi kapali."
            )
            QMessageBox.warning(self, "Plan Yetersiz", message)
            return
        import requests
        try:
            requests.get("https://app.golder.biz", timeout=2)
        except Exception:
            QMessageBox.warning(self, "Baglanti Hatasi", "Internet baglantisi bulunamadi.")
            return

        row = self.table.currentRow()
        if row < 0:
            for r in range(self.table.rowCount()):
                if not self.table.item(r, 0).text().strip():
                    row = r
                    self.table.setCurrentCell(row, 0)
                    break
            if row < 0:
                return

        from ui.dialogs import RemoteBarcodeScannerDialog
        dlg = RemoteBarcodeScannerDialog(self)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            if dlg.scanned_barcode:
                self.table.item(row, 0).setText(dlg.scanned_barcode)
                self._on_cell_changed(row, 0)
                if row + 1 < self.table.rowCount():
                    self.table.setCurrentCell(row + 1, 0)

    def verify(self):
        rows = []
        seen_barcodes = set()

        existing_stock_barcodes = {
            str(getattr(stock_row, "unique_barcode", "") or "").strip()
            for stock_row in (self.stock_manager.get_all() if self.stock_manager else [])
            if str(getattr(stock_row, "unique_barcode", "") or "").strip()
        }

        for r in range(self.table.rowCount()):
            bc = self.table.item(r, 0).text().strip()
            gr_txt = self.table.item(r, 1).text().replace(',', '.')
            kr = self.table.item(r, 2).text().strip()

            if not self.use_barcode and not bc:
                bc = f"SYS-{uuid.uuid4().hex[:8].upper()}"

            if not bc:
                QMessageBox.warning(self, "Eksik", f"{r+1}. satirda barkod girilmedi.")
                return
            if bc in seen_barcodes:
                QMessageBox.warning(self, "Hata", "Ayni barkodu birden fazla kez girdiniz!")
                return
            seen_barcodes.add(bc)

            if self.mode == "Out":
                stock_match = next((s for s in (self.stock_manager.get_all() if self.stock_manager else [])
                                    if str(s.unique_barcode).strip() == bc and str(s.item_type) == self.product_name and float(s.quantity) > 0), None)
                if not stock_match:
                    QMessageBox.warning(self, "Hata", "Girdiginiz barkod bulunamadi veya bu urune ait degil!")
                    return
                if not gr_txt or not kr:
                    QMessageBox.warning(self, "Hata", "Bu barkod yoktur!")
                    return

            if self.mode == "In" and self.use_barcode:
                if bc in existing_stock_barcodes:
                    QMessageBox.warning(self, "Hata", f"Bu barkod ({bc}) sistemde zaten mevcut!")
                    return
                exists_in_history = False
                if self.barcode_history_manager:
                    try:
                        exists_in_history = self.barcode_history_manager.get_history(bc) is not None
                    except Exception:
                        pass
                if exists_in_history:
                    QMessageBox.warning(self, "Hata", f"Bu barkod ({bc}) sistemde zaten mevcut!")
                    return

            try:
                gr = float(gr_txt)
            except Exception:
                QMessageBox.warning(self, "Hata", f"{r+1}. satirda gramaj hatali.")
                return
            rows.append({"barcode": bc, "gram": gr, "karat": kr})

        self.data_rows = rows
        self.accept()

    def get_details(self):
        return self.data_rows





