"""Magento fixed-price kitchen package bundles."""

from __future__ import annotations

import copy
import logging
from typing import Any, Dict, List, Optional

from db.kitchen_packages import KitchenPackage
from magento.magento_api import MagentoRestClient

logger = logging.getLogger(__name__)

# Magento\Bundle\Model\Product\Price: PRICE_TYPE_FIXED=1, PRICE_TYPE_DYNAMIC=0.
# sku_type / weight_type use the same 1=fixed, 0=dynamic convention.
MAGENTO_PRICE_TYPE_FIXED = "1"
MAGENTO_SKU_TYPE_FIXED = "1"
MAGENTO_WEIGHT_TYPE_FIXED = "1"
MAGENTO_PRICE_VIEW_PRICE_RANGE = "0"


def build_magento_fixed_bundle_payload(
    package: KitchenPackage,
    *,
    attribute_set_id: int,
    website_ids: Optional[List[int]] = None,
    status: int = 1,
    visibility: int = 4,
    skip_missing_components: bool = True,
) -> Dict[str, Any]:
    """Build Magento REST product payload for a fixed-price bundle."""
    options: List[Dict[str, Any]] = []
    position = 1
    for comp in package.components:
        if skip_missing_components and not comp.master_exists:
            continue
        title = comp.description or comp.brochure_sku or comp.master_sku
        option_id = position - 1
        options.append(
            {
                "option_id": option_id,
                "title": title[:255],
                "required": True,
                "type": "select",
                "position": position,
                # Unique option SKU for product+saveOptions create (Adobe tutorial).
                # options/add overwrites this with the parent package SKU.
                "sku": _option_sku(package.package_master_sku, position, title),
                "product_links": [
                    {
                        "sku": comp.master_sku,
                        "option_id": position,
                        "qty": float(comp.qty),
                        "position": 1,
                        # Required select with a single link must be default.
                        "is_default": True,
                        "price": 0,
                        "price_type": 0,
                        "can_change_quantity": 0,
                    }
                ],
            }
        )
        position += 1

    if not options:
        raise ValueError(
            f"Package {package.package_master_sku} has no linkable Magento components "
            f"(missing master SKUs: {package.missing_component_skus()[:10]})"
        )

    name = _package_title(package.collection_name, package.label)
    custom_attributes = [
        {"attribute_code": "price_type", "value": MAGENTO_PRICE_TYPE_FIXED},
        {"attribute_code": "sku_type", "value": MAGENTO_SKU_TYPE_FIXED},
        {"attribute_code": "weight_type", "value": MAGENTO_WEIGHT_TYPE_FIXED},
        {"attribute_code": "price_view", "value": MAGENTO_PRICE_VIEW_PRICE_RANGE},
        {"attribute_code": "shipment_type", "value": "0"},
        {"attribute_code": "options_container", "value": "container2"},
        {"attribute_code": "tax_class_id", "value": "2"},
        {"attribute_code": "url_key", "value": _url_key(package.package_master_sku, name)},
    ]
    extension: Dict[str, Any] = {
        "bundle_product_options": options,
    }
    if website_ids:
        extension["website_ids"] = list(website_ids)
    # Adobe's bundle tutorial includes stock_item; some Magento installs reject
    # option writes on parents that never got an inventory row.
    extension["stock_item"] = {
        "is_in_stock": True,
        "manage_stock": False,
        "use_config_manage_stock": False,
    }

    return {
        "sku": package.package_master_sku,
        "name": name,
        "attribute_set_id": int(attribute_set_id),
        "price": float(package.price_usd),
        "status": int(status),
        "visibility": int(visibility),
        "type_id": "bundle",
        "weight": 0,
        "extension_attributes": extension,
        "custom_attributes": custom_attributes,
    }


