"""
Universal data normalizer.

Turns ANYTHING into plain Python the mind can reason about.
NumPy, Pandas, Polars, PyTorch, TensorFlow, JAX, PIL, NetworkX,
SciPy, scikit-learn, Arrow, Spark, Dask, Ray, URLs, files,
databases, generators, dataclasses, Pydantic, bytes, streams —
all of it. No hard dependencies. Everything lazy-loaded.

The mind never sees anything except list, dict, str, int, float, bool, None.
"""

from __future__ import annotations

import json
import csv
import io
import os
import re
import math
import base64
import hashlib
import struct
import urllib.request
import urllib.parse
from pathlib import Path, PurePath
from typing import Any, Callable
from collections.abc import Iterable, Mapping, Iterator, Generator
from datetime import datetime, date, time as dt_time, timedelta
from decimal import Decimal
from fractions import Fraction
from enum import Enum
from dataclasses import fields, is_dataclass
from importlib import import_module


_SENTINEL = object()


def normalize(data: Any) -> Any:
    """
    Convert any data from any library into plain Python.

    Rules:
      - list/tuple/set  -> [normalize(each)]
      - dict            -> {k: normalize(v)}
      - numpy.ndarray   -> .tolist()
      - pandas.DataFrame -> {"columns":..., "data":..., "shape":...}
      - torch.Tensor    -> .detach().cpu().tolist()
      - tf.Tensor       -> .numpy().tolist()
      - jax.Array       -> np.array(x).tolist()
      - PIL.Image       -> {"width":..., "height":..., "mode":..., "pixels_base64":...}
      - networkx.Graph  -> {"nodes":..., "edges":..., "directed":...}
      - scipy.sparse    -> {"shape":..., "nnz":..., "rows":..., "cols":..., "vals":...}
      - sklearn Bunch   -> dict(x)
      - polars.DataFrame -> {"columns":..., "data":..., "shape":...}
      - pyarrow.Table   -> {"columns":..., "data":..., "shape":...}
      - spark DataFrame -> {"columns":..., "data":..., "shape":...}
      - dask DataFrame  -> .compute() then normalize
      - ray Dataset     -> .to_pandas() then normalize
      - Path/str file   -> read and parse (.csv, .json, .jsonl, .tsv, .parquet, .xlsx, .h5, .pkl, .npy, .npz, .xml, .yaml, .toml, .ini, .proto, .arrow, .feather, .sqlite, .db)
      - URL str         -> fetch then parse
      - bytes           -> try JSON -> CSV -> image -> base64
      - dataclass       -> {field: normalize(val)}
      - Pydantic model  -> .model_dump()
      - Enum            -> .value
      - Decimal/Fraction -> float
      - datetime/date   -> ISO string
      - timedelta       -> total_seconds
      - callable        -> {"type": "callable", "name":...}
      - generator       -> list (capped at 10_000)
      - object with .tolist() -> .tolist()
      - object with .to_dict() -> .to_dict()
      - object with .to_json() -> json.loads(.to_json())
      - object with __array__ -> numpy -> .tolist()
      - anything else   -> {"type": str(class), "repr": str(data)[:200]}
    """
    if data is None or isinstance(data, (bool, int, float, str)):
        return data

    if isinstance(data, list):
        return [normalize(x) for x in data]

    if isinstance(data, tuple):
        return [normalize(x) for x in data]

    if isinstance(data, set):
        return sorted([normalize(x) for x in data], key=_sort_key)

    if isinstance(data, frozenset):
        return sorted([normalize(x) for x in data], key=_sort_key)

    if isinstance(data, dict):
        return {str(k): normalize(v) for k, v in data.items()}

    if isinstance(data, Mapping):
        return {str(k): normalize(v) for k, v in data.items()}

    if isinstance(data, Decimal):
        return float(data)

    if isinstance(data, Fraction):
        return float(data)

    if isinstance(data, datetime):
        return data.isoformat()

    if isinstance(data, date):
        return data.isoformat()

    if isinstance(data, dt_time):
        return data.isoformat()

    if isinstance(data, timedelta):
        return data.total_seconds()

    if isinstance(data, Enum):
        return normalize(data.value)

    if isinstance(data, bytes):
        return _normalize_bytes(data)

    if isinstance(data, bytearray):
        return _normalize_bytes(bytes(data))

    if isinstance(data, memoryview):
        return _normalize_bytes(bytes(data))

    if isinstance(data, range):
        return {"type": "range", "start": data.start, "stop": data.stop, "step": data.step}

    if isinstance(data, complex):
        return {"real": data.real, "imag": data.imag}

    if isinstance(data, PurePath):
        return _normalize_file(data)

    if isinstance(data, str):
        result = _try_string(data)
        if result is not _SENTINEL:
            return result
        return data

    if isinstance(data, (Generator, Iterator)):
        return _normalize_generator(data)

    # Protocol-based (duck typing) — check before generic Iterable
    # so objects like Pydantic models, SQLAlchemy results, etc. are
    # handled by their specific protocol before falling through to
    # generic iteration.
    result = _try_protocols(data)
    if result is not _SENTINEL:
        return result

    # Iterable (but not str/bytes, not handled by protocol above)
    if isinstance(data, Iterable):
        return _normalize_iterable(data)

    result = _try_known_libraries(data)
    if result is not _SENTINEL:
        return result

    if is_dataclass(data) and not isinstance(data, type):
        return _normalize_dataclass(data)

    if hasattr(data, '_fields') and hasattr(data, '_asdict'):
        try:
            return {k: normalize(v) for k, v in data._asdict().items()}
        except Exception:
            pass

    if callable(data):
        name = getattr(data, '__name__', repr(data))
        return {"type": "callable", "name": str(name)[:200]}

    return _normalize_unknown(data)


