"""Push kitchen package fixed-price bundles to Magento and/or Shopify.

Examples:
  # Dry-run from brochure JSON
  python -m app.jobs.push_kitchen_packages --file ../plytixftp.../GraceSnowWhite_0426.json

  # From DB commerce profiles, Magento connection 4
  python -m app.jobs.push_kitchen_packages --from-db --channels magento \\
      --magento-connection-id 4 --apply

  # Both channels for one collection code
  python -m app.jobs.push_kitchen_packages --from-db --collection-code GSW \\
      --channels magento,shopify --magento-connection-id 1 --shopify-connection-id 1000001 --apply
"""

from __future__ import annotations

import argparse
import json
import logging
from typing import Any, Dict, List, Optional

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def run_push_kitchen_packages(
    *,
    file_path: Optional[str] = None,
    from_db: bool = False,
    collection_codes: Optional[List[str]] = None,
    channels: Optional[List[str]] = None,
    magento_connection_id: Optional[int] = None,
    shopify_connection_id: Optional[int] = None,
    apply: bool = False,
    upsert_master: bool = True,
    skip_missing_components: bool = True,
    require_components_exist: bool = False,
    force_recreate: bool = False,
) -> Dict[str, Any]:
    from db.kitchen_packages import (
        list_kitchen_packages_from_profiles,
        resolve_kitchen_packages_from_payload,
        upsert_package_master_products,
    )
    from db.session import get_session

    dry_run = not apply
    wanted_channels = [c.strip().lower() for c in (channels or ["magento", "shopify"]) if c.strip()]
    packages = []
    source = ""
    master_result = None
    results: List[Dict[str, Any]] = []

    if file_path and not from_db:
        from db.collection_brochure_seed import load_collection_brochure_seed

        payload = load_collection_brochure_seed(file_path)
        # Resolve without DB first so dry-runs work offline; enrich if DB is up.
        packages = resolve_kitchen_packages_from_payload(payload, session=None)
        source = f"file:{file_path}"
        try:
            with get_session() as session:
                packages = resolve_kitchen_packages_from_payload(payload, session=session)
                if upsert_master and packages:
                    master_result = upsert_package_master_products(session, packages)
                    if apply:
                        session.commit()
                    else:
                        session.rollback()
                if "magento" in wanted_channels:
                    if not magento_connection_id:
                        results.append(
                            {
                                "channel": "magento",
                                "status": "skipped",
                                "reason": "magento_connection_id required",
                            }
                        )
                    else:
                        results.extend(
                            _push_magento(
                                session,
                                packages,
                                connection_id=int(magento_connection_id),
                                dry_run=dry_run,
                                skip_missing_components=skip_missing_components,
                                force_recreate=force_recreate,
                            )
                        )
                if "shopify" in wanted_channels:
                    results.extend(
                        _push_shopify(
                            session,
                            packages,
                            connection_id=shopify_connection_id,
                            dry_run=dry_run,
                            skip_missing_components=skip_missing_components,
                        )
                    )
        except Exception as exc:
            logger.warning("DB session unavailable for file push enrichment: %s", exc)
            # Offline dry-run: still emit Magento/Shopify payloads without live API.
            if "magento" in wanted_channels:
                from magento.bundle_product import upsert_magento_kitchen_package

                for pkg in packages:
                    try:
                        # Force components linkable for payload preview when offline
                        from dataclasses import replace

                        preview = replace(
                            pkg,
                            components=[replace(c, master_exists=True) for c in pkg.components],
                        )
                        results.append(
                            upsert_magento_kitchen_package(
                                api=None,
                                package=preview,
                                attribute_set_id=4,
                                dry_run=True,
                                skip_missing_components=False,
                            )
                        )
                    except Exception as pkg_exc:
                        results.append(
                            {
                                "channel": "magento",
                                "sku": pkg.package_master_sku,
                                "status": "dry_run_payload_error",
                                "error": str(pkg_exc),
                            }
                        )
            if "shopify" in wanted_channels:
                from dataclasses import replace

                from shopify.bundle_product import upsert_shopify_kitchen_package

                for pkg in packages:
                    preview = replace(
                        pkg,
                        components=[replace(c, master_exists=True) for c in pkg.components],
                    )
                    results.append(
                        upsert_shopify_kitchen_package(
                            None,
                            preview,
                            dry_run=True,
                            skip_missing_components=False,
                        )
                    )

        if require_components_exist:
            packages = [p for p in packages if not p.missing_component_skus()]

        return {
            "status": "ok",
            "dry_run": dry_run,
            "source": source,
            "package_count": len(packages),
            "packages": [
                {
                    "sku": p.package_master_sku,
                    "package_code": p.package_code,
                    "collection_code": p.collection_code,
                    "price_usd": p.price_usd,
                    "components": len(p.components),
                    "missing_components": p.missing_component_skus(),
                }
                for p in packages
            ],
            "master_upsert": master_result,
            "channel_results": results,
        }

    with get_session() as session:
        if from_db:
            packages = list_kitchen_packages_from_profiles(
                session,
                collection_codes=collection_codes,
                only_known_codes=True,
            )
            source = "db:collection_commerce_profile"
        else:
            raise ValueError("Provide --file or --from-db")

        if require_components_exist:
            packages = [p for p in packages if not p.missing_component_skus()]

        if upsert_master and packages:
            master_result = upsert_package_master_products(session, packages)
            if apply:
                session.commit()
            else:
                session.rollback()

        if "magento" in wanted_channels:
            if not magento_connection_id:
                results.append({"channel": "magento", "status": "skipped", "reason": "magento_connection_id required"})
            else:
                results.extend(
                    _push_magento(
                        session,
                        packages,
                        connection_id=int(magento_connection_id),
                        dry_run=dry_run,
                        skip_missing_components=skip_missing_components,
                        force_recreate=force_recreate,
                    )
                )

        if "shopify" in wanted_channels:
            results.extend(
                _push_shopify(
                    session,
                    packages,
                    connection_id=shopify_connection_id,
                    dry_run=dry_run,
                    skip_missing_components=skip_missing_components,
                )
            )

        if apply:
            session.commit()
        else:
            session.rollback()

    return {
        "status": "ok",
        "dry_run": dry_run,
        "source": source,
        "package_count": len(packages),
        "packages": [
            {
                "sku": p.package_master_sku,
                "package_code": p.package_code,
                "collection_code": p.collection_code,
                "price_usd": p.price_usd,
                "components": len(p.components),
                "missing_components": p.missing_component_skus(),
            }
            for p in packages
        ],
        "master_upsert": master_result,
        "channel_results": results,
    }