def upsert_magento_kitchen_package(
    api: Optional[MagentoRestClient],
    package: KitchenPackage,
    *,
    attribute_set_id: int,
    website_ids: Optional[List[int]] = None,
    dry_run: bool = True,
    skip_missing_components: bool = True,
    options_api: Optional[MagentoRestClient] = None,
    force_recreate: bool = False,
) -> Dict[str, Any]:
    """Upsert a fixed-price Magento bundle for a kitchen package.

    ``api`` should be an admin/store-0 Magento REST client for kitchen packages.
    Magento persists bundle options against the admin product row; mixed
    store-view product writes + admin option writes commonly fail with the
    opaque "The option couldn't be saved." (including bare options).

    ``options_api`` is optional; when set it is used only for option GET/add/
    update/delete. Prefer the same admin client for both.

    ``force_recreate`` deletes the existing Magento product by SKU before create.
    That changes Magento entity_id and drops Magento-side media/category/URL
    rewrite associations. Callers should clear local catalog_state/sync_state
    for the SKU (see push_kitchen_packages._clear_magento_package_dependent_state).
    force_recreate does **not** fix option-save failures caused by composite
    component SKUs or Magento-side option persistence bugs.
    """
    payload = build_magento_fixed_bundle_payload(
        package,
        attribute_set_id=attribute_set_id,
        website_ids=website_ids,
        skip_missing_components=skip_missing_components,
    )
    result: Dict[str, Any] = {
        "channel": "magento",
        "sku": package.package_master_sku,
        "package_code": package.package_code,
        "price_usd": package.price_usd,
        "component_count": len(payload["extension_attributes"]["bundle_product_options"]),
        "missing_components": package.missing_component_skus(),
        "dry_run": dry_run,
        "payload": payload if dry_run else {"sku": payload["sku"], "type_id": "bundle", "price": payload["price"]},
        "force_recreate": bool(force_recreate),
    }
    if dry_run:
        result["status"] = "dry_run"
        return result
    if api is None:
        raise ValueError("Magento API client is required when dry_run=False")

    option_client = options_api or api
    option_payloads = copy.deepcopy(payload["extension_attributes"].get("bundle_product_options") or [])
    link_resolution = _validate_magento_bundle_link_skus(api, option_payloads)
    result["link_resolution"] = {"issues": link_resolution["issues"]}
    if link_resolution["failed"]:
        result["status"] = "failed"
        result["action"] = "preflight"
        result["error"] = {
            "message": (
                "Bundle components must already be Magento simple/virtual/downloadable SKUs. "
                "Configurable/bundle/grouped parents are rejected (no silent child remap). "
                "Map brochure components to an explicit simple SKU before pushing."
            ),
            "issues": link_resolution["issues"],
        }
        logger.warning(
            "Magento bundle preflight failed sku=%s issues=%s",
            package.package_master_sku,
            link_resolution["issues"],
        )
        return result

    existing = api.get_product(package.package_master_sku)
    if force_recreate and existing:
        result["previous_magento_product_id"] = existing.get("id")
        result["recreate_warning"] = (
            "force_recreate deletes the Magento product by SKU and creates a new entity_id; "
            "catalog_state, media, category links, and URL rewrites tied to the old id may break."
        )
        logger.warning(
            "force_recreate=True for %s — deleting Magento product id=%s (entity_id will change)",
            package.package_master_sku,
            existing.get("id"),
        )
        delete_status, delete_err = api.delete_product(package.package_master_sku)
        if delete_status not in (200, 201, 204, 404):
            result["status"] = "failed"
            result["action"] = "delete"
            result["error"] = {
                "message": "force_recreate delete failed",
                "error": delete_err,
                "blast_radius": result["recreate_warning"],
            }
            return result
        existing = None

    product_payload = copy.deepcopy(payload)
    if existing:
        status, body = api.put_product(
            package.package_master_sku,
            product_payload,
            save_options=True,
        )
        result["action"] = "update"
    else:
        status, body = api.post_product(product_payload, save_options=True)
        result["action"] = "recreate" if force_recreate else "create"
    result["http_status"] = status
    if isinstance(body, dict) and body.get("id") is not None:
        result["magento_product_id"] = body.get("id")

    if status not in (200, 201):
        logger.warning(
            "Magento inline bundle save failed sku=%s status=%s body=%s; ensuring shell then options API",
            package.package_master_sku,
            status,
            body,
        )
        if not _ensure_shell_bundle_product(
            api,
            package_sku=package.package_master_sku,
            payload=payload,
        ):
            result["status"] = "failed"
            result["error"] = {
                "message": "Magento refused product save and shell fallback failed.",
                "body": body,
                "parent_snapshot": _parent_bundle_snapshot(api, package.package_master_sku),
            }
            return result
        # Preserve original failure for diagnostics; options sync may still succeed.
        result["inline_save_error"] = body

    verified = _verify_bundle_options(option_client, package.package_master_sku, option_payloads)
    if verified["ok"]:
        result["bundle_option_results"] = verified["results"]
        result["status"] = "ok"
        return result

    option_sync = _sync_bundle_options(
        option_client,
        package_sku=package.package_master_sku,
        desired_options=option_payloads,
    )
    result["bundle_option_results"] = option_sync["results"]
    if option_sync["failed"]:
        result["status"] = "failed"
        result["error"] = {
            "message": "Bundle product saved but one or more bundle options failed.",
            "details": option_sync["results"],
            "parent_snapshot": _parent_bundle_snapshot(api, package.package_master_sku),
            "hint": (
                "force_recreate only helps when the existing shell is corrupt; it does not fix "
                "composite component SKUs or Magento option persistence failures. Prefer mapping "
                "brochure components to Magento simples, and push package product+options via "
                "admin/V1 (store-0) scope."
            ),
            "bare_option_probe": _probe_bare_option_save(
                option_client, package.package_master_sku, option_payloads
            ),
        }
        logger.warning(
            "Magento bundle option sync failed sku=%s details=%s",
            package.package_master_sku,
            option_sync["results"],
        )
    else:
        result["status"] = "ok"
    return result


