"""Fix Magento product status + visibility only (no price, no MSI).

Safe when NetSuite owns pricing / inventory:
  - status=1 (Enabled) for all products
  - visibility=4 (Catalog, Search) for simples + configurable parents
  - visibility=1 (Not Visible Individually) for configurable children

Examples:
  python -m app.jobs.magento_fix_status_visibility --connection-id 1 --dry-run
  python -m app.jobs.magento_fix_status_visibility --connection-id 1 --apply
  python -m app.jobs.magento_fix_status_visibility --connection-id 1 --skus SKU1,SKU2 --apply
"""

from __future__ import annotations

import argparse
import json
import logging
import sys
import time
from dataclasses import asdict, dataclass
from typing import Any, Dict, List, Optional, Sequence, Set

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

STATUS_ENABLED = 1
VISIBILITY_NOT_VISIBLE = 1
VISIBILITY_CATALOG_SEARCH = 4


@dataclass(frozen=True)
class StatusVisibilityFix:
    sku: str
    role: str  # parent | child | simple
    type_id: str
    attribute_set_id: Optional[int]
    url_key: Optional[str]
    current_status: Optional[int]
    current_visibility: Optional[int]
    target_status: int
    target_visibility: int

    @property
    def needs_update(self) -> bool:
        return (
            int(self.current_status or 0) != self.target_status
            or int(self.current_visibility or 0) != self.target_visibility
        )

    def to_dict(self) -> Dict[str, Any]:
        return {**asdict(self), "needs_update": self.needs_update}

    def write_payload(self, *, url_key: Optional[str] = None) -> Dict[str, Any]:
        """Minimal Magento product payload — never price / MSI."""
        payload: Dict[str, Any] = {
            "sku": self.sku,
            "status": self.target_status,
            "visibility": self.target_visibility,
            "type_id": self.type_id or "simple",
        }
        if self.attribute_set_id is not None:
            payload["attribute_set_id"] = int(self.attribute_set_id)
        key = (url_key if url_key is not None else self.url_key) or ""
        key = str(key).strip()
        if key:
            payload["custom_attributes"] = [{"attribute_code": "url_key", "value": key}]
        return payload


def _as_int(value: Any) -> Optional[int]:
    try:
        if value is None or value == "":
            return None
        return int(value)
    except (TypeError, ValueError):
        return None


def _custom_attr(product: Dict[str, Any], code: str) -> Optional[str]:
    for attr in product.get("custom_attributes") or []:
        if isinstance(attr, dict) and str(attr.get("attribute_code") or "").strip() == code:
            text = str(attr.get("value") or "").strip()
            return text or None
    return None


def _unique_url_key_for_sku(sku: str) -> str:
    """SKU-based url_key that differs from the bare SKU slug (common conflict source)."""
    from channel.url_canonical import slugify_segment

    base = slugify_segment(str(sku or "").strip()) or "product"
    return f"{base}-sku"


def _error_text(status: int, body: Any) -> str:
    if isinstance(body, dict):
        return (str(body.get("message", "")) + str(body.get("parameters", []))).lower()
    return str(body or "").lower()


def _is_url_key_exists_error(status: int, body: Any) -> bool:
    if status != 400 or not body:
        return False
    text = _error_text(status, body)
    return "url key" in text and ("already exists" in text or "exists" in text)


def _iter_magento_products(api: Any, *, page_size: int = 200) -> List[Dict[str, Any]]:
    page = 1
    items: List[Dict[str, Any]] = []
    while True:
        status, batch, total, err = api.list_products(page_size=page_size, current_page=page)
        if status != 200 or not batch:
            if status != 200:
                raise RuntimeError(f"list_products failed HTTP {status}: {err}")
            break
        items.extend(item for item in batch if isinstance(item, dict) and item.get("sku"))
        if len(items) >= int(total or 0) or len(batch) < page_size:
            break
        page += 1
    return items


def _child_skus_for_configurables(api: Any, parent_skus: Sequence[str]) -> Set[str]:
    children: Set[str] = set()
    for parent in parent_skus:
        status, child_list, err = api.get_configurable_children(parent)
        if status != 200:
            logger.warning("get_configurable_children failed for %s: %s", parent, err)
            continue
        for child in child_list or []:
            text = str(child or "").strip()
            if text:
                children.add(text)
    return children


