from util.generic.string cimport TString
from util.generic.ptr cimport THolder, MakeHolder, TIntrusivePtr
from util.string.cast cimport FromString, ToString
from util.system.types cimport ui64, ui32, ui16
from cpython.ref cimport PyObject
from libcpp cimport bool
from libcpp.cast cimport dynamic_cast

import datetime
import enum
import logging
import json
import six

import concurrent.futures as futures

cdef extern from "<functional>" namespace "std" nogil:
    cdef cppclass function[T]:
        pass

cdef extern from "Python.h" nogil:
    cdef void PyEval_InitThreads()

cdef object _to_python_str(const TString& str):
    # python strings are different in py2 and py3
    # convert c-style strings `char*` as binary type to string type
    return six.ensure_str(str.c_str()[:str.length()])

cdef TString _from_python_str(str) nogil:
    # TString may be constracted from c-style strings `char*`
    # convert python string type to binary type
    # it casts to `char*` as is
    with gil:
        return six.ensure_binary(str)

cdef extern from "util/datetime/base.h" nogil:
    cdef cppclass TDuration:
        ui64 Seconds()
        @staticmethod
        TDuration MilliSeconds(ui64)
        @staticmethod
        TDuration Seconds(ui64)

cdef extern from "library/cpp/threading/future/future.h" namespace "NThreading" nogil:
    cdef cppclass TFuture[T]:
        T& GetValue()
        void Subscribe[F](F&& callback)

cdef extern from "library/cpp/tvmauth/type.h" namespace "NTvmAuth" nogil:
    ctypedef ui32 TTvmId

cdef extern from "library/cpp/json/writer/json_value.h" namespace "NJson" nogil:
    cdef cppclass TJsonValue:
        pass

cdef extern from "saas/util/json/json.h" namespace "NUtil" nogil:
    cdef TJsonValue JsonFromString(const TString& string)
    cdef TString JsonToString(const TJsonValue& value, bool validateUtf8)

cdef object _to_python_json(const TJsonValue& json_value):
    cdef TString serialized = JsonToString(json_value, False)
    return json.loads(_to_python_str(serialized))

cdef TJsonValue _from_python_json(json_value) nogil:
    cdef TString serialized
    with gil:
        serialized = _from_python_str(json.dumps(json_value))
    return JsonFromString(serialized)

cdef extern from "saas/protos/rtyserver.pb.h" namespace "NRTYServer" nogil:
    cdef cppclass TMessage:
        bool ParseFromString(const TString& str) except +

cdef extern from "saas/library/persqueue/writer/shards_info.h" namespace "NSaas" nogil:
    cdef cppclass TSearchMapInputSettings:
        TSearchMapInputSettings()

        TString Ctype
        ui16 DMPort
        TString DMHost

        ui16 StaticaPort
        TString StaticaHost
        TString StaticaQuery

        TString File

        TDuration UpdatePeriod

class KeyPrefixCheckType(enum.Enum):
    any = 1
    zero = 2
    non_zero = 3

cdef extern from "saas/library/check_message/check_message.h" namespace "NSaas" nogil:
    cdef enum EKeyPrefixCheckType:
        pass

    cdef cppclass TCheckMessageSettings:
        TCheckMessageSettings& SetDisabled(bool value) except +
        TCheckMessageSettings& SetCheckMaxSizeBytes(ui32 bytes) except +
        TCheckMessageSettings& SetCheckAttributes(bool value) except +
        TCheckMessageSettings& SetKeyPrefixCheckType(EKeyPrefixCheckType value) except +

        bool GetDisabled() const
        ui32 GetCheckMaxSizeBytes() const
        bool GetCheckAttributes() const
        EKeyPrefixCheckType GetKeyPrefixCheckType() const

cdef extern from "saas/library/check_message/check_message.h" namespace "NSaas::EKeyPrefixCheckType" nogil:
    cdef EKeyPrefixCheckType EKeyPrefixCheckType_any "NSaas::EKeyPrefixCheckType::any"
    cdef EKeyPrefixCheckType EKeyPrefixCheckType_zero "NSaas::EKeyPrefixCheckType::zero"
    cdef EKeyPrefixCheckType EKeyPrefixCheckType_non_zero "NSaas::EKeyPrefixCheckType::non_zero"

cdef EKeyPrefixCheckType _to_c_key_prefix_type(value):
    return FromString[EKeyPrefixCheckType](value.name)

