import copy
import hashlib
import os
import re
from pathlib import Path
from typing import (
    TYPE_CHECKING,
    Any,
    Callable,
    Iterator,
    List,
    Literal,
    Optional,
    Tuple,
    Union,
    cast,
)

import numpy
import pandas
import pyarrow
import pyarrow.fs
from sqlalchemy.sql.compiler import RESERVED_WORDS

from ..catalog.orm import Node
from ..structures.core import Spec, StructureFamily
from ..structures.data_source import Asset, DataSource, Storage
from ..structures.table import TableStructure
from ..type_aliases import JSON
from ..utils import path_from_uri
from .array import ArrayAdapter
from .utils import init_adapter_from_catalog

if TYPE_CHECKING:
    import adbc_driver_manager.dbapi
DIALECTS = Literal["postgresql", "sqlite", "duckdb"]


class SQLAdapter:
    """SQLAdapter Class"""

    structure_family = StructureFamily.table

    def __init__(
        self,
        data_uri: str,
        structure: TableStructure,
        table_name: str,
        dataset_id: int,
        metadata: Optional[JSON] = None,
        specs: Optional[List[Spec]] = None,
    ) -> None:
        """
        Construct the SQLAdapter object.
        Parameters
        ----------
        data_uri : the uri of the database, starting either with "duckdb://" or "postgresql://"
        structure : the structure of the data; structure is not optional for sql database
        metadata : the optional metadata of the data.
        specs : the specs.
        """
        self.uri = data_uri

        self.conn = create_connection(self.uri)

        self._metadata = metadata or {}
        self._structure = structure
        self.specs = list(specs or [])
        self.table_name = table_name
        self.dataset_id = dataset_id

    def close(self) -> None:
        self.conn.close()

    def metadata(self) -> JSON:
        """The metadata representing the actual data.

        Returns
        -------
        The metadata representing the actual data.
        """
        return self._metadata

    @classmethod
    def init_storage(
        cls,
        storage: Storage,
        data_source: DataSource[TableStructure],
        path_parts: List[str],
    ) -> DataSource[TableStructure]:
        """
        Class to initialize the list of assets for given uri.

        Parameters
        ----------
        storage: Storage
        data_source : DataSource
        path_parts : List[str]
            Not used by this adapter

        Returns
        -------
        A modified copy of the data source
        """
        data_source = copy.deepcopy(data_source)  # Do not mutate caller input.

        # Create a table_name based on the hash of Arrow schema
        schema = data_source.structure.arrow_schema_decoded
        encoded = schema.serialize()
        default_table_name = "table_" + hashlib.md5(encoded).hexdigest()
        table_name = data_source.parameters.setdefault("table_name", default_table_name)
        check_table_name(table_name)

        data_uri = storage.get("sql")  # TODO scrub credential

        conn = create_connection(data_uri)
        dialect, _ = data_uri.split(":", 1)
        # Prefix columns with internal _dataset_id, _partition_id, ...
        schema = schema.insert(0, pyarrow.field("_partition_id", pyarrow.int16()))
        schema = schema.insert(0, pyarrow.field("_dataset_id", pyarrow.int32()))
        create_table_statement = arrow_schema_to_create_table(
            schema, table_name, cast(DIALECTS, dialect)
        )

        create_index_statement = (
            "CREATE INDEX IF NOT EXISTS dataset_and_partition_index "
            f"ON {table_name}(_dataset_id, _partition_id)"
        )

        with conn.cursor() as cursor:
            cursor.execute(create_table_statement)
        with conn.cursor() as cursor:
            cursor.execute(create_index_statement)
        # Just once, create a SEQUENCE (or the closest analogue in SQLite) to
        # provide unique dataset_id for each dataset in this database.
        # (If it exists, do nothing.)
        # Then obtain the next value in the SEQUENCE for the dataset we are
        # initializing here.
        if dialect == "sqlite":
            # Create single-row table with a counter, if it does not exist.
            with conn.cursor() as cursor:
                cursor.execute(
                    "CREATE TABLE IF NOT EXISTS _dataset_id_counter (value INTEGER NOT NULL)"
                )
            with conn.cursor() as cursor:
                cursor.execute(
                    "INSERT INTO _dataset_id_counter (value) "
                    "SELECT 0 WHERE NOT EXISTS (SELECT 1 FROM _dataset_id_counter)"
                )
            with conn.cursor() as cursor:
                # Increment the counter.
                cursor.execute(
                    "UPDATE _dataset_id_counter SET value = value + 1 "
                    "RETURNING value"
                )
                (dataset_id,) = cursor.fetchone()
        else:
            with conn.cursor() as cursor:
                cursor.execute("CREATE SEQUENCE IF NOT EXISTS _dataset_id_counter")
            with conn.cursor() as cursor:
                cursor.execute("SELECT nextval('_dataset_id_counter')")
                (dataset_id,) = cursor.fetchone()
        data_source.parameters["dataset_id"] = dataset_id
        conn.commit()

        data_source.assets.append(
            Asset(
                data_uri=data_uri,
                is_directory=False,
                parameter="data_uri",
                num=None,
            )
        )
        conn.close()
        return data_source

    @classmethod
    def from_catalog(
        cls,
        data_source: DataSource[TableStructure],
        node: Node,
        /,
        **kwargs: Optional[Any],
    ) -> "SQLAdapter":
        return init_adapter_from_catalog(cls, data_source, node, **kwargs)  # type: ignore

    def structure(self) -> TableStructure:
        """
        The structure of the actual data.

        Returns
        -------
        The structure of the data.
        """
        return self._structure

    def get(self, key: str) -> Union[ArrayAdapter, None]:
        """Get the data for a specific key

        Parameters
        ----------
        key : a string to indicate which column to be retrieved

        Returns
        -------
        The column for the associated key.
        """
        if key not in self.structure().columns:
            return None
        return self[key]

    def __getitem__(self, key: str) -> ArrayAdapter:
        """Get the data for a specific key.

        Parameters
        ----------
        key : a string to indicate which column to be retrieved

        Returns
        -------
        The column for the associated key.
        """

        # Must compute to determine shape.
        return ArrayAdapter.from_array(self.read([key])[key].values)

    def items(self) -> Iterator[Tuple[str, ArrayAdapter]]:
        """Iterate over the SQLAdapter data.

        Returns
        -------
        An iterator for the data in the associated database.
        """
        yield from ((key, self[key]) for key in self._structure.columns)

    def append_partition(
        self,
        data: Union[
            List[pyarrow.record_batch],
            pyarrow.record_batch,
            pandas.DataFrame,
            pyarrow.Table,
        ],
        partition: int,
    ) -> None:
        """Write the data as arrow format

        Parameters
        ----------
        data : data to append to the database. Can be a record_batch, a list of record batches, pyarrow table, or a
            pandas dataframe.
        partition : the partition index to write.
        """
        # Convert the data to pyarrow table
        if isinstance(data, pyarrow.Table):
            table = data
        elif isinstance(data, pandas.DataFrame):
            table = pyarrow.Table.from_pandas(data)
        else:
            if not isinstance(data, list):
                batches = [data]
            else:
                batches = data
            table = pyarrow.Table.from_batches(batches)

        # Prepend columns for internal dataset_id and partition number.
        dataset_id_column = self.dataset_id * numpy.ones(len(table), dtype=numpy.int32)
        partition_id_column = partition * numpy.ones(len(table), dtype=numpy.int16)
        table = table.add_column(
            0, pyarrow.field("_partition_id", pyarrow.int16()), [partition_id_column]
        )
        table = table.add_column(
            0, pyarrow.field("_dataset_id", pyarrow.int32()), [dataset_id_column]
        )
        with self.conn.cursor() as cursor:
            cursor.adbc_ingest(self.table_name, table, mode="append")
        self.conn.commit()

    def read(self, fields: Optional[List[str]] = None) -> pandas.DataFrame:
        """
        The concatenated data from given set of partitions as pyarrow table.
        Parameters
        ----------
        fields: optional string to return the data in the specified field.
        Returns
        -------
        Returns the concatenated pyarrow table as pandas dataframe.
        """

        query = (
            f"SELECT * FROM {self.table_name} "
            f"WHERE _dataset_id={self.dataset_id} ORDER BY _partition_id"
        )
        with self.conn.cursor() as cursor:
            cursor.execute(query)
            data = cursor.fetch_arrow_table()
        self.conn.commit()

        # The database may have stored this in a coarser type, such as
        # storing uint8 data as int16. Cast it to the original type.
        data = data.drop_columns(["_partition_id", "_dataset_id"])
        schema = self.structure().arrow_schema_decoded
        data.cast(schema)

        table = data.to_pandas()
        if fields is not None:
            table = table[fields]
        return table

    def read_partition(
        self, partition: int, fields: Optional[List[str]] = None
    ) -> pandas.DataFrame:
        """
        Read a batch of data from a given partition.

        Parameters
        ----------
        partition : int
        fields : Optional[List[str]]
            Optional list of field names to select. By default return all.

        Returns
        -------
        Returns the concatenated pyarrow table as pandas dataframe.
        """
        query = (
            f"SELECT * FROM {self.table_name} "
            f"WHERE _dataset_id={self.dataset_id} AND _partition_id={int(partition)}"
        )
        with self.conn.cursor() as cursor:
            cursor.execute(query)
            data = cursor.fetch_arrow_table()
        self.conn.commit()

        # The database may have stored this in a coarser type, such as
        # storing uint8 data as int16. Cast it to the original type.
        data = data.drop_columns(["_partition_id", "_dataset_id"])
        schema = self.structure().arrow_schema_decoded
        data.cast(schema)

        table = data.to_pandas()
        if fields is not None:
            table = table[fields]
        return table


