"""Fake Shopify provider — realistic mock data matching the actual GraphQL API.

Generates data that matches the exact response shapes of the Shopify Admin
GraphQL API (2025-01+), enabling full pipeline testing without a real store.

Produces complete GraphQL-style node dicts with:
    - Shopify GID format ("gid://shopify/Product/123")
    - MoneyV2 / MoneyBag structures ({ amount: "10.00", currencyCode: "USD" })
    - numberOfOrders as String (UnsignedInt64 scalar)
    - Connection patterns (edges/node) for paginated responses
    - Flat JSONL format with __parentId for bulk operation output
    - ISO 8601 datetime strings with timezone

Use for integration testing, pipeline development, and demo data.

Usage:
    from platoon.data.providers.shopify.fake import FakeShopifyProvider

    provider = FakeShopifyProvider(seed=42, num_products=50, num_customers=200)
    result = await provider.fetch("products")
    print(f"Got {result.count} products")

    # Or get raw GraphQL-shaped nodes directly
    nodes = provider.generate_product_nodes(count=50)
"""

from __future__ import annotations

import hashlib
import random
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional

from platoon.data.provider import DataProvider, DataProviderResult
from platoon.data.providers.shopify.transform import ShopifyTransformer


# ---------------------------------------------------------------------------
# Realistic product catalogs for a small Shopify store
# ---------------------------------------------------------------------------

PRODUCT_CATALOG = [
    {"title": "Classic Cotton T-Shirt", "type": "Apparel", "vendor": "ThreadCo",
     "variants": [("S", 24.99, "TC-SHIRT-S"), ("M", 24.99, "TC-SHIRT-M"), ("L", 24.99, "TC-SHIRT-L"), ("XL", 26.99, "TC-SHIRT-XL")]},
    {"title": "Wireless Bluetooth Earbuds", "type": "Electronics", "vendor": "SoundPeak",
     "variants": [("Black", 49.99, "SP-BUDS-BLK"), ("White", 49.99, "SP-BUDS-WHT")]},
    {"title": "Organic Green Tea (50 bags)", "type": "Food & Beverage", "vendor": "LeafWell",
     "variants": [("Default", 12.99, "LW-TEA-GRN50")]},
    {"title": "Bamboo Phone Stand", "type": "Accessories", "vendor": "NaturTech",
     "variants": [("Natural", 18.99, "NT-STAND-NAT"), ("Dark", 18.99, "NT-STAND-DRK")]},
    {"title": "Premium Yoga Mat", "type": "Sports", "vendor": "FlexFit",
     "variants": [("Purple", 39.99, "FF-MAT-PUR"), ("Blue", 39.99, "FF-MAT-BLU"), ("Black", 39.99, "FF-MAT-BLK")]},
    {"title": "Stainless Steel Water Bottle", "type": "Accessories", "vendor": "HydroCore",
     "variants": [("500ml", 22.99, "HC-BTL-500"), ("750ml", 27.99, "HC-BTL-750"), ("1L", 32.99, "HC-BTL-1000")]},
    {"title": "USB-C Charging Cable (6ft)", "type": "Electronics", "vendor": "CableMax",
     "variants": [("White", 9.99, "CM-USBC-6W"), ("Black", 9.99, "CM-USBC-6B")]},
    {"title": "Scented Soy Candle", "type": "Home", "vendor": "GlowCraft",
     "variants": [("Lavender", 16.99, "GC-CNDL-LAV"), ("Vanilla", 16.99, "GC-CNDL-VAN"), ("Cedar", 16.99, "GC-CNDL-CED")]},
    {"title": "Leather Minimalist Wallet", "type": "Accessories", "vendor": "HideGood",
     "variants": [("Brown", 34.99, "HG-WLLT-BRN"), ("Black", 34.99, "HG-WLLT-BLK")]},
    {"title": "Plant-Based Protein Powder", "type": "Food & Beverage", "vendor": "GreenFuel",
     "variants": [("Chocolate (1lb)", 29.99, "GF-PROT-CHC"), ("Vanilla (1lb)", 29.99, "GF-PROT-VAN"), ("Unflavored (1lb)", 24.99, "GF-PROT-UNF")]},
    {"title": "Ceramic Pour-Over Coffee Dripper", "type": "Home", "vendor": "BrewArt",
     "variants": [("White", 28.99, "BA-DRIP-WHT"), ("Matte Black", 28.99, "BA-DRIP-BLK")]},
    {"title": "Resistance Band Set (5 pack)", "type": "Sports", "vendor": "FlexFit",
     "variants": [("Default", 19.99, "FF-BAND-5PK")]},
    {"title": "Recycled Notebook (A5)", "type": "Stationery", "vendor": "EcoWrite",
     "variants": [("Lined", 8.99, "EW-NOTE-LN"), ("Dot Grid", 8.99, "EW-NOTE-DG"), ("Blank", 8.99, "EW-NOTE-BL")]},
    {"title": "Wireless Phone Charger Pad", "type": "Electronics", "vendor": "CableMax",
     "variants": [("Black", 24.99, "CM-CHRG-BLK"), ("White", 24.99, "CM-CHRG-WHT")]},
    {"title": "Organic Lip Balm (3 pack)", "type": "Beauty", "vendor": "PureGlow",
     "variants": [("Default", 7.99, "PG-LIP-3PK")]},
    {"title": "Insulated Lunch Bag", "type": "Accessories", "vendor": "HydroCore",
     "variants": [("Navy", 21.99, "HC-LNCH-NVY"), ("Grey", 21.99, "HC-LNCH-GRY")]},
    {"title": "Desktop LED Ring Light", "type": "Electronics", "vendor": "SoundPeak",
     "variants": [("10 inch", 29.99, "SP-RING-10"), ("12 inch", 39.99, "SP-RING-12")]},
    {"title": "Bamboo Cutting Board Set", "type": "Home", "vendor": "NaturTech",
     "variants": [("3-piece", 24.99, "NT-CBOARD-3")]},
    {"title": "Running Socks (3 pair)", "type": "Sports", "vendor": "FlexFit",
     "variants": [("S/M", 14.99, "FF-SOCK-SM"), ("L/XL", 14.99, "FF-SOCK-LXL")]},
    {"title": "Essential Oil Diffuser", "type": "Home", "vendor": "GlowCraft",
     "variants": [("Wood Grain", 32.99, "GC-DIFF-WD"), ("White", 32.99, "GC-DIFF-WHT")]},
]

