"""Push collection landing metadata to Shopify collection metafields."""

from __future__ import annotations

import json
import time
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from sqlalchemy import select
from sqlalchemy.orm import Session

from db.collection_landing_schema import (
    COLLECTION_LANDING_FIELDS,
    SHOPIFY_NAMESPACE,
    build_channel_field_payload,
    field_label,
    shopify_metafield_key,
)
from db.collection_assets import CollectionAsset, fetch_collection_asset
from db.models import ShopifyAttributeRegistry
from shopify.image_upload import stage_file_bytes


METAFIELD_DEFINITION_CREATE = """
mutation metafieldDefinitionCreate($definition: MetafieldDefinitionInput!) {
  metafieldDefinitionCreate(definition: $definition) {
    createdDefinition { id namespace key }
    userErrors { field message }
  }
}
"""

COLLECTION_METAFIELD_DEFINITIONS = """
query collectionMetafieldDefinitions($first: Int!) {
  metafieldDefinitions(first: $first, ownerType: COLLECTION) {
    nodes { id name namespace key type { name } }
  }
}
"""

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

COLLECTION_UPDATE = """
mutation collectionUpdate($input: CollectionInput!) {
  collectionUpdate(input: $input) {
    collection { id handle title }
    userErrors { field message }
  }
}
"""

FILES_BY_FILENAME = """
query filesByFilename($query: String!) {
  files(first: 1, query: $query) {
    nodes {
      ... on MediaImage { id fileStatus image { url } }
      ... on GenericFile { id fileStatus url }
    }
  }
}
"""

FILE_CREATE = """
mutation fileCreate($files: [FileCreateInput!]!) {
  fileCreate(files: $files) {
    files {
      ... on MediaImage { id fileStatus image { url } }
      ... on GenericFile { id fileStatus url }
    }
    userErrors { field message }
  }
}
"""

COLLECTION_ASSET_KEYS = {"thumbnail_image_url", "category_icon_url", "pdf_url", "hero_images"}


def bootstrap_shopify_collection_metafields(
    session: Session,
    client: Any,
    *,
    shop_code: str,
    dry_run: bool = False,
) -> Dict[str, Any]:
    if client is not None and not dry_run:
        _refresh_collection_metafield_registry(session, client, shop_code=shop_code)
    existing = _existing_collection_metafield_keys(session, shop_code)
    planned: List[str] = []
    created: List[str] = []
    skipped: List[str] = []
    errors: List[str] = []

    for field in COLLECTION_LANDING_FIELDS:
        key = shopify_metafield_key(field["key"])
        registry_key = f"collection_metafield:{SHOPIFY_NAMESPACE}.{key}"
        if registry_key in existing:
            skipped.append(key)
            continue
        planned.append(key)
        if dry_run:
            continue
        definition = {
            "name": field_label(key),
            "namespace": SHOPIFY_NAMESPACE,
            "key": key,
            "type": field["shopify_type"],
            "ownerType": "COLLECTION",
        }
        try:
            data = client.graphql(METAFIELD_DEFINITION_CREATE, {"definition": definition})
            result = data.get("metafieldDefinitionCreate") or {}
            user_errors = result.get("userErrors") or []
            if user_errors:
                errors.append(f"{key}: {user_errors}")
                continue
            created.append(key)
            created_definition = result.get("createdDefinition") or {}
            session.add(
                ShopifyAttributeRegistry(
                    shop_code=shop_code,
                    owner_type="collection_metafield",
                    attribute_code=registry_key,
                    name=field_label(key),
                    namespace=SHOPIFY_NAMESPACE,
                    key=key,
                    data_type=field["shopify_type"],
                    is_standard=False,
                    raw_json={
                        "source": "collection_landing_bootstrap",
                        "definition_id": created_definition.get("id"),
                    },
                    fetched_at=datetime.now(timezone.utc),
                )
            )
        except Exception as exc:
            errors.append(f"{key}: {exc}")

    if not dry_run and (created or skipped):
        session.flush()

    return {
        "channel": "shopify",
        "dry_run": dry_run,
        "planned": planned,
        "created": created,
        "skipped": skipped,
        "errors": errors,
    }


def _refresh_collection_metafield_registry(
    session: Session,
    client: Any,
    *,
    shop_code: str,
) -> None:
    data = client.graphql(COLLECTION_METAFIELD_DEFINITIONS, {"first": 250})
    nodes = ((data.get("metafieldDefinitions") or {}).get("nodes")) or []
    existing = {
        row.attribute_code: row
        for row in session.scalars(
            select(ShopifyAttributeRegistry).where(
                ShopifyAttributeRegistry.shop_code == shop_code,
                ShopifyAttributeRegistry.owner_type == "collection_metafield",
            )
        ).all()
    }
    now = datetime.now(timezone.utc)
    for node in nodes:
        if not isinstance(node, dict) or node.get("namespace") != SHOPIFY_NAMESPACE or not node.get("key"):
            continue
        key = str(node["key"])
        attribute_code = f"collection_metafield:{SHOPIFY_NAMESPACE}.{key}"
        row = existing.get(attribute_code)
        if row is None:
            row = ShopifyAttributeRegistry(
                shop_code=shop_code,
                owner_type="collection_metafield",
                attribute_code=attribute_code,
            )
            session.add(row)
            existing[attribute_code] = row
        field_type = node.get("type") or {}
        row.name = node.get("name") or field_label(key)
        row.namespace = SHOPIFY_NAMESPACE
        row.key = key
        row.data_type = field_type.get("name") if isinstance(field_type, dict) else str(field_type)
        row.is_standard = False
        row.raw_json = {**node, "definition_id": node.get("id")}
        row.fetched_at = now
    session.flush()


