"""Entry points for loading `YadsSpec` from various sources.

This module provides simple functions for loading a `YadsSpec` from common
formats:

- `from_yaml_string`: Load from YAML content provided as a string.
- `from_yaml_path`: Load from a filesystem path to a YAML file.
- `from_yaml_stream`: Load from a file-like stream (text or binary).
- `from_yaml`: Convenience loader that accepts a path (`str` or
  `pathlib.Path`) or a file-like stream. It does not accept arbitrary
  content strings.

All functions return a validated immutable `YadsSpec` instance.
"""

# pyright: reportUnsupportedDunderAll=none
# PyArrow typing stubs progress: https://github.com/apache/arrow/pull/47609

from __future__ import annotations

from pathlib import Path
from typing import IO, Any, cast, Literal, TYPE_CHECKING

from .base import BaseLoader, BaseLoaderConfig, ConfigurableLoader, DictLoader
from .yaml_loader import YamlLoader

if TYPE_CHECKING:
    from ..spec import YadsSpec
    from ..types import YadsType


def __getattr__(name: str):
    if name in ("PyArrowLoader", "PyArrowLoaderConfig"):
        from . import pyarrow_loader

        return getattr(pyarrow_loader, name)
    if name in ("PySparkLoader", "PySparkLoaderConfig"):
        from . import pyspark_loader

        return getattr(pyspark_loader, name)
    if name in ("PolarsLoader", "PolarsLoaderConfig"):
        from . import polars_loader

        return getattr(polars_loader, name)
    if name in ("SqlLoader", "SqlLoaderConfig"):
        from .sql import base

        return getattr(base, name)
    if name == "PostgreSqlLoader":
        from .sql import postgres_loader

        return getattr(postgres_loader, name)
    if name == "SqlServerLoader":
        from .sql import sqlserver_loader

        return getattr(sqlserver_loader, name)
    raise AttributeError(name)


__all__ = [
    "from_dict",
    "from_yaml_string",
    "from_yaml_path",
    "from_yaml_stream",
    "from_yaml",
    "from_pyarrow",
    "from_pyspark",
    "from_polars",
    "from_postgresql",
    "from_sqlserver",
    "BaseLoader",
    "BaseLoaderConfig",
    "ConfigurableLoader",
    "DictLoader",
    "YamlLoader",
    "PyArrowLoader",
    "PyArrowLoaderConfig",
    "PySparkLoader",
    "PySparkLoaderConfig",
    "PolarsLoader",
    "PolarsLoaderConfig",
    "SqlLoader",
    "SqlLoaderConfig",
    "PostgreSqlLoader",
    "SqlServerLoader",
]


def from_dict(data: dict[str, Any]) -> YadsSpec:
    """Load a `YadsSpec` from a dictionary.

    Args:
        data: The dictionary representation of the spec.

    Returns:
        A validated immutable `YadsSpec` instance.

    Example:
        ```python
        data = {
            "name": "users",
            "version": 1,
            "columns": [
                {
                    "name": "id",
                    "type": "integer",
                },
                {
                    "name": "email",
                    "type": "string",
                }
            ]
        }
        spec = from_dict(data)
        ```
    """
    return DictLoader().load(data)


def from_yaml_string(content: str) -> YadsSpec:
    """Load a spec from YAML string content.

    Args:
        content: YAML content as a string.

    Returns:
        A validated immutable `YadsSpec` instance.
    """
    return YamlLoader().load(content)


def from_yaml_path(path: str | Path, *, encoding: str = "utf-8") -> YadsSpec:
    """Load a spec from a YAML file path.

    Args:
        path: Filesystem path to a YAML file.
        encoding: Text encoding used to read the file.

    Returns:
        A validated immutable `YadsSpec` instance.

    Raises:
        FileNotFoundError: If the file does not exist.
    """
    text = Path(path).read_text(encoding=encoding)
    return YamlLoader().load(text)


def from_yaml_stream(stream: IO[str] | IO[bytes], *, encoding: str = "utf-8") -> YadsSpec:
    """Load a spec from a file-like stream.

    The stream is not closed by this function.

    Args:
        stream: File-like object opened in text or binary mode.
        encoding: Used only if `stream` is binary.

    Returns:
        A validated immutable `YadsSpec` instance.
    """
    raw = stream.read()
    text = raw.decode(encoding) if isinstance(raw, (bytes, bytearray)) else raw
    return YamlLoader().load(text)


