import datetime
from decimal import Decimal
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type

import boto3
from pydantic import BaseModel

from .pydataapi import DataAPI

apilevel: str = '2.0'
threadsafety: int = 2
paramstyle: str = 'named'

# https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html
"""

JDBC Data Type                                        | Data API Data Type
INTEGER, TINYINT, SMALLINT, BIGINT                    | LONG
FLOAT, REAL, DOUBLE                                   | DOUBLE
DECIMAL                                               | STRING
BOOLEAN, BIT                                          | BOOLEAN
BLOB, BINARY, LONGVARBINARY, VARBINARY                | BLOB
CLOB                                                  | STRING
Other types (including types related to date and time)| STRING
"""

# https://docs.oracle.com/javase/8/docs/api/constant-values.html#java.sql.Types
JDBC_TYPES: Dict[int, Type[Any]] = {
    # 2003: list # ARRAY  2003
    -5: int,  # BIGINT -5
    # BINARY  -2
    -7: bytes,  # BIT  -7
    2004: bytes,  # BLOB  2004
    16: bool,  # BOOLEAN  16
    1: str,  # CHAR  1
    2005: bytes,  # CLOB  2005
    # DATALINK  70
    91: datetime.date,  # DATE	91
    3: Decimal,  # DECIMAL	3
    # DISTINCT	2001
    8: float,  # DOUBLE	8
    6: float,  # FLOAT	6
    4: int,  # INTEGER	4
    # JAVA_OBJECT	2000
    # LONGNVARCHAR	-16
    # LONGVARBINARY	-4
    # LONGVARCHAR	-1
    # NCHAR	-15
    # NCLOB	2011
    # NULL	0
    # NUMERIC	2
    # NVARCHAR	-9
    # OTHER	1111
    # REAL	7
    # REF	2006
    # REF_CURSOR	2012
    # ROWID	-8
    5: int,  # SMALLINT	5
    # SQLXML	2009
    # STRUCT	2002
    92: datetime.time,  # TIME	92
    # TIME_WITH_TIMEZONE	2013
    93: datetime.datetime,  # TIMESTAMP	93
    # TIMESTAMP_WITH_TIMEZONE	2014
    -6: int,  # TINYINT	-6
    -3: bytes,  # VARBINARY	-3
    12: str,  # VARCHAR	12
}


def get_description(column_metadata: List[Dict[str, Any]]) -> Tuple[Any, ...]:
    return tuple(
        (
            meta['label'],  # name
            JDBC_TYPES.get(meta['type']),  # type_code,
            0,  # display_size,
            0,  # internal_size,
            meta['precision'],  # precision,
            meta['scale'],  # scale,
            meta['nullable'],
        )
        for meta in column_metadata
    )


class Error(Exception):
    pass


class ConnectArgs(BaseModel):
    secret_arn: str
    resource_arn: Optional[str]
    resource_name: Optional[str]
    database: Optional[str] = None
    transaction_id: Optional[str] = None
    client: Optional[Any] = None
    rollback_exception: Optional[Type[Exception]] = None
    rds_client: Optional[Any] = None
    auto_transaction: Optional[bool] = True