def build_shopify_metafields_set_input(
    collection_gid: str,
    collection_row: Dict[str, Any],
    *,
    field_keys: Optional[set[str]] = None,
) -> List[Dict[str, Any]]:
    values = build_channel_field_payload(collection_row, field_keys=field_keys)
    metafields: List[Dict[str, Any]] = []
    type_by_key = {field["key"]: field["shopify_type"] for field in COLLECTION_LANDING_FIELDS}
    for key, value in values.items():
        if value is None:
            continue
        metafields.append(
            {
                "ownerId": collection_gid,
                "namespace": SHOPIFY_NAMESPACE,
                "key": shopify_metafield_key(key),
                "type": type_by_key.get(key, "single_line_text_field"),
                "value": value,
            }
        )
    return metafields


def push_shopify_collection_landing(
    client: Any,
    *,
    collection_gid: str,
    collection_row: Dict[str, Any],
    field_keys: Optional[set[str]] = None,
    dry_run: bool = False,
) -> Dict[str, Any]:
    asset_plan = _collection_asset_plan(collection_row, field_keys=field_keys)
    localized_row = collection_row
    uploaded_assets: List[Dict[str, str]] = []
    if not dry_run and asset_plan:
        localized_row, uploaded_assets = localize_shopify_collection_assets(
            client,
            collection_row,
            field_keys=field_keys,
        )
    metafields = build_shopify_metafields_set_input(
        collection_gid,
        localized_row,
        field_keys=field_keys,
    )
    short_description = localized_row.get("short_description")
    description_html = localized_row.get("description_html")

    if dry_run:
        return {
            "channel": "shopify",
            "dry_run": True,
            "collection_gid": collection_gid,
            "metafield_count": len(metafields),
            "metafields": metafields,
            "description_html": description_html,
            "asset_plan": asset_plan,
        }

    if description_html and field_keys is None:
        data = client.graphql(
            COLLECTION_UPDATE,
            {"input": {"id": collection_gid, "descriptionHtml": str(description_html)}},
        )
        result = data.get("collectionUpdate") or {}
        errors = result.get("userErrors") or []
        if errors:
            raise RuntimeError(f"collectionUpdate userErrors: {errors}")

    if not metafields:
        return {
            "channel": "shopify",
            "collection_gid": collection_gid,
            "metafield_count": 0,
            "pushed": 0,
            "uploaded_assets": uploaded_assets,
        }

    pushed = 0
    # Shopify caps metafieldsSet at 25 inputs per mutation.
    for offset in range(0, len(metafields), 25):
        batch = metafields[offset : offset + 25]
        data = client.graphql(METAFIELDS_SET, {"metafields": batch})
        result = data.get("metafieldsSet") or {}
        errors = result.get("userErrors") or []
        if errors:
            raise RuntimeError(f"metafieldsSet userErrors: {errors}")
        pushed += len(result.get("metafields") or batch)
    return {
        "channel": "shopify",
        "collection_gid": collection_gid,
        "metafield_count": len(metafields),
        "pushed": pushed,
        "short_description": short_description,
        "uploaded_assets": uploaded_assets,
    }


