"""Prune or rewrite stale Start Shopping category refs on collection landing pages.

When taxonomy categories are removed or renamed, `start_shopping_config` JSON on
collection landings can keep orphan path slugs. This module is the domain service
that rewrites (rename/merge) or drops (delete/missing) those refs, then refreshes
materialized shopping intersections so outbound payloads stay consistent.
"""

from __future__ import annotations

from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Set, Tuple

from sqlalchemy import select
from sqlalchemy.orm import Session

from channel.url_canonical import normalize_plp_path
from db.master_taxonomy_hierarchy import active_lineage_path_set
from db.models import CollectionLandingPage


def active_taxonomy_path_set(session: Session) -> Set[str]:
    return active_lineage_path_set(session)


def rewrite_path_slug(
    path: Optional[str],
    path_rewrites: Mapping[str, Optional[str]],
) -> Optional[str]:
    """Apply exact or longest-prefix rewrite. ``None`` rewrite value means drop."""
    slug = normalize_plp_path(path)
    if not slug:
        return None
    if not path_rewrites:
        return slug

    if slug in path_rewrites:
        new = path_rewrites[slug]
        return normalize_plp_path(new) if new else None

    best_old: Optional[str] = None
    best_new: Optional[str] = None
    best_len = -1
    for old_raw, new_raw in path_rewrites.items():
        old = normalize_plp_path(old_raw)
        if not old:
            continue
        if slug == old or slug.startswith(f"{old}/"):
            if len(old) > best_len:
                best_old = old
                best_new = normalize_plp_path(new_raw) if new_raw else None
                best_len = len(old)
    if best_old is None:
        return slug
    if best_new is None:
        return None
    suffix = slug[len(best_old) :]
    return normalize_plp_path(f"{best_new}{suffix}")


def prune_start_shopping_config_dict(
    config: Any,
    *,
    active_paths: Set[str],
    path_rewrites: Optional[Mapping[str, Optional[str]]] = None,
) -> Tuple[Dict[str, Any], Dict[str, int]]:
    """Return normalized config with stale refs rewritten/dropped plus change counts."""
    from db.collection_landing_pages import _normalize_start_shopping_config

    rewrites = dict(path_rewrites or {})
    normalized = _normalize_start_shopping_config(config)
    counts = {
        "rewritten_paths": 0,
        "dropped_hub": 0,
        "dropped_l1": 0,
        "dropped_l2": 0,
    }

    hub = normalized.get("hub_path_slug")
    if hub:
        rewritten = rewrite_path_slug(hub, rewrites)
        if rewritten != hub:
            counts["rewritten_paths"] += 1
        if rewritten and rewritten in active_paths:
            normalized["hub_path_slug"] = rewritten
        else:
            if hub:
                counts["dropped_hub"] += 1
            normalized["hub_path_slug"] = None
    else:
        normalized["hub_path_slug"] = None

    items: List[Dict[str, Any]] = []
    seen_l1: Set[str] = set()
    for item in normalized.get("items") or []:
        if not isinstance(item, dict):
            continue
        l1 = item.get("l1_path_slug")
        rewritten_l1 = rewrite_path_slug(l1, rewrites)
        if rewritten_l1 != normalize_plp_path(l1):
            counts["rewritten_paths"] += 1
        if not rewritten_l1 or rewritten_l1 not in active_paths:
            counts["dropped_l1"] += 1
            continue
        if rewritten_l1 in seen_l1:
            counts["dropped_l1"] += 1
            continue
        seen_l1.add(rewritten_l1)

        l2_out: List[str] = []
        seen_l2: Set[str] = set()
        for raw_l2 in item.get("l2_path_slugs") or []:
            rewritten_l2 = rewrite_path_slug(raw_l2, rewrites)
            if rewritten_l2 != normalize_plp_path(raw_l2):
                counts["rewritten_paths"] += 1
            if not rewritten_l2 or rewritten_l2 not in active_paths:
                counts["dropped_l2"] += 1
                continue
            if rewritten_l2 in seen_l2:
                counts["dropped_l2"] += 1
                continue
            seen_l2.add(rewritten_l2)
            l2_out.append(rewritten_l2)
        items.append({"l1_path_slug": rewritten_l1, "l2_path_slugs": l2_out})

    normalized["items"] = items
    return normalized, counts


