"""Retransmission tracking for syncConfig requests (SERVER-1754).

The agent asks the server to (re-)push the latest config for a configId by
sending a PARAM_INFRA_AGENT_CONF_REQUEST parampack. The server answers by
pushing a setConfig (PARAM_SET_CONFIG) parampack. When that answer is lost the
agent silently misses config, so each request now carries a random correlation
id in the parampack ``request`` field. The server echoes it back in the
setConfig parampack ``response`` field, and requests that stay unacknowledged
past syncconfig.response.timeout.sec seconds are retransmitted up to
syncconfig.retry.count times.

The locally cached keys of a group are cleared and replaced only when its
setConfig response arrives (the control handler calls ClearConfigKeysFor keyed
by the requestId's configId), so a group whose response never arrives keeps its
last-known keys instead of being wiped (SERVER-1754).

Both ``request`` and ``response`` are pre-existing parampack fields, so this is
backward compatible: a server that ignores them leaves ``response`` at 0 and the
agent simply retransmits until it gives up.
"""
import random
import threading
import time

import whatap.agent.data.data_pack as data_pack
import whatap.agent.conf.configure as config
import whatap.util.logging_util as logging_util
from whatap.util.date_util import DateUtil as dateutil

DEFAULT_RETRY_COUNT = 5
DEFAULT_RESPONSE_TIMEOUT_SEC = 10

# 0 means "no request" in ParamPack, so correlation ids stay strictly positive.
_MAX_REQUEST_ID = 0x7fffffff

_pending = {}
_pendingLock = threading.Lock()
_retryThreadStarted = False
_retryThreadLock = threading.Lock()


class _PendingSync(object):
    def __init__(self, requestId, configId, buildPack):
        self.requestId = requestId
        self.configId = configId
        # buildPack(requestId) returns a fresh parampack for each (re)send.
        self.buildPack = buildPack
        self.sentTime = dateutil.now()
        self.retries = 0


def _maxRetries():
    try:
        return int(config.GetConfig().SyncConfigRetryCount)
    except Exception, e:
        logging_util.debug("syncConfig retry count lookup failed: {}".format(e))
        return DEFAULT_RETRY_COUNT


def _responseTimeoutMs():
    try:
        return int(config.GetConfig().SyncConfigResponseTimeout) * 1000
    except Exception, e:
        logging_util.debug("syncConfig response timeout lookup failed: {}".format(e))
        return DEFAULT_RESPONSE_TIMEOUT_SEC * 1000


def _newRequestIdLocked():
    """Return a fresh correlation id not already pending.

    Caller must hold ``_pendingLock``.
    """
    requestId = random.randint(1, _MAX_REQUEST_ID)
    while requestId in _pending:
        requestId = random.randint(1, _MAX_REQUEST_ID)
    return requestId


def sendTracked(configId, buildPack):
    """Send a syncConfig request and track it until the server echoes it back.

    buildPack(requestId) must return a fresh ParamPack whose ``request`` field
    is set to requestId. Each (re)send uses a fresh correlation id, so buildPack
    is called again per attempt to carry the current id/time values.
    """
    with _pendingLock:
        requestId = _newRequestIdLocked()
        _pending[requestId] = _PendingSync(requestId, configId, buildPack)
    data_pack.SendSecure(buildPack(requestId))
    _ensureRetryThread()


def acknowledge(responseId):
    """Match an incoming setConfig pack against a pending syncConfig request.

    Returns ``(configId, True)`` when the setConfig pack echoes a request we
    sent, meaning it is the server's answer to our syncConfig: the caller clears
    and replaces that configId's keys and must not send a response back. Returns
    ``(None, False)`` for a server-initiated setConfig (response 0 or unknown id)
    so the caller keeps the normal request/response behavior (SERVER-1754).
    """
    if not responseId:
        return None, False
    with _pendingLock:
        pending = _pending.pop(responseId, None)
    if pending is not None:
        logging_util.debug(
            "syncConfig ack configId={} requestId={} after {} retransmit(s)".format(
                pending.configId, responseId, pending.retries))
        return pending.configId, True
    return None, False


def _ensureRetryThread():
    global _retryThreadStarted
    with _retryThreadLock:
        if _retryThreadStarted:
            return
        _retryThreadStarted = True
    t = threading.Thread(target=_retryLoop, name='SyncConfigRetry')
    t.setDaemon(True)
    t.start()


def _retryLoop():
    while True:
        try:
            _processRetries()
        except Exception, e:
            logging_util.debugStack(e)
        time.sleep(1)


def _processRetries():
    now = dateutil.now()
    maxRetries = _maxRetries()
    intervalMs = _responseTimeoutMs()
    resend = []
    with _pendingLock:
        for requestId in list(_pending.keys()):
            pending = _pending[requestId]
            if now - pending.sentTime < intervalMs:
                continue
            if pending.retries >= maxRetries:
                logging_util.error(
                    "syncConfig no response for configId={} requestId={} "
                    "after {} retransmit(s)".format(
                        pending.configId, requestId, pending.retries))
                del _pending[requestId]
                continue
            # Re-issue a fresh correlation id per retransmit and drop the old
            # one: a late response to a superseded/timed-out attempt then no
            # longer matches, so it can't ack the wrong request (SERVER-1754).
            del _pending[requestId]
            prevRequestId = pending.requestId
            pending.requestId = _newRequestIdLocked()
            pending.retries += 1
            pending.sentTime = now
            _pending[pending.requestId] = pending
            resend.append((pending, prevRequestId))
    for pending, prevRequestId in resend:
        logging_util.debug(
            "syncConfig retransmit configId={} requestId={} (was {}) attempt {}/{}".format(
                pending.configId, pending.requestId, prevRequestId,
                pending.retries, maxRetries))
        try:
            data_pack.SendSecure(pending.buildPack(pending.requestId))
        except Exception, e:
            logging_util.error(
                "syncConfig retransmit failed configId={} requestId={}: {}".format(
                    pending.configId, pending.requestId, e))