def _push_magento(
    session,
    packages,
    *,
    connection_id: int,
    dry_run: bool,
    skip_missing_components: bool,
    force_recreate: bool = False,
) -> List[Dict[str, Any]]:
    from db.models import MagentoConnection
    from magento.bundle_product import upsert_magento_kitchen_package
    from magento.magento_api import MagentoRestClient
    from magento.oauth_client import MagentoOAuthClient, build_magento_oauth_kwargs
    from settings import load_magento_baseline_config

    conn = session.get(MagentoConnection, connection_id)
    if conn is None:
        return [{"channel": "magento", "status": "failed", "error": f"connection {connection_id} not found"}]

    # Kitchen-package bundles only: Magento persists bundle options against the
    # admin/store-0 product row. Creating/updating the parent under a store-view
    # REST prefix (e.g. us_eng/V1) and then writing options via admin/V1 produces
    # Magento's opaque "The option couldn't be saved." — including bare options
    # with no product_links. Keep this admin scope local to package upserts;
    # general catalog sync still uses the connection store view.
    api = MagentoRestClient(
        MagentoOAuthClient(
            **build_magento_oauth_kwargs(
                conn,
                rest_base_path_override=(conn.rest_base_path or "V1"),
                skip_store_code=True,
            )
        )
    )
    attr_set_id = _resolve_attribute_set_id(session, connection_id)
    website_ids = None
    try:
        bl = load_magento_baseline_config()
        if bl.default_website_id:
            website_ids = [int(bl.default_website_id)]
    except Exception:
        website_ids = None

    out = []
    for pkg in packages:
        try:
            result = upsert_magento_kitchen_package(
                api,
                pkg,
                attribute_set_id=attr_set_id,
                website_ids=website_ids,
                dry_run=dry_run,
                skip_missing_components=skip_missing_components,
                force_recreate=force_recreate,
            )
            if (
                force_recreate
                and not dry_run
                and result.get("action") in {"recreate", "create", "delete"}
            ):
                result["dependent_state"] = _clear_magento_package_dependent_state(
                    session,
                    connection_id=connection_id,
                    sku=pkg.package_master_sku,
                )
            out.append(result)
        except Exception as exc:
            logger.exception("Magento package push failed for %s", pkg.package_master_sku)
            out.append(
                {
                    "channel": "magento",
                    "sku": pkg.package_master_sku,
                    "status": "failed",
                    "error": str(exc),
                }
            )
    return out