FIRST_NAMES = [
    "Emma", "Liam", "Olivia", "Noah", "Ava", "James", "Sophia", "Oliver",
    "Isabella", "William", "Mia", "Benjamin", "Charlotte", "Lucas", "Amelia",
    "Henry", "Harper", "Alexander", "Evelyn", "Daniel", "Luna", "Michael",
    "Ella", "Mason", "Chloe", "Ethan", "Penelope", "Logan", "Layla", "Jack",
]

LAST_NAMES = [
    "Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller",
    "Davis", "Rodriguez", "Martinez", "Hernandez", "Lopez", "Gonzalez",
    "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin",
    "Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark",
    "Ramirez", "Lewis", "Robinson",
]

LOCATIONS = [
    {"id": "gid://shopify/Location/1001", "name": "Main Warehouse"},
    {"id": "gid://shopify/Location/1002", "name": "Downtown Store"},
]

TAGS_POOL = [
    "sale", "new-arrival", "bestseller", "clearance", "eco-friendly",
    "gift-idea", "limited-edition", "seasonal", "vip-only", "bundle-eligible",
]


# ---------------------------------------------------------------------------
# Node generators — produce exact Shopify GraphQL response shapes
# ---------------------------------------------------------------------------


def _money(amount: float, currency: str = "USD") -> Dict:
    """MoneyV2 shape: { amount: "10.00", currencyCode: "USD" }."""
    return {"amount": f"{amount:.2f}", "currencyCode": currency}


def _money_bag(amount: float, currency: str = "USD") -> Dict:
    """MoneyBag shape used in orders: { shopMoney: { amount, currencyCode } }."""
    return {"shopMoney": _money(amount, currency)}


def _iso_dt(dt: datetime) -> str:
    """Format datetime as Shopify ISO string."""
    return dt.strftime("%Y-%m-%dT%H:%M:%SZ")


def _gid(resource_type: str, numeric_id: int) -> str:
    """Shopify GID format."""
    return f"gid://shopify/{resource_type}/{numeric_id}"


