from __future__ import annotations

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

from db.source_imports import normalize_column_key


# System fields we always want on a Plytix product record.
CORE_IDENTITY_FIELDS = (
    "sku",
    "id",
    "_parent_id",
    "parent_id",
    "num_variations",
    "label",
    "status",
    "thumbnail",
    "categories",
    "modified",
    "created",
    "product_type",
    "gtin",
    "family",
)

# Default PIM attributes to request when no catalog is loaded yet.
DEFAULT_PULL_ATTRIBUTES = (
    "sku",
    "label",
    "status",
    "thumbnail",
    "categories",
    "modified",
    "description",
    "short_description",
    "base_image",
    "additional_images",
    "assets",
    "product_type",
    "gtin",
    "price",
    "cost",
    "weight",
    "width",
    "height",
    "depth",
)

# Flattened/json_normalize noise — audit metadata, not product data.
NOISE_KEY_PATTERNS = (
    re.compile(r"^created_user_audit(\.|$)"),
    re.compile(r"^modified_user_audit(\.|$)"),
    re.compile(r".*_user_audit(\.|$)"),
    re.compile(r"^user_audit(\.|$)"),
)

NOISE_EXACT_KEYS = {
    "created_user_audit",
    "modified_user_audit",
    "user_audit",
}


def is_noise_field(key: str) -> bool:
    normalized = str(key or "").strip()
    if not normalized:
        return True
    lowered = normalized.lower()
    if lowered in NOISE_EXACT_KEYS:
        return True
    return any(pattern.search(normalized) for pattern in NOISE_KEY_PATTERNS)


def chunk_attributes(attributes: Sequence[str], *, chunk_size: int = 20) -> List[List[str]]:
    """Plytix search limits attribute selection — batch in chunks (20 is the v2 cap)."""
    ordered: List[str] = []
    seen: Set[str] = set()
    for raw in attributes:
        code = str(raw or "").strip()
        if not code or code in seen:
            continue
        seen.add(code)
        ordered.append(code)
    if not ordered:
        return []
    return [ordered[i : i + chunk_size] for i in range(0, len(ordered), chunk_size)]


def resolve_pull_attribute_names(
    catalog_codes: Optional[Iterable[str]] = None,
    *,
    extra: Optional[Iterable[str]] = None,
) -> List[str]:
    names: List[str] = []
    seen: Set[str] = set()
    for raw in (*CORE_IDENTITY_FIELDS, *DEFAULT_PULL_ATTRIBUTES, *(catalog_codes or []), *(extra or [])):
        code = str(raw or "").strip()
        if not code or code in seen:
            continue
        seen.add(code)
        names.append(code)
    return names


def merge_raw_products(existing: Optional[Dict[str, Any]], incoming: Dict[str, Any]) -> Dict[str, Any]:
    """Merge two raw Plytix API product payloads for the same SKU/id."""
    if not existing:
        return dict(incoming)
    merged = dict(existing)
    for key, value in incoming.items():
        if key == "attributes":
            merged["attributes"] = _merge_attributes(existing.get("attributes"), value)
            continue
        if value in (None, "", [], {}):
            continue
        merged[key] = value
    return merged


def normalize_plytix_api_product(raw: Dict[str, Any]) -> Dict[str, Any]:
    """Turn a raw Plytix API product into a flat, UI-friendly source snapshot payload."""
    if not isinstance(raw, dict):
        return {}

    out: Dict[str, Any] = {}
    audit: Dict[str, Any] = {}

    for key, value in raw.items():
        if key == "attributes":
            continue
        if value in (None, "", [], {}):
            continue
        if is_noise_field(key):
            if isinstance(value, dict):
                audit[key] = value
            continue
        if isinstance(value, dict):
            if key.endswith("_audit") or "user_audit" in key:
                audit[key] = value
                continue
            out[key] = value
            continue
        if isinstance(value, list) and key not in {"assets", "additional_images", "categories"}:
            out[key] = value
            continue
        out[key] = value

    for code, value in _flatten_attributes(raw.get("attributes")).items():
        if value in (None, "", [], {}):
            continue
        out[code] = value

    # Promote common identity fields if nested only under attributes.
    for field in CORE_IDENTITY_FIELDS:
        if field in out and out[field] not in (None, ""):
            continue
        attr_val = _flatten_attributes(raw.get("attributes")).get(field)
        if attr_val not in (None, "", [], {}):
            out[field] = attr_val

    sku = str(out.get("sku") or raw.get("sku") or "").strip()
    if sku:
        out["sku"] = sku

    thumbnail = _extract_thumbnail(out, raw)
    if thumbnail:
        out["thumbnail"] = thumbnail
        out["thumbnail_url"] = thumbnail

    categories = _extract_categories(out, raw)
    if categories:
        out["categories"] = categories

    if audit:
        out["_audit"] = audit

    return {key: value for key, value in out.items() if not is_noise_field(key) and value not in (None, "", [], {})}


