from typing import (
    Any,
    Dict,
    List,
    Optional,
)

from order_picture_base_model import OrderPicture


class GoldRowToOrderPictureMapper:
    """Maps one row from the gold-layer parquet
    `orders_by_store_code_and_business_dt` into an `OrderPicture`.

    The gold schema differs from the legacy JSON shape that
    `OrderPicture.create_from_dict` consumes (camelCase → struct fields,
    plus a few renames and one lossy transformation in `saleLines.context`).
    Rather than duplicating the OrderPicture / Tender / SaleLine factories,
    we reshape the gold row into a legacy-shaped dict and delegate.
    """

    @staticmethod
    def map(row: Dict[str, Any]) -> OrderPicture:
        legacy_dict = GoldRowToOrderPictureMapper._reshape_to_legacy(row)
        return OrderPicture.create_from_dict(record=legacy_dict)

    @staticmethod
    def _reshape_to_legacy(row: Dict[str, Any]) -> Dict[str, Any]:
        pos = row.get("pos") or {}
        totals = row.get("totals") or {}
        fiscal = row.get("fiscal")

        legacy: Dict[str, Any] = {
            "id": row["id"],
            "storeCode": row["storeCode"],
            "storeCNPJ": row["cnpj"],
            "orderCode": row["orderCode"],
            "businessDt": GoldRowToOrderPictureMapper._date_to_iso(row["businessDt"]),
            "creationDttm": GoldRowToOrderPictureMapper._datetime_to_iso(row["createdAt"]),
            "stateId": row["stateId"],
            "posCode": pos.get("id", 0),
            "podType": pos.get("type", ""),
            "saleTypeId": row["saleTypeId"],
            "totalGross": GoldRowToOrderPictureMapper._money(totals.get("gross")),
            "taxTotal": GoldRowToOrderPictureMapper._money(totals.get("tax")),
            "discountAmount": GoldRowToOrderPictureMapper._money(totals.get("discount")),
            "tenders": GoldRowToOrderPictureMapper._reshape_tenders(row.get("tenders") or []),
            "saleLines": GoldRowToOrderPictureMapper._reshape_sale_lines(row.get("saleLines") or []),
            "customProperties": GoldRowToOrderPictureMapper._coerce_map(row.get("customProperties")),
        }

        if fiscal is not None and fiscal.get("saleXml") is not None:
            legacy["fiscalXml"] = fiscal["saleXml"]
            cancel_xml = fiscal.get("cancelXml")
            if cancel_xml is not None:
                legacy["cancel_xml"] = cancel_xml

        return legacy

    @staticmethod
    def _reshape_tenders(tenders: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        ret = []
        for tender in tenders:
            ret.append({
                "id": tender["id"],
                "tenderType": tender["type"],
                "tenderDesc": tender.get("name", ""),
                "tenderAmount": GoldRowToOrderPictureMapper._money(tender.get("amount")),
                "changeAmount": GoldRowToOrderPictureMapper._money(tender.get("change")),
                "details": GoldRowToOrderPictureMapper._coerce_map(tender.get("details")),
            })
        return ret

    @staticmethod
    def _reshape_sale_lines(sale_lines: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        ret = []
        for sl in sale_lines:
            line_number = sl["lineNumber"]
            level = sl.get("level", 0)
            gold_context = sl.get("context")
            tags_map = GoldRowToOrderPictureMapper._coerce_map(sl.get("tags"))

            item_id = GoldRowToOrderPictureMapper._reverse_context_to_item_id(gold_context=gold_context)

            ret.append({
                "id": sl["id"],
                "lineNumber": line_number,
                "level": level,
                "itemId": item_id,
                "partCode": sl["partCode"],
                "name": sl.get("name", ""),
                "itemType": sl["itemType"],
                "qty": GoldRowToOrderPictureMapper._money(sl.get("quantity")),
                "multipliedQty": GoldRowToOrderPictureMapper._money(sl.get("multipliedQty")),
                "defaultQty": GoldRowToOrderPictureMapper._money(sl.get("defaultQty")),
                "unitPrice": GoldRowToOrderPictureMapper._money(sl.get("unitPrice")),
                "addedUnitPrice": GoldRowToOrderPictureMapper._money(sl.get("addedUnitPrice")),
                "itemPrice": GoldRowToOrderPictureMapper._money(sl.get("itemPrice")),
                "itemDiscount": GoldRowToOrderPictureMapper._money(sl.get("discount")),
                "itemTax": GoldRowToOrderPictureMapper._money(sl.get("itemTax")),
                "customProperties": GoldRowToOrderPictureMapper._coerce_map(sl.get("customProperties")),
                "tags": [f"{k}={v}" for k, v in tags_map.items()],
            })
        return ret

    @staticmethod
    def _money(value) -> float:
        """Coerce a numeric field from the gold parquet to a float, rounded to
        4 decimal places. The silver/gold schema stores monetary values as
        ``float32``; reading them back yields tiny representational errors
        (e.g. ``2.335`` arrives as ``2.33500003814...``). Downstream tlog
        builders use ``round(value, 2)`` whose banker's-rounding outcome flips
        on those last bits — clamping to 4 decimals here neutralises the noise
        without losing real precision."""
        if value is None:
            return 0.0
        return round(float(value), 4)

    @staticmethod
    def _coerce_map(value) -> Dict[str, Any]:
        # PyArrow's `map<string, string>` arrays surface in `to_pylist()` as a
        # list of (key, value) tuples rather than a dict. Accept either shape.
        if value is None:
            return {}
        if isinstance(value, dict):
            return dict(value)
        return dict(value)

    @staticmethod
    def _reverse_context_to_item_id(
        gold_context: Optional[str],
    ) -> str:
        if gold_context is None:
            return "1"

        return f"1.{gold_context}"

    @staticmethod
    def _date_to_iso(value) -> str:
        if hasattr(value, "isoformat"):
            return value.isoformat()
        return str(value)

    @staticmethod
    def _datetime_to_iso(value) -> str:
        if hasattr(value, "isoformat"):
            return value.isoformat()
        return str(value)