def from_yaml(
    source: str | Path | IO[str] | IO[bytes], *, encoding: str = "utf-8"
) -> YadsSpec:
    """Load a spec from a path or a file-like stream.

    This convenience loader avoids ambiguity by not accepting arbitrary content
    strings. Pass content strings to `from_yaml_string` instead.

    Args:
        source: A filesystem path (`str` or `pathlib.Path`) or a file-like
            object opened in text or binary mode.
        encoding: Text encoding used when reading files or decoding binary
            streams.

    Returns:
        A validated immutable `YadsSpec` instance.
    """
    if hasattr(source, "read"):
        return from_yaml_stream(cast(IO[str] | IO[bytes], source), encoding=encoding)
    return from_yaml_path(cast(str | Path, source), encoding=encoding)


def from_pyarrow(
    schema: Any,
    *,
    mode: Literal["raise", "coerce"] = "coerce",
    fallback_type: YadsType | None = None,
    name: str,
    version: int,
    description: str | None = None,
) -> YadsSpec:
    """Load a spec from a `pyarrow.Schema`.

    Args:
        schema: An instance of `pyarrow.Schema`.
        mode: Loading mode. "raise" will raise exceptions on unsupported
            features. "coerce" will attempt to coerce unsupported features to
            supported ones with warnings. Defaults to "coerce".
        fallback_type: A yads type to use as fallback when an unsupported
            PyArrow type is encountered. Only used when mode is "coerce".
            Must be either String or Binary, or None. Defaults to None.
        name: Fully-qualified spec name to assign.
        version: Spec version string.
        description: Optional human-readable description.

    Returns:
        A validated immutable `YadsSpec` instance.

    Example:
        ```python
        import pyarrow as pa
        schema = pa.schema([
            pa.field("id", pa.int64()),
            pa.field("name", pa.string()),
        ])
        spec = from_pyarrow(schema, name="users", version=1)
        ```
    """
    from . import pyarrow_loader  # type: ignore

    config = pyarrow_loader.PyArrowLoaderConfig(mode=mode, fallback_type=fallback_type)
    loader = cast(Any, pyarrow_loader.PyArrowLoader(config))
    return loader.load(schema, name=name, version=version, description=description)


def from_pyspark(
    schema: Any,
    *,
    mode: Literal["raise", "coerce"] = "coerce",
    fallback_type: YadsType | None = None,
    name: str,
    version: int,
    description: str | None = None,
) -> YadsSpec:
    """Load a spec from a `pyspark.sql.types.StructType`.

    Args:
        schema: An instance of `pyspark.sql.types.StructType`.
        mode: Loading mode. "raise" will raise exceptions on unsupported
            features. "coerce" will attempt to coerce unsupported features to
            supported ones with warnings. Defaults to "coerce".
        fallback_type: A yads type to use as fallback when an unsupported
            PySpark type is encountered. Only used when mode is "coerce".
            Must be either String or Binary, or None. Defaults to None.
        name: Fully-qualified spec name to assign.
        version: Spec version string.
        description: Optional human-readable description.

    Returns:
        A validated immutable `YadsSpec` instance.

    Example:
        ```python
        from pyspark.sql.types import StructType, StructField, LongType, StringType
        schema = StructType([
            StructField("id", LongType(), nullable=False),
            StructField("name", StringType(), nullable=True),
        ])
        spec = from_pyspark(schema, name="users", version=1)
        ```
    """
    from . import pyspark_loader  # type: ignore

    config = pyspark_loader.PySparkLoaderConfig(mode=mode, fallback_type=fallback_type)
    loader = cast(Any, pyspark_loader.PySparkLoader(config))
    return loader.load(schema, name=name, version=version, description=description)


def from_polars(
    schema: Any,
    *,
    mode: Literal["raise", "coerce"] = "coerce",
    fallback_type: YadsType | None = None,
    name: str,
    version: int,
    description: str | None = None,
) -> YadsSpec:
    """Load a spec from a `polars.Schema`.

    Args:
        schema: An instance of `polars.Schema`.
        mode: Loading mode. "raise" will raise exceptions on unsupported
            features. "coerce" will attempt to coerce unsupported features to
            supported ones with warnings. Defaults to "coerce".
        fallback_type: A yads type to use as fallback when an unsupported
            Polars type is encountered. Only used when mode is "coerce".
            Must be either String or Binary, or None. Defaults to None.
        name: Fully-qualified spec name to assign.
        version: Spec version string.
        description: Optional human-readable description.

    Returns:
        A validated immutable `YadsSpec` instance.

    Example:
        ```python
        import polars as pl
        schema = pl.Schema({"id": pl.Int64, "name": pl.Utf8})
        spec = from_polars(schema, name="users", version=1)
        ```
    """
    from . import polars_loader  # type: ignore

    config = polars_loader.PolarsLoaderConfig(mode=mode, fallback_type=fallback_type)
    loader = cast(Any, polars_loader.PolarsLoader(config))
    return loader.load(schema, name=name, version=version, description=description)


