"""
Repair Magento visibility for configurable parent/child products.

Parents should be Catalog, Search (4). Children should be Not Visible Individually (1).

Examples:
  python -m app.jobs.repair_magento_configurable_visibility --connection-id 1 --dry-run --limit 20
  python -m app.jobs.repair_magento_configurable_visibility --connection-id 1 --skus RTA-ESW-MWEP-PARENT --include-children --dry-run
  python -m app.jobs.repair_magento_configurable_visibility --connection-id 1 --apply --limit 100
"""

from __future__ import annotations

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

from sqlalchemy import select
from sqlalchemy.orm import Session

from db.models import MasterProductRelation
from magento.payload_mapper import VISIBILITY_CATALOG_SEARCH, VISIBILITY_NOT_VISIBLE

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

_DEADLOCK_CODE_RE = re.compile(r'"code"\s*:\s*1213')


@dataclass(frozen=True)
class VisibilityRepairCandidate:
    sku: str
    role: str
    target_visibility: int
    parent_sku: Optional[str] = None

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)


def find_visibility_repair_candidates(
    session: Session,
    *,
    skus: Optional[Sequence[str]] = None,
    include_children: bool = True,
    limit: Optional[int] = None,
) -> List[VisibilityRepairCandidate]:
    wanted = {str(s).strip() for s in skus or [] if str(s).strip()}
    stmt = select(MasterProductRelation.parent_sku, MasterProductRelation.child_sku).order_by(
        MasterProductRelation.parent_sku,
        MasterProductRelation.sort_order,
        MasterProductRelation.child_sku,
    )
    rows = session.execute(stmt).all()
    parent_skus: List[str] = []
    children_by_parent: Dict[str, List[str]] = {}
    for parent_sku, child_sku in rows:
        parent = str(parent_sku or "").strip()
        child = str(child_sku or "").strip()
        if not parent:
            continue
        if parent not in children_by_parent:
            children_by_parent[parent] = []
            parent_skus.append(parent)
        if child:
            children_by_parent[parent].append(child)

    candidates: List[VisibilityRepairCandidate] = []
    seen: set[str] = set()
    for parent in parent_skus:
        children = children_by_parent.get(parent) or []
        parent_requested = not wanted or parent in wanted
        child_requested = bool(wanted and any(child in wanted for child in children))
        if not parent_requested and not child_requested:
            continue
        if parent_requested and parent not in seen:
            seen.add(parent)
            candidates.append(
                VisibilityRepairCandidate(
                    sku=parent,
                    role="parent",
                    target_visibility=VISIBILITY_CATALOG_SEARCH,
                )
            )
        if include_children:
            for child in children:
                if wanted and not parent_requested and child not in wanted:
                    continue
                if child in seen:
                    continue
                seen.add(child)
                candidates.append(
                    VisibilityRepairCandidate(
                        sku=child,
                        role="child",
                        target_visibility=VISIBILITY_NOT_VISIBLE,
                        parent_sku=parent,
                    )
                )
        if limit and len(candidates) >= limit:
            return candidates[:limit]
    return candidates


def fetch_current_visibility(
    session: Session,
    connection_id: int,
    candidates: Sequence[VisibilityRepairCandidate],
) -> Dict[str, Any]:
    api = _magento_api(session, connection_id)
    products: List[Dict[str, Any]] = []
    errors: List[Dict[str, Any]] = []
    for candidate in candidates:
        try:
            product = api.get_product(candidate.sku)
            products.append(
                {
                    **candidate.to_dict(),
                    "magento_found": bool(product),
                    "magento_visibility": product.get("visibility") if product else None,
                    "differs": bool(product) and int(product.get("visibility") or 0) != candidate.target_visibility,
                }
            )
        except Exception as exc:  # pragma: no cover - depends on remote Magento
            errors.append({"sku": candidate.sku, "error": str(exc)})
    return {
        "status": "partial" if errors else "success",
        "fetched": len(products),
        "products": products,
        "errors": errors,
    }