class Connection:
    paramstyle = paramstyle
    Error = Error

    def __init__(self, **kwargs: Any) -> None:
        connect_args = ConnectArgs.parse_obj(kwargs)
        self._data_api = DataAPI(
            secret_arn=connect_args.secret_arn,
            resource_arn=connect_args.resource_arn,
            resource_name=connect_args.resource_name,
            database=connect_args.database,
            transaction_id=connect_args.transaction_id,
            client=connect_args.client,
            rollback_exception=connect_args.rollback_exception,
            rds_client=connect_args.rds_client,
            auto_transaction=connect_args.auto_transaction,
        )

        self.closed = False
        self.cursors: List[Cursor] = []

    def close(self) -> None:
        self.closed = True

    def commit(self) -> None:
        if self._data_api.transaction_id:
            self._data_api.commit()
            self._data_api._transaction_id = None

    def rollback(self) -> None:
        if self._data_api.transaction_id:
            self._data_api.rollback()
            self._data_api._transaction_id = None

    def cursor(self) -> 'Cursor':
        if not self._data_api.transaction_id and self._data_api.auto_transaction:
            self._data_api.begin()
        cursor = Cursor(self._data_api)
        self.cursors.append(cursor)

        return cursor

    @classmethod
    def connect(cls, **kwargs: Any) -> 'Connection':
        return cls(**kwargs)

    def execute(self, operation: Any, parameters: Any = None) -> 'Cursor':
        return self.cursor().execute(operation, parameters)

    def __enter__(self) -> 'Connection':
        self._data_api.begin()
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        if exc_type is None:
            self.commit()
        else:
            if self._data_api.rollback_exception:
                if issubclass(exc_type, self._data_api.rollback_exception):
                    self.rollback()
                else:
                    self.commit()
            else:
                self.rollback()

    @staticmethod
    def Binary(x: Any) -> Any:
        return x  # pragma: no cover


class Cursor:
    def __init__(self, data_api: DataAPI) -> None:
        self._data_api: DataAPI = data_api
        self.arraysize = 1

        self.closed = False

        self.description: Optional[List[Any]] = None

        self._rows: List[List[Any]] = []
        self._rowcount: int = -1
        self._lastrowid: Optional[int] = None

    @property
    def rowcount(self) -> int:
        return self._rowcount

    @property
    def lastrowid(self) -> Optional[int]:
        return self._lastrowid

    def close(self) -> None:
        self.closed = True

    def execute(
        self, operation: Any, parameters: Optional[Dict[str, Any]] = None
    ) -> 'Cursor':
        self.description = None
        result = self._data_api.execute(operation, parameters)
        self.description = get_description(  # type: ignore
            getattr(result, '_column_metadata')
        )
        rows: List[List[Any]] = getattr(result, '_rows')
        self._rows = rows
        self._rowcount = len(rows) or result.number_of_records_updated
        self._lastrowid = result.generated_fields_first  # type: ignore
        return self

    def executemany(
        self, operation: Any, seq_of_parameters: Optional[List[Dict[str, Any]]] = None
    ) -> 'Cursor':
        self.description = None
        results = self._data_api.batch_execute(operation, seq_of_parameters)
        self._rows = [result.generated_fields for result in results]
        self._rowcount = len(self._rows)
        self.description = []
        self._lastrowid = (
            results[-1].generated_fields_first if results else None  # type: ignore
        )
        return self

    def fetchone(self) -> Optional[List[Any]]:
        try:
            return self._rows.pop(0)
        except IndexError:
            return None

    def fetchmany(self, size: Optional[int] = None) -> List[List[Any]]:
        size = size or self.arraysize
        result, self._rows = self._rows[:size], self._rows[size:]
        return result

    def fetchall(self) -> List[List[Any]]:
        rows = self._rows
        self._rows = []
        return rows

    def setinputsizes(self, sizes: Any) -> None:  # pragma: no cover
        pass

    def setoutputsizes(self, sizes: Any) -> None:  # pragma: no cover
        pass

    def __iter__(self) -> Iterator[List[Any]]:
        return iter(self._rows)


def connect(
    secret_arn: str,
    resource_arn: Optional[str] = None,
    resource_name: Optional[str] = None,
    database: Optional[str] = None,
    transaction_id: Optional[str] = None,
    client: Optional[boto3.session.Session.client] = None,
    rollback_exception: Optional[Type[Exception]] = None,
    rds_client: Optional[boto3.session.Session.client] = None,
    **kwargs: Any
) -> Connection:
    return Connection(
        secret_arn=secret_arn,
        resource_arn=resource_arn,
        resource_name=resource_name,
        database=database,
        transaction_id=transaction_id,
        client=client,
        rollback_exception=rollback_exception,
        rds_client=rds_client,
        **kwargs
    )
