import whatap.net as net
import whatap.agent.counter.counter_manager as counter
import whatap.agent.control.control_handler as control_handler
import whatap.agent.counter.houseKeepingHelper as houseKeepingHelper
import whatap.agent.counter.inspection_forwarder as inspection_forwarder
import whatap.downcheck as downcheck
import whatap.agentless.checkmain as agentless
import whatap.agent.conf.configure as config
import whatap.agent.conf.config_sync as config_sync
import whatap.agent.cron.cronjob as cronjob
import whatap.agent.cron.script_manager as script_manager
import whatap.agent.control.agent_patch as agent_patch
from datetime import datetime, timedelta
import socket
import whatap.util.logging_util as logging_util
import whatap.util.thread_util as thread_util
import whatap.util.panic_util as panic_util
import whatap.agent.secure.security_master as secure
import whatap.net.tcpsession as tcpsession
import whatap.osinfo as osinfo
import whatap.pack as pack
import whatap.agent.data.data_pack as data_pack
from whatap.util.date_util import DateUtil as dateutil

module_loaded = datetime.now()


def _getInventoryHostIdentity():
    # Returns (hostname, osType) using the exact same sources as the
    # server_inventory host info collection (see TaskHostInfo.sendHostInfo in
    # whatap/agent/counter/task_hostinfo.py): socket.gethostname() for the
    # hostname and osinfo.GetServerInfoV2()["OSType"] for the OS type. Keeping a
    # single source guarantees the syncConfig request and the inventory record
    # report identical values (SERVER-1732).
    try:
        hostname = socket.gethostname()
    except Exception as e:
        logging_util.error("syncConfig hostname lookup failed: {}".format(e))
        hostname = ""

    osType = ""
    try:
        osinfodict = osinfo.GetServerInfoV2()
        if osinfodict:
            osType = osinfodict.get("OSType", "") or ""
    except Exception as e:
        logging_util.error("syncConfig osType lookup failed: {}".format(e))
    return hostname, osType


def _buildConfigSyncPack(configId, requestId):
    """Build a config-sync request parampack for the given configId.

    Carries hostname/osType so the server can identify the requesting agent
    (SERVER-1732). ParamPack is a key-value map, so adding these keys is
    backward compatible with servers that only read configId. requestId is a
    correlation id the server echoes back in the setConfig pack's response
    field so lost pushes can be retransmitted (SERVER-1754).
    """
    secu = secure.GetSecurityMaster()
    p = pack.NewParamPack()
    p.pcode = secu.pcode
    p.oid = secu.oid
    p.time = dateutil.now()
    p.id = net.PARAM_INFRA_AGENT_CONF_REQUEST
    p.request = requestId
    p.putString("configId", configId)
    # Only emit the keys when a value is available, mirroring the
    # server_inventory collection which omits these on lookup failure.
    hostname, osType = _getInventoryHostIdentity()
    if hostname:
        p.putString("hostname", hostname)
    if osType:
        p.putString("OSType", osType)
    return p


def requestConfigSync(configId):
    """Request config sync for configId, tracked for retransmission.

    The request is retransmitted if the server does not echo it back within
    the configured window (SERVER-1754).
    """
    config_sync.sendTracked(
        configId,
        lambda requestId: _buildConfigSyncPack(configId, requestId))


def syncConfig():
    """Request the server to re-push the sync-managed config groups.

    Sends two separate parampack requests (procgroup_ and the log.file./
    log.filecheck./log.event. watch groups) so the server re-pushes each group.

    The local keys are NOT cleared here. Each group is cleared and replaced only
    when its setConfig response arrives (see control_handler and
    configure.ClearConfigKeysFor), so a group whose response never arrives keeps
    its last-known keys instead of being wiped (SERVER-1754).
    """
    requestConfigSync(config.ProcessGroupConfigId)
    requestConfigSync(config.LogEventConfigId)