def create_connection(
    uri: str,
) -> "adbc_driver_manager.dbapi.Connection":
    """
    Function to create an adbc connection of type duckdb , sqlite or postgresql.
    Parameters
    ----------
    uri : the uri which is used to create a connection
    Returns
    -------
    Returns a connection of type duckdb , sqlite or postgresql.
    """
    if uri.startswith("duckdb:"):
        import adbc_driver_duckdb.dbapi

        filepath = _ensure_writable_location(uri)
        conn = adbc_driver_duckdb.dbapi.connect(str(filepath))
    elif uri.startswith("sqlite:"):
        import adbc_driver_sqlite.dbapi

        filepath = _ensure_writable_location(uri)
        conn = adbc_driver_sqlite.dbapi.connect(str(filepath))
    elif uri.startswith("postgresql:"):
        import adbc_driver_postgresql.dbapi

        conn = adbc_driver_postgresql.dbapi.connect(uri)
    else:
        raise ValueError(
            "The database uri must start with `duckdb:`, `sqlite:`, or `postgresql:`"
        )
    return conn


def _ensure_writable_location(uri: str) -> Path:
    "Ensure path is writable to avoid a confusing error message from driver."
    filepath = path_from_uri(uri)
    directory = Path(filepath).parent
    if directory.exists():
        if not os.access(directory, os.X_OK | os.W_OK):
            raise ValueError(
                f"The directory {directory} exists but is not writable and executable."
            )
        if Path(filepath).is_file() and (not os.access(filepath, os.W_OK)):
            raise ValueError(f"The path {filepath} exists but is not writable.")
    else:
        raise ValueError(f"The directory {directory} does not exist.")
    return filepath