def _clear_magento_package_dependent_state(
    session,
    *,
    connection_id: int,
    sku: str,
) -> Dict[str, Any]:
    """Drop local rows keyed to the old Magento entity after force_recreate delete."""
    from sqlalchemy import delete

    from db.models import MagentoCatalogState, MagentoSyncState

    catalog_deleted = session.execute(
        delete(MagentoCatalogState).where(
            MagentoCatalogState.connection_id == int(connection_id),
            MagentoCatalogState.sku == str(sku),
        )
    ).rowcount
    sync_deleted = session.execute(
        delete(MagentoSyncState).where(
            MagentoSyncState.connection_id == int(connection_id),
            MagentoSyncState.sku == str(sku),
        )
    ).rowcount
    return {
        "catalog_state_rows_deleted": int(catalog_deleted or 0),
        "sync_state_rows_deleted": int(sync_deleted or 0),
        "note": (
            "Cleared local catalog/sync state for this SKU. Magento URL rewrites, "
            "category links, and media on the old entity_id are gone with the delete; "
            "re-pull Magento catalog state after a successful recreate."
        ),
    }

def _push_shopify(
    session,
    packages,
    *,
    connection_id: Optional[int],
    dry_run: bool,
    skip_missing_components: bool,
) -> List[Dict[str, Any]]:
    from db.channel_sku_mapping import channel_sku_for_master, upsert_single_mapping
    from db.compat_connections import resolve_compat_connection_id
    from db.models import ShopifyConnection
    from shopify.bundle_product import upsert_shopify_kitchen_package
    from shopify.connections import build_client, get_active_connection

    client = None
    conn = None
    if not dry_run:
        if connection_id is not None:
            conn = session.get(ShopifyConnection, int(connection_id))
        if conn is None:
            conn = get_active_connection(session)
        if conn is None:
            return [{"channel": "shopify", "status": "failed", "error": "no active Shopify connection"}]
        client = build_client(conn)
    compat_connection_id = resolve_compat_connection_id("shopify", conn.id if conn is not None else connection_id)

    out = []
    for pkg in packages:
        try:
            component_channel_skus = {
                component.master_sku: channel_sku_for_master(
                    session,
                    component.master_sku,
                    "shopify",
                    connection_id=compat_connection_id,
                )
                for component in pkg.components
            }
            result = (
                upsert_shopify_kitchen_package(
                    client,
                    pkg,
                    dry_run=dry_run,
                    skip_missing_components=skip_missing_components,
                    component_channel_skus=component_channel_skus,
                )
            )
            if (
                not dry_run
                and result.get("status") == "ok"
                and compat_connection_id is not None
            ):
                upsert_single_mapping(
                    session,
                    master_sku=pkg.package_master_sku,
                    channel_sku=pkg.package_master_sku,
                    channel_code="shopify",
                    connection_id=compat_connection_id,
                    remote_id=result.get("product_id"),
                    match_source="kitchen_package_push",
                    notes="Native Shopify kitchen package bundle push",
                )
            out.append(result)
        except Exception as exc:
            logger.exception("Shopify package push failed for %s", pkg.package_master_sku)
            out.append(
                {
                    "channel": "shopify",
                    "sku": pkg.package_master_sku,
                    "status": "failed",
                    "error": str(exc),
                }
            )
    return out


