from __future__ import annotations

import math
from decimal import Decimal, InvalidOperation
import pandas as pd
from typing import Any, Dict, List, Optional, Tuple
import re
from settings import MagentoNormalizeConfig


def _is_empty(v: object) -> bool:
    if v is None:
        return True
    try:
        if isinstance(v, float) and math.isnan(v):
            return True
    except Exception:
        pass
    s = str(v).strip()
    return s == "" or s.lower() in {"nan", "none", "null"}


def _relation_sku(v: object) -> str:
    """Parent/child SKU reference; empty when missing or pandas NaN."""
    if _is_empty(v):
        return ""
    return str(v).strip()


def _escape_value(v: str) -> str:
    v = v.replace("\\", "\\\\")
    v = v.replace(",", "\\,")
    v = v.replace("=", "\\=")
    return v


def _case_insensitive_col(df: pd.DataFrame, wanted: str) -> Optional[str]:
    w = wanted.strip().lower()
    for c in df.columns:
        if str(c).strip().lower() == w:
            return c
    return None


def _parse_kv_blob(blob: object) -> Dict[str, str]:
    if _is_empty(blob):
        return {}
    s = str(blob)

    # split on unescaped commas
    parts: List[str] = []
    cur: List[str] = []
    esc = False
    for ch in s:
        if esc:
            cur.append(ch)
            esc = False
        elif ch == "\\":
            esc = True
        elif ch == ",":
            parts.append("".join(cur))
            cur = []
        else:
            cur.append(ch)
    parts.append("".join(cur))

    out: Dict[str, str] = {}
    for p in parts:
        if not p:
            continue

        # split on first unescaped '='
        key: List[str] = []
        val: List[str] = []
        esc = False
        saw_eq = False
        for ch in p:
            if esc:
                (val if saw_eq else key).append(ch)
                esc = False
            elif ch == "\\":
                esc = True
            elif ch == "=" and not saw_eq:
                saw_eq = True
            else:
                (val if saw_eq else key).append(ch)

        k = "".join(key).strip()
        v = "".join(val).strip()
        if k:
            out[k] = v
    return out


def _format_kv_blob(d: Dict[str, str]) -> str:
    pairs: List[str] = []
    for k, v in d.items():
        if _is_empty(v):
            continue
        pairs.append(f"{k}={_escape_value(str(v).strip())}")
    return ",".join(pairs)


def _extract_axis_codes_from_labels(labels: object) -> List[str]:
    # 'width=Width (inches),height=Height (inches)' -> ['width','height']
    if _is_empty(labels):
        return []
    s = str(labels).strip()
    axes: List[str] = []
    for seg in [x.strip() for x in s.split(",") if x.strip()]:
        if "=" not in seg:
            continue
        axis = seg.split("=", 1)[0].strip()
        if axis:
            axes.append(axis)

    seen = set()
    out = []
    for a in axes:
        if a not in seen:
            out.append(a)
            seen.add(a)
    return out


def _extract_axis_codes(axis_raw: object) -> List[str]:
    # supports comma or pipe: "width,height" OR "width|height"
    if _is_empty(axis_raw):
        return []
    s = str(axis_raw).strip()
    s = s.replace("|", ",")
    axes = [x.strip() for x in s.split(",") if x.strip()]
    seen = set()
    out = []
    for a in axes:
        if a not in seen:
            out.append(a)
            seen.add(a)
    return out


def _extract_child_skus(seed: object) -> List[str]:
    # confirmed: comma-separated SKUs
    if _is_empty(seed):
        return []
    return [x.strip() for x in str(seed).split(",") if x.strip()]


def _try_derive_height_from_sku(sku: str) -> Optional[str]:
    """
    Fallback when height column is missing or has wrong value (e.g. Plytix sends width).
    Common cabinet SKU patterns: RTA-ASW-W1542 (42\" H), W1536 (36\" H), W1530 (30\" H).
    Extracts last 2 digits as height when in plausible range (6-96 inches).
    """
    if not sku or not isinstance(sku, str):
        return None
    sku = sku.strip()
    m = re.search(r"(\d{2})\s*$", sku)
    if not m:
        return None
    val = int(m.group(1))
    if 6 <= val <= 96:
        return str(val)
    return None


