"""Push master catalog gallery images to Shopify products."""

from __future__ import annotations

import time
from datetime import datetime
from typing import Any, Callable, Dict, List, Optional

from sqlalchemy import select
from sqlalchemy.orm import Session

from db.channel_image_push import build_image_push_items, summarize_image_push_items
from db.image_urls import prepare_image_urls
from db.models import ChannelPublishState, ShopifyConnection
from db.repositories import SqlAlchemyIngestRepository
from db.url_shortener import resolve_destination_image_urls
from shopify.connections import build_client, get_active_connection
from shopify.product_sync import _ensure_publish_state
from shopify.image_upload import fetch_and_stage_image, is_invalid_image_url_error

CHANNEL_CODE = "shopify"

PRODUCT_MEDIA_QUERY = """
query productMedia($id: ID!) {
  product(id: $id) {
    media(first: 50) {
      nodes {
        ... on MediaImage {
          id
          image { url }
        }
      }
    }
  }
}
"""

PRODUCT_CREATE_MEDIA = """
mutation productCreateMedia($productId: ID!, $media: [CreateMediaInput!]!) {
  productCreateMedia(productId: $productId, media: $media) {
    media {
      id
      mediaContentType
    }
    mediaUserErrors { field message }
  }
}
"""

VARIANT_LOOKUP_QUERY = """
query variantBySku($query: String!) {
  productVariants(first: 1, query: $query) {
    nodes { product { id } }
  }
}
"""


def push_product_images(
    session: Session,
    *,
    dry_run: bool = True,
    limit: Optional[int] = None,
    skus: Optional[List[str]] = None,
    shop_code: Optional[str] = None,
    connection_id: Optional[int] = None,
    force: bool = False,
    on_progress: Optional[Callable[[Dict[str, Any]], None]] = None,
) -> Dict[str, Any]:
    """Upload master gallery URLs to linked Shopify products via productCreateMedia."""
    connection = None
    compat_connection_id = connection_id
    if connection_id:
        from db.compat_connections import compat_connection_id as make_compat_id
        from db.compat_connections import decode_compat_connection_id

        channel_type, native_id = decode_compat_connection_id(connection_id)
        if channel_type == "shopify":
            connection = session.get(ShopifyConnection, native_id)
            compat_connection_id = make_compat_id("shopify", native_id)
    if connection is None:
        connection = get_active_connection(session, shop_code=shop_code)
        if connection is not None and compat_connection_id is None:
            from db.compat_connections import compat_connection_id as make_compat_id

            compat_connection_id = make_compat_id("shopify", connection.id)
    has_rows = session.scalar(select(ShopifyConnection.id).limit(1)) is not None
    if connection is None and has_rows:
        return {"status": "disabled", "detail": "No active Shopify connection (kill switch engaged)"}

    items = build_image_push_items(
        session,
        CHANNEL_CODE,
        skus=skus,
        limit=limit,
        connection_id=compat_connection_id,
    )
    states = _publish_states(session, [item["master_sku"] for item in items])

    from db.channel_sku_mapping import remote_ids_by_master

    linked_remote_ids = remote_ids_by_master(
        session,
        CHANNEL_CODE,
        [item["master_sku"] for item in items],
        connection_id=compat_connection_id,
    )

    summary: Dict[str, Any] = {
        "status": "ok",
        "dry_run": dry_run,
        "mode": "images_only",
        **summarize_image_push_items(items),
        "pushed": 0,
        "uploaded_images": 0,
        "skipped_unchanged": 0,
        "skipped_no_product": 0,
        "failed": 0,
        "errors": [],
        "planned": [],
    }
    if not items:
        return summary

    summary["total"] = len(items)
    processed = 0

    def _emit_progress() -> None:
        if on_progress is None:
            return
        on_progress(
            {
                "total": summary["total"],
                "completed": processed,
                "pushed": summary["pushed"],
                "failed": summary["failed"],
                "skipped_unchanged": summary["skipped_unchanged"],
                "skipped_no_product": summary["skipped_no_product"],
            }
        )

    client = None
    url_repo = SqlAlchemyIngestRepository(session)
    if not dry_run:
        client = build_client(connection)

    for item in items:
        master_sku = item["master_sku"]
        channel_sku = item["channel_sku"]
        urls = prepare_image_urls(resolve_destination_image_urls(item["image_urls"], repo=url_repo))
        images_hash = item["images_hash"]
        state = states.get(master_sku)
        stored_hash = _stored_images_hash(state)
        unchanged = stored_hash == images_hash and state is not None and state.last_status == "success"
        if unchanged and not force:
            summary["skipped_unchanged"] += 1
        elif dry_run:
            remote_id = (state.remote_id if state else None) or linked_remote_ids.get(master_sku)
            summary["pushed"] += 1
            summary["planned"].append(
                {
                    "sku": master_sku,
                    "channel_sku": channel_sku,
                    "image_count": len(urls),
                    "action": "upload_media" if remote_id else "lookup_or_skip",
                }
            )
        else:
            remote_id = (state.remote_id if state else None) or linked_remote_ids.get(master_sku)
            try:
                if not remote_id:
                    remote_id = _lookup_product_id(client, channel_sku)
                if not remote_id:
                    summary["skipped_no_product"] += 1
                    if len(summary["errors"]) < 50:
                        summary["errors"].append(
                            {
                                "sku": master_sku,
                                "channel_sku": channel_sku,
                                "error": "Shopify product not found for linked SKU",
                            }
                        )
                else:
                    existing_urls = _existing_media_urls(client, remote_id)
                    to_upload = [url for url in urls if url not in existing_urls]
                    uploaded = 0
                    upload_errors: List[str] = []
                    if to_upload:
                        uploaded, upload_errors = _create_media(
                            client,
                            remote_id,
                            to_upload,
                            alt=item.get("name") or channel_sku,
                        )
                    if upload_errors and uploaded == 0:
                        raise RuntimeError("; ".join(upload_errors[:3]))

                    _write_state(
                        session,
                        state,
                        sku=master_sku,
                        images_hash=images_hash,
                        remote_id=remote_id,
                        uploaded=uploaded,
                        skipped=len(urls) - len(to_upload),
                        status="success",
                        error=None,
                    )
                    summary["pushed"] += 1
                    summary["uploaded_images"] += uploaded
                    session.commit()
            except Exception as exc:
                _write_state(
                    session,
                    state,
                    sku=master_sku,
                    images_hash=images_hash,
                    remote_id=remote_id,
                    uploaded=0,
                    skipped=0,
                    status="error",
                    error=str(exc),
                )
                summary["failed"] += 1
                if len(summary["errors"]) < 50:
                    summary["errors"].append(
                        {"sku": master_sku, "channel_sku": channel_sku, "error": str(exc)}
                    )
                session.commit()

        processed += 1
        _emit_progress()

    summary["planned"] = summary["planned"][:200]
    if not dry_run:
        if summary["failed"] > 0 and summary["pushed"] == 0:
            summary["status"] = "failed"
            summary["message"] = f"All {summary['failed']} image push(es) failed"
        elif summary["failed"] > 0:
            summary["message"] = (
                f"{summary['pushed']} pushed, {summary['failed']} failed, "
                f"{summary['skipped_unchanged']} skipped unchanged"
            )
        errors = summary.get("errors") or []
        if errors and not summary.get("message"):
            summary["message"] = errors[0].get("error", "")
    return summary