cdef _to_py_key_prefix_type(EKeyPrefixCheckType value):
    return KeyPrefixCheckType[ToString(value)]


cdef extern from "library/cpp/logger/priority.h" nogil:
    cpdef enum ELogPriority:
        TLOG_EMERG       # = 0,
        TLOG_ALERT       # = 1,
        TLOG_CRIT        # = 2,
        TLOG_ERR         # = 3,
        TLOG_WARNING     # = 4,
        TLOG_NOTICE      # = 5,
        TLOG_INFO        # = 6,
        TLOG_DEBUG       # = 7,
        TLOG_RESOURCES   # = 8

cdef ELogPriority _from_python_log_level(int level):
    if level == logging.CRITICAL:
        return TLOG_CRIT
    elif level == logging.ERROR:
        return TLOG_ERR
    elif level == logging.WARNING:
        return TLOG_WARNING
    elif level == logging.INFO:
        return TLOG_INFO
    else:
        return TLOG_RESOURCES

cdef int _to_python_log_level(int level):
    if level == TLOG_EMERG or level == TLOG_ALERT or level == TLOG_CRIT:
        return logging.CRITICAL
    elif level == TLOG_ERR:
        return logging.ERROR
    elif level == TLOG_WARNING:
        return logging.WARNING
    elif level == TLOG_NOTICE or level == TLOG_INFO:
        return logging.INFO
    else:
        return logging.DEBUG

cdef extern from "saas/library/persqueue/writer/python/callbacks.h" namespace "NSaas" nogil:
    ctypedef void (*TCWriteCallback)(PyObject* future, const TFuture[TWriteResult]& cfuture)
    ctypedef function[void (const TFuture[TWriteResult]& response)] TWriteCallback
    cdef TWriteCallback MakeWriteCallback(TCWriteCallback callback, PyObject* future)

    ctypedef void (*TPyLoggerCallback)(PyObject*, const TString &msg, const TString& sourceId, const TString& sessionId, int level)

    cdef cppclass TPythonLogger:
        TPythonLogger(TPyLoggerCallback callback, PyObject* obj, int log_level)

cdef extern from "kikimr/persqueue/sdk/deprecated/cpp/v2/logger.h" namespace "NPersQueue" nogil:
    cdef cppclass ILogger:
        pass

cdef void _logger_callback_thunk(PyObject* obj, const TString &msg, const TString& source_id, const TString& session_id,
                                 int level) nogil:
    with gil:
        source = _to_python_str(source_id)
        session = _to_python_str(session_id)
        log_msg = datetime.datetime.now().isoformat()
        if source:
            log_msg = 'source_id={}'.format(source)

        if session:
            log_msg += 'session_id={} '.format(session)

        log_msg += _to_python_str(msg)
        (<object>obj).log(_to_python_log_level(level), log_msg)

def _repr_proxy_object(obj):
    return '{0}({1})'.format(obj.__class__, ', '.join('{0}={1}'.format(k, repr(v)) for k, v in obj.as_dict().items()))

cdef class SearchMapInputSettings:

    cdef TSearchMapInputSettings _cvalue

    def __repr__(self):
        return _repr_proxy_object(self)

    property ctype:
        def __get__(self):
            return self._cvalue.Ctype
        def __set__(self, value):
            self._cvalue.Ctype = _from_python_str(value)

    property dm_port:
        def __get__(self):
            return self._cvalue.DMPort
        def __set__(self, value):
            self._cvalue.DMPort = value

    property dm_host:
        def __get__(self):
            return self._cvalue.DMHost
        def __set__(self, value):
            self._cvalue.DMHost = _from_python_str(value)

    property statica_port:
        def __get__(self):
            return self._cvalue.StaticaPort
        def __set__(self, value):
            self._cvalue.StaticaPort = value

    property statica_host:
        def __get__(self):
            return self._cvalue.StaticaHost
        def __set__(self, value):
            self._cvalue.StaticaHost = _from_python_str(value)

    property statica_query:
        def __get__(self):
            return self._cvalue.StaticaQuery
        def __set__(self, value):
            self._cvalue.StaticaQuery = _from_python_str(value)

    property file:
        def __get__(self):
            return self._cvalue.File
        def __set__(self, value):
            self._cvalue.File = _from_python_str(value)

    property update_period_sec:
        def __get__(self):
            return self._cvalue.UpdatePeriod.Seconds()
        def __set__(self, ui32 value):
            self._cvalue.UpdatePeriod = TDuration.Seconds(value)

    def as_dict(self):
        return {
            'ctype': self.ctype,
            'dm_port': self.dm_port,
            'dm_host': self.dm_host,
            'statica_port': self.statica_port,
            'statica_host': self.statica_host,
            'statica_query': self.statica_query,
            'file': self.file,
            'update_period_sec': self.update_period_sec
        }