def _infer_axis_codes_from_variations(cv_text: object) -> List[str]:
    cv = str(cv_text or "").strip()
    if not cv or "sku=" not in cv.lower():
        return []
    codes: List[str] = []
    seen: set[str] = set()
    for seg in cv.split("|"):
        for pair in seg.split(","):
            if "=" not in pair:
                continue
            key = pair.split("=", 1)[0].strip().lower()
            if key and key != "sku" and key not in seen:
                seen.add(key)
                codes.append(key)
    return codes


def _has_built_configurable_variations(cv: object) -> bool:
    """True when relation fields already contain Magento-style sku= segments."""
    text = str(cv or "").strip()
    return bool(text and "sku=" in text.lower())


_AXIS_CHILD_COLUMN_FALLBACKS: Dict[str, tuple[str, ...]] = {
    "width": ("variation_width_in", "width_in", "width"),
    "height": ("variation_height_in", "height_in", "height"),
    "depth": ("variation_depth_in", "depth_in", "depth"),
}


def _axis_lookup_columns(axis: str) -> List[str]:
    axis_lower = axis.strip().lower()
    seen: set[str] = set()
    ordered: List[str] = []
    for candidate in (axis_lower, *_AXIS_CHILD_COLUMN_FALLBACKS.get(axis_lower, ())):
        key = candidate.strip().lower()
        if key and key not in seen:
            seen.add(key)
            ordered.append(key)
    return ordered


def _get_child_value(df: pd.DataFrame, child_sku: str, axis: str) -> Optional[str]:
    from settings import get_dimension_axes

    sku_col = _case_insensitive_col(df, "sku") or "sku"
    child = df[df[sku_col].astype(str) == str(child_sku)]
    if child.empty:
        return None

    dimension_axes = get_dimension_axes()
    for axis_name in _axis_lookup_columns(axis):
        axis_lower = axis_name.strip().lower()
        axis_in_col = _case_insensitive_col(df, f"{axis_name}_in")
        axis_col = _case_insensitive_col(df, axis_name)
        axis_inches_col = _case_insensitive_col(df, f"{axis_name}_inches")

        if axis_lower in dimension_axes and axis_in_col:
            v = child.iloc[0].get(axis_in_col, "")
            if not _is_empty(v):
                return _normalize_axis_value(str(v).strip())

        if axis_col:
            v = child.iloc[0].get(axis_col, "")
            if not _is_empty(v):
                return _normalize_axis_value(str(v).strip())

        if axis_in_col:
            v = child.iloc[0].get(axis_in_col, "")
            if not _is_empty(v):
                return _normalize_axis_value(str(v).strip())

        if axis_inches_col:
            v = child.iloc[0].get(axis_inches_col, "")
            if not _is_empty(v):
                return _normalize_axis_value(str(v).strip())

    if axis.strip().lower() == "height":
        derived = _try_derive_height_from_sku(child_sku)
        if derived:
            return derived

    return None


def _normalize_axis_code(axis: str, columns: List[str], allowed_attribute_codes: Optional[set[str]]) -> str:
    return axis.strip().lower()


def _normalize_axis_value(value: str) -> str:
    text = value.strip()
    if not text:
        return text
    try:
        num = Decimal(text)
    except (InvalidOperation, ValueError):
        return text
    if num % 1 == 0:
        return str(int(num))
    return text


def _normalize_width_value(value: str) -> str:
    return _normalize_axis_value(value)


def _seed_children_for_row(row: Any, seed_value: object) -> List[str]:
    """Resolve child SKUs for a configurable parent (seed col, then variant_list)."""
    children = _extract_child_skus(seed_value)
    if children:
        return children
    if hasattr(row, "get"):
        return _extract_child_skus(row.get("variant_list", ""))
    return []


# Relation metadata required downstream by sync_planner / SET_RELATIONS — never drop from output.
_RELATION_FIELD_COLS = frozenset({
    "product_type",
    "variant_of",
    "variant_list",
    "configurable_attributes",
    "configurable_variations",
    "configurable_variation_axis",
    "configurable_variation_labels",
})


def _axis_codes_for_row(row: Any, *, axis_col: str, labels_col: str) -> List[str]:
    axes = _extract_axis_codes(row.get(axis_col, ""))
    if not axes:
        axes = _extract_axis_codes_from_labels(row.get(labels_col, ""))
    if not axes:
        axes = _extract_axis_codes(row.get("configurable_attributes", ""))
    return axes


