"""
Resolve master-catalog attribute values to channel-native identifiers.

Rule: when a channel attribute has a predefined option set (Magento select/multiselect,
Shopify list/metafield choices, product options, etc.), the publish path must resolve
both attribute identity *and* option value identity — not pass raw catalog labels alone.

Dependency inversion: callers depend on AttributeOptionRegistryPort; SQL/cache adapters
live in db/magento_repositories (and future Shopify equivalents).
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Dict, List, Optional, Protocol, Sequence, Set, Union


class AttributeInputKind(str, Enum):
    """Normalized attribute input kinds across channels."""

    TEXT = "text"
    TEXTAREA = "textarea"
    SELECT = "select"
    MULTISELECT = "multiselect"
    BOOLEAN = "boolean"
    NUMBER = "number"
    # Shopify-oriented kinds (future list/metafield option sets)
    LIST_SINGLE = "list.single_line_text_field"
    LIST_MULTI = "list.list.single_line_text_field"


MAGENTO_SELECT_KINDS: Set[str] = {
    AttributeInputKind.SELECT.value,
    AttributeInputKind.MULTISELECT.value,
}

SHOPIFY_OPTION_KINDS: Set[str] = {
    AttributeInputKind.LIST_SINGLE.value,
    AttributeInputKind.LIST_MULTI.value,
}


def norm_option_label(label: str) -> str:
    """Normalize option label for registry lookup: lower, trim, collapse spaces."""
    s = str(label).strip().lower()
    return re.sub(r"\s+", " ", s) if s else ""


# Fallback aliases when exact option label match fails (catalog vs remote label variants)
OPTION_LABEL_ALIASES: Dict[str, List[str]] = {
    "no": ["0", "false"],
    "yes": ["1", "true"],
    "use config": ["use_config", "useconfig", "default"],
    # Short master-catalog labels → Magento cabinet_type option labels
    "base": ["base cabinets", "basecabinets"],
    "wall": ["wall cabinets", "wallcabinets"],
    "panel": ["panels and fillers", "panelsandfillers"],
    "panels": ["panels and fillers", "panelsandfillers"],
    "filler": ["panels and fillers", "panelsandfillers"],
    "moulding": ["mouldings", "mouldings"],
    "molding": ["mouldings", "mouldings"],
    "accessory": ["accessories", "accessories"],
    "storage accessories": ["accessories", "accessories"],
}

# Well-known Magento default option mappings: (attribute_code, catalog_label_norm) -> value_index.
OPTION_FALLBACK_VALUE_INDEX: Dict[tuple, int] = {
    ("gift_message_available", "no"): 0,
    ("msrp_display_actual_price_type", "use config"): 0,
}


def requires_remote_value_id(
    frontend_input: Optional[str],
    *,
    channel: str = "magento",
) -> bool:
    """True when the channel expects an option id, not a free-text label."""
    kind = str(frontend_input or "").strip().lower()
    if not kind:
        return False
    if channel == "magento":
        return kind in MAGENTO_SELECT_KINDS
    if channel == "shopify":
        return kind in SHOPIFY_OPTION_KINDS or kind == "product_option"
    return False


def resolve_select_value_index(
    catalog_value: str,
    option_map: Dict[str, int],
    attr_code: str,
    *,
    known_value_indices: Optional[Set[int]] = None,
) -> Optional[int]:
    """
    Resolve a master-catalog label to a Magento value_index (or equivalent option id).

    Tries normalized label, aliases, case-insensitive key match, numeric passthrough,
    then well-known fallbacks for core Magento attributes.
    """
    norm = norm_option_label(catalog_value)
    if not norm:
        return None

    vi = option_map.get(norm)
    if vi is not None:
        return vi

    for alias in OPTION_LABEL_ALIASES.get(norm, []):
        vi = option_map.get(alias)
        if vi is not None:
            return vi

    norm_flat = norm.replace(" ", "")
    for key, candidate in option_map.items():
        if key.replace(" ", "") == norm_flat:
            return candidate

    vi = OPTION_FALLBACK_VALUE_INDEX.get((attr_code.strip().lower(), norm))
    if vi is not None:
        return vi

    if known_value_indices is not None:
        try:
            as_int = int(str(catalog_value).strip())
            if as_int in known_value_indices:
                return as_int
        except (ValueError, TypeError):
            pass

    return None


def resolve_select_value_indices(
    catalog_value: Union[str, Sequence[str]],
    option_map: Dict[str, int],
    attr_code: str,
) -> List[int]:
    """Resolve one or many catalog labels to value_index list (multiselect-safe)."""
    known = set(option_map.values())
    if isinstance(catalog_value, (list, tuple)):
        parts = [str(v).strip() for v in catalog_value if str(v).strip()]
    else:
        parts = [v.strip() for v in str(catalog_value).split(",") if v.strip()]

    resolved: List[int] = []
    for part in parts:
        if str(part).strip().lower() in ("nan", "none", "null"):
            continue
        vi = resolve_select_value_index(part, option_map, attr_code, known_value_indices=known)
        if vi is not None:
            resolved.append(int(vi))
    return resolved


def resolve_shopify_choice_label(catalog_value: str, choices: Sequence[str]) -> Optional[str]:
    """Map a master-catalog label to an exact Shopify metafield choice label."""
    text = str(catalog_value or "").strip()
    if not text or not choices:
        return None
    if text.lower() in {"nan", "none", "null"}:
        return None

    exact = {str(choice) for choice in choices}
    if text in exact:
        return text

    norm_to_choice: Dict[str, str] = {}
    for choice in choices:
        label = str(choice).strip()
        norm = norm_option_label(label)
        if norm:
            norm_to_choice[norm] = label

    norm = norm_option_label(text)
    if norm in norm_to_choice:
        return norm_to_choice[norm]

    for alias in OPTION_LABEL_ALIASES.get(norm, []):
        alias_norm = norm_option_label(alias)
        if alias_norm in norm_to_choice:
            return norm_to_choice[alias_norm]

    norm_flat = norm.replace(" ", "")
    for choice_norm, choice in norm_to_choice.items():
        if choice_norm.replace(" ", "") == norm_flat:
            return choice

    for choice_norm, choice in norm_to_choice.items():
        if norm in choice_norm or choice_norm in norm:
            return choice

    return None


class AttributeOptionRegistryPort(Protocol):
    """Port: label -> remote option id per attribute (Magento value_index, Shopify choice id, …)."""

    def get_options(
        self, connection_id: int, attribute_code: str, ttl_cutoff: datetime
    ) -> Dict[str, int]: ...


@dataclass(frozen=True)
class ResolvedChannelValue:
    """Outcome of resolving a catalog value for a channel attribute."""

    attribute_code: str
    catalog_value: str
    remote_value: Optional[Union[int, str]]
    resolved: bool
    frontend_input: str = ""


class MagentoAttributeValueResolver:
    """Magento-specific resolver backed by AttributeOptionRegistryPort."""

    SELECT_MULTISELECT = MAGENTO_SELECT_KINDS

    def __init__(self, registry: AttributeOptionRegistryPort) -> None:
        self._registry = registry

    def resolve_for_attribute(
        self,
        connection_id: int,
        attribute_code: str,
        catalog_value: Union[str, Sequence[str]],
        *,
        frontend_input: str,
        ttl_cutoff: datetime,
    ) -> ResolvedChannelValue:
        code = attribute_code.strip().lower()
        raw = catalog_value if isinstance(catalog_value, str) else ",".join(str(v) for v in catalog_value)
        kind = str(frontend_input or "").strip().lower()

        if kind not in self.SELECT_MULTISELECT:
            return ResolvedChannelValue(
                attribute_code=code,
                catalog_value=raw,
                remote_value=raw,
                resolved=True,
                frontend_input=kind,
            )

        option_map = self._registry.get_options(connection_id, code, ttl_cutoff)
        if not option_map:
            return ResolvedChannelValue(
                attribute_code=code,
                catalog_value=raw,
                remote_value=None,
                resolved=False,
                frontend_input=kind,
            )

        indices = resolve_select_value_indices(raw, option_map, code)
        if not indices:
            return ResolvedChannelValue(
                attribute_code=code,
                catalog_value=raw,
                remote_value=None,
                resolved=False,
                frontend_input=kind,
            )

        if kind == AttributeInputKind.MULTISELECT.value:
            remote: Union[int, str] = ",".join(str(i) for i in indices)
        else:
            remote = indices[0]

        return ResolvedChannelValue(
            attribute_code=code,
            catalog_value=raw,
            remote_value=remote,
            resolved=True,
            frontend_input=kind,
        )
