"""Admin catalog helpers for V2 dashboard contracts.

Keeps product status mapping, taxonomy lookup, secret redaction, and
brand-collection assignment out of the HTTP layer.
"""

from __future__ import annotations

from sqlalchemy import delete, or_, select
from sqlalchemy.orm import Session

from db.models import MasterBrand
from db.models_v2 import V2BrandCollection, V2Category, V2Product
from db.v2_taxonomy import require_valid_taxonomy_parent


PRODUCT_STATUS_TO_LIFECYCLE = {
    "active": "active",
    "pending": "draft",
    "archive": "archived",
    "archived": "archived",
    "draft": "draft",
    "disabled": "disabled",
}

LIFECYCLE_TO_PRODUCT_STATUS = {
    "active": "active",
    "draft": "pending",
    "archived": "archive",
    "disabled": "disabled",
}

CONNECTION_STATUSES = ("active", "inactive")
CONNECTION_ENVIRONMENTS = ("production", "staging")
SIGNATURE_METHODS = ("HMAC-SHA1", "HMAC-SHA256")
SECRET_CONFIG_KEYS = {
    "consumer_secret",
    "access_token",
    "access_token_secret",
    "consumer_key",
    "client_id",
    "client_secret",
    "admin_access_token",
    "SHOPIFY_CLIENT_ID",
    "SHOPIFY_CLIENT_SECRET",
    "SHOPIFY_ADMIN_ACCESS_TOKEN",
}


def product_status_from_lifecycle(lifecycle_status: str | None) -> str:
    return LIFECYCLE_TO_PRODUCT_STATUS.get(str(lifecycle_status or "").strip().lower(), str(lifecycle_status or "pending"))


def lifecycle_from_product_status(value: str | None) -> str | None:
    if value is None or str(value).strip() == "":
        return None
    text = str(value).strip().lower()
    if text not in PRODUCT_STATUS_TO_LIFECYCLE:
        raise ValueError("Product status must be Active, Pending, or Archive.")
    return PRODUCT_STATUS_TO_LIFECYCLE[text]


def parse_lifecycle_filter(raw: str | None) -> list[str] | None:
    if not raw or not str(raw).strip():
        return None
    out: list[str] = []
    for part in str(raw).split(","):
        token = part.strip().lower()
        if not token or token in {"all", "*"}:
            continue
        mapped = PRODUCT_STATUS_TO_LIFECYCLE.get(token, token)
        if mapped not in {"active", "draft", "disabled", "archived"}:
            continue
        if mapped not in out:
            out.append(mapped)
    return out or None


def derived_taxonomy_level(node_type: str, parent: V2Category | None) -> int:
    kind = str(node_type or "").strip().lower()
    if kind == "hub":
        return 1
    if kind == "collection":
        return 2
    if parent is None:
        return 1
    return int(parent.level or 1) + 1


def public_connection_config(config: dict | None) -> dict:
    payload = dict(config or {})
    for key in list(payload):
        if str(key) in SECRET_CONFIG_KEYS or "secret" in str(key).lower() or "token" in str(key).lower() or "password" in str(key).lower():
            payload.pop(key, None)
    return payload


def secrets_configured(values: dict) -> dict[str, bool]:
    flags: dict[str, bool] = {}
    for key, value in values.items():
        flags[key] = bool(str(value or "").strip())
    return flags


def skip_blank_secrets(payload: dict, secret_keys: tuple[str, ...]) -> dict:
    out = dict(payload)
    for key in secret_keys:
        if key in out and not str(out.get(key) or "").strip():
            out.pop(key)
    return out


def validate_connection_enums(*, status: str | None = None, environment: str | None = None, signature_method: str | None = None) -> None:
    if status is not None and str(status).strip().lower() not in CONNECTION_STATUSES:
        raise ValueError("Status must be Active or Inactive.")
    if environment is not None and str(environment).strip().lower() not in CONNECTION_ENVIRONMENTS:
        raise ValueError("Environment must be Production or Staging.")
    if signature_method is not None and str(signature_method).strip() not in SIGNATURE_METHODS:
        raise ValueError("Signature method must be HMAC-SHA1 or HMAC-SHA256.")


def taxonomy_options(
    session: Session,
    *,
    node_type: str,
    query: str = "",
    parent_id: int | None = None,
    limit: int = 30,
) -> list[dict]:
    kind = str(node_type or "").strip().lower()
    if kind not in {"hub", "category", "collection"}:
        raise ValueError("node_type must be hub, category, or collection")
    stmt = (
        select(V2Category)
        .where(V2Category.merged_into_id.is_(None), V2Category.node_type == kind)
        .order_by(V2Category.name.asc())
        .limit(max(1, min(int(limit), 100)))
    )
    if parent_id is not None:
        stmt = stmt.where(V2Category.parent_id == int(parent_id))
    if query.strip():
        stmt = stmt.where(V2Category.name.ilike(f"%{query.strip()}%"))
    rows = session.execute(stmt).scalars().all()
    parent_ids = {int(row.parent_id) for row in rows if row.parent_id is not None}
    parents = {
        int(row.id): row.name
        for row in session.execute(select(V2Category).where(V2Category.id.in_(parent_ids or [-1]))).scalars().all()
    }
    items = []
    for row in rows:
        parent_name = parents.get(int(row.parent_id)) if row.parent_id is not None else None
        items.append(
            {
                "id": int(row.id),
                "name": row.name,
                "node_type": row.node_type,
                "parent_id": int(row.parent_id) if row.parent_id is not None else None,
                "parent_name": parent_name,
                "label": f"{row.name} — {parent_name}" if parent_name else row.name,
            }
        )
    return items