def normalize_plytix_flat_record(record: Dict[str, Any]) -> Dict[str, Any]:
    """Clean an already-flattened row (legacy json_normalize imports)."""
    if not isinstance(record, dict):
        return {}
    raw_like: Dict[str, Any] = {}
    attrs: Dict[str, Any] = {}
    audit: Dict[str, Any] = {}

    for key, value in record.items():
        if value in (None, "", [], {}):
            continue
        text_key = str(key)
        if is_noise_field(text_key):
            audit[text_key] = value
            continue
        if text_key.startswith("attributes."):
            attrs[text_key.split(".", 1)[1]] = value
            continue
        raw_like[text_key] = value

    if attrs:
        raw_like["attributes"] = attrs
    if audit:
        raw_like["_audit"] = audit
    return normalize_plytix_api_product(raw_like)


def filter_plytix_display_fields(fields: Dict[str, Any]) -> Dict[str, Any]:
    """Strip audit noise from flattened Plytix fields shown in matrix/export."""
    if not fields:
        return {}
    cleaned = {
        key: value
        for key, value in fields.items()
        if not is_noise_field(str(key)) and str(key) != "_audit" and value not in (None, "", [], {})
    }
    return _stringify_complex_values(cleaned)


def _merge_attributes(left: Any, right: Any) -> Any:
    if isinstance(left, dict) and isinstance(right, dict):
        merged = dict(left)
        merged.update(right)
        return merged
    if isinstance(left, list) and isinstance(right, list):
        return left + right
    return right if right not in (None, "", [], {}) else left


def _flatten_attributes(attributes: Any) -> Dict[str, Any]:
    if attributes is None:
        return {}
    if isinstance(attributes, dict):
        out: Dict[str, Any] = {}
        for key, value in attributes.items():
            code = str(key or "").strip()
            if not code:
                continue
            out[normalize_column_key(code)] = _scalarize(value)
            out[code] = _scalarize(value)
        return out
    if isinstance(attributes, list):
        out = {}
        for item in attributes:
            if not isinstance(item, dict):
                continue
            code = str(
                item.get("attribute_code")
                or item.get("code")
                or item.get("name")
                or item.get("label")
                or ""
            ).strip()
            if not code:
                continue
            value = item.get("value")
            if value is None and "values" in item:
                value = item.get("values")
            out[normalize_column_key(code)] = _scalarize(value)
            out[code] = _scalarize(value)
        return out
    return {}


def _scalarize(value: Any) -> Any:
    if isinstance(value, list):
        if not value:
            return None
        if all(isinstance(item, (str, int, float, bool)) or item is None for item in value):
            return ", ".join(str(item) for item in value if item not in (None, ""))
        return value
    if isinstance(value, dict):
        for key in ("url", "value", "label", "name", "path"):
            if value.get(key) not in (None, ""):
                return value.get(key)
        return value
    return value


def _extract_thumbnail(out: Dict[str, Any], raw: Dict[str, Any]) -> Optional[str]:
    for candidate in (
        out.get("thumbnail"),
        out.get("thumbnail_url"),
        out.get("base_image"),
        raw.get("thumbnail"),
    ):
        url = _scalarize(candidate)
        if isinstance(url, str) and url.strip():
            return url.strip()
    assets = out.get("assets") or raw.get("assets")
    if isinstance(assets, list):
        for asset in assets:
            if not isinstance(asset, dict):
                continue
            url = asset.get("url") or asset.get("thumbnail") or asset.get("path")
            if url:
                return str(url).strip()
    return None


def _extract_categories(out: Dict[str, Any], raw: Dict[str, Any]) -> Optional[str]:
    for candidate in (out.get("categories"), raw.get("categories")):
        text = _scalarize(candidate)
        if isinstance(text, str) and text.strip():
            return text.strip()
        if isinstance(text, list):
            parts = [str(item).strip() for item in text if str(item).strip()]
            if parts:
                return " / ".join(parts)
    return None


def _stringify_complex_values(fields: Dict[str, Any]) -> Dict[str, Any]:
    import json

    out: Dict[str, Any] = {}
    for key, value in fields.items():
        if isinstance(value, (dict, list)):
            out[key] = json.dumps(value, sort_keys=True, default=str) if value else None
        else:
            out[key] = value
    return {key: value for key, value in out.items() if value not in (None, "", [], {})}