def localize_shopify_collection_assets(
    client: Any,
    collection_row: Dict[str, Any],
    *,
    field_keys: Optional[set[str]] = None,
) -> tuple[Dict[str, Any], List[Dict[str, str]]]:
    """Copy collection images/PDFs into Shopify Files and return Shopify CDN URLs."""
    out = dict(collection_row)
    uploaded: List[Dict[str, str]] = []
    cache: Dict[str, str] = {}

    def localize(source_url: str, field: str) -> str:
        source = str(source_url or "").strip()
        if not source:
            return ""
        if source.startswith("gid://shopify/"):
            return source
        if source in cache:
            return cache[source]
        asset = fetch_collection_asset(source)
        local_url, file_id, reused = _ensure_shopify_file(client, asset)
        cache[source] = local_url
        uploaded.append(
            {
                "field": field,
                "source_url": source,
                "file_id": file_id,
                "url": local_url,
                "filename": asset.filename,
                "status": "reused" if reused else "uploaded",
            }
        )
        return local_url

    wanted = COLLECTION_ASSET_KEYS if field_keys is None else COLLECTION_ASSET_KEYS & field_keys
    if "thumbnail_image_url" in wanted and out.get("thumbnail_image_url"):
        out["thumbnail_image_url"] = localize(out["thumbnail_image_url"], "thumbnail_image_url")
    if "category_icon_url" in wanted and out.get("category_icon_url"):
        out["category_icon_url"] = localize(out["category_icon_url"], "category_icon_url")
    if "pdf_url" in wanted and out.get("pdf_url"):
        out["pdf_url"] = localize(out["pdf_url"], "pdf_url")
    if "hero_images" in wanted and out.get("hero_images"):
        hero_values = out["hero_images"] if isinstance(out["hero_images"], list) else [out["hero_images"]]
        out["hero_images"] = [
            localize(value, "hero_images") for value in hero_values if str(value or "").strip()
        ]
    for node_key in ("child_taxonomy_nodes", "sibling_taxonomy_nodes"):
        nodes = out.get(node_key)
        if not isinstance(nodes, list):
            continue
        localized_nodes: List[Dict[str, Any]] = []
        for node in nodes:
            if not isinstance(node, dict):
                localized_nodes.append(node)
                continue
            node_out = dict(node)
            icon_url = str(node_out.get("category_icon_url") or "").strip()
            if icon_url and ("category_icon_url" in wanted or field_keys is None):
                node_out["category_icon_url"] = localize(icon_url, "category_icon_url")
            localized_nodes.append(node_out)
        out[node_key] = localized_nodes
    return out, uploaded


def _ensure_shopify_file(client: Any, asset: CollectionAsset) -> tuple[str, str, bool]:
    query_value = f'filename:"{asset.filename}"'
    data = client.graphql(FILES_BY_FILENAME, {"query": query_value})
    nodes = ((data.get("files") or {}).get("nodes")) or []
    if nodes and nodes[0].get("id") and _shopify_file_url(nodes[0]):
        return _shopify_file_url(nodes[0]), str(nodes[0]["id"]), True

    staged_source = stage_file_bytes(
        client,
        asset.content,
        filename=asset.filename,
        mime_type=asset.mime_type,
    )
    data = client.graphql(
        FILE_CREATE,
        {
            "files": [
                {
                    "originalSource": staged_source,
                    "contentType": "IMAGE" if asset.is_image else "FILE",
                    "alt": asset.filename,
                }
            ]
        },
    )
    result = data.get("fileCreate") or {}
    errors = result.get("userErrors") or []
    if errors:
        raise RuntimeError(f"fileCreate userErrors: {errors}")
    files = result.get("files") or []
    if not files or not files[0].get("id"):
        raise RuntimeError("fileCreate returned no Shopify file id")
    file_id = str(files[0]["id"])
    local_url = _shopify_file_url(files[0])
    for _ in range(30):
        if local_url:
            break
        time.sleep(1)
        refreshed = client.graphql(FILES_BY_FILENAME, {"query": query_value})
        refreshed_nodes = ((refreshed.get("files") or {}).get("nodes")) or []
        if refreshed_nodes:
            local_url = _shopify_file_url(refreshed_nodes[0])
    if not local_url:
        raise RuntimeError(f"Shopify file {file_id} did not become ready")
    return local_url, file_id, False


def _shopify_file_url(node: Dict[str, Any]) -> str:
    image = node.get("image") or {}
    return str(image.get("url") or node.get("url") or "").strip()


def _collection_asset_plan(
    collection_row: Dict[str, Any],
    *,
    field_keys: Optional[set[str]] = None,
) -> List[Dict[str, str]]:
    wanted = COLLECTION_ASSET_KEYS if field_keys is None else COLLECTION_ASSET_KEYS & field_keys
    plan: List[Dict[str, str]] = []
    for key in ("thumbnail_image_url", "category_icon_url", "pdf_url"):
        value = collection_row.get(key)
        if key in wanted and value:
            plan.append({"field": key, "source_url": str(value)})
    if "hero_images" in wanted:
        heroes = collection_row.get("hero_images") or []
        if not isinstance(heroes, list):
            heroes = [heroes]
        plan.extend(
            {"field": "hero_images", "source_url": str(value)}
            for value in heroes
            if value
        )
    for node_key in ("child_taxonomy_nodes", "sibling_taxonomy_nodes"):
        nodes = collection_row.get(node_key) or []
        if not isinstance(nodes, list):
            continue
        for node in nodes:
            if isinstance(node, dict) and node.get("category_icon_url"):
                plan.append(
                    {"field": "category_icon_url", "source_url": str(node["category_icon_url"])}
                )
    return plan


def _existing_collection_metafield_keys(session: Session, shop_code: str) -> set[str]:
    from sqlalchemy import select

    rows = session.scalars(
        select(ShopifyAttributeRegistry.attribute_code).where(
            ShopifyAttributeRegistry.shop_code == shop_code,
            ShopifyAttributeRegistry.owner_type == "collection_metafield",
        )
    ).all()
    return {str(code) for code in rows if code}