cdef class CheckMessageSettings:
    cdef TCheckMessageSettings _cvalue

    def __repr__(self):
        return _repr_proxy_object(self)

    property disabled:
        def __get__(self):
            return self._cvalue.GetDisabled()
        def __set__(self, bool value):
            self._cvalue.SetDisabled(value)

    property check_max_size_bytes:
        def __get__(self):
            return self._cvalue.GetCheckMaxSizeBytes()
        def __set__(self, ui32 bytes):
            self._cvalue.SetCheckMaxSizeBytes(bytes)

    property check_attributes:
        def __get__(self):
            return self._cvalue.GetCheckAttributes()
        def __set__(self, bool value):
            self._cvalue.SetCheckAttributes(value)

    property key_prefix_check_type:
        def __get__(self):
            return _to_py_key_prefix_type(self._cvalue.GetKeyPrefixCheckType())
        def __set__(self, value):
            self._cvalue.SetKeyPrefixCheckType(_to_c_key_prefix_type(value))


    def as_dict(self):
        return {
            'ctype': self.ctype,
            'dm_port': self.dm_port,
            'dm_host': self.dm_host,
            'statica_port': self.statica_port,
            'statica_host': self.statica_host,
            'statica_query': self.statica_query,
            'file': self.file,
            'update_period_sec': self.update_period_sec
        }

cdef extern from "saas/library/persqueue/writer/writer.h" namespace "NSaas::TPersQueueWriter" nogil:
    cdef cppclass TWriteResult:
        TJsonValue ToJson() except +

cdef extern from "saas/library/persqueue/telemetry/telemetry.h" namespace "NSaas" nogil:
    cdef const TDuration RECOMMENDED_TELEMETRY_INTERVAL

cdef extern from "saas/library/persqueue/writer/settings.h" namespace "NSaas" nogil:
    cdef cppclass TPersQueueWriterSettings:
        void SetLogger(TIntrusivePtr[ILogger]& logger) except +
        void SetServiceInfo(const TSearchMapInputSettings& searchMapSettings, const TString& service) except +
        void SetTvm(TTvmId srcClientId, TTvmId dstClientId, const TString& secret) except +
        void SetQloudTvm(TTvmId srcClientId, TTvmId dstClientId, ui32 port) except +
        void SetPersQueueSettings(const TString& server, const TString& directoryWithTopics) except +
        void SetCheckMessagesSettings(const TCheckMessageSettings& settings) except +
        void SetMaxAttempts(ui32 maxAttempts) except +
        void SetThreadsBlockingMode(bool blocking) except +
        void SetWriteThreadsCount(ui64 threadsCount) except +
        void SetMaxInFlightCount(ui64 maxInFlightCount) except +
        void SetConnectionTimeout(TDuration connectionTimeout) except +
        void SetSendTimeout(TDuration sendTimeout) except +
        void SetSourceIdPrefix(const TString& sourceIdPrefix) except +
        void SetNoWriteQueue(bool value) except +
        void SetThreadsName(const TString& name) except +
        void SetEnableTelemetry(const TDuration& interval) except +

cdef extern from "saas/library/persqueue/writer/writer.h" namespace "NSaas" nogil:
    cdef cppclass TPersQueueWriter:
        void Init(const TPersQueueWriterSettings& settings) except +
        TFuture[TWriteResult] Write(const TMessage& message) except +
        TFuture[TWriteResult] Write(const TJsonValue& document) except +

ctypedef ILogger* ILoggerPtr

cdef class Logger(object):
    cdef TIntrusivePtr[ILogger] _impl

    def __cinit__(self, logger, log_level=None):
        PyEval_InitThreads()
        if log_level is None:
            log_level = logger.getEffectiveLevel()
        self._impl.Reset(
            dynamic_cast[ILoggerPtr](
                new TPythonLogger(_logger_callback_thunk, <PyObject*>logger, _from_python_log_level(log_level))
            )
        )

    def __dealloc__(self):
        with nogil:
            self._impl.Drop()

