from __future__ import annotations

import re
from typing import Any, Dict, List, Optional, Set

from sqlalchemy import func, select
from sqlalchemy.orm import Session

from db.channel_assignments import active_skus_for_channel
from db.channel_exports import resolve_pipeline_channel_code, resolve_push_skus
from db.channel_capabilities import resolve_product_structure
from db.channel_sku_mapping import build_reconciliation_report, reconcile_connection_id_for_channel
from db.compat_connections import (
    default_native_connection_id,
    resolve_compat_connection_id,
    resolve_native_connection_id,
)
from db.master_relations import children_by_parent, parent_skus_with_children
from db.models import ChannelPublishState, MasterProduct
from db.channel_sku_prefix_mapping import list_prefix_mappings


CRITICAL_MASTER_ATTRIBUTES = (
    "sku",
    "title",
    "price",
    "description",
    "short_description",
    "base_image",
    "status",
    "meta_title",
    "meta_description",
)

CRITICAL_CHANNEL_DEFAULTS = {
    "magento": "sku",
    "shopify": "sku",
    "plytix": "sku",
}

# Image fields are pushed via the media pipeline, not Magento/Shopify EAV attribute mappings.
_MEDIA_PUSH_TARGET = "__media_push__"
# Shopify sell price is written on the default variant in productSet, not the attribute registry.
_VARIANT_PRICE_PUSH_TARGET = "__variant_price_push__"
# Shopify content/SEO fields are written via ProductSetInput, not pulled metafield registry rows.
_SHOPIFY_NATIVE_CONTENT_FIELDS = frozenset({"short_description", "meta_title", "meta_description"})
_PARENT_SHELL_SKU_RE = re.compile(r"(?:-PARENT|_PARENT)(?:-\d+)?$", re.IGNORECASE)


def _is_pending_parent_shell_sku(master_sku: str, parents_with_children: Set[str]) -> bool:
    """Configurable parent shells are created on first product push, not link-from-remote."""
    sku = str(master_sku or "").strip()
    if not sku:
        return False
    if sku in parents_with_children:
        return True
    return bool(_PARENT_SHELL_SKU_RE.search(sku))


def _split_unmapped_masters(
    session: Session,
    unmapped_masters: List[Dict[str, Any]],
) -> tuple[List[Dict[str, Any]], List[Dict[str, Any]]]:
    """Separate configurable parents (not yet in remote) from SKUs that should link to remote."""
    if not unmapped_masters:
        return [], []
    unlinked_skus = {str(item["master_sku"]).strip() for item in unmapped_masters if str(item.get("master_sku") or "").strip()}
    parents_with_children = parent_skus_with_children(session, unlinked_skus)
    parents_pending: List[Dict[str, Any]] = []
    truly_unlinked: List[Dict[str, Any]] = []
    for item in unmapped_masters:
        master_sku = str(item.get("master_sku") or "").strip()
        if _is_pending_parent_shell_sku(master_sku, parents_with_children):
            parents_pending.append(item)
        else:
            truly_unlinked.append(item)
    return parents_pending, truly_unlinked


def _registry_connection_id(channel: str, connection_id: Optional[int]) -> Optional[int]:
    """Normalize connection ids for registry lookups."""
    if connection_id is None:
        return None
    if channel == "shopify":
        return resolve_compat_connection_id("shopify", connection_id)
    if channel == "magento":
        return resolve_native_connection_id("magento", connection_id)
    return connection_id