# Mapping between Arrow types and PostgreSQL column type name.
ARROW_TO_PG_TYPES: dict[pyarrow.Field, str] = {
    # Boolean
    pyarrow.bool_(): "BOOLEAN",
    # Integers
    pyarrow.int8(): "SMALLINT",  # no native int8, use int16
    pyarrow.int16(): "SMALLINT",
    pyarrow.int32(): "INTEGER",
    pyarrow.int64(): "BIGINT",
    # Since PG has not native unsigned int,
    # use signed int one size up.
    pyarrow.uint8(): "SMALLINT",
    pyarrow.uint16(): "INTEGER",
    pyarrow.uint32(): "BIGINT",
    # pyarrow.uint64() not supported
    # Floating Point
    # pyarrow.float16(): "REAL",
    pyarrow.float32(): "REAL",
    pyarrow.float64(): "DOUBLE PRECISION",
    # String Types
    pyarrow.string(): "TEXT",
    pyarrow.large_string(): "TEXT",
    # TODO Consider adding support for these types, with testing.
    # # Binary Types
    # pyarrow.binary(): "bytea",
    # pyarrow.large_binary(): "bytea",
    # # Date/Time Types
    # pyarrow.date32(): "date",
    # pyarrow.date64(): "date",
    # pyarrow.time32("s"): "time",
    # pyarrow.time64("us"): "time",
    # pyarrow.timestamp(
    #     "us"
    # ): "timestamp",  # Note: becomes timestamptz if timezone specified
    # # Duration/Interval
    # pyarrow.duration("us"): "interval",
    # # Decimal Types
    # pyarrow.decimal128(precision=38, scale=9): "numeric",
    # pyarrow.decimal256(precision=76, scale=18): "numeric",
}