def normalize_plytix_df(
    df: pd.DataFrame,
    cfg: MagentoNormalizeConfig,
    allowed_attribute_codes: Optional[set[str]] = None,
    allowed_attribute_options: Optional[Dict[str, List[str]]] = None,
    enforce_required_name: bool = True,
    enforce_required_price: bool = True,
    prefer_seo_title: bool = True,
) -> Tuple[pd.DataFrame, pd.DataFrame]:
    """
    LEGACY — Plytix CSV/feed normalization. Reference only.

    Active Magento integration uses magento.catalog_normalize.normalize_master_catalog_df
    with rows from magento.master_catalog_rows.
    """
    from magento.catalog_normalize import normalize_master_catalog_df

    return normalize_master_catalog_df(
        df,
        cfg,
        allowed_attribute_codes=allowed_attribute_codes,
        allowed_attribute_options=allowed_attribute_options,
        enforce_required_name=enforce_required_name,
        enforce_required_price=enforce_required_price,
        prefer_seo_title=prefer_seo_title,
    )


def _filter_value_by_options(
    code: str,
    value: str,
    options_map: Optional[Dict[str, List[str]]],
) -> Optional[str]:
    if not options_map:
        return value
    key = str(code).strip().lower()
    allowed = options_map.get(key)
    if not allowed:
        return value

    allowed_set = {str(opt).strip() for opt in allowed if str(opt).strip()}
    if not allowed_set:
        return value

    parts = [p.strip() for p in value.replace("|", ",").split(",")]
    kept = [p for p in parts if p in allowed_set]
    if not kept:
        return None
    return ",".join(kept)

def _norm_col(col: str) -> str:
    """
    Normalize column names from Plytix:
    - trim, lowercase
    - replace spaces and slashes with underscore
    - collapse repeated underscores
    """
    col = (col or "").strip().replace("\ufeff", "").lower()
    col = re.sub(r"[\/\s]+", "_", col)           # "Variant of" -> "variant_of"
    col = re.sub(r"[^a-z0-9_]+", "_", col)       # remove punctuation
    col = re.sub(r"_+", "_", col).strip("_")
    return col

def canonicalize_plytix_columns(df: pd.DataFrame) -> pd.DataFrame:
    """
    Renames incoming columns to canonical snake_case and applies alias mapping.
    Also handles duplicate columns (e.g. Brand + brand).
    """
    # Step 1: normalize all incoming headers
    normed = [_norm_col(c) for c in df.columns]

    # Step 2: if duplicate normalized names exist, keep the first and drop the rest
    # (Pandas can keep duplicate column names; that becomes painful later.)
    seen = set()
    keep_idx = []
    for i, c in enumerate(normed):
        if c not in seen:
            keep_idx.append(i)
            seen.add(c)
    df = df.iloc[:, keep_idx]
    normed = [normed[i] for i in keep_idx]

    df.columns = normed

    # Step 3: alias mapping (map Plytix-style names to what your code expects)
    aliases = {
        "sku": ["sku"],                          # "SKU" -> sku after normalization
        "name": ["name"],
        "price": ["price"],
        "categories": ["categories"],
        "product_online": ["product_online"],
        "status": ["status"],

        # Variants
        "variant_of": ["variant_of", "variantof"],
        "variant_list": ["variant_list", "variants"],

        # Dimensions (Plytix "Height (inches)" -> height_inches; "Width (inches)" -> width_inches)
        "width_in": ["width_in", "width", "width_inches"],
        "height_in": ["height_in", "height", "height_inches"],
        "depth_in": ["depth_in", "depth", "depth_inches"],

        # Text
        "short_description": ["short_description"],
        "tax_class_name": ["tax_class_name"],

        # Media
        "base_image": ["base_image", "base_image_url", "base_imageurl", "base_image_"],
    }

    # Inject canonical columns from any alias that exists
    for canon, keys in aliases.items():
        if canon in df.columns:
            continue
        for k in keys:
            if k in df.columns:
                df[canon] = df[k]
                break

    return df


# Master catalog is the primary publish source; legacy name retained for imports.

# Legacy alias — prefer magento.catalog_normalize.normalize_master_catalog_df
normalize_catalog_df = normalize_plytix_df