"""Fetches a Shopify catalog and maps it to SourceProduct.

Shopify's schema is fixed, so this mapping is code rather than a declarative
field map -- only heterogeneous HTTP sources need configuration.
"""
import json
import logging

import httpx

from app.services.integrations.shopify_oauth import SHOPIFY_API_VERSION, ShopQueryError

logger = logging.getLogger(__name__)

PAGE_SIZE = 250

PRODUCTS_QUERY = """
query($cursor: String) {
  products(first: %d, after: $cursor) {
    nodes {
      id title handle status productType vendor tags onlineStoreUrl description
      category { fullName }
      featuredImage { url }
      collections(first: 20) { nodes { title } }
      tracksInventory
      variants(first: 100) {
        nodes {
          price compareAtPrice availableForSale
          selectedOptions { name value }
        }
      }
      metafields(first: 20) { nodes { key value } }
      complementary: metafield(
        namespace: "shopify--discovery--product_recommendation"
        key: "complementary_products"
      ) { value }
    }
    pageInfo { hasNextPage endCursor }
  }
}
""" % PAGE_SIZE


def to_source_product(node: dict, shop_domain: str) -> dict:
    return {
        "external_id": node.get("id"),
        "title": node.get("title"),
        "description": node.get("description"),
        "brand": node.get("vendor"),
        "handle": node.get("handle"),
        "product_url": node.get("onlineStoreUrl"),
        "image_url": (node.get("featuredImage") or {}).get("url"),
        "raw_category": (node.get("category") or {}).get("fullName"),
        "product_type": node.get("productType"),
        "tags": node.get("tags") or [],
        "collections": [c["title"] for c in
                        (node.get("collections") or {}).get("nodes", [])],
        "status": node.get("status"),
        "tracks_inventory": node.get("tracksInventory"),
        "variants": [
            {
                "price": v.get("price"),
                "compare_at_price": v.get("compareAtPrice"),
                "available": v.get("availableForSale"),
                "options": {o["name"]: o["value"]
                            for o in v.get("selectedOptions") or []},
            }
            for v in (node.get("variants") or {}).get("nodes", [])
        ],
        "attributes_raw": [
            {"key": m["key"], "value": m["value"], "source": "metafield"}
            for m in (node.get("metafields") or {}).get("nodes", [])
        ],
        "source_kind": "shopify",
        "source_ref": shop_domain,
        "tenant_relations": _complementary(node),
        "rating": None,
        "review_count": None,
        "featured_rank": None,
    }


def _complementary(node: dict) -> dict:
    """Shopify has no native related-products field; the Search & Discovery app
    stores hand-linked products in this metafield. A merchant or app can write
    anything into it, so a malformed value is ignored rather than fatal."""
    raw = (node.get("complementary") or {}).get("value")
    if not raw:
        return {}
    try:
        ids = json.loads(raw)
    except (TypeError, ValueError):
        logger.warning("Ignoring malformed complementary_products metafield")
        return {}
    if not isinstance(ids, list) or not ids:
        return {}
    return {"related": [str(i) for i in ids]}


async def fetch_products(shop: str, token: str, *, client=None) -> list[dict]:
    """Every page or nothing: a partial fetch must not be mistaken for a
    complete catalog, or the sync would delete products it simply never saw."""
    owned = client is None
    c = client or httpx.AsyncClient(timeout=60.0)
    nodes, cursor = [], None
    try:
        while True:
            resp = await c.post(
                f"https://{shop}/admin/api/{SHOPIFY_API_VERSION}/graphql.json",
                headers={"X-Shopify-Access-Token": token,
                         "Content-Type": "application/json"},
                json={"query": PRODUCTS_QUERY, "variables": {"cursor": cursor}},
            )
            if resp.status_code != 200:
                logger.error("Catalog fetch for %s failed: HTTP %s",
                             shop, resp.status_code)
                raise ShopQueryError(f"HTTP {resp.status_code}")

            body = resp.json()
            if body.get("errors"):
                logger.error("Catalog fetch for %s returned GraphQL errors", shop)
                raise ShopQueryError(str(body["errors"]))

            page = body["data"]["products"]
            nodes.extend(page["nodes"])
            if not page["pageInfo"]["hasNextPage"]:
                return nodes
            cursor = page["pageInfo"]["endCursor"]
    finally:
        if owned:
            await c.aclose()