def build_status_visibility_fixes(
    products: Sequence[Dict[str, Any]],
    child_skus: Set[str],
    *,
    only_skus: Optional[Set[str]] = None,
) -> List[StatusVisibilityFix]:
    fixes: List[StatusVisibilityFix] = []
    for product in products:
        sku = str(product.get("sku") or "").strip()
        if not sku:
            continue
        if only_skus is not None and sku not in only_skus:
            continue
        type_id = str(product.get("type_id") or "").strip().lower() or "simple"
        # Magento configurable child links win over type_id for visibility role.
        if sku in child_skus:
            role = "child"
            target_visibility = VISIBILITY_NOT_VISIBLE
        elif "configurable" in type_id:
            role = "parent"
            target_visibility = VISIBILITY_CATALOG_SEARCH
        else:
            role = "simple"
            target_visibility = VISIBILITY_CATALOG_SEARCH
        fixes.append(
            StatusVisibilityFix(
                sku=sku,
                role=role,
                type_id=type_id,
                attribute_set_id=_as_int(product.get("attribute_set_id")),
                url_key=_custom_attr(product, "url_key"),
                current_status=_as_int(product.get("status")),
                current_visibility=_as_int(product.get("visibility")),
                target_status=STATUS_ENABLED,
                target_visibility=target_visibility,
            )
        )
    return fixes


def _is_attribute_set_entity_type_error(status: int, body: Any) -> bool:
    if status != 400 or not body:
        return False
    return "invalid attribute set entity type" in _error_text(status, body)


def _put_or_post(api: Any, fix: StatusVisibilityFix, payload: Dict[str, Any]) -> tuple[int, Any]:
    status, body = api.put_product(fix.sku, payload)
    if status in (200, 201):
        return status, body
    # Magento often returns this when PUT path fails to load the product (esp. SKUs with '/').
    if _is_attribute_set_entity_type_error(status, body) or ("/" in fix.sku and status in (400, 404)):
        if fix.attribute_set_id is None:
            return status, body
        logger.info(
            "Retrying %s via POST upsert after PUT HTTP %s (include attribute_set_id=%s)",
            fix.sku,
            status,
            fix.attribute_set_id,
        )
        return api.post_product(payload)
    return status, body


def _write_status_visibility(api: Any, fix: StatusVisibilityFix) -> tuple[int, Any]:
    """PUT/POST status+visibility; on URL-key conflict retry with a SKU-unique url_key."""
    payload = fix.write_payload()
    status, body = _put_or_post(api, fix, payload)
    if status in (200, 201):
        return status, body
    if not _is_url_key_exists_error(status, body):
        return status, body

    unique_key = _unique_url_key_for_sku(fix.sku)
    logger.info(
        "Retrying %s with unique url_key=%r after URL key conflict",
        fix.sku,
        unique_key,
    )
    return _put_or_post(api, fix, fix.write_payload(url_key=unique_key))


def push_status_visibility_fixes(
    api: Any,
    fixes: Sequence[StatusVisibilityFix],
    *,
    dry_run: bool = True,
    delay_s: float = 0.0,
    force: bool = False,
) -> Dict[str, Any]:
    todo = [fix for fix in fixes if force or fix.needs_update]
    if dry_run:
        return {
            "status": "dry_run",
            "would_update": len(todo),
            "skipped_already_ok": len(fixes) - len(todo),
            "sample": [fix.to_dict() for fix in todo[:50]],
        }

    updated = 0
    skipped = len(fixes) - len(todo)
    errors: List[Dict[str, str]] = []
    for fix in todo:
        try:
            status, body = _write_status_visibility(api, fix)
            if status in (200, 201):
                updated += 1
            else:
                errors.append({"sku": fix.sku, "error": f"HTTP {status}: {body}"})
        except Exception as exc:  # pragma: no cover - remote Magento
            errors.append({"sku": fix.sku, "error": str(exc)})
            logger.warning("status/visibility fix failed for %s: %s", fix.sku, exc)
        if delay_s > 0:
            time.sleep(delay_s)
    return {
        "status": "partial" if errors else "success",
        "updated": updated,
        "skipped_already_ok": skipped,
        "errors": errors[:50],
        "error_count": len(errors),
    }


