"""Utilities for converting plugin data to tree nodes, summary, and CSV."""

import csv
import io
from typing import Any

# Map common biology keys to TreeNode types
_BIOLOGY_TYPE_MAP: dict[str, str] = {
    "plate": "plate", "plates": "plate",
    "sample": "sample", "samples": "sample",
    "treatment": "treatment", "treatments": "treatment",
    "cell_line": "cell_line", "cell_lines": "cell_line",
    "experiment": "experiment", "experiments": "experiment",
    "study": "study", "studies": "study",
    "clone": "clone", "clones": "clone",
    "passage": "passage", "passages": "passage",
}

# Keys commonly used as item labels
_LABEL_KEYS = ("name", "label", "id", "title", "plate_name", "sample_name")


def _infer_type(key: str) -> str:
    """Infer a TreeNode type from a dictionary key."""
    return _BIOLOGY_TYPE_MAP.get(key.lower(), "custom")


def _make_id(prefix: str, key: str | int) -> str:
    return f"{prefix}.{key}" if prefix else str(key)


def auto_json_to_tree(
    data: dict[str, Any], *, compact: bool = False, _prefix: str = ""
) -> list[dict[str, Any]]:
    """Convert a JSON dict to a list of TreeNode dicts.

    - Scalar values become leaf nodes with "key: value" labels.
    - Dicts become folder nodes with children.
    - Lists become folder nodes with numbered children.
    - Keys matching biology terms get appropriate node types.

    Args:
        data: JSON-serializable dict to convert.
        compact: If True, collapses leaf scalars into parent labels and
            filters null/empty values for a more concise tree.
    """
    nodes: list[dict[str, Any]] = []

    for key, value in data.items():
        if compact and _is_empty(value):
            continue

        node_id = _make_id(_prefix, key)

        if isinstance(value, dict):
            children = auto_json_to_tree(value, compact=compact, _prefix=node_id)
            if compact and _all_scalar(value):
                summary = ", ".join(
                    f"{k}: {v}" for k, v in value.items() if not _is_empty(v)
                )
                nodes.append({
                    "id": node_id,
                    "label": f"{key} ({summary})" if summary else str(key),
                    "type": _infer_type(key) if _infer_type(key) != "custom" else "folder",
                })
            else:
                nodes.append({
                    "id": node_id,
                    "label": str(key),
                    "type": _infer_type(key) if _infer_type(key) != "custom" else "folder",
                    "children": children,
                    "badge": len(children),
                })
        elif isinstance(value, list):
            children = []
            for i, item in enumerate(value):
                child_id = _make_id(node_id, i)
                if isinstance(item, dict):
                    label = _find_label(item, i)
                    if compact:
                        # Filter: skip label-key duplicates, nulls, and non-scalar values
                        visible = {
                            k: v for k, v in item.items()
                            if not _is_empty(v)
                            and k not in _LABEL_KEYS
                            and not isinstance(v, (dict, list))
                        }
                        # Also recurse for nested structures (lists/dicts)
                        nested_children = auto_json_to_tree(
                            {k: v for k, v in item.items() if isinstance(v, (dict, list)) and not _is_empty(v)},
                            compact=True, _prefix=child_id,
                        )
                        if not nested_children and not any(isinstance(v, (dict, list)) for v in item.values() if not _is_empty(v)):
                            # All-scalar item: collapse into label
                            summary = ", ".join(f"{k}: {v}" for k, v in visible.items())
                            children.append({
                                "id": child_id,
                                "label": f"{label} ({summary})" if summary else str(label),
                                "type": "custom",
                            })
                        else:
                            # Has nested structure: show scalar props + nested children
                            all_children = [
                                {
                                    "id": _make_id(child_id, k),
                                    "label": f"{k}: {v}",
                                    "type": "custom",
                                }
                                for k, v in visible.items()
                            ] + nested_children
                            children.append({
                                "id": child_id,
                                "label": str(label),
                                "type": "custom",
                                "children": all_children,
                                "badge": None,
                            })
                    else:
                        grandchildren = auto_json_to_tree(item, compact=False, _prefix=child_id)
                        children.append({
                            "id": child_id,
                            "label": str(label),
                            "type": "custom",
                            "children": grandchildren,
                        })
                else:
                    children.append({
                        "id": child_id,
                        "label": f"[{i}]: {item}",
                        "type": "custom",
                    })
            nodes.append({
                "id": node_id,
                "label": str(key),
                "type": _infer_type(key),
                "children": children,
                "badge": len(children),
            })
        else:
            nodes.append({
                "id": node_id,
                "label": f"{key}: {value}",
                "type": "custom",
                "metadata": {"key": key, "value": value},
            })

    return nodes


def _is_empty(value: Any) -> bool:
    """Check if a value is null/empty for filtering purposes."""
    if value is None:
        return True
    if isinstance(value, str) and not value.strip():
        return True
    if isinstance(value, (list, dict)) and len(value) == 0:
        return True
    return False


def _all_scalar(d: dict[str, Any]) -> bool:
    """Check if all values in a dict are scalar (not dict/list)."""
    return all(not isinstance(v, (dict, list)) for v in d.values())


def _find_label(item: dict[str, Any], index: int) -> str:
    """Find a suitable label for a dict item."""
    for key in _LABEL_KEYS:
        if key in item and item[key]:
            return str(item[key])
    return f"[{index}]"