cdef void _write_callback(PyObject* future, const TFuture[TWriteResult]& cfuture) with gil:
    json_value = _to_python_json(cfuture.GetValue().ToJson())
    (<object>future).set_result(json_value)

cdef class Settings:
    cdef TPersQueueWriterSettings _settings

    def set_logger(self, Logger logger):
        with nogil:
            self._settings.SetLogger(logger._impl)

    def set_service_info(self, SearchMapInputSettings search_map_settings, service):
        cdef TSearchMapInputSettings settings = search_map_settings._cvalue
        with nogil:
            self._settings.SetServiceInfo(settings, _from_python_str(service))

    def set_tvm(self, TTvmId src_client_id, TTvmId dst_client_id, secret):
        with nogil:
            self._settings.SetTvm(src_client_id, dst_client_id, _from_python_str(secret))

    def set_qloud_tvm(self, TTvmId src_client_id, TTvmId dst_client_id, ui32 port=1):
        with nogil:
            self._settings.SetQloudTvm(src_client_id, dst_client_id, port)

    def set_persqueue_settings(self, server, directory_with_topics):
        with nogil:
            self._settings.SetPersQueueSettings(
                _from_python_str(server),
                _from_python_str(directory_with_topics)
            )

    def set_check_messages_settings(self, CheckMessageSettings settings):
        with nogil:
            self._settings.SetCheckMessagesSettings(settings._cvalue)

    def set_max_attempts(self, ui32 max_attempts):
        with nogil:
            self._settings.SetMaxAttempts(max_attempts)

    def set_threads_blocking_mode(self, bool blocking):
        with nogil:
            self._settings.SetThreadsBlockingMode(blocking)

    def set_write_threads_count(self, ui64 write_threads_count):
        with nogil:
            self._settings.SetWriteThreadsCount(write_threads_count)

    def set_max_in_flight_count(self, ui64 max_in_flight_count):
        with nogil:
            self._settings.SetMaxInFlightCount(max_in_flight_count)

    def set_connection_timeout(self, ui32 timeout_ms):
        with nogil:
            self._settings.SetConnectionTimeout(TDuration.MilliSeconds(timeout_ms))

    def set_send_timeout(self, ui32 timeout_ms):
        with nogil:
            self._settings.SetSendTimeout(TDuration.MilliSeconds(timeout_ms))

    def set_source_id_prefix(self, source_id_prefix):
        with nogil:
            self._settings.SetSourceIdPrefix(_from_python_str(source_id_prefix))

    def set_no_write_queue(self, bool value):
        with nogil:
            self._settings.SetNoWriteQueue(value)

    def set_threads_name(self, name):
        with nogil:
            self._settings.SetThreadsName(_from_python_str(name))

    def set_enable_telemetry(self, interval_sec=None):
        cdef TDuration c_interval = RECOMMENDED_TELEMETRY_INTERVAL
        if interval_sec:
            c_interval = TDuration.Seconds(interval_sec)
        with nogil:
            self._settings.SetEnableTelemetry(c_interval)


cdef class Writer:
    cdef THolder[TPersQueueWriter] __writer

    cdef __convert_future(self, const TFuture[TWriteResult]& cfuture):
        future = futures.Future()
        cfuture.Subscribe(MakeWriteCallback(_write_callback, <PyObject*>future))
        return future

    def __cinit__(self):
        with nogil:
            self.__writer = MakeHolder[TPersQueueWriter]()

    def init(self, Settings settings):
        with nogil:
            self.__writer.Get().Init(settings._settings)

    def write_json(self, json_message):
        with nogil:
            message = _from_python_json(json_message)
            cfuture = self.__writer.Get().Write(message)
        return self.__convert_future(cfuture)

    def write_proto(self, proto_message):
        cdef TString serialized = proto_message.SerializeToString()
        cdef TMessage message
        cdef bool Y_PROTOBUF_SUPPRESS_NODISCARD
        Y_PROTOBUF_SUPPRESS_NODISCARD = message.ParseFromString(serialized)
        with nogil:
            cfuture = self.__writer.Get().Write(message)
        return self.__convert_future(cfuture)

    def __dealloc__(self):
        with nogil:
            self.__writer.Destroy()