def _parent_bundle_snapshot(api: MagentoRestClient, sku: str) -> Dict[str, Any]:
    product = api.get_product(sku)
    if not product:
        return {"exists": False}
    customs = {
        str(item.get("attribute_code") or ""): item.get("value")
        for item in (product.get("custom_attributes") or [])
        if item.get("attribute_code")
    }
    options = (product.get("extension_attributes") or {}).get("bundle_product_options") or []
    return {
        "exists": True,
        "id": product.get("id"),
        "type_id": product.get("type_id"),
        "attribute_set_id": product.get("attribute_set_id"),
        "price": product.get("price"),
        "price_type": customs.get("price_type"),
        "sku_type": customs.get("sku_type"),
        "weight_type": customs.get("weight_type"),
        "shipment_type": customs.get("shipment_type"),
        "options_count": len(options) if isinstance(options, list) else None,
    }

def _ensure_shell_bundle_product(
    api: MagentoRestClient,
    *,
    package_sku: str,
    payload: Dict[str, Any],
) -> bool:
    shell_payload = copy.deepcopy(payload)
    shell_extension = dict(shell_payload.get("extension_attributes") or {})
    shell_extension.pop("bundle_product_options", None)
    shell_payload["extension_attributes"] = shell_extension
    existing_after = api.get_product(package_sku)
    if existing_after:
        status, body = api.put_product(package_sku, shell_payload, save_options=False)
    else:
        status, body = api.post_product(shell_payload, save_options=False)
    if status not in (200, 201):
        logger.warning(
            "Magento bundle shell upsert failed sku=%s status=%s body=%s",
            package_sku,
            status,
            body,
        )
        return False
    return True