def _flatten_dict(
    data: dict[str, Any], prefix: str = ""
) -> list[tuple[str, Any]]:
    """Flatten nested dict to dot-notation key-value pairs."""
    items: list[tuple[str, Any]] = []
    for key, value in data.items():
        full_key = f"{prefix}.{key}" if prefix else key
        if isinstance(value, dict):
            items.extend(_flatten_dict(value, full_key))
        elif isinstance(value, list):
            items.append((full_key, str(value)))
        else:
            items.append((full_key, value))
    return items


def _find_table_lists(data: dict[str, Any]) -> list[tuple[str, list[dict]]]:
    """Find lists of dicts suitable for table export."""
    tables: list[tuple[str, list[dict]]] = []
    for key, value in data.items():
        if isinstance(value, list) and value and isinstance(value[0], dict):
            tables.append((key, value))
    return tables


def auto_json_to_csv(data: dict[str, Any]) -> str:
    """Convert a JSON dict to CSV string.

    - Lists of dicts become tables with headers from dict keys.
    - Other values become key,value rows.
    - Nested dicts flatten to dot-notation keys.
    """
    output = io.StringIO()

    # Check for list-of-dict tables first
    tables = _find_table_lists(data)
    if tables:
        # Export the first (largest) table
        name, rows = max(tables, key=lambda t: len(t[1]))
        headers = list(rows[0].keys())
        writer = csv.DictWriter(output, fieldnames=headers)
        writer.writeheader()
        for row in rows:
            writer.writerow({k: row.get(k, "") for k in headers})
    else:
        # Flat key-value export
        writer = csv.writer(output)
        writer.writerow(["key", "value"])
        for key, value in _flatten_dict(data):
            writer.writerow([key, value])

    return output.getvalue()


def _humanize_key(key: str) -> str:
    """Convert snake_case key to human-readable label."""
    return key.replace("_", " ").title()


def _is_scalar(value: Any) -> bool:
    """Check if a value is scalar (not dict/list)."""
    return not isinstance(value, (dict, list))


def _collect_columns(rows: list[dict[str, Any]]) -> list[str]:
    """Collect scalar, non-null columns from rows, preserving insertion order.

    Skips columns that are entirely null/empty or contain non-scalar values
    (dicts, lists) which can't be displayed in a table cell.
    """
    columns: list[str] = []
    for row in rows:
        for key in row:
            if key not in columns:
                columns.append(key)
    return [
        col for col in columns
        if any(
            not _is_empty(row.get(col)) and _is_scalar(row.get(col))
            for row in rows
        )
    ]


def _find_nested_list_key(item: dict[str, Any]) -> str | None:
    """Find the first key in a dict that is a list of dicts (e.g., 'samples')."""
    for key, value in item.items():
        if isinstance(value, list) and value and isinstance(value[0], dict):
            return key
    return None


def auto_json_to_summary(data: dict[str, Any]) -> dict[str, Any]:
    """Convert JSON data to a structured summary with metadata and sections.

    Separates scalar metadata from structural data (arrays-of-objects).
    For nested groups (e.g., plates containing samples), returns group cards
    with embedded table rows. Filters null/empty columns.

    Returns:
        Dict with 'metadata' (scalar key-value pairs) and 'sections' (list
        of section dicts with type 'table' or 'group').
    """
    metadata: dict[str, Any] = {}
    sections: list[dict[str, Any]] = []

    for key, value in data.items():
        if isinstance(value, list) and value and isinstance(value[0], dict):
            # Check if items contain nested lists (group pattern)
            nested_key = _find_nested_list_key(value[0])

            if nested_key:
                # Group pattern: plates -> samples
                items = []
                for item in value:
                    nested_rows = item.get(nested_key, [])
                    # Scalar fields of the group item as metadata
                    item_label = _find_label(item, len(items))
                    item_meta = {
                        k: v for k, v in item.items()
                        if not isinstance(v, (dict, list)) and not _is_empty(v)
                    }
                    columns = _collect_columns(nested_rows) if nested_rows else []
                    rows = [
                        {col: row.get(col) for col in columns}
                        for row in nested_rows
                    ]
                    items.append({
                        "label": item_label,
                        "metadata": item_meta,
                        "item_count": len(nested_rows),
                        "item_key": nested_key,
                        "columns": columns,
                        "rows": rows,
                    })
                sections.append({
                    "key": key,
                    "label": _humanize_key(key),
                    "type": "group",
                    "items": items,
                })
            else:
                # Flat table pattern: list of dicts without nested lists
                columns = _collect_columns(value)
                rows = [
                    {col: row.get(col) for col in columns}
                    for row in value
                ]
                sections.append({
                    "key": key,
                    "label": _humanize_key(key),
                    "type": "table",
                    "columns": columns,
                    "rows": rows,
                    "row_count": len(rows),
                })
        elif isinstance(value, dict):
            # Nested scalar dicts get flattened into metadata
            for k, v in value.items():
                if not isinstance(v, (dict, list)) and not _is_empty(v):
                    metadata[f"{key}.{k}"] = v
        else:
            if not _is_empty(value):
                metadata[key] = value

    return {"metadata": metadata, "sections": sections}