def from_postgresql(
    connection: Any,
    table_name: str,
    *,
    schema: str = "public",
    mode: Literal["raise", "coerce"] = "coerce",
    fallback_type: YadsType | None = None,
    name: str | None = None,
    version: int = 1,
    description: str | None = None,
) -> YadsSpec:
    """Load a spec from a PostgreSQL table.

    Queries PostgreSQL catalog tables (information_schema, pg_catalog) to
    extract complete table schema information including column types,
    constraints, defaults, and generated columns.

    Args:
        connection: A DBAPI-compatible PostgreSQL connection (e.g., psycopg2,
            psycopg, asyncpg in sync mode).
        table_name: Name of the table to load.
        schema: PostgreSQL schema name. Defaults to "public".
        mode: Loading mode. "raise" will raise exceptions on unsupported
            features. "coerce" will attempt to coerce unsupported features to
            supported ones with warnings. Defaults to "coerce".
        fallback_type: A yads type to use as fallback when an unsupported
            PostgreSQL type is encountered. Only used when mode is "coerce".
            Must be either String or Binary, or None. Defaults to None.
        name: Spec name to assign. Defaults to "{schema}.{table_name}".
        version: Spec version integer. Defaults to 1.
        description: Optional human-readable description.

    Returns:
        A validated immutable `YadsSpec` instance.

    Example:
        ```python
        import psycopg2
        conn = psycopg2.connect("postgresql://localhost/mydb")
        spec = from_postgresql(conn, "users", schema="public")
        ```
    """
    from .sql.postgres_loader import PostgreSqlLoader
    from .sql.base import SqlLoaderConfig

    config = SqlLoaderConfig(mode=mode, fallback_type=cast(Any, fallback_type))
    loader = PostgreSqlLoader(connection, config)
    return loader.load(
        table_name,
        schema=schema,
        name=name,
        version=version,
        description=description,
    )


def from_sqlserver(
    connection: Any,
    table_name: str,
    *,
    schema: str = "dbo",
    mode: Literal["raise", "coerce"] = "coerce",
    fallback_type: YadsType | None = None,
    name: str | None = None,
    version: int = 1,
    description: str | None = None,
) -> YadsSpec:
    """Load a spec from a SQL Server table.

    Queries SQL Server catalog views (INFORMATION_SCHEMA, sys.*) to extract
    complete table schema information including column types, constraints,
    defaults, identity columns, and computed columns.

    Args:
        connection: A DBAPI-compatible SQL Server connection (e.g., pyodbc,
            pymssql).
        table_name: Name of the table to load.
        schema: SQL Server schema name. Defaults to "dbo".
        mode: Loading mode. "raise" will raise exceptions on unsupported
            features. "coerce" will attempt to coerce unsupported features to
            supported ones with warnings. Defaults to "coerce".
        fallback_type: A yads type to use as fallback when an unsupported
            SQL Server type is encountered. Only used when mode is "coerce".
            Must be either String or Binary, or None. Defaults to None.
        name: Spec name to assign. Defaults to "{catalog}.{schema}.{table_name}".
        version: Spec version integer. Defaults to 1.
        description: Optional human-readable description.

    Returns:
        A validated immutable `YadsSpec` instance.

    Example:
        ```python
        import pyodbc
        conn = pyodbc.connect("DRIVER={ODBC Driver 18 for SQL Server};...")
        spec = from_sqlserver(conn, "users", schema="dbo")
        ```
    """
    from .sql.sqlserver_loader import SqlServerLoader
    from .sql.base import SqlLoaderConfig

    config = SqlLoaderConfig(mode=mode, fallback_type=cast(Any, fallback_type))
    loader = SqlServerLoader(connection, config)
    return loader.load(
        table_name,
        schema=schema,
        name=name,
        version=version,
        description=description,
    )
