import threading, Queue
import whatap.agent.conf.configure as config
import whatap.agent.secure.security_master as secure
from whatap.io.data_outputx import DataOutputX
import whatap.net as net
from whatap.pack.pack import Pack as pack
from whatap.util.date_util import DateUtil as dateutil
import whatap.util.logging_util as logging_util
import whatap.net.tcpsession as tcpsession
import time

# G2/G3: data-gap visibility. Packs dropped here (session not open, cypher not
# ready yet, send failed) and a backing-up queue are otherwise invisible, so a
# server-side "Agent Data Timeout" cannot be tied to an agent-side cause. Count
# drops by reason and log both drops and backlog on a throttle to avoid spam.
_dropCounts = {}
_dropTotal = 0
_lastDropLog = 0.0
_lastBacklogLog = 0.0
_LOG_THROTTLE_SEC = 30

def _recordDrop(reason):
    global _dropTotal, _lastDropLog
    _dropCounts[reason] = _dropCounts.get(reason, 0) + 1
    _dropTotal += 1
    nowsec = time.time()
    if nowsec - _lastDropLog >= _LOG_THROTTLE_SEC:
        _lastDropLog = nowsec
        summary = ", ".join("%s=%d" % (k, v) for k, v in sorted(_dropCounts.items()))
        logging_util.error("[Sender] dropped %d packs (not sent to server) by reason: %s" % (_dropTotal, summary))

def _maybeLogBacklog(qsize):
    global _lastBacklogLog
    try:
        warn = int(getattr(config.GetConfig(), "SenderBacklogWarnSize"))
    except Exception:
        warn = 0
    if warn <= 0 or qsize < warn:
        return
    nowsec = time.time()
    if nowsec - _lastBacklogLog >= _LOG_THROTTLE_SEC:
        _lastBacklogLog = nowsec
        logging_util.error(
            "[Sender] send queue backlog qsize=%d (>= warn=%d): outbound data is backing up (server unreachable?)" % (qsize, warn))

def _sendOrDrop(session, flag, b):
    # session.Send returns True (sent), False (client was None = session already
    # closed), or None (send raised, already logged). Distinguish the drop reason
    # so "session_closed" is not lumped in with a real "send_failed" (G2).
    r = session.Send(flag, b)
    if r is None:
        _recordDrop("send_failed")
    elif r is False:
        _recordDrop("session_closed")

class TcpSend:
    # SERVER-1943: pcode/licenseHash are set only for oneway sends (a pack bound
    # for a project other than this session's). Both default to 0, which means
    # "normal frame" and keeps every existing Send() caller unchanged.
    def __init__(self, f, p, pcode=0, licenseHash=0):
        self.__flag = f
        self.__pack = p
        self.__pcode = pcode
        self.__licenseHash = licenseHash

    @property
    def flag(self):
        return self.__flag

    @property
    def pack(self):
        return self.__pack

    @property
    def pcode(self):
        return self.__pcode

    @property
    def licenseHash(self):
        return self.__licenseHash

    @property
    def isOneway(self):
        return self.__pcode > 0 and self.__licenseHash != 0

def Send(f , p):
    __sender.queue(TcpSend(f, p))

def SendEx(pcode, licenseHash, f, p):
    """Queue a pack for a specific project (oneway frame).

    SERVER-1943. Mirrors go-apm net.SendEx: with a target pcode and license hash
    the pack goes out in a sessionless frame addressed to that project; without
    them it degrades to the normal Send() path.

    pcode/licenseHash must be UNSIGNED - see TcpSession.SendOneway for why.
    """
    if not (pcode > 0 and licenseHash != 0):
        __sender.queue(TcpSend(f, p))
        return
    __sender.queue(TcpSend(f, p, pcode, licenseHash))

class __Sender(threading.Thread):
    def __init__(self) :
        threading.Thread.__init__(self, name='__Sender')
        self.setDaemon(True)
        self.__queue = Queue.Queue()

    def queue(self, sendinfo):
        self.__queue.put(sendinfo)

    def run(self):
        conf = config.GetConfig()
        cypher_level = conf.CypherLevel

        last_time_sync = int(0)
        while True:
            try:
                conf = config.GetConfig()
                p = self.__queue.get()
                _maybeLogBacklog(self.__queue.qsize())  # G3: outbound backlog
                secuTcp = secure.GetSecuritySession()
                session = tcpsession.GetTcpSession()

                if not session.isOpen():
                    _recordDrop("session_not_open")  # G2
                    time.sleep(0.001)
                    continue
                if cypher_level != conf.CypherLevel:
                    cypher_level = conf.CypherLevel
                    session.Close()
                    _recordDrop("cypher_level_changed")  # G2: this pack is skipped
                    time.sleep(0.001)
                    continue
                now = dateutil.now()

                if now > last_time_sync+conf.TimeSyncInterval * 1000:
                    last_time_sync = now
                    session.Send(net.NET_TIME_SYNC, DataOutputX.toBytesLong(now))

                # SERVER-1943: a oneway pack is addressed to a different project,
                # so it must not be encrypted with this session's key - the
                # target project cannot decrypt it. Send the plain pack bytes in
                # a oneway frame and skip the cypher branches below entirely.
                if p.isOneway:
                    b = pack.toBytesPack(p.pack)
                    r = session.SendOneway(p.pcode, p.licenseHash, b)
                    if r is None:
                        _recordDrop("oneway_send_failed")
                    elif r is False:
                        _recordDrop("oneway_session_closed")
                    continue

                if conf.CypherLevel == 0:
                    b = pack.toBytesPack(p.pack)
                    _sendOrDrop(session, p.flag, b)
                else:
                    secureMask = net.GetSecureMask(p.flag)

                    if secureMask == net.NET_SECURE_HIDE:
                        if secuTcp.cypher is not None:
                            b = pack.toBytesPack(p.pack)
                            b = secuTcp.cypher.HideLong(b)
                            _sendOrDrop(session, p.flag, b)
                        else:
                            _recordDrop("cypher_not_ready")  # G2: session up but key not set yet
                    elif secureMask == net.NET_SECURE_CYPHER:
                        if secuTcp.cypher is not None:
                            b = pack.toBytesPackECB(p.pack, int(conf.CypherLevel/8))
                            b = secuTcp.cypher.Encrypt(b)
                            _sendOrDrop(session, p.flag, b)
                        else:
                            _recordDrop("cypher_not_ready")  # G2
                    else:
                        b = pack.toBytesPack(p.pack)
                        _sendOrDrop(session, p.flag, b)
            except Exception, e:
                # A pack that raises during serialize/encrypt/send is a drop too;
                # count it (G2) so the drop total is complete, not just stacked.
                _recordDrop("exception")
                logging_util.logStack(e)
                time.sleep(0.001)
__sender = __Sender()
__sender.start()
