import json
import logging
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

# 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 = 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 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):
        self.connection = connect_to_db(
            self.database_type, self.config, self.using_connection_string
        )
        return self.connection

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

    def exec_with_reconnect(self, sql):
        reconnect_count = 0
        last_error = None
        while reconnect_count < 10:
            try:
                if self._is_connection_closed():
                    self._open_connection()
                return run_query_by_db(self.database_type, sql, self.connection)
            except Exception as err:
                last_error = err
                reconnect_count += 1
                self._close_connection()
            except Exception:
                self._close_connection()
                raise
        diag = getattr(last_error, "diag", None)
        position = getattr(diag, "statement_position", None)
        raise PgQueryError(last_error or "Failed to execute query", sql, position)

    def exec(self, sql):
        try:
            return self.exec_with_reconnect(sql)
        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", []),
        }