def _logBootVersion():
    # First-boot version banner: log the agent version and OS once at startup so
    # the infra log always records which agent build produced the run
    # (SERVER-1899). Uses the same version/OS sources as the server-reported
    # "ver" tag (version_util). Guarded so a lookup failure never blocks boot.
    try:
        import os
        from whatap.util.version_util import GetAgentVersion, GetOSName
        logging_util.info(
            "WhaTap infra agent version %s (os=%s, pid=%s)" % (
                GetAgentVersion(), GetOSName(), os.getpid()))
    except Exception as e:
        logging_util.error("boot version banner failed: {}".format(e))


def boot():
    _logBootVersion()
    net.startNet()
    syncConfig()
    counter.startCounterManager()
    startExtension()
    control_handler.initControlHandler()
    downcheck.startHello()

    agentless.start()
    osinfo.InitSyslogRedirect()
    script_manager.init_script_manager()
    agent_patch.init_agent_patch()
    cronjob.start_cronjob()

    thread_util.async(houseKeepingHelper.startHouseKeeping)
    # SERVER-1943: start watching inspection.license so cronjob results from
    # inspection jobs have a routing target as soon as one is configured.
    inspection_forwarder.InitInspectionForwarder()

def startExtension():
    import whatap.ext as _

def isOK():
    ok, _reason = checkHealth()
    return ok


def checkHealth():
    """Return (ok, reason). reason names the first failing check so the watchdog
    (which only sees OK/PROBLEM over the health socket) can log WHY the child was
    restarted instead of a bare "bad health" (data-gap investigation)."""
    if datetime.now() - module_loaded <= timedelta(minutes=config.GetConfig().HealthCheckCoolTime):
        return True, ""

    logging_util.debug("AgentBoot.IsOK step -0")
    secu = secure.GetSecurityMaster()
    logging_util.debug("AgentBoot.IsOK step -1")

    if secu.pcode == 0:
        message = "AgentBoot.IsOK pcode check failed"
        logging_util.error(message)
        panic_util.AgentEventAsync({"event":"Error"}, {"log":message})
        return False, "pcode"
    if secu.oid == 0:
        message = "AgentBoot.IsOK oid check failed"
        logging_util.error(message)
        panic_util.AgentEventAsync({"event":"Error"}, {"log":message})
        return False, "oid"
    if not tcpsession.isSessionOK():
        reason = "session:%s" % (tcpsession.lastSessionFailReason or "unknown")
        message = "AgentBoot.IsOK session check failed - %s" % reason
        logging_util.error(message)
        panic_util.AgentEventAsync({"event":"Error"}, {"log":message})
        return False, reason
    if not counter.isOK():
        reason = "counter:%s" % (counter.lastHealthFailReason or "unknown")
        message = "AgentBoot.IsOK counter check failed - %s" % reason
        logging_util.error(message)
        panic_util.AgentEventAsync({"event":"Error"}, {"log":message})
        return False, reason
    return True, ""

def boot_test():
    import os, sys
    from whatap.agent.counter.counter_manager import AGENT_VERSION
    from whatap.net.tcpsession import net_test
    import whatap.agent.conf.configure as conf

    logging_util.info("Agent Boot Test Start")

    c = config.GetConfig()
    if not c.Enabled:
        logging_util.error("Server Already Closed")
        print("FAIL: Server Already Closed")
        return False

    config_path = conf.GetConfigPath()
    logging_util.info("Config file : %s" % config_path)
    logging_util.info("Version : %s" % AGENT_VERSION)
    logging_util.info("PID : %s" % os.getpid())

    print("Config file : %s" % config_path)
    print("Version : %s" % AGENT_VERSION)
    print("PID : %s" % os.getpid())

    err = net_test()
    if err is not None:
        logging_util.error("Network Test Failed: %s" % err)
        print("FAIL: Network Test Failed: %s" % err)
        return False

    logging_util.info("Agent Boot Test Success")
    print("Agent Boot Test Success")
    return True

def end():
    pass