def _verify_bundle_options(
    api: MagentoRestClient,
    package_sku: str,
    desired_options: List[Dict[str, Any]],
) -> Dict[str, Any]:
    status, existing_options, err = api.get_bundle_options(package_sku)
    if status not in (200, 404):
        return {
            "ok": False,
            "results": [
                {
                    "step": "verify_options",
                    "status": "failed",
                    "http_status": status,
                    "error": err or f"HTTP {status}",
                }
            ],
        }
    existing_by_key = {
        _normalize_option_key(opt): opt
        for opt in (existing_options or [])
        if _normalize_option_key(opt)
    }
    results: List[Dict[str, Any]] = []
    ok = True
    for desired in desired_options:
        key = _normalize_option_key(desired)
        existing = existing_by_key.get(key)
        has_links = bool(existing and (existing.get("product_links") or []))
        if existing and existing.get("option_id") is not None and has_links:
            results.append(
                {
                    "step": "verify_options",
                    "title": desired.get("title"),
                    "option_id": existing.get("option_id"),
                    "status": "ok",
                    "http_status": status,
                }
            )
        else:
            ok = False
            results.append(
                {
                    "step": "verify_options",
                    "title": desired.get("title"),
                    "option_id": (existing or {}).get("option_id"),
                    "status": "missing",
                    "http_status": status,
                }
            )
    if len(existing_options or []) < len(desired_options):
        ok = False
    return {"ok": ok, "results": results}

def _sync_bundle_options(
    api: MagentoRestClient,
    *,
    package_sku: str,
    desired_options: List[Dict[str, Any]],
) -> Dict[str, Any]:
    status, existing_options, err = api.get_bundle_options(package_sku)
    if status not in (200, 404):
        return {
            "failed": True,
            "results": [
                {
                    "step": "get_existing_options",
                    "status": "failed",
                    "http_status": status,
                    "error": err or f"HTTP {status}",
                }
            ],
        }

    existing_by_title = {
        _normalize_option_key(opt): opt
        for opt in (existing_options or [])
        if _normalize_option_key(opt)
    }
    used_option_ids: set[int] = set()
    results: List[Dict[str, Any]] = []
    failed = False

    for desired in desired_options:
        normalized_key = _normalize_option_key(desired)
        existing = existing_by_title.get(normalized_key)
        if existing and existing.get("option_id") is not None:
            option_id = int(existing["option_id"])
            used_option_ids.add(option_id)
            option_payload = _prepare_bundle_option_for_update(
                desired,
                existing,
                package_sku=package_sku,
            )
            option_status, option_body, option_err = api.update_bundle_option(option_id, option_payload)
            results.append(
                {
                    "step": "update_option",
                    "title": desired.get("title"),
                    "option_id": option_id,
                    "http_status": option_status,
                    "status": "ok" if option_status in (200, 201) else "failed",
                    "body": option_body,
                    "error": option_err,
                }
            )
            if option_status not in (200, 201):
                failed = True
            continue

        option_payload = _prepare_bundle_option_for_create(
            desired,
            package_sku=package_sku,
        )
        links = list(option_payload.get("product_links") or [])
        # Create the option shell first. Magento's opaque "couldn't be saved" often
        # comes from selection/link persistence; splitting surfaces clearer errors.
        bare_option = copy.deepcopy(option_payload)
        bare_option["product_links"] = []
        bare_status, bare_body, bare_err = api.add_bundle_option(bare_option)
        new_option_id = _coerce_int(bare_body)
        if bare_status in (200, 201) and new_option_id is not None:
            option_status, option_body, option_err = bare_status, bare_body, bare_err
        else:
            # Fallback: try create with links in one shot (Adobe-style payload).
            option_status, option_body, option_err = api.add_bundle_option(option_payload)
            new_option_id = _coerce_int(option_body)
            results.append(
                {
                    "step": "add_option",
                    "title": desired.get("title"),
                    "option_id": new_option_id,
                    "http_status": option_status,
                    "status": "ok" if option_status in (200, 201) else "failed",
                    "body": option_body,
                    "error": option_err,
                    "bare_attempt": {
                        "http_status": bare_status,
                        "body": bare_body,
                        "error": bare_err,
                    },
                }
            )
            if option_status not in (200, 201):
                failed = True
            elif new_option_id is not None:
                used_option_ids.add(new_option_id)
            continue

        used_option_ids.add(new_option_id)
        link_results: List[Dict[str, Any]] = []
        links_failed = False
        for link in links:
            link_payload = copy.deepcopy(link)
            link_payload.pop("id", None)
            link_payload["option_id"] = new_option_id
            link_payload["is_default"] = True if link_payload.get("is_default") is None else bool(
                link_payload.get("is_default")
            )
            link_status, link_body, link_err = api.add_bundle_product_link(
                package_sku,
                new_option_id,
                link_payload,
            )
            link_results.append(
                {
                    "sku": link_payload.get("sku"),
                    "http_status": link_status,
                    "status": "ok" if link_status in (200, 201) else "failed",
                    "body": link_body,
                    "error": link_err,
                }
            )
            if link_status not in (200, 201):
                links_failed = True
                failed = True
        results.append(
            {
                "step": "add_option_then_links",
                "title": desired.get("title"),
                "option_id": new_option_id,
                "http_status": option_status,
                "status": "failed" if links_failed else "ok",
                "body": option_body,
                "links": link_results,
            }
        )
    for existing in existing_options or []:
        option_id = _coerce_int(existing.get("option_id"))
        if option_id is None or option_id in used_option_ids:
            continue
        delete_status, delete_err = api.delete_bundle_option(package_sku, option_id)
        results.append(
            {
                "step": "delete_option",
                "title": existing.get("title"),
                "option_id": option_id,
                "http_status": delete_status,
                "status": "ok" if delete_status in (200, 201, 204, 404) else "failed",
                "error": delete_err,
            }
        )
        if delete_status not in (200, 201, 204, 404):
            failed = True

    return {"failed": failed, "results": results}