class FakeShopifyProvider(DataProvider):
    """Generates realistic mock Shopify data matching the actual Admin GraphQL API.

    Produces complete GraphQL-style node dicts that pass through the
    ShopifyTransformer unchanged, enabling full pipeline testing.

    Args:
        seed: Random seed for reproducible data
        num_products: How many products to generate (uses catalog cycling)
        num_customers: Number of customer records
        num_orders: Number of order records
        date_range_days: How far back in time to spread data
    """

    name = "fake_shopify"
    entity_types = ["products", "orders", "customers", "inventory"]

    def __init__(
        self,
        seed: int = 42,
        num_products: int = 20,
        num_customers: int = 100,
        num_orders: int = 500,
        date_range_days: int = 365,
    ):
        self._rng = random.Random(seed)
        self._num_products = num_products
        self._num_customers = num_customers
        self._num_orders = num_orders
        self._date_range_days = date_range_days
        self._transformer = ShopifyTransformer()

        # Pre-generate node data
        self._product_nodes = self._gen_products()
        self._customer_nodes = self._gen_customers()
        self._order_nodes = self._gen_orders()
        self._inventory_nodes = self._gen_inventory()

    async def fetch(
        self,
        entity_type: str,
        since: Optional[datetime] = None,
        config: Optional[Dict[str, Any]] = None,
    ) -> DataProviderResult:
        """Fetch mock data, optionally filtered by watermark."""
        generators = {
            "products": (self._product_nodes, self._transformer.products),
            "orders": (self._order_nodes, self._transformer.orders),
            "customers": (self._customer_nodes, self._transformer.customers),
            "inventory": (self._inventory_nodes, self._transformer.inventory),
        }
        if entity_type not in generators:
            raise ValueError(f"Unknown entity type: {entity_type}")

        nodes, transform = generators[entity_type]

        # Filter by watermark if provided
        if since:
            since_str = _iso_dt(since)
            date_field = "createdAt" if entity_type == "orders" else "updatedAt"
            nodes = [n for n in nodes if n.get(date_field, "") >= since_str]

        records = transform(nodes)
        watermark = self._latest_dt(nodes, entity_type)

        return DataProviderResult(
            records=records,
            watermark=watermark,
            entity_type=entity_type,
            raw_count=len(nodes),
        )

    async def check_connection(self) -> bool:
        return True

    # ----- Raw node access for advanced testing -----

    def get_product_nodes(self) -> List[Dict]:
        """Get raw product nodes (Shopify GraphQL shape)."""
        return self._product_nodes

    def get_customer_nodes(self) -> List[Dict]:
        """Get raw customer nodes (Shopify GraphQL shape)."""
        return self._customer_nodes

    def get_order_nodes(self) -> List[Dict]:
        """Get raw order nodes (Shopify GraphQL shape)."""
        return self._order_nodes

    def get_inventory_nodes(self) -> List[Dict]:
        """Get raw inventory item nodes (Shopify GraphQL shape)."""
        return self._inventory_nodes

    def get_bulk_jsonl(self, entity_type: str = "products") -> List[Dict]:
        """Get data as flat JSONL lines with __parentId (bulk operation format).

        Useful for testing _reassemble_bulk_jsonl().
        """
        if entity_type == "products":
            return self._products_to_bulk_jsonl()
        raise ValueError(f"Bulk JSONL not implemented for {entity_type}")

    # ----- Internal generators -----

    def _random_dt(self, max_age_days: Optional[int] = None) -> datetime:
        """Generate a random datetime within the date range."""
        days = max_age_days or self._date_range_days
        offset = self._rng.randint(0, days * 24 * 3600)
        return datetime(2026, 2, 26, tzinfo=timezone.utc) - timedelta(seconds=offset)

    def _random_tags(self, max_tags: int = 3) -> List[str]:
        """Pick random tags from the pool."""
        n = self._rng.randint(0, max_tags)
        return self._rng.sample(TAGS_POOL, min(n, len(TAGS_POOL)))

    def _gen_products(self) -> List[Dict]:
        """Generate product nodes matching Shopify products query shape."""
        nodes = []
        variant_counter = 1000

        for i in range(self._num_products):
            catalog_entry = PRODUCT_CATALOG[i % len(PRODUCT_CATALOG)]
            product_id = 100000 + i
            created = self._random_dt(max_age_days=self._date_range_days)
            updated = created + timedelta(days=self._rng.randint(0, 60))

            # Build variant edges (matching exact Shopify response shape)
            variant_edges = []
            for var_title, price, sku in catalog_entry["variants"]:
                compare_at = f"{price + self._rng.choice([5, 10, 15]):.2f}" if self._rng.random() < 0.3 else None
                variant_edges.append({
                    "node": {
                        "id": _gid("ProductVariant", variant_counter),
                        "sku": f"{sku}-{i:03d}" if i >= len(PRODUCT_CATALOG) else sku,
                        "title": var_title,
                        "price": f"{price:.2f}",  # String scalar in Shopify
                        "compareAtPrice": compare_at,
                        "inventoryQuantity": self._rng.randint(0, 500),
                        "weight": round(self._rng.uniform(0.1, 5.0), 2),
                        "barcode": f"{self._rng.randint(100000000000, 999999999999)}",
                    }
                })
                variant_counter += 1

            suffix = f" #{i - len(PRODUCT_CATALOG) + 1}" if i >= len(PRODUCT_CATALOG) else ""
            nodes.append({
                "id": _gid("Product", product_id),
                "title": catalog_entry["title"] + suffix,
                "descriptionHtml": f"<p>Premium {catalog_entry['title'].lower()}. Carefully crafted for quality.</p>",
                "vendor": catalog_entry["vendor"],
                "productType": catalog_entry["type"],
                "tags": self._random_tags(),
                "status": self._rng.choice(["ACTIVE"] * 9 + ["DRAFT"]),  # 90% active
                "createdAt": _iso_dt(created),
                "updatedAt": _iso_dt(updated),
                "variants": {"edges": variant_edges},
            })

        return nodes

    def _gen_customers(self) -> List[Dict]:
        """Generate customer nodes matching Shopify customers query shape.

        Uses numberOfOrders (String) and amountSpent (MoneyV2) matching
        the actual Shopify Admin API 2025-01+.
        """
        nodes = []
        for i in range(self._num_customers):
            cust_id = 200000 + i
            first = self._rng.choice(FIRST_NAMES)
            last = self._rng.choice(LAST_NAMES)
            created = self._random_dt()
            orders_count = self._rng.choices(
                [0, 1, 2, 3, 5, 8, 12, 20, 50],
                weights=[10, 25, 20, 15, 10, 8, 5, 4, 3],
            )[0]
            avg_order_value = self._rng.uniform(20, 120)
            total_spent = round(orders_count * avg_order_value, 2)

            nodes.append({
                "id": _gid("Customer", cust_id),
                "email": f"{first.lower()}.{last.lower()}{i}@example.com",
                "firstName": first,
                "lastName": last,
                "numberOfOrders": str(orders_count),  # UnsignedInt64 → String
                "amountSpent": _money(total_spent),
                "tags": self._random_tags(max_tags=2),
                "createdAt": _iso_dt(created),
                "updatedAt": _iso_dt(created + timedelta(days=self._rng.randint(0, 90))),
            })

        return nodes

    def _gen_orders(self) -> List[Dict]:
        """Generate order nodes matching Shopify orders query shape.

        Creates realistic orders with line items referencing actual products
        and customers from the generated data.
        """
        nodes = []
        line_item_counter = 500000

        for i in range(self._num_orders):
            order_id = 300000 + i
            created = self._random_dt()
            customer = self._rng.choice(self._customer_nodes)

            # Pick 1-4 random products for line items
            num_items = self._rng.choices([1, 2, 3, 4], weights=[40, 35, 15, 10])[0]
            selected_products = self._rng.sample(
                self._product_nodes, min(num_items, len(self._product_nodes))
            )

            line_item_edges = []
            subtotal = 0.0
            for product_node in selected_products:
                # Pick a random variant from this product
                variant_edges = product_node["variants"]["edges"]
                variant_node = self._rng.choice(variant_edges)["node"]
                quantity = self._rng.choices([1, 2, 3, 5], weights=[50, 30, 15, 5])[0]
                unit_price = float(variant_node["price"])
                item_discount = round(unit_price * 0.1, 2) if self._rng.random() < 0.15 else 0.0

                line_item_edges.append({
                    "node": {
                        "id": _gid("LineItem", line_item_counter),
                        "title": product_node["title"],
                        "sku": variant_node["sku"],
                        "quantity": quantity,
                        "originalUnitPriceSet": _money_bag(unit_price),
                        "totalDiscountSet": _money_bag(item_discount * quantity),
                        "product": {"id": product_node["id"]},
                        "variant": {"id": variant_node["id"]},
                    }
                })
                subtotal += unit_price * quantity - item_discount * quantity
                line_item_counter += 1

            tax = round(subtotal * 0.08, 2)  # ~8% tax
            total_discount = sum(
                float(e["node"]["totalDiscountSet"]["shopMoney"]["amount"])
                for e in line_item_edges
            )
            total = round(subtotal + tax, 2)

            financial = self._rng.choices(
                ["PAID", "PARTIALLY_REFUNDED", "REFUNDED", "PENDING"],
                weights=[85, 5, 3, 7],
            )[0]
            fulfillment = "FULFILLED" if financial == "PAID" and self._rng.random() < 0.9 else "UNFULFILLED"

            nodes.append({
                "id": _gid("Order", order_id),
                "name": f"#{1000 + i}",
                "email": customer.get("email"),
                "displayFinancialStatus": financial,
                "displayFulfillmentStatus": fulfillment,
                "currencyCode": "USD",
                "createdAt": _iso_dt(created),
                "updatedAt": _iso_dt(created + timedelta(hours=self._rng.randint(0, 48))),
                "totalPriceSet": _money_bag(total),
                "subtotalPriceSet": _money_bag(subtotal),
                "totalTaxSet": _money_bag(tax),
                "totalDiscountsSet": _money_bag(total_discount),
                "customer": {"id": customer["id"]},
                "lineItems": {"edges": line_item_edges},
            })

        return nodes

    def _gen_inventory(self) -> List[Dict]:
        """Generate inventory item nodes matching Shopify inventoryItems query shape.

        Creates one InventoryItem per variant with levels at each location,
        using the quantities(names:) field format.
        """
        nodes = []
        item_counter = 400000

        for product_node in self._product_nodes:
            for variant_edge in product_node["variants"]["edges"]:
                variant = variant_edge["node"]
                item_id = item_counter
                item_counter += 1

                cost = round(float(variant["price"]) * self._rng.uniform(0.3, 0.6), 2)
                created = product_node["createdAt"]

                # Inventory levels per location
                level_edges = []
                for loc in LOCATIONS:
                    on_hand = self._rng.randint(0, 500)
                    committed = self._rng.randint(0, min(on_hand, 50))
                    reserved = self._rng.randint(0, min(on_hand - committed, 20))
                    available = on_hand - committed - reserved
                    incoming = self._rng.randint(0, 200) if self._rng.random() < 0.3 else 0

                    level_edges.append({
                        "node": {
                            "id": _gid("InventoryLevel", item_id * 10 + len(level_edges)),
                            "location": loc,
                            "quantities": [
                                {"name": "available", "quantity": available},
                                {"name": "on_hand", "quantity": on_hand},
                                {"name": "committed", "quantity": committed},
                                {"name": "incoming", "quantity": incoming},
                                {"name": "reserved", "quantity": reserved},
                            ],
                        }
                    })

                nodes.append({
                    "id": _gid("InventoryItem", item_id),
                    "sku": variant["sku"],
                    "unitCost": _money(cost),
                    "tracked": True,
                    "createdAt": created,
                    "updatedAt": product_node["updatedAt"],
                    "variant": {"id": variant["id"]},
                    "inventoryLevels": {"edges": level_edges},
                })

        return nodes

    def _products_to_bulk_jsonl(self) -> List[Dict]:
        """Convert product nodes to flat JSONL format with __parentId.

        This is what Shopify's bulk operation API actually returns —
        flat lines where children reference parents via __parentId.
        """
        lines = []
        for product_node in self._product_nodes:
            # Parent line (product) — no edges/node wrapping
            parent = {
                "id": product_node["id"],
                "title": product_node["title"],
                "descriptionHtml": product_node["descriptionHtml"],
                "vendor": product_node["vendor"],
                "productType": product_node["productType"],
                "tags": product_node["tags"],
                "status": product_node["status"],
                "createdAt": product_node["createdAt"],
                "updatedAt": product_node["updatedAt"],
            }
            lines.append(parent)

            # Child lines (variants) with __parentId
            for variant_edge in product_node["variants"]["edges"]:
                v = variant_edge["node"]
                child = {
                    "__parentId": product_node["id"],
                    "id": v["id"],
                    "sku": v["sku"],
                    "title": v["title"],
                    "price": v["price"],
                    "compareAtPrice": v.get("compareAtPrice"),
                    "inventoryQuantity": v["inventoryQuantity"],
                    "weight": v.get("weight"),
                    "barcode": v.get("barcode"),
                }
                lines.append(child)

        return lines

    def _latest_dt(self, nodes: List[Dict], entity_type: str) -> Optional[datetime]:
        """Extract the latest datetime from nodes for watermark."""
        field = "createdAt" if entity_type == "orders" else "updatedAt"
        dates = [n.get(field) for n in nodes if n.get(field)]
        if not dates:
            return None
        latest = max(dates)
        return datetime.fromisoformat(latest.replace("Z", "+00:00"))