def _normalize_bytes(data: bytes) -> Any:
    try:
        return normalize(json.loads(data.decode("utf-8")))
    except (UnicodeDecodeError, json.JSONDecodeError):
        pass

    try:
        text = data.decode("utf-8")
        if "," in text.split("\n")[0]:
            reader = csv.DictReader(io.StringIO(text))
            rows = list(reader)
            if rows:
                return {
                    "columns": list(rows[0].keys()),
                    "data": [list(r.values()) for r in rows],
                    "shape": [len(rows), len(rows[0])],
                }
    except Exception:
        pass

    try:
        from PIL import Image
        img = Image.open(io.BytesIO(data))
        return _image_to_dict(img)
    except Exception:
        pass

    return {
        "type": "bytes",
        "size": len(data),
        "sha256": hashlib.sha256(data).hexdigest()[:16],
        "base64_prefix": base64.b64encode(data[:64]).decode("ascii"),
    }


def _try_string(data: str) -> Any:
    p = Path(data)
    if p.exists() and p.is_file():
        return _normalize_file(p)

    if re.match(r"^https?://", data) or re.match(r"^(s3|gs|azure|ftp)://", data):
        return _normalize_url(data)

    stripped = data.strip()
    if stripped and stripped[0] in "{[":
        try:
            return normalize(json.loads(stripped))
        except json.JSONDecodeError:
            pass

    lines = stripped.split("\n")
    if len(lines) >= 2 and "," in lines[0]:
        try:
            reader = csv.DictReader(io.StringIO(stripped))
            rows = list(reader)
            if rows:
                return {
                    "columns": list(rows[0].keys()),
                    "data": [list(r.values()) for r in rows],
                    "shape": [len(rows), len(rows[0])],
                }
        except Exception:
            pass

    if stripped.startswith("<?xml") or stripped.startswith("<"):
        return _normalize_xml_string(stripped)

    if ": " in stripped and not stripped.startswith("{"):
        result = _try_yaml(stripped)
        if result is not _SENTINEL:
            return result

    if "=" in stripped and "[" in stripped:
        result = _try_toml(stripped)
        if result is not _SENTINEL:
            return result

    if "=" in stripped and "[" in stripped:
        result = _try_ini(stripped)
        if result is not _SENTINEL:
            return result

    return _SENTINEL


_FILE_READERS: dict[str, Callable] = {}


def _register(suffix: str):
    def decorator(fn):
        _FILE_READERS[suffix] = fn
        return fn
    return decorator