def _prepare_bundle_option_for_create(
    option: Dict[str, Any],
    *,
    package_sku: str,
) -> Dict[str, Any]:
    payload = copy.deepcopy(option)
    # Omit option_id on create. Sending 0 can confuse Magento option collections
    # that treat 0 as a real id on some versions.
    payload.pop("option_id", None)
    # Magento ProductOptionManagementInterface resolves the parent from option.sku.
    payload["sku"] = package_sku
    payload.pop("extension_attributes", None)
    links = payload.get("product_links")
    if links is None:
        payload["product_links"] = []
        links = payload["product_links"]
    for link in links:
        link.pop("id", None)
        link.pop("option_id", None)
        link["is_default"] = True if link.get("is_default") is None else bool(link.get("is_default"))
        link.pop("extension_attributes", None)
    return payload


def _prepare_bundle_option_for_update(
    option: Dict[str, Any],
    existing: Dict[str, Any],
    *,
    package_sku: str,
) -> Dict[str, Any]:
    payload = copy.deepcopy(option)
    option_id = int(existing["option_id"])
    payload["option_id"] = option_id
    payload["sku"] = package_sku
    payload.pop("extension_attributes", None)
    existing_links_by_sku = {
        str(link.get("sku") or "").strip().upper(): link
        for link in (existing.get("product_links") or [])
        if str(link.get("sku") or "").strip()
    }
    for link in payload.get("product_links") or []:
        existing_link = existing_links_by_sku.get(str(link.get("sku") or "").strip().upper()) or {}
        existing_id = existing_link.get("id")
        if existing_id is not None:
            link["id"] = str(existing_id)
        else:
            link.pop("id", None)
        link["option_id"] = option_id
        link["is_default"] = True if link.get("is_default") is None else bool(link.get("is_default"))
        link.pop("extension_attributes", None)
    return payload