def run_fix_status_visibility(
    connection_id: int,
    *,
    skus: Optional[Sequence[str]] = None,
    apply: bool = False,
    force: bool = False,
    delay_s: float = 0.0,
    page_size: int = 200,
) -> Dict[str, Any]:
    from db.magento_repositories import SqlAlchemyMagentoConnectionRepository
    from db.session import get_session
    from magento.magento_api import MagentoRestClient
    from magento.oauth_client import MagentoOAuthClient, build_magento_oauth_kwargs

    only = {str(s).strip() for s in (skus or []) if str(s).strip()} or None
    with get_session() as session:
        conn = SqlAlchemyMagentoConnectionRepository(session).get_for_sync(connection_id)
        if not conn:
            return {"status": "failed", "error": f"Connection {connection_id} not found"}
        api = MagentoRestClient(MagentoOAuthClient(**build_magento_oauth_kwargs(conn)))

        logger.info("Scanning Magento catalog for status/visibility repair...")
        catalog = _iter_magento_products(api, page_size=page_size)
        logger.info("Loaded %d Magento product(s)", len(catalog))

        if only:
            by_sku = {
                str(p.get("sku") or "").strip(): p
                for p in catalog
                if str(p.get("sku") or "").strip()
            }
            products = []
            for sku in sorted(only):
                product = by_sku.get(sku) or api.get_product(sku)
                if product:
                    products.append(product)
                else:
                    logger.warning("SKU not found on Magento: %s", sku)
        else:
            products = catalog

        parent_skus = [
            str(p.get("sku") or "").strip()
            for p in catalog
            if "configurable" in str(p.get("type_id") or "").lower()
            and str(p.get("sku") or "").strip()
        ]
        child_skus = _child_skus_for_configurables(api, parent_skus)
        logger.info(
            "Classified %d configurable parent(s), %d linked child SKU(s)",
            len(parent_skus),
            len(child_skus),
        )

        fixes = build_status_visibility_fixes(products, child_skus, only_skus=only)
        result = push_status_visibility_fixes(
            api,
            fixes,
            dry_run=not apply,
            delay_s=delay_s,
            force=force,
        )
        return {
            "status": result.get("status") or "success",
            "connection_id": connection_id,
            "scanned": len(products),
            "fix_count": len(fixes),
            "needs_update": sum(1 for f in fixes if f.needs_update),
            "roles": {
                "parent": sum(1 for f in fixes if f.role == "parent"),
                "child": sum(1 for f in fixes if f.role == "child"),
                "simple": sum(1 for f in fixes if f.role == "simple"),
            },
            "magento": result,
            "note": "Writes only status+visibility; never price or MSI/source-items",
        }


def _parse_skus(value: Optional[str]) -> Optional[List[str]]:
    if not value:
        return None
    return [part.strip() for part in value.split(",") if part.strip()]


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "Enable all Magento products and set visibility "
            "(Catalog/Search for simples+parents, Not Visible for children). "
            "Does not touch price or MSI."
        )
    )
    parser.add_argument("--connection-id", type=int, required=True)
    parser.add_argument("--skus", help="Comma-separated SKUs (default: entire Magento catalog)")
    parser.add_argument("--apply", action="store_true", help="Write status/visibility to Magento")
    parser.add_argument("--dry-run", action="store_true", help="Explicit preview mode (default)")
    parser.add_argument(
        "--force",
        action="store_true",
        help="PUT even when Magento already has the target status/visibility",
    )
    parser.add_argument("--delay-ms", type=int, default=0, help="Delay between PUTs")
    parser.add_argument("--page-size", type=int, default=200)
    args = parser.parse_args()

    apply = bool(args.apply) and not bool(args.dry_run)
    result = run_fix_status_visibility(
        args.connection_id,
        skus=_parse_skus(args.skus),
        apply=apply,
        force=args.force,
        delay_s=max(0, int(args.delay_ms or 0)) / 1000.0,
        page_size=max(1, int(args.page_size or 200)),
    )
    print(json.dumps(result, indent=2, default=str))
    return 0 if result.get("status") not in {"failed"} else 1


if __name__ == "__main__":
    sys.exit(main())
