"""Low-level DB access for the barcode WMS client.

The goal of this module is to mirror the legacy C# barcode form as closely as
possible while keeping SQL isolated from the UI.
"""

from __future__ import annotations

from dataclasses import dataclass
from runtime_support import log_exception, log_runtime_event
from typing import Any

from version_info import module_version

__version__ = module_version(__name__)


SQL_NEXT_PICKING = """
SELECT TOP (1)
    Documento,
    CodNazione,
    NAZIONE,
    Stato,
    Pallet,
    Cella,
    Ubicazione,
    Ordinamento,
    IDStato
FROM dbo.py_XMag_ViewPackingList AS pl
WHERE Ordinamento > 0
  AND IDStato = :id_stato
  AND NOT EXISTS (
      SELECT 1
      FROM dbo.py_BarcodePickingListSkip AS s
      WHERE s.Documento COLLATE Latin1_General_CI_AS =
            CAST(pl.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS
        AND s.Pallet COLLATE Latin1_General_CI_AS =
            CAST(pl.Pallet AS varchar(50)) COLLATE Latin1_General_CI_AS
        AND s.IDStato = pl.IDStato
        AND s.Risolto = 0
  )
ORDER BY Ordinamento;
"""

SQL_PICKING_BY_PALLET = """
SELECT TOP (1)
    Documento,
    CodNazione,
    NAZIONE,
    Stato,
    Pallet,
    Cella,
    Ubicazione,
    Ordinamento,
    IDStato
FROM dbo.py_XMag_ViewPackingList
WHERE Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
ORDER BY Ordinamento;
"""

SQL_TRACE_BY_PALLET = """
SELECT TOP (1)
    Pallet,
    Lotto,
    Prodotto,
    Descrizione
FROM dbo.vXTracciaProdotti
WHERE Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
ORDER BY Lotto;
"""

SQL_CURRENT_LOCATION_BY_PALLET = """
SELECT TOP (1)
    g.BarcodePallet,
    g.IDCella,
    RTRIM(c.Corsia) AS Corsia,
    RTRIM(CAST(c.Colonna AS varchar(32))) AS Colonna,
    RTRIM(CAST(c.Fila AS varchar(32))) AS Fila
FROM dbo.XMag_GiacenzaPallet AS g
LEFT JOIN dbo.Celle AS c
  ON c.ID = g.IDCella
WHERE g.BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS;
"""

SQL_OPEN_LOCATIONS_BY_PALLET = """
SELECT
    g.BarcodePallet,
    g.IDCella,
    RTRIM(c.Corsia) AS Corsia,
    RTRIM(CAST(c.Colonna AS varchar(32))) AS Colonna,
    RTRIM(CAST(c.Fila AS varchar(32))) AS Fila
FROM dbo.XMag_GiacenzaPallet AS g
LEFT JOIN dbo.Celle AS c
  ON c.ID = g.IDCella
WHERE g.BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
ORDER BY g.IDCella;
"""

SQL_OPEN_PALLETS_BY_CELL = """
SELECT
    g.BarcodePallet,
    g.IDCella
FROM dbo.XMag_GiacenzaPallet AS g
WHERE g.IDCella = :id_cella
ORDER BY g.BarcodePallet;
"""

SQL_CLOSED_PICKING_BY_PALLET = """
SELECT TOP (1)
    g.BarcodePallet,
    g.NumeroPallet,
    g.IDMagazzino,
    g.IDArea,
    g.IDCella,
    g.IDStato
FROM dbo.XMag_GiacenzaPallet AS g
WHERE g.IDCella <> 9999
  AND g.BarcodePallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
  AND EXISTS (
      SELECT 1
      FROM SAMA1.dbo.LOTSER AS ls
      INNER JOIN SAMA1.dbo.LOTTIBF AS lb
          ON lb.IDLOTSER = ls.ID
      INNER JOIN SAMA1.dbo.FATRIG AS fr
          ON lb.IDFATRIG = fr.ID
      INNER JOIN SAMA1.dbo.BAMTES AS bt
          ON bt.ID = fr.IDBAM
      WHERE bt.ANNDOC >= 2011
        AND bt.STATO = 'D'
        AND LEFT(ls.NUMSER, 6) COLLATE Latin1_General_CI_AS =
            :pallet COLLATE Latin1_General_CI_AS
  )
ORDER BY g.IDCella DESC;
"""