def _resolve_attribute_set_id(session, connection_id: int) -> int:
    import os

    from sqlalchemy import select

    from db.models import MagentoAttributeSetRegistry
    from settings import load_magento_baseline_config

    env_id = os.getenv("MAGENTO_DEFAULT_ATTRIBUTE_SET_ID", "").strip()
    if env_id.isdigit():
        return int(env_id)
    try:
        bl = load_magento_baseline_config()
        name = (bl.default_attribute_set_name or "Default").strip()
    except Exception:
        name = "Default"
    row = session.scalar(
        select(MagentoAttributeSetRegistry)
        .where(MagentoAttributeSetRegistry.connection_id == connection_id)
        .where(MagentoAttributeSetRegistry.attribute_set_name.ilike(name))
        .limit(1)
    )
    if row is not None:
        return int(row.attribute_set_id)
    # Magento default attribute set is commonly 4
    return 4


def main() -> int:
    parser = argparse.ArgumentParser(description="Push kitchen package fixed-price bundles")
    parser.add_argument("--file", default=None, help="Brochure JSON with kitchen_packages")
    parser.add_argument("--from-db", action="store_true", help="Load packages from commerce profiles")
    parser.add_argument("--collection-code", action="append", default=[], help="Filter by collection code (repeatable)")
    parser.add_argument(
        "--channels",
        default="magento,shopify",
        help="Comma-separated: magento,shopify",
    )
    parser.add_argument("--magento-connection-id", type=int, default=None)
    parser.add_argument("--shopify-connection-id", type=int, default=None)
    parser.add_argument("--apply", action="store_true")
    parser.add_argument("--no-upsert-master", action="store_true")
    parser.add_argument(
        "--include-missing-components",
        action="store_true",
        help="Link Magento/Shopify options even when component master SKU is missing locally",
    )
    parser.add_argument(
        "--require-components-exist",
        action="store_true",
        help="Skip packages that still have missing component master SKUs",
    )
    parser.add_argument(
        "--force-recreate",
        action="store_true",
        help=(
            "Delete existing Magento package SKUs before create (changes entity_id; "
            "can break catalog_state/media/URL rewrites). Use only to rebuild bad shells."
        ),
    )
    args = parser.parse_args()

    if not args.file and not args.from_db:
        parser.error("Provide --file or --from-db")

    result = run_push_kitchen_packages(
        file_path=args.file,
        from_db=args.from_db,
        collection_codes=args.collection_code or None,
        channels=[c.strip() for c in str(args.channels).split(",") if c.strip()],
        magento_connection_id=args.magento_connection_id,
        shopify_connection_id=args.shopify_connection_id,
        apply=args.apply,
        upsert_master=not args.no_upsert_master,
        skip_missing_components=not args.include_missing_components,
        require_components_exist=args.require_components_exist,
        force_recreate=args.force_recreate,
    )
    print(json.dumps(result, indent=2, default=str))
    failed = [
        r
        for r in result.get("channel_results") or []
        if r.get("status") == "failed"
    ]
    return 1 if failed else 0


if __name__ == "__main__":
    raise SystemExit(main())