def _normalize_file(path: PurePath) -> Any:
    path = Path(path)
    if not path.exists():
        return {"type": "file_not_found", "path": str(path)}
    if path.is_dir():
        return _normalize_directory(path)

    suffix = path.suffix.lower()

    reader = _FILE_READERS.get(suffix)
    if reader:
        try:
            return reader(path)
        except Exception as e:
            return {"type": "file_read_error", "path": str(path), "error": str(e)}

    if suffix in {".txt", ".md", ".rst", ".log", ".csv", ".tsv", ".json", ".jsonl",
                  ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf",
                  ".py", ".js", ".ts", ".java", ".c", ".cpp", ".h", ".hpp",
                  ".html", ".htm", ".css", ".sql", ".sh", ".bash", ".zsh",
                  ".r", ".R", ".m", ".pl", ".rb", ".go", ".rs", ".swift",
                  ".kt", ".scala", ".clj", ".hs", ".ml", ".ex", ".exs",
                  ".lua", ".vim", ".el", ".lisp", ".tcl", ".proto"}:
        try:
            text = path.read_text(encoding="utf-8", errors="replace")
            return _try_string(text) if suffix in {".json", ".csv", ".xml", ".yaml", ".yml", ".toml"} else text
        except Exception:
            pass

    try:
        data = path.read_bytes()
        return _normalize_bytes(data)
    except Exception:
        return {"type": "file", "path": str(path), "size": path.stat().st_size}


def _normalize_directory(path: Path) -> Any:
    entries = sorted(path.iterdir(), key=lambda p: p.name)
    return {
        "type": "directory",
        "path": str(path),
        "entries": [
            {
                "name": e.name,
                "type": "dir" if e.is_dir() else "file",
                "size": e.stat().st_size if e.is_file() else None,
            }
            for e in entries[:200]
        ],
        "count": len(entries),
    }


@_register(".json")
def _read_json(path: Path) -> Any:
    return normalize(json.loads(path.read_text(encoding="utf-8")))


@_register(".jsonl")
def _read_jsonl(path: Path) -> Any:
    return [normalize(json.loads(line)) for line in path.read_text().strip().split("\n") if line.strip()]


@_register(".csv")
def _read_csv(path: Path) -> Any:
    return _csv_to_dict(path)


@_register(".tsv")
def _read_tsv(path: Path) -> Any:
    return _csv_to_dict(path, sep="\t")


@_register(".xml")
def _read_xml(path: Path) -> Any:
    return _normalize_xml_string(path.read_text())


@_register(".yaml")
@_register(".yml")
def _read_yaml(path: Path) -> Any:
    return _try_yaml(path.read_text())


@_register(".toml")
def _read_toml(path: Path) -> Any:
    return _try_toml(path.read_text())


@_register(".ini")
@_register(".cfg")
@_register(".conf")
def _read_ini(path: Path) -> Any:
    return _try_ini(path.read_text())


@_register(".parquet")
def _read_parquet(path: Path) -> Any:
    try:
        import pandas as pd
        return normalize(pd.read_parquet(path))
    except ImportError:
        try:
            import pyarrow.parquet as pq
            return normalize(pq.read_table(path))
        except ImportError:
            return {"type": "parquet", "error": "install pandas or pyarrow", "path": str(path)}


@_register(".arrow")
@_register(".feather")
def _read_feather(path: Path) -> Any:
    try:
        import pandas as pd
        return normalize(pd.read_feather(path))
    except ImportError:
        try:
            import pyarrow.feather as f
            return normalize(f.read_table(path))
        except ImportError:
            return {"type": "feather", "error": "install pandas or pyarrow", "path": str(path)}


@_register(".xlsx")
@_register(".xls")
def _read_excel(path: Path) -> Any:
    try:
        import pandas as pd
        return normalize(pd.read_excel(path))
    except ImportError:
        return {"type": "excel", "error": "install pandas + openpyxl", "path": str(path)}


@_register(".h5")
@_register(".hdf5")
@_register(".hdf")
def _read_hdf5(path: Path) -> Any:
    try:
        import pandas as pd
        return normalize(pd.read_hdf(path))
    except ImportError:
        try:
            import h5py
            result = {}
            with h5py.File(path, "r") as f:
                for key in list(f.keys())[:50]:
                    result[key] = f[key][()].tolist()
            return result
        except ImportError:
            return {"type": "hdf5", "error": "install pandas or h5py", "path": str(path)}


@_register(".pkl")
@_register(".pickle")
def _read_pickle(path: Path) -> Any:
    import pickle
    with open(path, "rb") as f:
        return normalize(pickle.load(f))


@_register(".npy")
def _read_npy(path: Path) -> Any:
    try:
        import numpy as np
        return normalize(np.load(path, allow_pickle=True))
    except ImportError:
        return {"type": "npy", "error": "install numpy", "path": str(path)}