SQL_RESOLVE_PHYSICAL_CELL = """
DECLARE @raw int = TRY_CONVERT(int, :destination);
DECLARE @cell_id int =
    CASE
        WHEN @raw IS NULL THEN NULL
        WHEN @raw >= 9000000 THEN @raw - 9000000
        ELSE @raw
    END;

SELECT TOP (1)
    c.ID AS IDCella,
    CAST(9000000 + c.ID AS varchar(8)) AS BarcodeCella,
    9000000 + c.ID AS NumeroCella,
    CONCAT(RTRIM(c.Corsia), '.', RTRIM(CAST(c.Colonna AS varchar(32))), '.', RTRIM(CAST(c.Fila AS varchar(32)))) AS Ubicazione
FROM dbo.Celle AS c
WHERE c.ID = @cell_id
  AND c.ID <> 9999
  AND c.DelDataOra IS NULL;
"""

SQL_LEGACY_MOVE = """
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @RC int = 0;
DECLARE @RefreshPickingReservation bit = :refresh_picking_reservation;

EXEC dbo.sp_xMagGestioneMagazziniPallet
     @IDOperatore = :id_operatore,
     @BarcodeCella = :barcode_cella,
     @BarcodePallet = :barcode_pallet,
     @NumeroCella = :numero_cella,
     @RC = @RC OUTPUT;

IF @RefreshPickingReservation = 1
BEGIN
    EXEC dbo.py_sp_ControllaPrenotazionePackingListPalletNew;
END;

SELECT
    @RC AS RC,
    :barcode_cella AS BarcodeCella,
    :barcode_pallet AS BarcodePallet,
    :numero_cella AS NumeroCella;
"""

SQL_SKIP_PICKING_PALLET = """
SET NOCOUNT ON;

IF NOT EXISTS (
    SELECT 1
    FROM dbo.py_BarcodePickingListSkip
    WHERE Documento = :documento
      AND Pallet COLLATE Latin1_General_CI_AS = :pallet COLLATE Latin1_General_CI_AS
      AND IDStato = :id_stato
      AND Risolto = 0
)
BEGIN
    INSERT INTO dbo.py_BarcodePickingListSkip (
        Documento,
        Pallet,
        IDStato,
        IDOperatore,
        DataOra,
        Motivo,
        Risolto
    )
    VALUES (
        :documento,
        :pallet,
        :id_stato,
        :id_operatore,
        SYSDATETIME(),
        :motivo,
        0
    );
END;

SELECT
    :documento AS Documento,
    :pallet AS Pallet,
    :id_stato AS IDStato;
"""

SQL_ACTIVE_SKIPPED_DOCUMENT = """
SELECT TOP (1)
    s.Documento,
    COUNT(*) AS SkippedCount
FROM dbo.py_BarcodePickingListSkip AS s
WHERE s.IDStato = :id_stato
  AND s.Risolto = 0
  AND EXISTS (
      SELECT 1
      FROM dbo.py_ViewPackingListRestante AS pl
      WHERE CAST(pl.Documento AS varchar(50)) COLLATE Latin1_General_CI_AS =
            s.Documento COLLATE Latin1_General_CI_AS
        AND pl.IDStato = :id_stato
  )
GROUP BY s.Documento
ORDER BY MIN(s.DataOra);
"""

SQL_RELEASE_PICKING_DOCUMENT = """
SET NOCOUNT ON;
DECLARE @RC int = 0;

EXEC dbo.py_sp_xExePackingListPallet
    @IDOperatore = :id_operatore,
    @Documento = :documento,
    @Azione = 'S',
    @RC = @RC OUTPUT;

SELECT CAST(@RC AS int) AS RC;
"""


def _rows_to_dicts(res: dict[str, Any] | None) -> list[dict[str, Any]]:
    """Convert ``query_json`` payloads to a list of row dictionaries."""

    if not isinstance(res, dict):
        return []
    rows = res.get("rows") or []
    cols = res.get("columns") or []
    if rows and isinstance(rows[0], dict):
        return [row for row in rows if isinstance(row, dict)]
    out: list[dict[str, Any]] = []
    for row in rows:
        if isinstance(row, (list, tuple)) and cols:
            out.append({str(cols[i]): row[i] for i in range(min(len(cols), len(row)))})
    return out