def _validate_magento_bundle_link_skus(
    api: MagentoRestClient,
    options: List[Dict[str, Any]],
) -> Dict[str, Any]:
    """Fail closed unless every product_link SKU is Magento simple/virtual/downloadable.

    Magento rejects composite selections. We deliberately do **not** remap
    configurable parents to an arbitrary first child — that can publish the wrong
    finish/size for kitchen packages. Callers must supply an explicit simple SKU.
    """
    allowed = {"simple", "virtual", "downloadable"}
    composite = {"configurable", "bundle", "grouped"}
    issues: List[Dict[str, Any]] = []
    cache: Dict[str, Dict[str, Any]] = {}

    def inspect(sku: str) -> Dict[str, Any]:
        key = sku.upper()
        if key in cache:
            return cache[key]
        product = api.get_product(sku)
        if product is None:
            info = {"sku": sku, "exists": False, "type_id": None}
            cache[key] = info
            return info
        info = {
            "sku": sku,
            "exists": True,
            "type_id": str(product.get("type_id") or "").strip().lower() or None,
        }
        cache[key] = info
        return info

    for option in options:
        for link in option.get("product_links") or []:
            original = str(link.get("sku") or "").strip()
            if not original:
                issues.append({"sku": "", "reason": "empty_link_sku", "title": option.get("title")})
                continue
            info = inspect(original)
            if not info["exists"]:
                issues.append(
                    {
                        "sku": original,
                        "reason": "missing_in_magento",
                        "title": option.get("title"),
                    }
                )
                continue
            type_id = info["type_id"]
            if type_id in allowed:
                continue
            if type_id == "configurable":
                issues.append(
                    {
                        "sku": original,
                        "reason": "configurable_parent_not_allowed",
                        "type_id": type_id,
                        "title": option.get("title"),
                        "hint": "Map this brochure component to an explicit Magento simple SKU.",
                    }
                )
                continue
            if type_id in composite:
                issues.append(
                    {
                        "sku": original,
                        "reason": "unsupported_type",
                        "type_id": type_id,
                        "title": option.get("title"),
                    }
                )
                continue
            issues.append(
                {
                    "sku": original,
                    "reason": "unsupported_type",
                    "type_id": type_id,
                    "title": option.get("title"),
                }
            )

    return {"failed": bool(issues), "issues": issues}


def _probe_bare_option_save(
    api: MagentoRestClient,
    package_sku: str,
    desired_options: List[Dict[str, Any]],
) -> Dict[str, Any]:
    """Try saving one option with no product_links to isolate Magento failures."""
    if not desired_options:
        return {"attempted": False}
    # Ensure parent exists as a shell before probing.
    if api.get_product(package_sku) is None:
        return {"attempted": False, "reason": "parent_missing"}
    sample = _prepare_bundle_option_for_create(desired_options[0], package_sku=package_sku)
    sample["product_links"] = []
    sample["title"] = f"DIAG {sample.get('title') or 'option'}"[:255]
    sample["position"] = 999
    status, body, err = api.add_bundle_option(sample)
    option_id = _coerce_int(body)
    if option_id is not None:
        api.delete_bundle_option(package_sku, option_id)
    return {
        "attempted": True,
        "http_status": status,
        "body": body,
        "error": err,
        "interpretation": (
            "bare_option_ok_links_are_the_problem"
            if status in (200, 201)
            else "bare_option_also_fails_parent_or_magento_config"
        ),
    }


def _normalize_option_key(option: Dict[str, Any]) -> str:
    title = str(option.get("title") or "").strip().lower()
    position = int(option.get("position") or 0)
    return f"{position}:{title}"


def _coerce_int(value: Any) -> Optional[int]:
    try:
        if value is None or value == "":
            return None
        if isinstance(value, dict):
            for key in ("option_id", "id"):
                if value.get(key) is not None:
                    return int(value[key])
            return None
        return int(value)
    except (TypeError, ValueError):
        return None


def _url_key(sku: str, name: str) -> str:
    import re

    base = re.sub(r"[^a-z0-9]+", "-", f"{name}-{sku}".lower()).strip("-")
    return base[:200] or sku.lower()


def _option_sku(package_sku: str, position: int, title: str) -> str:
    import re

    slug = re.sub(r"[^a-z0-9]+", "-", str(title or "").lower()).strip("-")
    slug = slug[:48] if slug else f"option-{position}"
    return f"{package_sku}-{slug}"[:64]


def _package_title(collection_name: str, label: str) -> str:
    collection = str(collection_name or "").strip()
    package_label = str(label or "").strip()
    if collection and package_label:
        return f"{collection} - {package_label}"
    return collection or package_label