@_register(".npz")
def _read_npz(path: Path) -> Any:
    try:
        import numpy as np
        data = np.load(path, allow_pickle=True)
        return {k: normalize(data[k]) for k in data.files}
    except ImportError:
        return {"type": "npz", "error": "install numpy", "path": str(path)}


@_register(".sqlite")
@_register(".db")
def _read_sqlite(path: Path) -> Any:
    import sqlite3
    conn = sqlite3.connect(str(path))
    try:
        tables = [row[0] for row in conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table'"
        ).fetchall()]
        result = {}
        for table in tables[:20]:
            rows = conn.execute(f"SELECT * FROM [{table}] LIMIT 1000").fetchall()
            cols = [d[0] for d in conn.execute(f"SELECT * FROM [{table}] LIMIT 0").description]
            result[table] = {"columns": cols, "data": rows, "shape": [len(rows), len(cols)]}
        return result
    finally:
        conn.close()


@_register(".png")
@_register(".jpg")
@_register(".jpeg")
@_register(".bmp")
@_register(".gif")
@_register(".tiff")
@_register(".tif")
@_register(".webp")
def _read_image(path: Path) -> Any:
    try:
        from PIL import Image
        return _image_to_dict(Image.open(path))
    except ImportError:
        data = path.read_bytes()
        return {"type": "image", "path": str(path), "size": len(data),
                "format": path.suffix.lstrip(".")}


@_register(".wav")
@_register(".mp3")
@_register(".flac")
@_register(".ogg")
def _read_audio(path: Path) -> Any:
    size = path.stat().st_size
    return {"type": "audio", "format": path.suffix.lstrip("."), "size": size, "path": str(path)}


@_register(".mp4")
@_register(".avi")
@_register(".mkv")
@_register(".mov")
@_register(".webm")
def _read_video(path: Path) -> Any:
    size = path.stat().st_size
    return {"type": "video", "format": path.suffix.lstrip("."), "size": size, "path": str(path)}


@_register(".zip")
@_register(".tar")
@_register(".gz")
@_register(".bz2")
@_register(".xz")
@_register(".7z")
@_register(".rar")
def _read_archive(path: Path) -> Any:
    return {"type": "archive", "format": path.suffix.lstrip("."), "size": path.stat().st_size, "path": str(path)}


@_register(".pdf")
def _read_pdf(path: Path) -> Any:
    size = path.stat().st_size
    try:
        import fitz
        doc = fitz.open(str(path))
        text = ""
        for page in doc:
            text += page.get_text()
        return {"type": "pdf", "pages": len(doc), "text": text[:5000], "size": size}
    except ImportError:
        return {"type": "pdf", "size": size, "error": "install PyMuPDF for text extraction", "path": str(path)}


def _normalize_url(url: str) -> Any:
    if url.startswith("s3://"):
        return _normalize_s3(url)
    if url.startswith("gs://"):
        return _normalize_gs(url)

    try:
        req = urllib.request.Request(url, headers={"User-Agent": "aalgoi/2.0"})
        with urllib.request.urlopen(req, timeout=30) as resp:
            content_type = resp.headers.get("Content-Type", "")
            data = resp.read()

        if "json" in content_type:
            return normalize(json.loads(data.decode("utf-8")))
        if "csv" in content_type or "text/plain" in content_type:
            return _normalize_bytes(data)
        if "xml" in content_type:
            return _normalize_xml_string(data.decode("utf-8"))
        if "html" in content_type:
            return {"type": "html", "size": len(data), "url": url}
        if "image" in content_type:
            try:
                from PIL import Image
                return _image_to_dict(Image.open(io.BytesIO(data)))
            except Exception:
                return {"type": "image", "size": len(data), "url": url}

        try:
            return normalize(json.loads(data.decode("utf-8")))
        except Exception:
            pass

        return _normalize_bytes(data)

    except Exception as e:
        return {"type": "url_error", "url": url, "error": str(e)}


def _normalize_s3(url: str) -> Any:
    try:
        import boto3
        s3 = boto3.client("s3")
        bucket, key = url[5:].split("/", 1)
        obj = s3.get_object(Bucket=bucket, Key=key)
        data = obj["Body"].read()
        return _normalize_bytes(data)
    except ImportError:
        return {"type": "s3", "url": url, "error": "install boto3"}
    except Exception as e:
        return {"type": "s3_error", "url": url, "error": str(e)}


