"""
I/O — Fast multi-format data readers with automatic type inference and chunked loading.
"""

from __future__ import annotations

import io
import logging
from pathlib import Path
from typing import Any, Iterator

import pandas as pd

from bizanalyticsai.core.dataframe import BizFrame

logger = logging.getLogger(__name__)


def _to_bizframe(df: pd.DataFrame) -> BizFrame:
    return BizFrame(df) if not isinstance(df, BizFrame) else df


def read_csv(
    path: str | Path | io.IOBase,
    *,
    encoding: str = "utf-8-sig",
    sep: str | None = None,          # auto-detect if None
    dtype: dict | None = None,
    parse_dates: list[str] | bool = True,
    low_memory: bool = False,
    chunksize: int | None = None,
    **kwargs: Any,
) -> BizFrame | Iterator[BizFrame]:
    """
    Read a CSV file into a BizFrame.

    Auto-detects separator (comma, semicolon, pipe, tab).
    Supports chunked reading for large files.

    Args:
        path:        File path, URL, or file-like object.
        encoding:    File encoding (default utf-8-sig handles BOM).
        sep:         Column separator. Auto-detected if None.
        dtype:       Column type mapping.
        parse_dates: List of date columns or True for auto-detection.
        low_memory:  Use low memory mode (slower but handles mixed types).
        chunksize:   Yield chunks of this size instead of full DataFrame.

    Returns:
        BizFrame or iterator of BizFrame when chunksize is set.
    """
    if sep is None:
        # sniff separator
        sample = _sniff(path)
        import csv
        try:
            sep = csv.Sniffer().sniff(sample, delimiters=",;\t|").delimiter
        except csv.Error:
            sep = ","

    opts: dict[str, Any] = dict(
        sep=sep, encoding=encoding, dtype=dtype,
        parse_dates=parse_dates, low_memory=low_memory, **kwargs,
    )

    if chunksize:
        chunks = pd.read_csv(path, chunksize=chunksize, **opts)
        return (_to_bizframe(chunk) for chunk in chunks)

    df = pd.read_csv(path, **opts)
    logger.info("read_csv: %s rows × %s cols from %s", len(df), len(df.columns), path)
    return _to_bizframe(df)


def read_json(
    path: str | Path | io.IOBase,
    *,
    orient: str | None = None,
    lines: bool = False,
    normalize: bool = False,
    record_path: str | list | None = None,
    **kwargs: Any,
) -> BizFrame:
    """
    Read a JSON file.

    Supports line-delimited JSON (NDJSON), nested records, and pandas orient formats.
    """
    if normalize or record_path:
        import json
        with open(path, encoding="utf-8") as f:
            raw = json.load(f)
        df = pd.json_normalize(raw, record_path=record_path)
    else:
        df = pd.read_json(path, orient=orient, lines=lines, **kwargs)

    logger.info("read_json: %s rows × %s cols from %s", len(df), len(df.columns), path)
    return _to_bizframe(df)


def read_excel(
    path: str | Path,
    *,
    sheet_name: str | int | list = 0,
    header: int = 0,
    dtype: dict | None = None,
    **kwargs: Any,
) -> BizFrame | dict[str, BizFrame]:
    """
    Read an Excel file (.xlsx, .xls, .ods).

    When sheet_name is a list or None, returns a dict of BizFrames.
    """
    result = pd.read_excel(path, sheet_name=sheet_name, header=header, dtype=dtype, **kwargs)
    if isinstance(result, dict):
        return {k: _to_bizframe(v) for k, v in result.items()}
    logger.info("read_excel: %s rows × %s cols from %s", len(result), len(result.columns), path)
    return _to_bizframe(result)


def read_parquet(
    path: str | Path,
    *,
    columns: list[str] | None = None,
    filters: list | None = None,
    **kwargs: Any,
) -> BizFrame:
    """
    Read a Parquet file (or directory of partitioned Parquet files).

    Uses PyArrow engine for maximum performance.
    Supports column pruning and predicate pushdown via filters.
    """
    df = pd.read_parquet(path, columns=columns, filters=filters, engine="pyarrow", **kwargs)
    logger.info("read_parquet: %s rows × %s cols from %s", len(df), len(df.columns), path)
    return _to_bizframe(df)


def read_sql(
    query: str,
    connection_string: str | Any,
    *,
    params: dict | None = None,
    chunksize: int | None = None,
    **kwargs: Any,
) -> BizFrame | Iterator[BizFrame]:
    """
    Execute a SQL query and return a BizFrame.

    Accepts a SQLAlchemy connection string or existing engine/connection.

    Example::

        df = bai.read_sql(
            "SELECT * FROM orders WHERE status = :s",
            "postgresql://user:pass@localhost/mydb",
            params={"s": "active"},
        )
    """
    try:
        from sqlalchemy import create_engine, text
        engine = (
            create_engine(connection_string)
            if isinstance(connection_string, str)
            else connection_string
        )
        with engine.connect() as conn:
            if chunksize:
                chunks = pd.read_sql(text(query), conn, params=params, chunksize=chunksize, **kwargs)
                return (_to_bizframe(c) for c in chunks)
            df = pd.read_sql(text(query), conn, params=params, **kwargs)
    except ImportError:
        raise ImportError("SQLAlchemy is required for read_sql: pip install sqlalchemy") from None

    logger.info("read_sql: %s rows × %s cols", len(df), len(df.columns))
    return _to_bizframe(df)


def read_supabase(
    table: str,
    url: str,
    key: str,
    *,
    select: str = "*",
    filters: list[tuple[str, str, Any]] | None = None,
    limit: int | None = None,
) -> BizFrame:
    """
    Read data from a Supabase table.

    Requires ``pip install bizanalyticsai[supabase]``.

    Example::

        df = bai.io.read_supabase(
            "orders",
            url="https://xxx.supabase.co",
            key="eyJ...",
            filters=[("status", "eq", "active")],
            limit=10_000,
        )
    """
    try:
        from supabase import create_client
    except ImportError:
        raise ImportError(
            "supabase-py is required: pip install bizanalyticsai[supabase]"
        ) from None

    client = create_client(url, key)
    q = client.table(table).select(select)
    for col, op, val in (filters or []):
        q = getattr(q, op)(col, val)
    if limit:
        q = q.limit(limit)

    response = q.execute()
    df = pd.DataFrame(response.data)
    logger.info("read_supabase: %s rows × %s cols from table '%s'", len(df), len(df.columns), table)
    return _to_bizframe(df)


def read_dict(data: list[dict] | dict) -> BizFrame:
    """Convert a list of dicts or nested dict to BizFrame."""
    return _to_bizframe(pd.DataFrame(data if isinstance(data, list) else [data]))


# ── Helpers ───────────────────────────────────────────────────────────────────
def _sniff(path) -> str:
    """Read first 4KB of a file for separator sniffing."""
    try:
        if hasattr(path, "read"):
            pos = path.tell()
            sample = path.read(4096)
            path.seek(pos)
            return sample if isinstance(sample, str) else sample.decode("utf-8", errors="replace")
        with open(path, "r", encoding="utf-8-sig", errors="replace") as f:
            return f.read(4096)
    except Exception:
        return ""