@dataclass
class LegacyMoveResult:
    """Result of one movement executed through the legacy stored procedure."""

    rc: int
    barcode_cella: str
    barcode_pallet: str
    numero_cella: int


@dataclass
class DestinationCell:
    """Resolved warehouse cell accepted by the legacy movement procedure."""

    barcode_cella: str
    numero_cella: int
    id_cella: int
    ubicazione: str


class BarcodeRepository:
    """Thin async repository used by the lightweight barcode client."""

    def __init__(self, db_client):
        self.db_client = db_client

    async def fetch_next_picking(self, id_stato: int) -> dict[str, Any] | None:
        """Return the next pallet proposed by the legacy F1/F2 queue logic."""

        res = await self.db_client.query_json(SQL_NEXT_PICKING, {"id_stato": int(id_stato)})
        rows = _rows_to_dicts(res)
        return rows[0] if rows else None

    async def fetch_picking_by_pallet(self, pallet: str) -> dict[str, Any] | None:
        """Return one picking row for the given pallet, if still present in the queue."""

        res = await self.db_client.query_json(SQL_PICKING_BY_PALLET, {"pallet": str(pallet or "").strip()})
        rows = _rows_to_dicts(res)
        return rows[0] if rows else None

    async def fetch_trace_by_pallet(self, pallet: str) -> dict[str, Any] | None:
        """Return trace information used by the C# fallback confirmation path."""

        res = await self.db_client.query_json(SQL_TRACE_BY_PALLET, {"pallet": str(pallet or "").strip()})
        rows = _rows_to_dicts(res)
        return rows[0] if rows else None

    async def fetch_current_location_by_pallet(self, pallet: str) -> dict[str, Any] | None:
        """Return the current stock location for a pallet, if it exists in giacenza."""

        res = await self.db_client.query_json(SQL_CURRENT_LOCATION_BY_PALLET, {"pallet": str(pallet or "").strip()})
        rows = _rows_to_dicts(res)
        return rows[0] if rows else None

    async def fetch_open_locations_by_pallet(self, pallet: str) -> list[dict[str, Any]]:
        """Return all positive stock locations currently open for a pallet."""

        res = await self.db_client.query_json(SQL_OPEN_LOCATIONS_BY_PALLET, {"pallet": str(pallet or "").strip()})
        return _rows_to_dicts(res)

    async def fetch_open_pallets_by_cell(self, id_cella: int) -> list[dict[str, Any]]:
        """Return pallets currently present in a physical cell."""

        res = await self.db_client.query_json(SQL_OPEN_PALLETS_BY_CELL, {"id_cella": int(id_cella)})
        return _rows_to_dicts(res)

    async def fetch_closed_picking_by_pallet(self, pallet: str) -> dict[str, Any] | None:
        """Return a closed picking-list row for a pallet already considered shipped."""

        res = await self.db_client.query_json(SQL_CLOSED_PICKING_BY_PALLET, {"pallet": str(pallet or "").strip()})
        rows = _rows_to_dicts(res)
        return rows[0] if rows else None

    async def skip_picking_pallet(
        self,
        *,
        documento: str,
        pallet: str,
        id_stato: int,
        operator_id: int,
        motivo: str = "UDC non trovata dal magazziniere",
    ) -> None:
        """Persist one skipped picking-list pallet so the queue can advance."""

        params = {
            "documento": str(documento or "").strip(),
            "pallet": str(pallet or "").strip(),
            "id_stato": int(id_stato),
            "id_operatore": int(operator_id),
            "motivo": str(motivo or "").strip(),
        }
        log_runtime_event(
            "Barcode WMS",
            (
                "PICKING SKIP START "
                f"documento={params['documento']} "
                f"pallet={params['pallet']} "
                f"id_stato={params['id_stato']} "
                f"operator={params['id_operatore']}"
            ),
        )
        await self.db_client.query_json(SQL_SKIP_PICKING_PALLET, params, commit=True)
        log_runtime_event(
            "Barcode WMS",
            (
                "PICKING SKIP OK "
                f"documento={params['documento']} "
                f"pallet={params['pallet']} "
                f"id_stato={params['id_stato']}"
            ),
        )

    async def fetch_active_skipped_document(self, id_stato: int) -> dict[str, Any] | None:
        """Return a reserved document whose remaining pallets were all skipped."""

        res = await self.db_client.query_json(SQL_ACTIVE_SKIPPED_DOCUMENT, {"id_stato": int(id_stato)})
        rows = _rows_to_dicts(res)
        return rows[0] if rows else None

    async def release_picking_document(self, *, documento: str, operator_id: int) -> int:
        """Release a reserved picking-list document without closing its residual rows."""

        params = {
            "documento": str(documento or "").strip(),
            "id_operatore": int(operator_id),
        }
        log_runtime_event(
            "Barcode WMS",
            f"PICKING AUTO RELEASE START documento={params['documento']} operator={params['id_operatore']}",
        )
        res = await self.db_client.query_json(SQL_RELEASE_PICKING_DOCUMENT, params, as_dict_rows=True, commit=True)
        rows = _rows_to_dicts(res)
        rc = 0
        if rows:
            try:
                rc = int(rows[0].get("RC") or 0)
            except Exception:
                rc = 0
        log_runtime_event("Barcode WMS", f"PICKING AUTO RELEASE END documento={params['documento']} rc={rc}")
        return rc

    async def resolve_physical_cell(self, destination: str) -> DestinationCell | None:
        """Accept either an internal cell ID or the scanned legacy cell barcode."""

        res = await self.db_client.query_json(SQL_RESOLVE_PHYSICAL_CELL, {"destination": str(destination or "").strip()})
        rows = _rows_to_dicts(res)
        if not rows:
            return None
        row = rows[0]
        return DestinationCell(
            barcode_cella=str(row.get("BarcodeCella") or ""),
            numero_cella=int(row.get("NumeroCella") or 0),
            id_cella=int(row.get("IDCella") or 0),
            ubicazione=str(row.get("Ubicazione") or ""),
        )

    async def execute_legacy_move(
        self,
        *,
        operator_id: int,
        barcode_cella: str,
        barcode_pallet: str,
        numero_cella: int,
        refresh_picking_reservation: bool = True,
    ) -> LegacyMoveResult:
        """Execute the same stored procedure used by the C# barcode form."""

        params = {
            "id_operatore": int(operator_id),
            "barcode_cella": str(barcode_cella or "").strip(),
            "barcode_pallet": str(barcode_pallet or "").strip(),
            "numero_cella": int(numero_cella),
            "refresh_picking_reservation": 1 if refresh_picking_reservation else 0,
        }
        log_runtime_event(
            "Barcode WMS",
            (
                "MOVE START "
                f"operator={params['id_operatore']} "
                f"cell={params['barcode_cella']} "
                f"pallet={params['barcode_pallet']} "
                f"refresh_pl={params['refresh_picking_reservation']}"
            ),
        )
        try:
            res = await self.db_client.query_json(SQL_LEGACY_MOVE, params, commit=True)
        except Exception as exc:
            log_exception(
                "Barcode WMS",
                exc,
                context=(
                    "execute_legacy_move "
                    f"operator={params['id_operatore']} "
                    f"cell={params['barcode_cella']} "
                    f"pallet={params['barcode_pallet']}"
                ),
            )
            raise
        rows = _rows_to_dicts(res)
        row = rows[0] if rows else {}
        result = LegacyMoveResult(
            rc=int(row.get("RC") or 0),
            barcode_cella=str(row.get("BarcodeCella") or params["barcode_cella"]),
            barcode_pallet=str(row.get("BarcodePallet") or params["barcode_pallet"]),
            numero_cella=int(row.get("NumeroCella") or params["numero_cella"]),
        )
        log_runtime_event(
            "Barcode WMS",
            (
                "MOVE OK "
                f"rc={result.rc} "
                f"cell={result.barcode_cella} "
                f"pallet={result.barcode_pallet}"
            ),
        )
        return result
