"""Shopify integration helpers using shopify_python_api (GraphQL + REST)."""

import os
import json
import logging
from decimal import Decimal, ROUND_DOWN
from typing import Any

import shopify
from django.conf import settings

from webhooks.models import ShopifyStore
from products.models import Product


logger = logging.getLogger(__name__)


class ShopifyService:
    """Thin wrapper around the Shopify Admin API for product / order operations."""

    # ------------------------------
    # GraphQL Queries
    # ------------------------------

    FETCH_PRODUCT_QUERY = """
    query ($id: ID!) {
      product(id: $id) {
        id
        title
        handle
        bodyHtml
        images(first: 10) {
          edges {
            node {
              url
            }
          }
        }
        variants(first: 1) {
          edges {
            node {
              id
              price
            }
          }
        }
      }
    }
    """

    LIST_PRODUCTS_QUERY = """
    query ($first: Int!) {
      products(first: $first) {
        edges {
          node {
            id
          }
        }
      }
    }
    """

    VARIANT_BUNDLE_COMPONENTS_QUERY = """
    query VariantBundleComponents($id: ID!) {
      productVariant(id: $id) {
        id
        displayName
        sku
        productVariantComponents(first: 30) {
          nodes {
            quantity
            productVariant {
              id
              price
            }
          }
        }
      }
    }
    """

    @staticmethod
    def _activate_session() -> None:
        """
        Activate a Shopify API session using the stored access token.

        - Uses SHOPIFY_STORE from the environment to find the shop domain.
        - Looks up the access token in ShopifyStore.
        """
        store_domain = os.getenv("SHOPIFY_STORE")

        if not store_domain:
            raise RuntimeError("SHOPIFY_STORE is not configured")

        if settings.SHOPIFY_API_KEY and settings.SHOPIFY_API_SECRET:
            shopify.Session.setup(
                api_key=settings.SHOPIFY_API_KEY,
                secret=settings.SHOPIFY_API_SECRET,
            )

        shop_record = ShopifyStore.objects.filter(
            shop_domain=store_domain
        ).first()

        if not shop_record or not shop_record.access_token:
            raise RuntimeError(
                f"No access token stored for Shopify store {store_domain}"
            )

        session = shopify.Session(
            store_domain,
            settings.SHOPIFY_API_VERSION,
            shop_record.access_token,
        )

        shopify.ShopifyResource.activate_session(session)

    @staticmethod
    def _graphql(
        query: str,
        variables: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Execute a GraphQL query and return parsed JSON."""

        ShopifyService._activate_session()

        client = shopify.GraphQL()
        raw = client.execute(query, variables=variables or {})
        data = json.loads(raw)

        if "errors" in data:
            raise RuntimeError(f"Shopify GraphQL error: {data['errors']}")

        return data

    @staticmethod
    def fetch_product(product_id: str) -> dict[str, Any] | None:
        """
        Fetch a single product by numeric ID from Shopify via GraphQL.
        """

        gid = f"gid://shopify/Product/{product_id}"

        result = ShopifyService._graphql(
            ShopifyService.FETCH_PRODUCT_QUERY,
            {"id": gid},
        )

        product = result.get("data", {}).get("product")

        if not product:
            return None

        images = product.get("images", {}).get("edges", []) or []
        image_list: list[dict[str, Any]] = []
        for edge in images:
            node = edge.get("node") or {}
            url = node.get("url")
            if url:
                image_list.append({"src": url})

        # First variant: id + price for Digistore payment plan.
        # In Admin API, `price` on ProductVariant is often a Money scalar (string),
        # not MoneyV2 — do not select amount/currencyCode under price.
        variants = product.get("variants", {}).get("edges", []) or []
        default_variant_id: str | None = None
        price_amount: float | None = None
        currency_code = (os.getenv("DIGISTORE_CURRENCY") or "EUR").strip().upper()[:3] or "EUR"
        if variants:
            v_node = variants[0].get("node") or {}
            v_gid = v_node.get("id")
            if v_gid:
                default_variant_id = v_gid.split("/")[-1]
            raw_price = v_node.get("price")
            if raw_price is not None:
                try:
                    if isinstance(raw_price, str):
                        price_amount = float(raw_price.strip())
                    elif isinstance(raw_price, (int, float)):
                        price_amount = float(raw_price)
                    elif isinstance(raw_price, dict):
                        # Fallback if API returns an object in some versions
                        amt = raw_price.get("amount")
                        if amt is not None:
                            price_amount = float(amt)
                        cc = (raw_price.get("currencyCode") or "").strip()
                        if len(cc) == 3:
                            currency_code = cc.upper()
                except (TypeError, ValueError):
                    price_amount = None

        return {
            "id": product_id,
            "title": product.get("title") or "",
            "handle": product.get("handle") or "",
            "body_html": product.get("bodyHtml") or "",
            "images": image_list,
            "default_variant_id": default_variant_id,
            "price_amount": price_amount,
            "currency": currency_code,
        }

    @staticmethod
    def list_product_ids(limit: int = 250) -> list[str]:
        """
        Return a list of Shopify product IDs as simple numeric strings.
        """

        first = min(limit, 250)

        result = ShopifyService._graphql(
            ShopifyService.LIST_PRODUCTS_QUERY,
            {"first": first},
        )

        edges = (
            result.get("data", {})
            .get("products", {})
            .get("edges", [])
            or []
        )

        ids: list[str] = []

        for edge in edges:
            node = edge.get("node") or {}
            gid = node.get("id")

            if not gid:
                continue

            simple_id = gid.split("/")[-1]
            ids.append(simple_id)

        return ids

    # --- Order helpers (used by Digistore IPN) ----------------------------

    @staticmethod
    def to_gid(resource: str, numeric_id: str | int) -> str:
        """Build a Shopify Admin GraphQL global ID from resource type and numeric id."""
        rid = str(numeric_id).strip()
        return f"gid://shopify/{resource}/{rid}"

    ORDER_FULFILLMENT_TRACKING_QUERY = """
    query OrderFulfillmentTracking($id: ID!) {
      order(id: $id) {
        id
        name
        displayFulfillmentStatus
        fulfillments(first: 20) {
          status
          displayStatus
          createdAt
          trackingInfo {
            company
            number
            url
          }
        }
      }
    }
    """

    @staticmethod
    def get_order_fulfillment_tracking(shopify_order_id: str) -> dict[str, Any]:
        """
        Fetch Shopify fulfillment tracking for one order.

        Returns:
          {
            "shopify_order_id": "...",
            "order_name": "#1001",
            "display_fulfillment_status": "FULFILLED",
            "tracking": {
              "number": "...",
              "url": "...",
              "company": "UPS",
            } | None,
            "fulfillments": [...],
          }
        """
        sid = str(shopify_order_id or "").strip()
        if not sid:
            raise ValueError("shopify_order_id_required")
        if sid.startswith("gid://"):
            order_gid = sid
            sid = sid.rsplit("/", 1)[-1]
        else:
            order_gid = ShopifyService.to_gid("Order", sid)

        data = ShopifyService._graphql(
            ShopifyService.ORDER_FULFILLMENT_TRACKING_QUERY,
            {"id": order_gid},
        )
        order = (data.get("data") or {}).get("order")
        if not order:
            raise RuntimeError(f"Shopify order not found: {sid}")

        fulfillments = order.get("fulfillments") or []
        if not isinstance(fulfillments, list):
            fulfillments = []

        chosen: dict[str, str] | None = None
        for fulfillment in fulfillments:
            if not isinstance(fulfillment, dict):
                continue
            infos = fulfillment.get("trackingInfo") or []
            if not isinstance(infos, list):
                continue
            for info in infos:
                if not isinstance(info, dict):
                    continue
                number = str(info.get("number") or "").strip()
                if not number:
                    continue
                chosen = {
                    "number": number,
                    "url": str(info.get("url") or "").strip(),
                    "company": str(info.get("company") or "").strip(),
                }
                break
            if chosen:
                break

        return {
            "shopify_order_id": sid,
            "order_name": str(order.get("name") or "").strip(),
            "display_fulfillment_status": str(
                order.get("displayFulfillmentStatus") or ""
            ).strip(),
            "tracking": chosen,
            "fulfillments": fulfillments,
        }

    @staticmethod
    def delete_shopify_order(shopify_order_id: str) -> str:
        """Permanently delete an unfulfilled Shopify order. Returns deleted GID."""
        ShopifyService._activate_session()
        order_gid = ShopifyService.to_gid("Order", shopify_order_id)
        mutation = """
        mutation orderDelete($orderId: ID!) {
          orderDelete(orderId: $orderId) {
            deletedId
            userErrors { field message code }
          }
        }
        """
        data = ShopifyService._graphql(mutation, {"orderId": order_gid})
        payload = (data.get("data") or {}).get("orderDelete") or {}
        errors = payload.get("userErrors") or []
        if errors:
            raise RuntimeError(f"orderDelete failed: {errors}")
        deleted = (payload.get("deletedId") or "").strip()
        if not deleted:
            raise RuntimeError("orderDelete returned no deletedId")
        return deleted

    @staticmethod
    def _get_total_amount(ipn_data: dict[str, Any]) -> float:
        """Order gross total from Digistore on_payment IPN (amount_brutto / amount)."""
        for key in ("amount_brutto", "amount"):
            raw = ipn_data.get(key)
            if raw is None or raw == "":
                continue
            try:
                return float(str(raw).replace(",", "."))
            except (TypeError, ValueError):
                continue
        return 0.0

    @staticmethod
    def _resolve_cart_gift_variant_id(
        gift_tier: dict[str, str | float], ds_product_id: str
    ) -> str:
        configured = str(gift_tier.get("shopify_variant_id") or "").strip()
        if configured:
            return configured
        product = Product.objects.filter(
            digistore_product_id=ds_product_id
        ).first()
        if product and product.shopify_variant_id:
            return str(product.shopify_variant_id).strip()
        return ""

    @staticmethod
    def _resolve_digistore_variant_id(ds_product_id: str) -> str:
        ds_id = str(ds_product_id or "").strip()
        overrides = getattr(settings, "DIGISTORE_VARIANT_OVERRIDES", None) or {}
        override = str(overrides.get(ds_id) or "").strip()
        if override:
            return override

        product = Product.objects.filter(digistore_product_id=ds_id).first()
        if not product or not product.shopify_variant_id:
            raise ValueError(f"Missing Shopify variant for Digistore product {ds_id}")
        return str(product.shopify_variant_id).strip()

    @staticmethod
    def _ipn_float(ipn_data: dict[str, Any], key: str, default: float = 0.0) -> float:
        raw = ipn_data.get(key)
        if raw is None or raw == "":
            return default
        try:
            return float(str(raw).replace(",", "."))
        except (TypeError, ValueError):
            return default

    @staticmethod
    def _digistore_line_pricing(
        ipn_data: dict[str, Any], suffix: str, quantity: int
    ) -> tuple[float, float]:
        """
        Tax-inclusive unit price and shipping from Digistore IPN.

        product_txn_amount is product gross (net + VAT), separate from shipping.
        """
        qty = max(1, quantity)
        gross = ShopifyService._ipn_float(ipn_data, f"product_txn_amount{suffix}")
        if gross <= 0:
            netto = ShopifyService._ipn_float(
                ipn_data, f"product_txn_netto_amount{suffix}"
            )
            if netto <= 0:
                netto = ShopifyService._ipn_float(
                    ipn_data, f"product_netto_amount{suffix}"
                )
            vat = ShopifyService._ipn_float(
                ipn_data, f"product_txn_vat_amount{suffix}"
            )
            if vat <= 0:
                vat = ShopifyService._ipn_float(
                    ipn_data, f"product_vat_amount{suffix}"
                )
            gross = netto + vat
        if gross <= 0:
            gross = ShopifyService._ipn_float(ipn_data, f"product_amount{suffix}")

        shipping = ShopifyService._ipn_float(
            ipn_data, f"product_txn_shipping{suffix}"
        )
        if shipping <= 0:
            shipping = ShopifyService._ipn_float(
                ipn_data, f"product_shipping_amount{suffix}"
            )

        return gross / qty, shipping

    @staticmethod
    def _build_digistore_line_specs(
        ipn_data: dict[str, Any],
        *,
        skip_product_ids: set[str] | frozenset[str] | None = None,
    ) -> tuple[list[dict[str, Any]], float, str]:
        """
        Build normalized line specs from a Digistore IPN payload.

        Each spec: variant_id (numeric str), quantity, unit_price, line_shipping.
        """
        if skip_product_ids is None:
            skip_product_ids = {
                str(pid).strip()
                for pid in (getattr(settings, "SKIP_DIGISTORE_PRODUCT_IDS", None) or [])
                if str(pid).strip()
            }
        else:
            skip_product_ids = {
                str(pid).strip() for pid in skip_product_ids if str(pid).strip()
            }

        currency_code = (
            ipn_data.get("transaction_currency")
            or ipn_data.get("currency")
            or "EUR"
        ).strip().upper()[:3] or "EUR"

        try:
            item_count = max(1, int(str(ipn_data.get("item_count") or "1")))
        except (TypeError, ValueError):
            item_count = 1

        gift_tiers_by_ds_id = {
            str(tier["digistore_product_id"]).strip(): tier
            for tier in settings.CART_GIFT_TIERS
            if str(tier.get("digistore_product_id") or "").strip()
        }
        cart_total = ShopifyService._get_total_amount(ipn_data)

        specs: list[dict[str, Any]] = []
        shipping_total = 0.0

        for idx in range(1, item_count + 1):
            suffix = "" if idx == 1 else f"_{idx}"
            ds_product_id = str(
                ipn_data.get(f"product_id{suffix}") or ""
            ).strip()

            if ds_product_id in skip_product_ids:
                continue

            gift_tier = gift_tiers_by_ds_id.get(ds_product_id)
            if gift_tier:
                try:
                    min_cart = float(gift_tier.get("min_cart_amount") or 0)
                except (TypeError, ValueError):
                    min_cart = 0.0
                if cart_total + 0.001 < min_cart:
                    logger.info(
                        "Skipping Digistore gift %s: cart %.2f below threshold %.2f",
                        ds_product_id,
                        cart_total,
                        min_cart,
                    )
                    continue

                gift_variant_id = ShopifyService._resolve_cart_gift_variant_id(
                    gift_tier, ds_product_id
                )
                if not gift_variant_id:
                    raise ValueError(
                        f"Missing Shopify variant for gift Digistore product {ds_product_id}"
                    )
                gift_shipping = ShopifyService._ipn_float(
                    ipn_data, f"product_txn_shipping{suffix}"
                )
                if gift_shipping <= 0:
                    gift_shipping = ShopifyService._ipn_float(
                        ipn_data, f"product_shipping_amount{suffix}"
                    )
                shipping_total += gift_shipping
                specs.append(
                    {
                        "variant_id": str(gift_variant_id).strip(),
                        "quantity": 1,
                        "unit_price": 0.0,
                        "line_shipping": gift_shipping,
                    }
                )
                continue

            try:
                quantity = max(
                    1,
                    int(str(ipn_data.get(f"quantity{suffix}") or "1")),
                )
            except (TypeError, ValueError):
                quantity = 1

            unit_price, shipping_amount = ShopifyService._digistore_line_pricing(
                ipn_data, suffix, quantity
            )
            shipping_total += shipping_amount

            variant_id = ShopifyService._resolve_digistore_variant_id(ds_product_id)

            specs.append(
                {
                    "variant_id": variant_id,
                    "quantity": quantity,
                    "unit_price": unit_price,
                    "line_shipping": shipping_amount,
                }
            )

        return specs, shipping_total, currency_code

    @staticmethod
    def _bundle_components(variant_gid: str) -> tuple[dict[str, Any], list[dict[str, Any]]]:
        """Return the parent variant and its fixed-bundle components, if any."""
        result = ShopifyService._graphql(
            ShopifyService.VARIANT_BUNDLE_COMPONENTS_QUERY,
            {"id": variant_gid},
        )
        parent = (result.get("data") or {}).get("productVariant") or {}
        nodes = (
            (parent.get("productVariantComponents") or {}).get("nodes") or []
        )
        components: list[dict[str, Any]] = []
        for node in nodes:
            component_variant = node.get("productVariant") or {}
            component_id = component_variant.get("id")
            try:
                component_quantity = int(node.get("quantity") or 0)
            except (TypeError, ValueError):
                component_quantity = 0
            if component_id and component_quantity > 0:
                components.append(
                    {
                        "variant_id": component_id,
                        "quantity": component_quantity,
                        "price": Decimal(str(component_variant.get("price") or "0")),
                    }
                )
        return parent, components

    @staticmethod
    def _bundle_order_line_items(
        *,
        parent: dict[str, Any],
        components: list[dict[str, Any]],
        bundle_quantity: int,
        bundle_unit_price: float,
        currency_code: str,
    ) -> list[dict[str, Any]]:
        """Expand a bundle while retaining its total price and component inventory."""
        quantity = max(1, int(bundle_quantity))
        unit_total = Decimal(str(bundle_unit_price)).quantize(Decimal("0.01"))
        total_cents = int(unit_total * 100)

        weights = [
            max(Decimal("0"), component["price"])
            * int(component["quantity"])
            for component in components
        ]
        weight_total = sum(weights, Decimal("0"))
        if weight_total <= 0:
            weights = [Decimal(int(component["quantity"])) for component in components]
            weight_total = sum(weights, Decimal("0"))

        unit_prices_cents: list[int] = []
        for component, weight in zip(components, weights):
            component_quantity = int(component["quantity"])
            allocated_line_cents = (
                Decimal(total_cents) * weight / weight_total
                if weight_total > 0
                else Decimal("0")
            )
            unit_prices_cents.append(
                int(
                    (allocated_line_cents / component_quantity).to_integral_value(
                        rounding=ROUND_DOWN
                    )
                )
            )

        allocated_cents = sum(
            int(component["quantity"]) * unit_price
            for component, unit_price in zip(components, unit_prices_cents)
        )
        remainder_cents = total_cents - allocated_cents

        # A quantity-one component can absorb rounding without changing line quantity.
        for index, component in enumerate(components):
            if remainder_cents and int(component["quantity"]) == 1:
                unit_prices_cents[index] += remainder_cents
                remainder_cents = 0
                break

        parent_id = str(parent.get("id") or "")
        parent_title = str(parent.get("displayName") or "Bundle")
        properties = [
            {"name": "_bundle_parent_variant", "value": parent_id},
            {"name": "_bundle_parent_title", "value": parent_title},
        ]

        line_items: list[dict[str, Any]] = []
        for component, unit_price_cents in zip(components, unit_prices_cents):
            line_items.append(
                {
                    "variantId": component["variant_id"],
                    "quantity": quantity * int(component["quantity"]),
                    "requiresShipping": True,
                    "properties": properties,
                    "priceSet": {
                        "shopMoney": {
                            "amount": f"{Decimal(unit_price_cents) / 100:.2f}",
                            "currencyCode": currency_code,
                        }
                    },
                }
            )

        # If every component has a multiple quantity, cent-perfect allocation may
        # be impossible. A small custom line retains the exact external order total.
        if remainder_cents:
            line_items.append(
                {
                    "title": f"{parent_title} price adjustment",
                    "quantity": quantity,
                    "requiresShipping": False,
                    "properties": properties,
                    "priceSet": {
                        "shopMoney": {
                            "amount": f"{Decimal(remainder_cents) / 100:.2f}",
                            "currencyCode": currency_code,
                        }
                    },
                }
            )

        return line_items

    @staticmethod
    def create_order_from_digistore_ipn(
        ipn_data: dict[str, Any],
        *,
        skip_product_ids: set[str] | frozenset[str] | None = None,
    ) -> dict[str, Any]:
        ShopifyService._activate_session()

        digistore_order_id = str(ipn_data.get("order_id") or "")
        email = ipn_data.get("email") or ""
        first_name = ipn_data.get("address_first_name") or ""
        last_name = ipn_data.get("address_last_name") or ""

        address1 = (ipn_data.get("address_street") or "").strip()
        address2 = (ipn_data.get("address_street2") or "").strip()

        address_city = ipn_data.get("address_city") or ""
        address_state = ipn_data.get("address_state") or ""
        address_zipcode = ipn_data.get("address_zipcode") or ""
        address_country = ipn_data.get("address_country") or "DE"

        line_specs, shipping_total, currency_code = (
            ShopifyService._build_digistore_line_specs(
                ipn_data,
                skip_product_ids=skip_product_ids,
            )
        )

        line_items = []
        for spec in line_specs:
            variant_gid = ShopifyService.to_gid(
                "ProductVariant",
                spec["variant_id"],
            )
            parent, components = ShopifyService._bundle_components(variant_gid)
            if components:
                line_items.extend(
                    ShopifyService._bundle_order_line_items(
                        parent=parent,
                        components=components,
                        bundle_quantity=int(spec["quantity"]),
                        bundle_unit_price=spec["unit_price"],
                        currency_code=currency_code,
                    )
                )
            else:
                line_items.append(
                    {
                        "variantId": variant_gid,
                        "quantity": int(spec["quantity"]),
                        "requiresShipping": True,
                        "priceSet": {
                            "shopMoney": {
                                "amount": f"{spec['unit_price']:.2f}",
                                "currencyCode": currency_code,
                            }
                        },
                    }
                )

        if not line_items:
            raise ValueError("No syncable line items for Shopify order")

        # ---------------------------------------------------
        # SHIPPING
        # ---------------------------------------------------

        shipping_lines = []

        if shipping_total > 0:
            shipping_lines.append({
                "title": "Standard",
                "code": "standard",
                "source": "Digistore24",
                "priceSet": {
                    "shopMoney": {
                        "amount": f"{shipping_total:.2f}",
                        "currencyCode": currency_code,
                    }
                },
            })

        mutation = """
        mutation OrderCreate(
            $order: OrderCreateOrderInput!,
            $options: OrderCreateOptionsInput
        ) {
            orderCreate(order: $order, options: $options) {

                userErrors {
                    field
                    message
                }

                order {
                    id
                    name
                }
            }
        }
        """

        variables = {
            "order": {

                "email": email or None,

                "billingAddress": {
                    "firstName": first_name,
                    "lastName": last_name,
                    "address1": address1,
                    "address2": address2,
                    "city": address_city,
                    "province": address_state,
                    "zip": address_zipcode,
                    "countryCode": address_country,
                },

                "shippingAddress": {
                    "firstName": first_name,
                    "lastName": last_name,
                    "address1": address1,
                    "address2": address2,
                    "city": address_city,
                    "province": address_state,
                    "zip": address_zipcode,
                    "countryCode": address_country,
                },

                "lineItems": line_items,
                "shippingLines": shipping_lines,

                "taxesIncluded": True,

                "tags": [
                    "Digistore24",
                    f"DS24-{digistore_order_id}"
                ],

                "note": (
                    f"Imported from Digistore24 order {digistore_order_id}"
                    if digistore_order_id
                    else "Imported from Digistore24"
                ),

                "financialStatus": "PAID",
            },

            "options": {
                "inventoryBehaviour": "DECREMENT_OBEYING_POLICY"
            }
        }

        response = shopify.GraphQL().execute(
            mutation,
            variables=variables
        )

        data = json.loads(response)

        if data.get("errors"):
            raise RuntimeError(
                f"Shopify orderCreate GraphQL errors: {data.get('errors')}"
            )

        payload = (
            (data.get("data") or {})
            .get("orderCreate")
            or {}
        )

        user_errors = payload.get("userErrors") or []

        if user_errors:
            raise RuntimeError(
                f"Shopify orderCreate failed: {user_errors}"
            )

        order = payload.get("order")

        if not isinstance(order, dict):
            raise RuntimeError(
                f"Shopify orderCreate returned no order object: {payload}"
            )

        shopify_order_id = order.get("id")

        if not shopify_order_id:
            raise RuntimeError(
                f"Shopify orderCreate returned order without id: {order}"
            )

        return order

    # @staticmethod
    # def create_order_from_digistore_ipn(ipn_data: dict[str, Any]) -> dict[str, Any]:
    #     """
    #     Create a Shopify order based on a Digistore24 on_payment IPN payload.

    #     Uses the REST Order resource from shopify_python_api for simplicity.
    #     """
    #     ShopifyService._activate_session()

    #     order_id = str(ipn_data.get("order_id") or "")
    #     email = ipn_data.get("email") or ""
    #     first_name = ipn_data.get("address_first_name") or ""
    #     last_name = ipn_data.get("address_last_name") or ""

    #     address_street = ipn_data.get("address_street_name") or ""
    #     address_street_no = ipn_data.get("address_street_number") or ""
    #     address_city = ipn_data.get("address_city") or ""
    #     address_state = ipn_data.get("address_state") or ""
    #     address_zipcode = ipn_data.get("address_zipcode") or ""
    #     address_country = ipn_data.get("address_country") or ""

    #     address1 = (address_street or "").strip()
    #     if address_street_no:
    #         address1 = f"{address1} {address_street_no}".strip()

    #     # Build line items for each Digistore cart position:
    #     # product_id, product_id_2, ..., up to item_count
    #     item_count_raw = ipn_data.get("item_count") or "1"
    #     try:
    #         item_count = max(1, int(str(item_count_raw)))
    #     except (TypeError, ValueError):
    #         item_count = 1

    #     line_items: list[dict[str, Any]] = []

    #     for idx in range(1, item_count + 1):
    #         suffix = "" if idx == 1 else f"_{idx}"

    #         ds_product_id = str(ipn_data.get(f"product_id{suffix}") or "").strip()
    #         name = (
    #             ipn_data.get(f"product_name{suffix}")
    #             or ipn_data.get("product_name")
    #             or "Digistore24 product"
    #         )
    #         qty_raw = ipn_data.get(f"quantity{suffix}") or "1"
    #         try:
    #             quantity = max(1, int(str(qty_raw)))
    #         except (TypeError, ValueError):
    #             quantity = 1

    #         # Price per item: prefer product_txn_netto_amount_N, then product_amount_N
    #         price_raw = (
    #             ipn_data.get(f"product_txn_netto_amount{suffix}")
    #             or ipn_data.get(f"product_amount{suffix}")
    #             or ipn_data.get("product_txn_netto_amount")
    #             or ipn_data.get("product_amount")
    #             or "0.0"
    #         )
    #         try:
    #             price_str = f"{float(price_raw):.2f}"
    #         except (TypeError, ValueError):
    #             price_str = "0.00"

    #         variant_id: str | None = None
    #         if ds_product_id:
    #             local_product = Product.objects.filter(
    #                 digistore_product_id=ds_product_id
    #             ).first()
    #             if local_product and local_product.shopify_variant_id:
    #                 variant_id = local_product.shopify_variant_id

    #         if variant_id:
    #             line_items.append(
    #                 {
    #                     "variant_id": variant_id,
    #                     "quantity": quantity,
    #                 }
    #             )
    #         else:
    #             line_items.append(
    #                 {
    #                     "title": name,
    #                     "quantity": quantity,
    #                     "price": price_str,
    #                 }
    #             )

    #     shipping_address = {
    #         "first_name": first_name,
    #         "last_name": last_name,
    #         "address1": address1,
    #         "city": address_city,
    #         "province": address_state,
    #         "zip": address_zipcode,
    #         "country": address_country or "DE",
    #     }

    #     order = shopify.Order()
    #     if email:
    #         order.email = email
    #     order.line_items = line_items
    #     order.shipping_address = shipping_address
    #     order.billing_address = shipping_address
    #     order.financial_status = "paid"
    #     order.tags = "Digistore24"
    #     if order_id:
    #         order.note = f"Imported from Digistore24 order {order_id}"

    #     if not order.save():
    #         errors = getattr(order, "errors", None)
    #         raise RuntimeError(f"Failed to create Shopify order: {errors}")

    #     return order.to_dict() if hasattr(order, "to_dict") else order.attributes

    # --- Metafields -------------------------------------------------------

    @staticmethod
    def set_digistore_metafield(product_id: str | int, digistore_product_id: str | int) -> None:
        ShopifyService._activate_session()

        value = str(digistore_product_id)

        # Shopify GraphQL product GID
        gid = f"gid://shopify/Product/{product_id}"

        mutation = """
        mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
          metafieldsSet(metafields: $metafields) {
            metafields {
              id
              key
              namespace
              value
            }
            userErrors {
              field
              message
            }
          }
        }
        """

        variables = {
            "metafields": [
                {
                    "ownerId": gid,
                    "namespace": "custom",
                    "key": "digistore24_product_id",
                    "type": "single_line_text_field",
                    "value": value,
                }
            ]
        }

        result = shopify.GraphQL().execute(
            mutation,
            variables=variables,
        )

        data = shopify.json.loads(result)

        errors = data["data"]["metafieldsSet"]["userErrors"]

        if errors:
            raise Exception(f"Shopify metafield error: {errors}")

    @staticmethod
    def set_volume_pricing_metafield(product_id: str, data: dict):
        ShopifyService._activate_session()

        gid = f"gid://shopify/Product/{product_id}"

        mutation = """
        mutation metafieldsSet($metafields: [MetafieldsSetInput!]!) {
        metafieldsSet(metafields: $metafields) {
            metafields { id key }
            userErrors { field message }
        }
        }
        """

        variables = {
            "metafields": [
                {
                    "ownerId": gid,
                    "namespace": "custom",
                    "key": "volume_pricing",
                    "type": "json",
                    "value": json.dumps(data),
                }
            ]
        }

        result = shopify.GraphQL().execute(mutation, variables=variables)
        resp = shopify.json.loads(result)

        errors = resp["data"]["metafieldsSet"]["userErrors"]

        if errors:
            raise Exception(f"Shopify metafield error: {errors}")