def prune_start_shopping_configs(
    session: Session,
    *,
    path_rewrites: Optional[Mapping[str, Optional[str]]] = None,
    collection_path_slugs: Optional[Sequence[str]] = None,
    hub_path_slug: Optional[str] = None,
    dry_run: bool = True,
    refresh_intersections: bool = True,
) -> Dict[str, Any]:
    """Scan collection landings and prune/rewrite Start Shopping category refs.

    - ``path_rewrites``: old_slug -> new_slug (rename/merge) or None (explicit remove).
    - Always drops refs that are not active taxonomy paths after rewrite.
    - When ``refresh_intersections`` and not dry-run, resyncs shopping intersections
      for updated collections so outbound ``shopping_intersections`` match config.
    """
    from db.collection_landing_pages import _normalize_start_shopping_config, _row_dict
    from db.listing_intersections import sync_shopping_intersections_for_collection

    active_paths = active_taxonomy_path_set(session)
    rewrites = {
        normalize_plp_path(old): (normalize_plp_path(new) if new else None)
        for old, new in (path_rewrites or {}).items()
        if normalize_plp_path(old)
    }

    stmt = select(CollectionLandingPage).where(CollectionLandingPage.is_active.is_(True))
    scoped = [
        normalize_plp_path(slug)
        for slug in (collection_path_slugs or [])
        if normalize_plp_path(slug)
    ]
    if scoped:
        stmt = stmt.where(CollectionLandingPage.path_slug.in_(scoped))
    hub = normalize_plp_path(hub_path_slug) if hub_path_slug else None
    if hub:
        stmt = stmt.where(
            (CollectionLandingPage.parent_path_slug == hub)
            | (CollectionLandingPage.path_slug == hub)
            | (CollectionLandingPage.path_slug.like(f"{hub}/%"))
        )

    rows = list(session.scalars(stmt).all())
    result: Dict[str, Any] = {
        "dry_run": dry_run,
        "hub_path_slug": hub,
        "scanned_count": 0,
        "updated_count": 0,
        "unchanged_count": 0,
        "skipped_empty_count": 0,
        "rewritten_paths": 0,
        "dropped_hub": 0,
        "dropped_l1": 0,
        "dropped_l2": 0,
        "intersections_refreshed": 0,
        "updated_path_slugs": [],
        "collections": [],
    }

    for row in rows:
        raw = row.start_shopping_config
        if not isinstance(raw, dict) or not (raw.get("items") or raw.get("hub_path_slug")):
            result["skipped_empty_count"] += 1
            continue
        result["scanned_count"] += 1
        before = _normalize_start_shopping_config(raw)
        after, counts = prune_start_shopping_config_dict(
            before,
            active_paths=active_paths,
            path_rewrites=rewrites,
        )
        changed = after != before
        entry = {
            "path_slug": row.path_slug,
            "changed": changed,
            "before": before,
            "after": after,
            **counts,
        }
        if not changed:
            result["unchanged_count"] += 1
            result["collections"].append(entry)
            continue

        result["updated_count"] += 1
        result["updated_path_slugs"].append(str(row.path_slug))
        result["rewritten_paths"] += counts["rewritten_paths"]
        result["dropped_hub"] += counts["dropped_hub"]
        result["dropped_l1"] += counts["dropped_l1"]
        result["dropped_l2"] += counts["dropped_l2"]
        result["collections"].append(entry)

        if dry_run:
            continue

        row.start_shopping_config = after
        session.flush()
        if refresh_intersections:
            page = _row_dict(row)
            sync_shopping_intersections_for_collection(session, page)
            result["intersections_refreshed"] += 1

    if not dry_run:
        session.flush()
    return result


def enqueue_start_shopping_outbound_pushes(
    session: Session,
    path_slugs: Sequence[str],
    *,
    connections: Sequence[Mapping[str, Any]],
    dry_run: bool = False,
    sync_products: bool = True,
    notes: str = "start_shopping_fix_outbound",
) -> Dict[str, Any]:
    """Queue Magento/Shopify collection landing pushes for updated Start Shopping landings.

    Each ``connections`` entry must include:
    ``id`` (compat connection id), ``native_id``, ``channel_type``, and optionally ``channel_code``.
    """
    from app.jobs.channel_jobs import enqueue_collection_push_job, job_to_dict

    slugs = sorted({normalize_plp_path(slug) for slug in path_slugs if normalize_plp_path(slug)})
    channels = []
    for raw in connections or []:
        channel_type = str(raw.get("channel_type") or raw.get("channel_code") or "").strip().lower()
        native_id = raw.get("native_id")
        compat_id = raw.get("id")
        if channel_type not in {"magento", "shopify"} or native_id is None or compat_id is None:
            continue
        channels.append(
            {
                "id": int(compat_id),
                "native_id": int(native_id),
                "channel_type": channel_type,
                "channel_code": str(raw.get("channel_code") or channel_type),
            }
        )

    result: Dict[str, Any] = {
        "dry_run": dry_run,
        "path_count": len(slugs),
        "channel_count": len(channels),
        "would_queue": 0,
        "queued": 0,
        "jobs": [],
        "skipped_channels": [],
    }
    if not slugs:
        return result
    if not channels:
        result["skipped_channels"].append("no_connections")
        return result

    for path_slug in slugs:
        for connection in channels:
            entry = {
                "path_slug": path_slug,
                "channel": connection["channel_type"],
                "connection_id": connection["native_id"],
            }
            if dry_run:
                result["would_queue"] += 1
                entry["status"] = "would_queue"
                result["jobs"].append(entry)
                continue
            job = enqueue_collection_push_job(
                session,
                channel_connection_id=connection["id"],
                native_connection_id=connection["native_id"],
                channel_code=connection["channel_code"],
                channel_type=connection["channel_type"],
                path_slug=path_slug,
                only_with_landing_content=True,
                dry_run=False,
                provision_missing_remote=True,
                apply_listing_paths=True,
                sync_products=sync_products,
                sync_shopping_icons=True,
                limit=1,
                bootstrap_first=True,
                notes=f"{notes}:{connection['channel_type']}:{path_slug}",
            )
            result["queued"] += 1
            result["jobs"].append({**entry, "status": "queued", **job_to_dict(job)})
    return result


def path_rewrites_from_updated_nodes(
    updated_nodes: Iterable[Mapping[str, Any]],
) -> Dict[str, Optional[str]]:
    """Build old→new rewrite map from taxonomy cascade ``updated_nodes``."""
    out: Dict[str, Optional[str]] = {}
    for row in updated_nodes or []:
        old = normalize_plp_path(row.get("old_path_slug"))
        new = normalize_plp_path(row.get("new_path_slug"))
        if old and new and old != new:
            out[old] = new
    return out


def path_rewrites_for_removed_slugs(slugs: Iterable[str]) -> Dict[str, Optional[str]]:
    """Build old→None map for deleted/deactivated taxonomy paths."""
    return {
        normalize_plp_path(slug): None
        for slug in slugs
        if normalize_plp_path(slug)
    }