def _normalize_gs(url: str) -> Any:
    try:
        from google.cloud import storage
        client = storage.Client()
        bucket_name, blob_name = url[5:].split("/", 1)
        bucket = client.bucket(bucket_name)
        data = bucket.blob(blob_name).download_as_bytes()
        return _normalize_bytes(data)
    except ImportError:
        return {"type": "gcs", "url": url, "error": "install google-cloud-storage"}
    except Exception as e:
        return {"type": "gcs_error", "url": url, "error": str(e)}


def _try_protocols(data: Any) -> Any:
    if hasattr(data, "tolist") and callable(data.tolist):
        try:
            result = data.tolist()
            if isinstance(result, (list, dict, int, float, str)):
                return normalize(result)
        except Exception:
            pass

    if hasattr(data, "to_dict") and callable(data.to_dict):
        try:
            return normalize(data.to_dict())
        except Exception:
            pass

    if hasattr(data, "model_dump") and callable(data.model_dump):
        try:
            return normalize(data.model_dump())
        except Exception:
            pass

    if hasattr(data, "dict") and callable(data.dict) and not isinstance(data, type):
        try:
            return normalize(data.dict())
        except Exception:
            pass

    if hasattr(data, "to_json") and callable(data.to_json):
        try:
            raw = data.to_json()
            if isinstance(raw, str):
                return normalize(json.loads(raw))
            if isinstance(raw, bytes):
                return normalize(json.loads(raw.decode("utf-8")))
        except Exception:
            pass

    if hasattr(data, "__array__") and callable(data.__array__):
        try:
            import numpy as np
            return normalize(np.asarray(data).tolist())
        except Exception:
            pass

    if hasattr(data, "cpu") and hasattr(data, "numpy"):
        try:
            return normalize(data.cpu().numpy().tolist())
        except Exception:
            pass

    if hasattr(data, "detach") and hasattr(data, "cpu"):
        try:
            return normalize(data.detach().cpu().tolist())
        except Exception:
            pass

    if hasattr(data, "numpy") and callable(data.numpy):
        try:
            return normalize(data.numpy().tolist())
        except Exception:
            pass

    if hasattr(data, "compute") and callable(data.compute):
        try:
            return normalize(data.compute())
        except Exception:
            pass

    if hasattr(data, "to_pandas") and callable(data.to_pandas):
        try:
            return normalize(data.to_pandas())
        except Exception:
            pass

    if hasattr(data, "to_polars") and callable(data.to_polars):
        try:
            return normalize(data.to_polars())
        except Exception:
            pass

    if hasattr(data, "to_arrow") and callable(data.to_arrow):
        try:
            return normalize(data.to_arrow())
        except Exception:
            pass
    if hasattr(data, "to_pyarrow") and callable(data.to_pyarrow):
        try:
            return normalize(data.to_pyarrow())
        except Exception:
            pass

    if hasattr(data, "to_list") and callable(data.to_list):
        try:
            return normalize(data.to_list())
        except Exception:
            pass

    if hasattr(data, "value") and not isinstance(data, type):
        try:
            return normalize(data.value)
        except Exception:
            pass

    if hasattr(data, "values") and hasattr(data, "columns"):
        try:
            return normalize(data.values.tolist())
        except Exception:
            pass

    if hasattr(data, "fetchall") and callable(data.fetchall):
        try:
            rows = data.fetchall()
            if rows and hasattr(rows[0], "_fields"):
                return [normalize(dict(row._mapping)) for row in rows]
            return [normalize(list(row)) for row in rows]
        except Exception:
            pass

    if hasattr(data, "execute") and hasattr(data, "description"):
        try:
            cols = [d[0] for d in data.description] if data.description else []
            rows = data.fetchall()
            return {"columns": cols, "data": [list(r) for r in rows]}
        except Exception:
            pass

    return _SENTINEL