def product_taxonomy_display(session: Session, product: V2Product) -> dict:
    hub_row = None
    category_row = None
    if product.shopping_l1_category_id is not None:
        candidate = session.get(V2Category, int(product.shopping_l1_category_id))
        if candidate is not None and candidate.node_type == "hub":
            hub_row = candidate
        elif candidate is not None and candidate.node_type == "category":
            category_row = candidate
    if product.shopping_l2_category_id is not None:
        candidate = session.get(V2Category, int(product.shopping_l2_category_id))
        if candidate is not None and candidate.node_type == "category":
            category_row = candidate
    if product.primary_category_id is not None:
        candidate = session.get(V2Category, int(product.primary_category_id))
        if candidate is not None:
            if candidate.node_type == "hub" and hub_row is None:
                hub_row = candidate
            if candidate.node_type == "category" and category_row is None:
                category_row = candidate
    if hub_row is None and category_row is not None and category_row.parent_id is not None:
        parent = session.get(V2Category, int(category_row.parent_id))
        if parent is not None and parent.node_type == "hub":
            hub_row = parent
    attrs = product.attributes if isinstance(product.attributes, dict) else {}
    return {
        "hub_id": int(hub_row.id) if hub_row is not None else None,
        "hub_name": hub_row.name if hub_row is not None else (attrs.get("category_l1") or None),
        "category_id": int(category_row.id) if category_row is not None else None,
        "category_name": category_row.name if category_row is not None else (attrs.get("category") or None),
    }


def _belongs_to_hub(session: Session, node: V2Category, hub_id: int) -> bool:
    walk: V2Category | None = node
    seen: set[int] = set()
    while walk is not None and int(walk.id) not in seen:
        seen.add(int(walk.id))
        if walk.node_type == "hub":
            return int(walk.id) == int(hub_id)
        if walk.parent_id is None:
            return False
        walk = session.get(V2Category, int(walk.parent_id))
    return False


def apply_hub_category(
    session: Session,
    *,
    hub_id: int | None,
    category_id: int | None,
) -> dict:
    if hub_id is not None:
        hub = session.get(V2Category, int(hub_id))
        if hub is None or hub.node_type != "hub":
            raise ValueError("Hub must be an existing Hub.")
    if category_id is not None:
        category = session.get(V2Category, int(category_id))
        if category is None or category.node_type != "category":
            raise ValueError("Category must be an existing Category.")
        if hub_id is not None and not _belongs_to_hub(session, category, int(hub_id)):
            raise ValueError("That Category does not belong to the selected Hub.")
    return {
        "shopping_l1_category_id": hub_id,
        "shopping_l2_category_id": category_id,
        "primary_category_id": category_id if category_id is not None else hub_id,
        "category_ids": [item for item in (hub_id, category_id) if item is not None],
    }


def list_brand_collections(session: Session, brand_id: int) -> list[dict]:
    rows = session.execute(
        select(V2Category, V2BrandCollection)
        .join(V2BrandCollection, V2BrandCollection.collection_id == V2Category.id)
        .where(V2BrandCollection.brand_id == int(brand_id), V2Category.node_type == "collection")
        .order_by(V2Category.name.asc())
    ).all()
    parent_ids = {int(category.parent_id) for category, _ in rows if category.parent_id is not None}
    parents = {
        int(row.id): row.name
        for row in session.execute(select(V2Category).where(V2Category.id.in_(parent_ids or [-1]))).scalars().all()
    }
    return [
        {
            "id": int(category.id),
            "name": category.name,
            "hub_id": int(category.parent_id) if category.parent_id is not None else None,
            "hub_name": parents.get(int(category.parent_id)) if category.parent_id is not None else None,
            "label": f"{category.name} — {parents.get(int(category.parent_id))}" if category.parent_id is not None and parents.get(int(category.parent_id)) else category.name,
        }
        for category, _ in rows
    ]


def replace_brand_collections(session: Session, brand_id: int, collection_ids: list[int]) -> list[dict]:
    brand = session.get(MasterBrand, int(brand_id))
    if brand is None:
        raise ValueError("Brand not found")
    wanted: list[int] = []
    seen: set[int] = set()
    for raw in collection_ids:
        collection_id = int(raw)
        if collection_id in seen:
            continue
        seen.add(collection_id)
        row = session.get(V2Category, collection_id)
        if row is None or row.node_type != "collection":
            raise ValueError("Only Collections can be assigned to a Brand.")
        wanted.append(collection_id)

    session.execute(delete(V2BrandCollection).where(V2BrandCollection.brand_id == int(brand_id)))
    for collection_id in wanted:
        session.execute(delete(V2BrandCollection).where(V2BrandCollection.collection_id == collection_id))
        session.add(V2BrandCollection(brand_id=int(brand_id), collection_id=collection_id))
    session.flush()
    return list_brand_collections(session, brand_id)