def arrow_field_to_pg_type(field: Union[pyarrow.Field, pyarrow.DataType]) -> str:
    """Get the PostgreSQL type name for a given PyArrow field.

    Parameters
    ----------
    field : pyarrow.Field
        The PyArrow field containing type and metadata information

    Returns
    -------
    str
        The corresponding PostgreSQL type name

    Raises
    ------
    ValueError
        If the field's type is not supported

    Examples
    --------
    >>> import pyarrow as pa
    >>> field = pyarrow.field('timestamp_col', pyarrow.timestamp('us', tz='UTC'))
    >>> get_pg_type(field)
    'timestamptz'
    >>> field = pyarrow.field('int_list', pyarrow.list_(pyarrow.int32()))
    >>> get_pg_type(field)
    'int4 ARRAY'
    >>> field = pyarrow.field('dict_col', pyarrow.dictionary(pyarrow.int8(), pyarrow.string()))
    >>> get_pg_type(field)
    'text'
    """

    def _resolve_type(
        arrow_type: Union[pyarrow.Field, pyarrow.DataType]
    ) -> Union[pyarrow.Field, pyarrow.DataType, str]:
        """Internal helper to resolve type, handling nested cases."""
        # Handle list types
        if pyarrow.types.is_list(arrow_type):
            value_type = _resolve_type(arrow_type.value_type)
            return f"{value_type} ARRAY"

        # Handle fixed size list types
        if pyarrow.types.is_fixed_size_list(arrow_type):
            value_type = _resolve_type(arrow_type.value_type)
            return f"{value_type} ARRAY"

        # Handle large list types
        if pyarrow.types.is_large_list(arrow_type):
            value_type = _resolve_type(arrow_type.value_type)
            return f"{value_type} ARRAY"

        # TODO Consider adding support for these types, with testing.

        # # Handle dictionary types - use value type
        # if pyarrow.types.is_dictionary(arrow_type):
        #     return _resolve_type(arrow_type.value_type)

        # # Handle timestamp with timezone
        # if pyarrow.types.is_timestamp(arrow_type) and arrow_type.tz is not None:
        #     return "timestamptz"

        # Look up base type
        pg_type = ARROW_TO_PG_TYPES.get(arrow_type)
        if pg_type is None:
            raise ValueError(f"Unsupported PyArrow type: {arrow_type}")

        # TODO Consider adding support for these types, with testing.

        # # Special handling for time types with different units
        # if pyarrow.types.is_time(arrow_type):
        #     return "time"

        # # Special handling for timestamp types without timezone
        # if pyarrow.types.is_timestamp(arrow_type):
        #     return "timestamp"

        # # Special handling for duration/interval types with different units
        # if pyarrow.types.is_duration(arrow_type):
        #     return "interval"

        return pg_type

    return _resolve_type(field.type)