def _try_known_libraries(data: Any) -> Any:
    module = type(data).__module__.split(".")[0]

    if module == "numpy":
        try:
            return normalize(data.tolist())
        except Exception:
            pass

    if module == "pandas":
        try:
            if hasattr(data, "columns"):
                return {
                    "columns": list(data.columns),
                    "data": data.values.tolist(),
                    "dtypes": [str(dt) for dt in data.dtypes],
                    "shape": list(data.shape),
                    "index": data.index.tolist()[:100],
                }
            if hasattr(data, "to_list"):
                return {"values": data.to_list(), "name": str(data.name)}
        except Exception:
            pass

    if module == "polars":
        try:
            return {
                "columns": data.columns,
                "data": data.rows(),
                "shape": list(data.shape),
                "dtypes": [str(dt) for dt in data.dtypes],
            }
        except Exception:
            pass

    if module == "torch":
        try:
            return normalize(data.detach().cpu().tolist())
        except Exception:
            pass

    if module in ("tensorflow", "tf"):
        try:
            return normalize(data.numpy().tolist())
        except Exception:
            pass

    if module == "jax":
        try:
            import numpy as np
            return normalize(np.asarray(data).tolist())
        except Exception:
            pass

    if module == "cupy":
        try:
            return normalize(data.get().tolist())
        except Exception:
            pass

    if module in ("PIL", "PIL.Image"):
        return _image_to_dict(data)

    if module == "networkx":
        return _graph_to_dict(data)

    if module.startswith("scipy"):
        if hasattr(data, "nnz"):
            coo = data.tocoo()
            return {
                "shape": list(coo.shape),
                "nnz": coo.nnz,
                "rows": coo.row.tolist(),
                "cols": coo.col.tolist(),
                "vals": coo.data.tolist(),
            }
        if hasattr(data, "tolist"):
            return normalize(data.tolist())

    if module == "sklearn" or module.startswith("sklearn."):
        if hasattr(data, "items") and hasattr(data, "__getitem__"):
            try:
                return normalize(dict(data))
            except Exception:
                pass

    if module == "pyarrow":
        try:
            return {
                "columns": data.column_names,
                "data": [col.to_pylist() for col in data.columns],
                "shape": [data.num_rows, data.num_columns],
            }
        except Exception:
            pass

    if module in ("pyspark", "pyspark.sql"):
        try:
            rows = data.limit(1000).collect()
            if rows:
                return {
                    "columns": data.columns,
                    "data": [list(r) for r in rows],
                    "shape": [data.count(), len(data.columns)],
                }
        except Exception:
            pass

    if module == "dask":
        try:
            return normalize(data.compute())
        except Exception:
            pass

    if module == "ray":
        try:
            return normalize(data.to_pandas())
        except Exception:
            pass

    if module == "xarray":
        try:
            return normalize(data.to_dict())
        except Exception:
            pass

    if module == "sparse":
        try:
            coo = data.tocoo()
            return {
                "shape": list(coo.shape),
                "nnz": coo.nnz,
                "coords": [c.tolist() for c in coo.coords],
                "vals": coo.data.tolist(),
            }
        except Exception:
            pass

    return _SENTINEL


def _image_to_dict(img: Any) -> dict:
    try:
        import numpy as np
        pixels = np.array(img).tolist()
    except ImportError:
        buf = io.BytesIO()
        img.save(buf, format="PNG")
        pixels = base64.b64encode(buf.getvalue()).decode("ascii")

    return {
        "type": "image",
        "width": img.width,
        "height": img.height,
        "mode": img.mode,
        "pixels": pixels if not isinstance(pixels, str) else None,
        "pixels_base64": pixels if isinstance(pixels, str) else None,
    }


def _graph_to_dict(g: Any) -> dict:
    return {
        "type": "graph",
        "directed": g.is_directed(),
        "multigraph": g.is_multigraph(),
        "nodes": list(g.nodes()),
        "edges": [
            {"source": u, "target": v, **(attr if isinstance(attr, dict) else {"weight": attr})}
            for u, v, attr in (g.edges(data=True) if g.is_multigraph() is False else g.edges(data=True, keys=False))
        ],
        "num_nodes": g.number_of_nodes(),
        "num_edges": g.number_of_edges(),
    }


def _csv_to_dict(path: Path, sep: str = ",") -> dict:
    with open(path, newline="", encoding="utf-8", errors="replace") as f:
        reader = csv.DictReader(f, delimiter=sep)
        rows = list(reader)
    return {
        "columns": list(rows[0].keys()) if rows else [],
        "data": [list(r.values()) for r in rows],
        "shape": [len(rows), len(rows[0]) if rows else 0],
    }


