import json
import logging
import asyncio
from datetime import date, datetime, time
from decimal import Decimal
from uuid import UUID
import redis
from quillsql.error import PgQueryError
from quillsql.db.db_helper import (
    connect_to_db,
    run_query_by_db,
    disconnect_from_db,
    create_async_connection,
    run_query_by_db_async,
    disconnect_from_db_async,
)

# The TTL for new cache entries (default: 1h)
DEFAULT_CACHE_TTL = 24 * 60 * 60
DEFAULT_REDIS_TIMEOUT_SECONDS = 3
logger = logging.getLogger(__name__)

def _cache_json_default(value):
    # Match JS JSON behavior for common DB scalar types.
    if isinstance(value, Decimal):
        return str(value)
    if isinstance(value, (datetime, date, time)):
        return value.isoformat()
    if isinstance(value, UUID):
        return str(value)
    if isinstance(value, (bytes, bytearray)):
        try:
            return value.decode("utf-8")
        except Exception:
            return value.hex()
    return str(value)


class CachedConnection:
    def __init__(self, database_type, config, cache_config, using_connection_string):
        self.database_type = database_type
        self.using_connection_string = using_connection_string
        self.config = config
        self.connection = None
        self.async_connection = None
        self._async_open_lock = asyncio.Lock()
        if self._is_postgres():
            self.async_connection = create_async_connection(
                database_type, config, using_connection_string
            )
        else:
            self.connection = connect_to_db(database_type, config, using_connection_string)
        self.cache = self.get_cache(cache_config)
        self.ttl = cache_config and cache_config.get("ttl") or DEFAULT_CACHE_TTL

    def _is_postgres(self):
        return self.database_type and self.database_type.lower() in (
            "postgresql",
            "postgres",
        )

    def get_cache(self, cache_config):
        cache_type = cache_config and cache_config.get("cache_type")
        if cache_type not in ("redis", "rediss"):
            return None
        try:
            timeout = cache_config.get(
                "timeout_seconds", DEFAULT_REDIS_TIMEOUT_SECONDS
            )
            client = redis.Redis(
                host=cache_config.get("host", "localhost"),
                port=cache_config.get("port", 6379),
                username=cache_config.get("username", "default"),
                password=cache_config.get("password"),
                socket_connect_timeout=timeout,
                socket_timeout=timeout,
                retry_on_timeout=True,
            )
            # Force auth/connect validation up-front.
            client.ping()
            return client
        except Exception as err:
            logger.warning(
                "Redis cache unavailable; continuing without cache: %s", str(err)
            )
        return None

    def _is_connection_closed(self):
        return self.connection is None or getattr(self.connection, "closed", True)

    def _open_connection(self):
        if self._is_postgres():
            raise RuntimeError(
                "Synchronous Postgres queries are not supported. "
                "Use async query methods."
            )
        self.connection = connect_to_db(
            self.database_type, self.config, self.using_connection_string
        )
        return self.connection

    def _is_async_connection_closed(self):
        return self.async_connection is None or getattr(
            self.async_connection, "closed", True
        )

    async def _open_connection_async(self):
        if self.async_connection is None:
            self.async_connection = create_async_connection(
                self.database_type, self.config, self.using_connection_string
            )
        if self._is_postgres():
            await self.async_connection.open(wait=False)
        return self.async_connection

    async def ensure_open(self):
        if not self._is_async_connection_closed():
            return self.async_connection
        async with self._async_open_lock:
            if self._is_async_connection_closed():
                await self._open_connection_async()
        return self.async_connection

    def _close_connection(self):
        if self.connection is None:
            return
        try:
            disconnect_from_db(self.database_type, self.connection)
        except Exception:
            pass
        finally:
            self.connection = None

    def close(self):
        self._close_connection()

    async def _close_connection_async(self):
        if self.async_connection is None:
            return
        try:
            await disconnect_from_db_async(self.database_type, self.async_connection)
        except Exception:
            pass
        finally:
            self.async_connection = None

    async def close_async(self):
        await self._close_connection_async()
        self._close_connection()

    def exec(self, sql):
        try:
            if self._is_connection_closed():
                self._open_connection()
            return run_query_by_db(self.database_type, sql, self.connection)
        except Exception as err:
            diag = getattr(err, "diag", None)
            position = getattr(diag, "statement_position", None)
            raise PgQueryError(err, sql, position)

    async def exec_async(self, sql):
        try:
            await self.ensure_open()
            return await run_query_by_db_async(
                self.database_type, sql, self.async_connection
            )
        except Exception as err:
            diag = getattr(err, "diag", None)
            position = getattr(diag, "statement_position", None)
            raise PgQueryError(err, sql, position)

    def query(self, sql, overwrite_cache=False):
        if not self.cache:
            return self.exec(sql)

        key = sql
        if not overwrite_cache:
            try:
                cached_result = self.cache.get(key)
            except Exception as err:
                logger.warning("Redis cache read failed; skipping cache: %s", str(err))
                cached_result = None
            if cached_result:
                try:
                    cached = json.loads(cached_result)
                except Exception as err:
                    logger.warning(
                        "Redis cached value parse failed; bypassing cache entry: %s",
                        str(err),
                    )
                    cached = None
                if isinstance(cached, dict):
                    return {
                        "rows": cached.get("rows", []),
                        "fields": cached.get("fields", []),
                    }
                # Backwards-compat: support legacy cache values containing only rows.
                if cached is not None:
                    return {"rows": cached, "fields": []}

        new_result = self.exec(sql)
        try:
            new_result_string = json.dumps(new_result, default=_cache_json_default)
            # Match Node query-cache behavior: no EX ttl in this path.
            self.cache.set(key, new_result_string)
        except Exception as err:
            logger.warning("Redis cache write failed; skipping cache: %s", str(err))
        return {
            "rows": new_result.get("rows", []),
            "fields": new_result.get("fields", []),
        }

    async def query_async(self, sql, overwrite_cache=False):
        await self.ensure_open()
        if not self.cache:
            return await self.exec_async(sql)

        key = sql
        if not overwrite_cache:
            try:
                cached_result = self.cache.get(key)
            except Exception as err:
                logger.warning("Redis cache read failed; skipping cache: %s", str(err))
                cached_result = None
            if cached_result:
                try:
                    cached = json.loads(cached_result)
                except Exception as err:
                    logger.warning(
                        "Redis cached value parse failed; bypassing cache entry: %s",
                        str(err),
                    )
                    cached = None
                if isinstance(cached, dict):
                    return {
                        "rows": cached.get("rows", []),
                        "fields": cached.get("fields", []),
                    }
                if cached is not None:
                    return {"rows": cached, "fields": []}

        new_result = await self.exec_async(sql)
        try:
            new_result_string = json.dumps(new_result, default=_cache_json_default)
            self.cache.set(key, new_result_string)
        except Exception as err:
            logger.warning("Redis cache write failed; skipping cache: %s", str(err))
        return {
            "rows": new_result.get("rows", []),
            "fields": new_result.get("fields", []),
        }