# Mapping between Arrow types and DuckDB column type names
ARROW_TO_DUCKDB_TYPES = {
    # Boolean
    pyarrow.bool_(): "BOOLEAN",
    # Integers
    pyarrow.int8(): "TINYINT",
    pyarrow.uint8(): "UTINYINT",
    pyarrow.int16(): "SMALLINT",
    pyarrow.uint16(): "USMALLINT",
    pyarrow.int32(): "INTEGER",
    pyarrow.uint32(): "UINTEGER",
    pyarrow.int64(): "BIGINT",
    pyarrow.uint64(): "UBIGINT",
    # Floating point
    # pyarrow.float16(): "REAL",  # Note: gets converted to float32 internally
    pyarrow.float32(): "REAL",
    pyarrow.float64(): "DOUBLE",
    # Decimal
    pyarrow.decimal128(precision=38, scale=9): "DECIMAL",
    pyarrow.decimal256(precision=76, scale=38): "DECIMAL",
    # String types
    pyarrow.string(): "VARCHAR",
    pyarrow.large_string(): "VARCHAR",
    # TODO Consider adding support for these types, with testing.
    # # Binary
    # pyarrow.binary(): 'BLOB',
    # pyarrow.large_binary(): 'BLOB',
    # # Temporal types
    # pyarrow.date32(): 'DATE',
    # pyarrow.date64(): 'DATE',
    # pyarrow.time32('s'): 'TIME',
    # pyarrow.time32('ms'): 'TIME',
    # pyarrow.time64('us'): 'TIME',
    # pyarrow.time64('ns'): 'TIME',
    # pyarrow.timestamp('s'): 'TIMESTAMP',
    # pyarrow.timestamp('ms'): 'TIMESTAMP',
    # pyarrow.timestamp('us'): 'TIMESTAMP',
    # pyarrow.timestamp('ns'): 'TIMESTAMP',
    # pyarrow.timestamp('us', tz='UTC'): 'TIMESTAMP WITH TIME ZONE',
    # # Interval/Duration
    # pyarrow.duration('s'): 'INTERVAL',
    # pyarrow.duration('ms'): 'INTERVAL',
    # pyarrow.duration('us'): 'INTERVAL',
    # pyarrow.duration('ns'): 'INTERVAL',
}


def arrow_field_to_duckdb_type(field: Union[pyarrow.Field, pyarrow.DataType]) -> str:
    """Get the DuckDB type name for a given PyArrow field.

    Parameters
    ----------
    field : Union[pyarrow.Field, pyarrow.DataType]
        The PyArrow field or type to convert

    Returns
    -------
    str
        The corresponding DuckDB type name

    Raises
    ------
    ValueError
        If the field's type is not supported or cannot be mapped

    Examples
    --------
    >>> import pyarrow as pa
    >>> get_duckdb_type(pyarrow.int32())
    'INTEGER'
    >>> get_duckdb_type(pyarrow.timestamp('us', tz='UTC'))
    'TIMESTAMP WITH TIME ZONE'
    >>> get_duckdb_type(pyarrow.list_(pyarrow.int32()))
    'INTEGER[]'
    >>> struct_type = pyarrow.struct([('x', pyarrow.int32()), ('y', pyarrow.string())])
    >>> get_duckdb_type(struct_type)
    'STRUCT(x INTEGER, y VARCHAR)'
    """

    def _resolve_type(
        arrow_type: Union[pyarrow.Field, pyarrow.DataType]
    ) -> Union[pyarrow.Field, pyarrow.DataType, str]:
        """Internal helper to resolve type, handling nested cases."""
        # Handle decimal types with custom precision/scale
        if pyarrow.types.is_decimal(arrow_type):
            return f"DECIMAL({arrow_type.precision}, {arrow_type.scale})"

        # Handle list types (including large lists and fixed size lists)
        if (
            pyarrow.types.is_list(arrow_type)
            or pyarrow.types.is_large_list(arrow_type)
            or pyarrow.types.is_fixed_size_list(arrow_type)
        ):
            value_type = _resolve_type(arrow_type.value_type)
            return f"{value_type}[]"

        # TODO Consider adding support for these types, with testing.

        # # Handle fixed size binary
        # if pyarrow.types.is_fixed_size_binary(arrow_type):
        #     return f'BLOB({arrow_type.byte_width})'

        # # Handle dictionary types - use value type
        # if pyarrow.types.is_dictionary(arrow_type):
        #     return _resolve_type(arrow_type.value_type)

        # # Handle timestamp with timezone
        # if pyarrow.types.is_timestamp(arrow_type):
        #     if arrow_type.tz is not None:
        #         return 'TIMESTAMP WITH TIME ZONE'
        #     return 'TIMESTAMP'

        # # Handle struct types
        # if pyarrow.types.is_struct(arrow_type):
        #     fields = []
        #     for field in arrow_type:
        #         field_type = _resolve_type(field.type)
        #         fields.append(f'{field.name} {field_type}')
        #     return f'STRUCT({", ".join(fields)})'

        # # Handle map types
        # if pyarrow.types.is_map(arrow_type):
        #     key_type = _resolve_type(arrow_type.key_type)
        #     item_type = _resolve_type(arrow_type.item_type)
        #     return f'MAP({key_type}, {item_type})'

        # Look up base type
        duckdb_type = ARROW_TO_DUCKDB_TYPES.get(arrow_type)
        if duckdb_type is None:
            raise ValueError(f"Unsupported PyArrow type: {arrow_type}")
        return duckdb_type

    arrow_type = field.type if isinstance(field, pyarrow.Field) else field
    return _resolve_type(arrow_type)