def push_visibility_repairs(
    session: Session,
    connection_id: int,
    candidates: Sequence[VisibilityRepairCandidate],
    *,
    dry_run: bool = True,
    delay_s: float = 0.0,
    retry_attempts: int = 3,
    retry_delay_s: float = 1.0,
) -> Dict[str, Any]:
    if dry_run:
        return {"status": "dry_run", "would_push": len(candidates)}
    api = _magento_api(session, connection_id)
    pushed = 0
    errors: List[Dict[str, Any]] = []
    for candidate in candidates:
        try:
            _update_visibility_with_retry(
                api,
                candidate,
                retry_attempts=retry_attempts,
                retry_delay_s=retry_delay_s,
            )
            pushed += 1
            if delay_s > 0:
                time.sleep(delay_s)
        except Exception as exc:  # pragma: no cover - depends on remote Magento
            errors.append({"sku": candidate.sku, "error": str(exc)})
            logger.warning("Magento visibility repair failed for %s: %s", candidate.sku, exc)
    return {
        "status": "partial" if errors else "success",
        "pushed": pushed,
        "errors": errors,
    }


def _update_visibility_with_retry(
    api: Any,
    candidate: VisibilityRepairCandidate,
    *,
    retry_attempts: int,
    retry_delay_s: float,
) -> None:
    attempts = max(1, int(retry_attempts))
    for attempt in range(1, attempts + 1):
        try:
            api.update_product(
                candidate.sku,
                {"sku": candidate.sku, "visibility": candidate.target_visibility},
            )
            return
        except Exception as exc:
            if not _is_magento_deadlock_error(exc) or attempt >= attempts:
                raise
            sleep_s = max(0.0, float(retry_delay_s)) * (2 ** (attempt - 1))
            logger.warning(
                "Magento visibility repair deadlock for %s (attempt %d/%d), retrying in %.2fs: %s",
                candidate.sku,
                attempt,
                attempts,
                sleep_s,
                exc,
            )
            if sleep_s > 0:
                time.sleep(sleep_s)


def _is_magento_deadlock_error(exc: Exception) -> bool:
    text = str(exc or "")
    lowered = text.lower()
    return "deadlock" in lowered and (
        "1213" in lowered
        or "database deadlock found when trying to get lock" in lowered
        or bool(_DEADLOCK_CODE_RE.search(text))
    )


def run_repair(
    *,
    connection_id: int,
    skus: Optional[Sequence[str]] = None,
    include_children: bool = True,
    include_current: bool = False,
    limit: Optional[int] = None,
    apply: bool = False,
    delay_s: float = 0.0,
) -> Dict[str, Any]:
    from db.session import get_session

    with get_session() as session:
        candidates = find_visibility_repair_candidates(
            session,
            skus=skus,
            include_children=include_children,
            limit=limit,
        )
        current = (
            fetch_current_visibility(session, connection_id, candidates)
            if include_current
            else {"status": "skipped", "reason": "not_requested"}
        )
        result = push_visibility_repairs(
            session,
            connection_id,
            candidates,
            dry_run=not apply,
            delay_s=delay_s,
        )
        return {
            "status": "success",
            "candidate_count": len(candidates),
            "candidates": [candidate.to_dict() for candidate in candidates[:100]],
            "truncated": len(candidates) > 100,
            "current_magento": current,
            "magento": result,
        }


def _magento_api(session: Session, connection_id: int):
    from db.magento_repositories import SqlAlchemyMagentoConnectionRepository
    from magento.magento_api import MagentoRestClient
    from magento.oauth_client import MagentoOAuthClient, build_magento_oauth_kwargs

    conn = SqlAlchemyMagentoConnectionRepository(session).get_for_sync(connection_id)
    if not conn:
        raise ValueError(f"Magento connection {connection_id} not found")
    oauth = MagentoOAuthClient(**build_magento_oauth_kwargs(conn))
    return MagentoRestClient(oauth)


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="Repair Magento configurable parent/child visibility")
    parser.add_argument("--connection-id", type=int, required=True)
    parser.add_argument("--skus", help="Comma-separated parent or child SKUs to inspect")
    parser.add_argument("--limit", type=int)
    parser.add_argument("--parents-only", action="store_true", help="Only repair parent SKUs")
    parser.add_argument("--include-current", action="store_true", help="Fetch current Magento visibility for comparison")
    parser.add_argument("--apply", action="store_true", help="Push visibility updates to Magento")
    parser.add_argument("--dry-run", action="store_true", help="Explicit no-op mode")
    parser.add_argument("--delay-ms", type=int, default=0)
    args = parser.parse_args()

    result = run_repair(
        connection_id=args.connection_id,
        skus=_parse_skus(args.skus),
        include_children=not args.parents_only,
        include_current=args.include_current,
        limit=args.limit,
        apply=args.apply and not args.dry_run,
        delay_s=max(0, args.delay_ms) / 1000.0,
    )
    print(json.dumps(result, indent=2, default=str))
    return 0 if result.get("status") != "failed" else 1


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