def _stored_images_hash(state: Optional[ChannelPublishState]) -> Optional[str]:
    if state is None or not state.last_payload:
        return None
    payload = state.last_payload
    if isinstance(payload, dict):
        stored = payload.get("images_hash")
        if stored:
            return str(stored)
    return None


def _publish_states(session: Session, skus: List[str]) -> Dict[str, ChannelPublishState]:
    if not skus:
        return {}
    rows = session.scalars(
        select(ChannelPublishState)
        .where(ChannelPublishState.channel_code == CHANNEL_CODE)
        .where(ChannelPublishState.sku.in_(skus))
    ).all()
    return {row.sku: row for row in rows}


def _lookup_product_id(client, sku: str) -> Optional[str]:
    data = client.graphql(VARIANT_LOOKUP_QUERY, {"query": f'sku:"{sku}"'})
    nodes = ((data.get("productVariants") or {}).get("nodes")) or []
    if nodes:
        return ((nodes[0] or {}).get("product") or {}).get("id")
    return None


def _existing_media_urls(client, product_id: str) -> set[str]:
    data = client.graphql(PRODUCT_MEDIA_QUERY, {"id": product_id})
    product = data.get("product") or {}
    urls: set[str] = set()
    for node in ((product.get("media") or {}).get("nodes")) or []:
        image = node.get("image") or {}
        url = str(image.get("url") or "").strip()
        if url:
            urls.add(url)
    return urls


def _product_create_image(client, product_id: str, original_source: str, *, alt: str) -> int:
    data = client.graphql(
        PRODUCT_CREATE_MEDIA,
        {
            "productId": product_id,
            "media": [
                {
                    "originalSource": original_source,
                    "mediaContentType": "IMAGE",
                    "alt": alt,
                }
            ],
        },
    )
    result = data.get("productCreateMedia") or {}
    user_errors = result.get("mediaUserErrors") or []
    if user_errors:
        raise RuntimeError(f"productCreateMedia userErrors: {user_errors}")
    return len(result.get("media") or [])


def _create_media(client, product_id: str, urls: List[str], *, alt: str) -> tuple[int, List[str]]:
    """Upload images one at a time; fall back to staged upload when Shopify rejects the R2 URL."""
    uploaded = 0
    errors: List[str] = []
    for idx, url in enumerate(urls):
        image_alt = alt if idx == 0 else ""
        try:
            for attempt in range(4):
                try:
                    uploaded += _product_create_image(client, product_id, url, alt=image_alt)
                    break
                except Exception as exc:
                    if is_invalid_image_url_error(exc):
                        staged_source = fetch_and_stage_image(client, url)
                        uploaded += _product_create_image(client, product_id, staged_source, alt=image_alt)
                        break
                    if _is_throttled(exc) and attempt < 3:
                        time.sleep(2.0 * (attempt + 1))
                        continue
                    raise
        except Exception as exc:
            errors.append(f"{url}: {exc}")
    return uploaded, errors


def _is_throttled(exc: Exception) -> bool:
    text = str(exc).upper()
    return "THROTTLED" in text or "429" in text


def _write_state(
    session: Session,
    state: Optional[ChannelPublishState],
    *,
    sku: str,
    images_hash: str,
    remote_id: Optional[str],
    uploaded: int,
    skipped: int,
    status: str,
    error: Optional[str],
) -> None:
    state = _ensure_publish_state(session, state, sku=sku)
    state.last_payload = {
        "images_hash": images_hash,
        "uploaded": uploaded,
        "skipped_existing": skipped,
        "mode": "images_only",
    }
    state.remote_id = remote_id
    state.last_status = status
    state.last_error = error
    if status == "success":
        state.last_published_at = datetime.utcnow()