ARROW_TO_SQLITE_TYPES: dict[pyarrow.Field, str] = {
    # Boolean - stored as INTEGER
    pyarrow.bool_(): "INTEGER",
    # Integers - all stored as INTEGER
    pyarrow.int8(): "INTEGER",
    pyarrow.uint8(): "INTEGER",
    pyarrow.int16(): "INTEGER",
    pyarrow.uint16(): "INTEGER",
    pyarrow.int32(): "INTEGER",
    pyarrow.uint32(): "INTEGER",
    pyarrow.int64(): "INTEGER",
    # pyarrow.uint64() not supported, may exceed SQLite INTEGER range
    # Floating point - stored as REAL
    # pyarrow.float16(): "REAL",
    pyarrow.float32(): "REAL",
    pyarrow.float64(): "REAL",
    # Decimal - stored as TEXT to preserve precision
    pyarrow.decimal128(precision=38, scale=9): "TEXT",
    pyarrow.decimal256(precision=76, scale=38): "TEXT",
    # String types - stored as TEXT
    pyarrow.string(): "TEXT",
    pyarrow.large_string(): "TEXT",
    # TODO Consider adding support for these types, with testing.
    # # Binary - stored as BLOB
    # pyarrow.binary(): 'BLOB',
    # pyarrow.large_binary(): 'BLOB',
    # pyarrow.fixed_size_binary_type(32): 'BLOB',
    # # Temporal types - stored as TEXT or INTEGER
    # pyarrow.date32(): 'TEXT',  # ISO8601 date string
    # pyarrow.date64(): 'TEXT',  # ISO8601 date string
    # pyarrow.time32('s'): 'TEXT',  # ISO8601 time string
    # pyarrow.time64('us'): 'TEXT',  # ISO8601 time string
    # pyarrow.timestamp('s'): 'INTEGER',  # Unix timestamp
    # pyarrow.timestamp('ms'): 'INTEGER',  # Unix timestamp
    # pyarrow.timestamp('us'): 'INTEGER',  # Unix timestamp
    # pyarrow.timestamp('ns'): 'INTEGER',  # Unix timestamp
    # pyarrow.timestamp('us', tz='UTC'): 'TEXT',  # ISO8601 timestamp with TZ
    # # Duration/Interval - stored as INTEGER (microseconds) or TEXT
    # pyarrow.duration('s'): 'INTEGER',
    # pyarrow.duration('ms'): 'INTEGER',
    # pyarrow.duration('us'): 'INTEGER',
    # pyarrow.duration('ns'): 'INTEGER'
}