def _critical_attribute_checks(
    session: Session,
    channel: str,
    *,
    magento_connection_id: Optional[int] = None,
    shopify_connection_id: Optional[int] = None,
) -> Dict[str, Any]:
    """Validate only critical master attributes for one channel (no full mapping matrix)."""
    from db.channel_aliases import channel_aliases_by_canonical
    from db.channel_attribute_options import channel_registry_codes_for
    from db.master_static_field_mapping import DEFAULT_CHANNEL_FIELD_ALIASES, IMAGE_FIELDS, static_alias_map_for_channel

    raw_connection_id = magento_connection_id if channel == "magento" else shopify_connection_id if channel == "shopify" else None
    connection_id = _registry_connection_id(channel, raw_connection_id)
    static_map = static_alias_map_for_channel(session, channel, master_codes=CRITICAL_MASTER_ATTRIBUTES)
    aliases = channel_aliases_by_canonical(session, channel, canonical_codes=CRITICAL_MASTER_ATTRIBUTES)
    default_aliases = DEFAULT_CHANNEL_FIELD_ALIASES.get(channel, {})

    targets: List[str] = []
    critical_rows: List[Dict[str, Any]] = []
    for master_code in CRITICAL_MASTER_ATTRIBUTES:
        mapped = static_map.get(master_code) or ""
        if not mapped:
            alias_rows = aliases.get(master_code) or []
            if alias_rows:
                static_aliases = [alias for alias in alias_rows if getattr(alias, "mapping_scope", "dynamic") == "static"]
                chosen = static_aliases or alias_rows
                mapped = str(chosen[0].channel_attribute_code or "").strip()
        if not mapped:
            mapped = str(default_aliases.get(master_code) or "").strip()
        if not mapped and master_code == "sku":
            mapped = CRITICAL_CHANNEL_DEFAULTS.get(channel, "sku")
        if not mapped and master_code in IMAGE_FIELDS:
            mapped = _MEDIA_PUSH_TARGET
        if not mapped and master_code == "price" and channel == "shopify":
            mapped = _VARIANT_PRICE_PUSH_TARGET
        if mapped and mapped not in {_MEDIA_PUSH_TARGET, _VARIANT_PRICE_PUSH_TARGET}:
            targets.append(mapped)
        critical_rows.append(
            {
                "master": master_code,
                channel: mapped or "",
                f"{channel}_is_match": 0,
            }
        )

    registry_codes = channel_registry_codes_for(session, channel, targets, connection_id=connection_id)
    for row in critical_rows:
        master_code = str(row.get("master") or "").strip()
        if master_code in IMAGE_FIELDS or (channel == "shopify" and master_code == "price"):
            row[f"{channel}_is_match"] = 1
            continue
        if channel == "shopify" and master_code in _SHOPIFY_NATIVE_CONTENT_FIELDS:
            row[f"{channel}_is_match"] = 1 if str(row.get(channel) or "").strip() else 0
            continue
        mapped = str(row.get(channel) or "").strip()
        row[f"{channel}_is_match"] = 1 if mapped and mapped in registry_codes else 0

    critical_mapped = sum(1 for row in critical_rows if int(row.get(f"{channel}_is_match") or 0) == 1)
    critical_with_target = sum(1 for row in critical_rows if str(row.get(channel) or "").strip())
    unmapped_critical = [
        row["master"]
        for row in critical_rows
        if not str(row.get(channel) or "").strip() or int(row.get(f"{channel}_is_match") or 0) != 1
    ]
    return {
        "critical_rows": critical_rows,
        "critical_mapped": critical_mapped,
        "critical_with_target": critical_with_target,
        "unmapped_critical": unmapped_critical,
    }


def _publish_state_summary(
    session: Session,
    channel: str,
    push_skus: Set[str],
) -> Dict[str, Any]:
    """Fast publish-state counts without rebuilding full channel payloads."""
    push_list = sorted({str(sku).strip() for sku in push_skus if str(sku).strip()})
    if not push_list:
        return {
            "payload_count": 0,
            "pending_hash_changes": 0,
            "never_pushed": 0,
            "pending_hash_skus": [],
            "publish_state_counts": _publish_counts(session, channel),
            "hash_drift_source": "status_endpoint",
        }

    published = {
        str(sku).strip()
        for (sku,) in session.execute(
            select(ChannelPublishState.sku).where(
                ChannelPublishState.channel_code == channel,
                ChannelPublishState.sku.in_(push_list),
            )
        ).all()
        if str(sku).strip()
    }
    never_pushed_count = len(push_list) - len(published)

    return {
        "payload_count": len(push_list),
        "pending_hash_changes": 0,
        "never_pushed": never_pushed_count,
        "pending_hash_skus": [],
        "publish_state_counts": _publish_counts(session, channel),
        "hash_drift_source": "status_endpoint",
    }