def _normalize_xml_string(text: str) -> dict:
    try:
        import xml.etree.ElementTree as ET
        root = ET.fromstring(text)
        return {"type": "xml", "tag": root.tag, "attrib": dict(root.attrib),
                "text": (root.text or "").strip()[:5000],
                "children": len(list(root))}
    except Exception as e:
        return {"type": "xml", "error": str(e), "text_prefix": text[:500]}


def _try_yaml(text: str) -> Any:
    try:
        import yaml
        return normalize(yaml.safe_load(text))
    except ImportError:
        return _SENTINEL
    except Exception:
        return _SENTINEL


def _try_toml(text: str) -> Any:
    try:
        import tomllib
        return normalize(tomllib.loads(text))
    except ImportError:
        try:
            import toml
            return normalize(toml.loads(text))
        except ImportError:
            return _SENTINEL
        except Exception:
            return _SENTINEL
    except Exception:
        return _SENTINEL


def _try_ini(text: str) -> Any:
    import configparser
    parser = configparser.ConfigParser()
    parser.read_string(text)
    result = {}
    for section in parser.sections():
        result[section] = dict(parser[section])
    return result


def _normalize_dataclass(data: Any) -> dict:
    result = {}
    for f in fields(data):
        result[f.name] = normalize(getattr(data, f.name))
    return result


def _normalize_generator(gen: Generator) -> list:
    result = []
    for i, item in enumerate(gen):
        if i >= 10_000:
            break
        result.append(normalize(item))
    return result


def _normalize_iterable(data: Iterable) -> list:
    try:
        result = []
        for i, item in enumerate(data):
            if i >= 10_000:
                break
            result.append(normalize(item))
        return result
    except Exception:
        return [{"type": "iterable", "repr": repr(data)[:200]}]


def _normalize_unknown(data: Any) -> dict:
    result = {
        "type": f"{type(data).__module__}.{type(data).__qualname__}",
        "repr": repr(data)[:200],
    }

    for attr in ("shape", "size", "dtype", "ndim", "len", "length", "count"):
        val = getattr(data, attr, None)
        if val is not None and not callable(val):
            try:
                result[attr] = normalize(val) if not isinstance(val, (int, float, str, bool)) else val
            except Exception:
                pass

    if hasattr(data, "__len__"):
        try:
            result["length"] = len(data)
        except Exception:
            pass

    return result


def _sort_key(x: Any) -> Any:
    if x is None:
        return (0, "")
    if isinstance(x, bool):
        return (1, int(x))
    if isinstance(x, (int, float)):
        return (2, x)
    if isinstance(x, str):
        return (3, x)
    return (4, str(x))


def detect_type(data: Any) -> str:
    if data is None:
        return "none"
    if isinstance(data, bool):
        return "bool"
    if isinstance(data, int):
        return "int"
    if isinstance(data, float):
        return "float"
    if isinstance(data, str):
        p = Path(data)
        if p.exists():
            return f"file({p.suffix})"
        if re.match(r"^https?://", data):
            return "url"
        return "str"
    if isinstance(data, (list, tuple)):
        return f"list({len(data)})"
    if isinstance(data, dict):
        return f"dict({len(data)} keys)"
    if isinstance(data, set):
        return f"set({len(data)})"

    module = type(data).__module__.split(".")[0]
    name = type(data).__name__

    if module == "numpy":
        return f"numpy.{name}"
    if module == "pandas":
        return f"pandas.{name}"
    if module == "polars":
        return f"polars.{name}"
    if module == "torch":
        return f"torch.{name}"
    if module in ("tensorflow", "tf"):
        return f"tf.{name}"
    if module == "jax":
        return f"jax.{name}"
    if module == "PIL":
        return "PIL.Image"
    if module == "networkx":
        return f"networkx.{name}"
    if module == "scipy":
        return f"scipy.{name}"
    if module.startswith("sklearn"):
        return f"sklearn.{name}"
    if module == "pyarrow":
        return f"pyarrow.{name}"
    if module in ("pyspark", "pyspark.sql"):
        return f"spark.{name}"
    if module == "dask":
        return f"dask.{name}"
    if module == "ray":
        return f"ray.{name}"

    if isinstance(data, PurePath):
        return f"path({Path(data).suffix})"

    if is_dataclass(data):
        return f"dataclass({type(data).__name__})"

    return f"{module}.{name}"


def normalize_with_metadata(data: Any) -> dict:
    original_type = detect_type(data)
    normalized = normalize(data)
    return {
        "original_type": original_type,
        "data": normalized,
    }
