# -*- coding: utf-8 -*-
"""Routing target for inspection data (SERVER-1943).

Mirrors go-apm's inspection_forwarder.go. The agent's session authenticates
against a single project, so data for the inspection project cannot go out
through the normal frame; it is sent in a oneway frame addressed by that
project's pcode plus a hash of its license string.

License handling follows the same shape as __SecurityMaster.resetLicense():
parse the configured license into a pcode and cache it, re-parsing only when
the configured value actually changes.
"""

import threading

import whatap.agent.conf.configure as config
import whatap.agent.data.data_pack as data
import whatap.lang.license as license
import whatap.util.logging_util as logging_util
from whatap.util.hash_util import HashUtil


class _InspectionTarget(object):
    def __init__(self, pcode, license_hash, lic):
        self.pcode = pcode
        self.license_hash = license_hash
        self.license = lic


_target = None
_target_lock = threading.Lock()


def _get_target():
    with _target_lock:
        return _target


def _set_target(t):
    global _target
    with _target_lock:
        _target = t


def UpdateInspectionTarget(lic):
    """Re-resolve the inspection target from the configured license.

    Cheap to call repeatedly: work happens only when the license string changed.
    """
    current = _get_target()

    if not lic:
        if current is None:
            return
        _set_target(None)
        logging_util.info("[Inspection] license cleared, inspection target removed")
        return

    if current is not None and current.license == lic:
        return

    try:
        pcode, _security_key = license.Parse(lic)
    except Exception as e:
        pcode = 0
        logging_util.error("[Inspection] license parse error:", e)

    if not pcode:
        # Keeping the previous target here would mean a bad license still ships
        # data to the project configured before it, which breaks the isolation
        # this feature exists for. Clear it so the misconfiguration surfaces as
        # "not sent" (logged by the caller) rather than as silent mis-routing.
        _set_target(None)
        logging_util.error("[Inspection] license parse failed, inspection target cleared")
        return

    # hash64go (not hash64) and unsigned throughout: the wire value has to match
    # go-apm's hash.Hash64Str, and the 32-bit python2 build used on AIX corrupts
    # negative long arithmetic. See HashUtil.hash64go.
    license_hash = HashUtil.hash64goFromString(lic)
    _set_target(_InspectionTarget(pcode, license_hash, lic))
    logging_util.info("[Inspection] license updated - pcode: {}".format(pcode))


def HasInspectionTarget():
    t = _get_target()
    return t is not None and t.pcode > 0


def GetInspectionPcode():
    """pcode of the configured inspection project, 0 when there is none.

    SERVER-1968: task_hostinfo reports this as inspectionPcode in
    server_inventory. It reads the resolved target rather than parsing
    inspection.license itself so that both consumers agree on what counts as a
    valid target - a second parse site would be free to drift on, say, whether
    a pcode of 0 or an unparseable license still counts.
    """
    t = _get_target()
    if t is None or t.pcode <= 0:
        return 0
    return t.pcode


def SendInspectionPack(p):
    """Send p to the inspection project.

    Returns True when it was handed to the sender, False when there is no
    inspection target configured. The caller reports False so that data which
    is deliberately not sent to the default project does not vanish silently.
    """
    t = _get_target()
    if t is None or t.pcode <= 0:
        return False

    # The pack was built with the default project's pcode; re-address it. Packs
    # are constructed per send in the cronjob path, so there is no shared
    # instance to corrupt by mutating pcode here.
    p.pcode = t.pcode
    data.SendOneway(t.pcode, t.license_hash, p)
    return True


class _InspectionLicenseWatcher(threading.Thread):
    """Poll the configured inspection license.

    Configuration in this agent is a plain reloaded property bag with no change
    observers, and __SecurityMaster watches its own license the same way, so
    follow that existing pattern rather than introducing a new mechanism.
    """

    def __init__(self, interval=3):
        threading.Thread.__init__(self, name='__InspectionLicenseWatcher')
        self.setDaemon(True)
        self.interval = interval

    def run(self):
        import time
        old = None
        while True:
            try:
                lic = config.GetConfig().getProperty("InspectionLicense", "")
                if lic != old:
                    old = lic
                    UpdateInspectionTarget(lic)
            except Exception as e:
                logging_util.error("[Inspection] license watch error:", e)
            time.sleep(self.interval)


_watcher = None
_watcher_lock = threading.Lock()


def InitInspectionForwarder():
    global _watcher
    with _watcher_lock:
        if _watcher is not None:
            return
        # Resolve once synchronously before the poller starts. The watcher
        # would get to it within one interval anyway, but the first
        # server_inventory report (SERVER-1968) can fire before that tick and
        # would then omit inspectionPcode until the next hourly report.
        # UpdateInspectionTarget is idempotent, so the watcher's first pass
        # re-running it is harmless.
        try:
            UpdateInspectionTarget(config.GetConfig().getProperty("InspectionLicense", ""))
        except Exception as e:
            logging_util.error("[Inspection] initial license resolve error:", e)
        _watcher = _InspectionLicenseWatcher()
        _watcher.start()