def build_channel_push_readiness(
    session: Session,
    channel_code: str,
    *,
    connection_id: Optional[int] = None,
    magento_connection_id: Optional[int] = None,
    shopify_connection_id: Optional[int] = None,
    remote_catalog: Optional[Dict[str, Optional[str]]] = None,
    remote_source: Optional[str] = None,
    only_assigned: bool = True,
) -> Dict[str, Any]:
    """Pre-push checklist: assignments, SKU links, attribute coverage, publish hashes, structure."""
    channel = resolve_pipeline_channel_code(channel_code)
    structure = resolve_product_structure(channel)
    native_magento_id = resolve_native_connection_id("magento", magento_connection_id)
    if channel == "magento" and native_magento_id is None and connection_id is not None:
        native_magento_id = resolve_native_connection_id("magento", connection_id)
    if channel == "magento" and native_magento_id is None:
        native_magento_id = default_native_connection_id(session, "magento")

    native_shopify_id = resolve_native_connection_id("shopify", shopify_connection_id)
    if channel == "shopify" and native_shopify_id is None and connection_id is not None:
        native_shopify_id = resolve_native_connection_id("shopify", connection_id)
    if channel == "shopify" and native_shopify_id is None:
        native_shopify_id = default_native_connection_id(session, "shopify")

    compat_shopify_id = resolve_compat_connection_id("shopify", connection_id or shopify_connection_id)

    active_master_count = session.scalar(
        select(func.count()).select_from(MasterProduct).where(MasterProduct.is_active.is_(True))
    ) or 0
    assigned = active_skus_for_channel(session, channel) if only_assigned else None
    assigned_count = len(assigned) if assigned is not None else active_master_count
    push_skus = resolve_push_skus(session, channel, only_assigned=only_assigned, product_structure=structure)

    reconcile_connection_id = reconcile_connection_id_for_channel(
        channel,
        compat_connection_id=compat_shopify_id if channel == "shopify" else connection_id,
        native_connection_id=native_magento_id if channel == "magento" else None,
    )
    if channel == "shopify" and reconcile_connection_id is None and compat_shopify_id is not None:
        reconcile_connection_id = compat_shopify_id
    reconcile = build_reconciliation_report(
        session,
        channel,
        connection_id=reconcile_connection_id,
        only_assigned=only_assigned,
        remote_catalog=remote_catalog,
        remote_source=remote_source,
    )

    critical = _critical_attribute_checks(
        session,
        channel,
        magento_connection_id=native_magento_id if channel == "magento" else magento_connection_id,
        shopify_connection_id=compat_shopify_id if channel == "shopify" else shopify_connection_id,
    )
    critical_mapped = critical["critical_mapped"]
    critical_with_target = critical["critical_with_target"]
    unmapped_critical = critical["unmapped_critical"]
    critical_rows = critical["critical_rows"]

    publish = _publish_state_summary(session, channel, set(push_skus))
    pending_hash: List[str] = publish["pending_hash_skus"]
    never_pushed = publish["never_pushed"]

    prefix_rule_count = len(
        list_prefix_mappings(session, channel_code=channel, connection_id=connection_id, status="active")
    )

    structure_issues: List[Dict[str, Any]] = []
    if structure == "parent_children" and assigned:
        parents = parent_skus_with_children(session, assigned)
        child_map = children_by_parent(session, parents)
        for parent in sorted(parents):
            children = child_map.get(parent) or []
            if not children:
                structure_issues.append(
                    {"parent_sku": parent, "issue": "parent_assigned_without_children_in_master_relation"}
                )

    unlinked = reconcile.get("unmapped_masters") or []
    parents_pending_creation, unmapped_rest = _split_unmapped_masters(session, unlinked)
    pending_remote_creation = [item for item in unmapped_rest if not item.get("in_remote_catalog")]
    truly_unlinked = [item for item in unmapped_rest if item.get("in_remote_catalog")]
    parents_pending_creation = parents_pending_creation + pending_remote_creation
    remote_id_gaps = reconcile.get("remote_id_backfill") or []
    link_candidates = len(reconcile.get("suggestions") or []) + len(remote_id_gaps)

    blockers: List[str] = []
    if assigned_count == 0:
        blockers.append("no_skus_assigned_to_channel")
    if truly_unlinked:
        blockers.append(f"{len(truly_unlinked)}_assigned_master_skus_not_linked_to_remote_catalog")
    if critical_with_target < len(CRITICAL_MASTER_ATTRIBUTES):
        blockers.append("critical_attribute_mappings_incomplete")
    if any(int(row.get(f"{channel}_is_match") or 0) == 0 for row in critical_rows if row.get(channel)):
        blockers.append("critical_attribute_targets_not_in_channel_registry")
    if structure_issues:
        blockers.append(f"{len(structure_issues)}_parent_child_structure_issues")

    recommendations: List[str] = []
    if channel == "shopify" and prefix_rule_count == 0 and truly_unlinked:
        recommendations.append("Configure Shopify SKU prefix rules (Mappings → prefix rules) or explicit SKU mappings.")
    if parents_pending_creation:
        recommendations.append(
            f"{len(parents_pending_creation)} assigned SKU(s) are not in the remote catalog yet; "
            "they will be created on the first product push."
        )
    if link_candidates:
        recommendations.append("Run POST /api/channel-sku-mappings/link-from-remote to persist SKU links and remote_ids.")
    elif truly_unlinked:
        recommendations.append(
            f"{len(truly_unlinked)} assigned SKU(s) exist in the remote catalog but have no mapping — "
            "run link-from-remote or add explicit SKU mappings."
        )
    if channel == "magento" and native_magento_id:
        recommendations.append(
            "Run POST /api/magento/sync-state/align to re-key magento_sync_state from master SKU to channel SKU."
        )
    if unmapped_critical:
        recommendations.append("Complete static attribute mappings and sync channel attribute registries.")
    if never_pushed:
        recommendations.append(f"{never_pushed} assigned SKUs have never been published to this channel.")

    return {
        "channel_code": channel,
        "product_structure": structure,
        "ready": len(blockers) == 0,
        "blockers": blockers,
        "recommendations": recommendations,
        "checks": {
            "catalog": {
                "active_master_products": active_master_count,
                "assigned_to_channel": assigned_count,
                "push_sku_count": len(push_skus),
            },
            "sku_identity": {
                "prefix_rule_count": prefix_rule_count,
                "mapped_count": reconcile.get("mapped_count", 0),
                "suggestion_count": reconcile.get("suggestion_count", 0),
                "unlinked_assigned_count": len(unlinked),
                "parents_pending_remote_creation_count": len(parents_pending_creation),
                "pending_remote_creation_count": len(pending_remote_creation),
                "unlinked_in_remote_count": len(truly_unlinked),
                "unlinked_not_in_remote_count": len(pending_remote_creation),
                "remote_id_backfill_count": len(remote_id_gaps),
                "orphan_remote_sku_count": reconcile.get("orphan_remote_sku_count", 0),
                "remote_source": reconcile.get("remote_source"),
                "magento_connection_id": native_magento_id if channel == "magento" else None,
            },
            "attributes": {
                "critical_attribute_count": len(CRITICAL_MASTER_ATTRIBUTES),
                "critical_with_target": critical_with_target,
                "critical_validated": critical_mapped,
                "unmapped_critical": unmapped_critical,
            },
            "publish_state": {
                "payload_count": publish["payload_count"],
                "pending_hash_changes": publish["pending_hash_changes"],
                "never_pushed": never_pushed,
                "publish_state_counts": publish["publish_state_counts"],
                "hash_drift_source": publish["hash_drift_source"],
            },
            "structure": {
                "product_structure": structure,
                "issues": structure_issues[:100],
            },
        },
        "samples": {
            "unlinked_masters": truly_unlinked[:25],
            "parents_pending_remote_creation": parents_pending_creation[:25],
            "pending_remote_creation": pending_remote_creation[:25],
            "orphan_remote_skus": (reconcile.get("orphan_remote_skus") or [])[:25],
            "pending_hash_skus": pending_hash[:25],
        },
    }


def _publish_counts(session: Session, channel: str) -> Dict[str, int]:
    return {
        str(status or "never_pushed"): int(count or 0)
        for status, count in session.execute(
            select(ChannelPublishState.last_status, func.count())
            .where(ChannelPublishState.channel_code == channel)
            .group_by(ChannelPublishState.last_status)
        ).all()
    }