def arrow_field_to_sqlite_type(field: Union[pyarrow.Field, pyarrow.DataType]) -> str:
    """Get the SQLite type name for a given PyArrow field.

    Parameters
    ----------
    field : Union[pyarrow.Field, pyarrow.DataType]
        The PyArrow field or type to convert

    Returns
    -------
    str
        The corresponding SQLite type name

    Raises
    ------
    ValueError
        If the field's type is not supported or cannot be mapped

    Examples
    --------
    >>> import pyarrow as pa
    >>> get_sqlite_type(pyarrow.int32())
    'INTEGER'
    >>> get_sqlite_type(pyarrow.string())
    'TEXT'
    >>> get_sqlite_type(pyarrow.timestamp('us', tz='UTC'))
    'TEXT'
    >>> get_sqlite_type(pyarrow.list_(pyarrow.int32()))
    'TEXT'  # JSON encoded array
    """

    def _resolve_type(arrow_type: pyarrow.DataType) -> str:
        # TODO Consider adding support for these, with tests.

        # Handle dictionary types - use value type
        # if pyarrow.types.is_dictionary(arrow_type):
        #     return _resolve_type(arrow_type.value_type)

        # Handle timestamp with timezone - store as TEXT
        # if pyarrow.types.is_timestamp(arrow_type) and arrow_type.tz is not None:
        #     return "TEXT"

        # Handle nested types (lists, structs, maps) - store as JSON TEXT
        # if (
        #     pyarrow.types.is_list(arrow_type)
        #     or pyarrow.types.is_struct(arrow_type)
        #     or pyarrow.types.is_map(arrow_type)
        #     or pyarrow.types.is_fixed_size_list(arrow_type)
        #     or pyarrow.types.is_large_list(arrow_type)
        # ):
        #     return "TEXT"  # JSON encoded

        # Look up base type
        sqlite_type = ARROW_TO_SQLITE_TYPES.get(arrow_type)
        if sqlite_type is None:
            raise ValueError(f"Unsupported PyArrow type: {arrow_type}")
        return sqlite_type

    arrow_type = field.type if isinstance(field, pyarrow.Field) else field
    return _resolve_type(arrow_type)


DIALECT_TO_TYPE_CONVERTER: dict[
    DIALECTS, Callable[[Union[pyarrow.Field, pyarrow.DataType]], str]
] = {
    "duckdb": arrow_field_to_duckdb_type,
    "sqlite": arrow_field_to_sqlite_type,
    "postgresql": arrow_field_to_pg_type,
}


def arrow_schema_to_column_defns(
    schema: pyarrow.Schema, dialect: DIALECTS
) -> dict[str, str]:
    """
    Given Arrow schema, return mapping of column names to type definitions.

    Example output: {'x': 'INTEGER NOT NULL'}
    """
    columns = {}
    converter = DIALECT_TO_TYPE_CONVERTER[dialect]
    for field in schema:
        sql_type = converter(field)
        nullable = "NULL" if field.nullable else "NOT NULL"
        columns[field.name] = f"{sql_type} {nullable}"
    return columns


def arrow_schema_to_create_table(
    schema: pyarrow.Schema, table_name: str, dialect: DIALECTS
) -> str:
    columns = arrow_schema_to_column_defns(schema, dialect)
    # Construct the CREATE TABLE statement
    create_statement = (
        f"""
    CREATE TABLE IF NOT EXISTS {table_name} (
        """
        + ",\n        ".join(f"{name} {type_}" for name, type_ in columns.items())
        + """)
    """
    )

    return create_statement


TABLE_NAME_PATTERN = re.compile(r"^[a-zA-Z][a-zA-Z0-9_]*$")


def check_table_name(table_name: str) -> None:
    if len(table_name) > 63:
        raise ValueError("Table name is too long, max character number is 63!")

    if TABLE_NAME_PATTERN.match(table_name) is None:
        raise ValueError("Illegal table name!")

    if table_name.lower() in RESERVED_WORDS:
        raise ValueError("Reserved SQL keywords are not allowed in the table name!")
