import whatap.net as net
import whatap.agent.conf.configure as config
import whatap.agent.secure.security_master as secure
import threading, time, socket, struct, errno
from whatap.io.data_inputx import DataInputX
from whatap.io.data_outputx import DataOutputX
import whatap.util.logging_util as logging_util
import whatap.util.process_util as process_util
from whatap.util.date_util import DateUtil as dateutil
from datetime import datetime, timedelta
import platform, subprocess


def _intConf(name, default):
    try:
        return int(getattr(config.GetConfig(), name))
    except Exception:
        return default


def _connectTimeoutSupported():
    """Whether socket.settimeout()-based connect works on this platform.
    HP-UX PA-RISC (machine 9000/xxx) on Python 2.7 breaks the non-blocking
    connect that a socket timeout uses internally - connect fails immediately
    with [Errno -1] even though a plain blocking connect succeeds. So on
    HP-UX PA-RISC we fall back to a blocking connect (bounded by the OS connect
    timeout). HP-UX IA64 and every other platform are fine."""
    try:
        if platform.system().lower() == 'hp-ux' and not platform.machine().lower().startswith('ia'):
            return False
    except Exception:
        pass
    return True


def _classifyConnErr(e):
    """Return a short cause label proving a connect failure category, so logs
    show WHY connect failed (network vs server vs DNS), not just that it did
    (data-gap investigation)."""
    if isinstance(e, socket.gaierror):
        return "DNS_RESOLVE_FAIL"
    if isinstance(e, socket.timeout):
        return "SYN_TIMEOUT(no_response;firewall/route/server_down)"
    err = getattr(e, 'errno', None)
    # Py2 socket.error sometimes carries the errno only in args[0].
    if err is None and getattr(e, 'args', None) and isinstance(e.args[0], int):
        err = e.args[0]
    mapping = {
        errno.ECONNREFUSED: "CONN_REFUSED(server_not_listening)",
        errno.ETIMEDOUT:    "CONN_TIMEOUT(syn_dropped)",
        errno.EHOSTUNREACH: "HOST_UNREACHABLE",
        errno.ENETUNREACH:  "NET_UNREACHABLE",
        errno.ECONNRESET:   "CONN_RESET",
        errno.EPIPE:        "BROKEN_PIPE",
        errno.EAGAIN:       "SEND_TIMEOUT(SO_SNDTIMEO/EAGAIN)",
        errno.EWOULDBLOCK:  "SEND_TIMEOUT(SO_SNDTIMEO/EWOULDBLOCK)",
    }
    if err in mapping:
        return mapping[err]
    if err == -1:
        # Not a real OS errno. This is the signature of a platform whose
        # timeout-mode (non-blocking) connect is broken - confirmed on HP-UX
        # PA-RISC (Python 2.7): settimeout()+connect() fails with [Errno -1]
        # while a blocking connect works. Name it explicitly with the fix so a
        # customer/engineer is not left with a bare "errno -1".
        return "NONBLOCKING_CONNECT_UNSUPPORTED(errno-1;platform timeout-connect broken e.g. HP-UX PA-RISC;set net.connect.timeout.sec=0 for blocking connect)"
    if err is not None:
        return "ERRNO_%s(%s)" % (err, errno.errorcode.get(err, "?"))
    return "UNKNOWN"


# Set by isSessionOK() so the health check / watchdog can log the exact session
# failure (data-gap investigation).
lastSessionFailReason = ""

# G1: proof that data actually left the socket. isSessionOK / the reconnect log
# key on lastReceived (RECEIVE side), which only catches an outbound stall
# indirectly and up to ~5 min late; these record the last SUCCESSFUL send so a
# data-gap probe can pinpoint when the agent last actually sent (data-gap invest).
lastSuccessfulSend = None
sentCount = 0


def _safeClose(sock):
    """Close a socket ignoring errors (used to avoid fd leaks on failed open)."""
    try:
        if sock is not None:
            sock.close()
    except Exception:
        pass


_sndTimeoutWarned = False

def _applySendTimeout(client):
    """Bound blocking sendall() with SO_SNDTIMEO so a half-open connection can't
    stall the sender thread (which the watchdog would read as a hang). recv is
    left blocking on purpose. Best-effort: if the platform lacks SO_SNDTIMEO the
    send hang guard is simply inactive, so warn once at error level so ops know
    (hang guard, data-gap investigation)."""
    global _sndTimeoutWarned
    sec = _intConf("NetSendTimeout", 0)
    if sec <= 0:
        return
    try:
        client.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, struct.pack('ll', sec, 0))
    except Exception, e:
        if not _sndTimeoutWarned:
            _sndTimeoutWarned = True
            logging_util.error(
                "[TcpSession] SO_SNDTIMEO unsupported; send hang guard inactive (set net.send.timeout.sec=0 to silence): %s" % e)

READ_MAX = 8 * 1024 * 1024
moduleLoaded = datetime.now()

class TcpSession :

    def __init__(self):
        self.client = None
        self.in_ = None
        self.dest = 0
        self.hosts = None
        self.started = 0
        self.lastReceived = datetime(year=1970, month=1, day=1)

    def isHostChanged(self):
        conf = config.GetConfig()
        hosts = conf.WhatapHost.split('/')
        if hosts is not None and len(hosts) > 0 :
            ismatch = False
            if self.hosts is not None and len(self.hosts) > 0 :
                for h1 in hosts :
                    for h2 in self.hosts :
                        if h1 == h2 :
                            ismatch = True

            if not ismatch :
                return True

        return False

    def open(self):
        self._openStep = "start"
        try:
            return self.__open()
        except Exception, e:
            # __open's connect/handshake failures return False (already logged in
            # detail); reaching here is an UNEXPECTED raise, so name the step
            # (getsockname/decideOid/finalize) instead of a bare "open failed".
            logging_util.error("[TcpSession] open failed step=%s type=%s err=%s" %
                               (getattr(self, '_openStep', '?'), e.__class__.__name__, e))
            self.Close()
            return False

    def __open(self):
        if self.isOpen() :
            return True

        pcode = secure.GetSecurityMaster().pcode
        if pcode == 0 :
            logging_util.error("[TcpSession] Pcode Error")
            return False

        conf = config.GetConfig()
        hosts = conf.WhatapHost.split('/')
        if hosts is None or len(hosts) == 0 :
            logging_util.error("[TcpSession] whatap host config Error")
            self.Close()
            return False
        if self.dest >= len(hosts) :
            self.dest = 0
        port = int(conf.WhatapPort)
        host = hosts[self.dest]
        # Set before connecting so connect/handshake failure logs know the host.
        self.hosts = hosts
        open_start = datetime.now()
        client = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
        client.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

        # Bound connect() AND the key handshake below so a black-holed or silent
        # host cannot block the session opener for minutes while it holds
        # sessionLock (which would stall every thread that needs the session, i.e.
        # a hang the watchdog restarts). The timeout stays active through the
        # handshake, then the socket is set blocking for the long-lived receiver
        # read and a send-only timeout is (re)applied (hang guard).
        connectTimeout = _intConf("NetConnectTimeout", 0)
        # HP-UX PA-RISC breaks timeout-mode (non-blocking) connect, so fall back
        # to a blocking connect there; elsewhere use the configured timeout.
        useConnectTimeout = connectTimeout > 0 and _connectTimeoutSupported()
        effTimeout = connectTimeout if useConnectTimeout else 0
        try:
            if useConnectTimeout:
                client.settimeout(connectTimeout)
            client.connect((host, port))
        except Exception, e:
            elapsed = int((datetime.now() - open_start).total_seconds() * 1000)
            logging_util.error(
                "[TcpSession] tcp connect failed host=%s port=%s dest=%d timeout=%ss elapsed=%dms cause=%s type=%s errno=%s err=%s" %
                (host, port, self.dest, effTimeout, elapsed,
                 _classifyConnErr(e), e.__class__.__name__, getattr(e, 'errno', None), e))
            self.dest += 1
            _safeClose(client)
            self.Close()
            return False

        self._openStep = "getsockname"
        myIP = client.getsockname()[0]
        if myIP == 0 and 'hp-ux' == platform.system().lower(): # HPUX pa not support getsockname
            # Match the actual server port (conf.WhatapPort), not a hardcoded one:
            # on HP-UX PA the established connection is on the configured port, so a
            # fixed "6600" grep found nothing and left myIP empty (data-gap invest.).
            command = "netstat -an | grep %s | grep ESTABLISHED | head -n 1 | awk '{print $4}' | awk -F'.' '{print $1\".\"$2\".\"$3\".\"$4}'" % port
            netstat_start = datetime.now()
            try:
                # Bounded so a stuck netstat cannot hang session open (hang guard).
                nstimeout = _intConf("NetstatCommandTimeout", 0)
                if nstimeout > 0:
                    myIP = process_util.executeCommandShellWithTimeout(command, timeout=nstimeout)
                else:
                    myIP = process_util.executeCommandShell(command)
                nselapsed = int((datetime.now() - netstat_start).total_seconds() * 1000)
                logging_util.debug("[TcpSession] hp-ux netstat getsockname fallback ip=%s elapsed=%dms" % (myIP, nselapsed))
            except Exception, e:
                logging_util.error("[TcpSession] hp-ux netstat fallback error: %s" % e)

        self._openStep = "decideOid"
        secure.GetSecurityMaster().DecideAgentOnameOid(myIP)

        self._openStep = "handshake"
        try:
            keyReset = self.keyReset()
            client.sendall(keyReset)
            self.in_ = DataInputX(client)
            data = self.readKeyReset(self.in_)
        except Exception, e:
            elapsed = int((datetime.now() - open_start).total_seconds() * 1000)
            logging_util.error(
                "[TcpSession] key handshake failed host=%s dest=%d elapsed=%dms cause=%s type=%s errno=%s err=%s" %
                (host, self.dest, elapsed,
                 _classifyConnErr(e), e.__class__.__name__, getattr(e, 'errno', None), e))
            self.dest += 1  # failover to next host (TCP up but handshake broken)
            _safeClose(client)
            self.Close()
            return False

        if data is None or len(data) < 1 :
            logging_util.error("[TcpSession] key read error host=%s dest=%d" % (host, self.dest))
            self.dest += 1  # failover to next host
            _safeClose(client)
            self.Close()  # reset session state, consistent with the paths above
            return False
        secure.UpdateNetCypherKey(data)

        # Handshake done: switch to blocking so the receiver can wait on server
        # pushes, then bound sends only. settimeout(None) clears SO_SNDTIMEO, so
        # _applySendTimeout must run after it (hang guard). Only touch the socket
        # timeout if we actually set one (not on the HP-UX PA blocking path).
        self._openStep = "finalize"
        if useConnectTimeout:
            client.settimeout(None)
        _applySendTimeout(client)

        self.client = client
        self.started = datetime.now()

        elapsed = int((datetime.now() - open_start).total_seconds() * 1000)
        logging_util.info("[TcpSession] session opened host=%s dest=%d elapsed=%dms" % (host, self.dest, elapsed))
        return True

    def isOpen(self):
        return self.client is not None

    def readKeyReset(self, in_):
        r = in_.readByte()
        r = in_.readByte()
        pcode = in_.readLong()
        oid = in_.readInt()
        r = in_.readInt()
        data = in_.readIntBytes(1024)
        if data is None:
            logging_util.error("[TcpSession] key reset error")
            return None

        secu = secure.GetSecurityMaster()

        #print 'readKeyReset ', pcode, secu.pcode, 'oid', oid, secu.oid
        if pcode != secu.pcode or oid != secu.oid :
            logging_util.error("[TcpSession] pcode, oid error")
            return None
        else:
            return data

    def keyReset(self):
        conf = config.GetConfig()
        secu = secure.GetSecurityMaster()
        secu.WaitForInit()
        dout = DataOutputX()

        msg = dout.writeText("hello").writeText(secu.oname).writeInt(secu.ip).toByteArray()
        if conf.CypherLevel > 0 :
            msg = secu.cypher.Encrypt(msg)
        dout = DataOutputX()
        dout.writeByte(net.NETSRC_AGENT_JAVA_EMBED)

        trkey = 0
        if conf.CypherLevel == 128 :
            dout.writeByte(net.NET_KEY_RESET)
        else:
            dout.writeByte(net.NET_KEY_EXTENSION)

            if conf.CypherLevel == 0 :
                trkey = 0
            else :
                b0 = 1
                b1 = conf.CypherLevel / 8
                trkey = DataOutputX.toInt([b0, b1, 0, 0], 0)

        #print 'keyReset', secu.pcode, secu.oid
        dout.writeLong(secu.pcode)
        dout.writeInt(secu.oid)
        dout.writeInt(trkey)
        dout.writeIntBytes(msg)

        return dout.toByteArray()

    def Send(self, code , b ):
        global lastSuccessfulSend, sentCount
        try:
            r = self.__send(code, b)
            if r:
                # G1: record that bytes were handed to TCP successfully.
                lastSuccessfulSend = datetime.now()
                sentCount += 1
            return r
        except Exception, e:
            # Previously swallowed: a send failure closes the session and triggers
            # a reconnect, so log it as a reconnect cause (data-gap investigation).
            # SO_SNDTIMEO surfaces here as a socket timeout on a half-open link.
            logging_util.error(
                "[TcpSession] Send failed code=%s, closing session: cause=%s type=%s errno=%s err=%s" %
                (code, _classifyConnErr(e), e.__class__.__name__, getattr(e, 'errno', None), e))
            self.Close()

    def SendOneway(self, pcode, licenseHash, b):
        """Send a pack to a project other than this session's own.

        SERVER-1943. The regular frame written by __send() identifies the sender
        with the session's pcode/oid/transferKey, so it can only ever reach the
        project this session authenticated against. A oneway frame carries the
        target pcode plus a hash of that project's license instead, and is not
        encrypted with the session key - it rides the same socket but is routed
        independently.

        pcode and licenseHash must be passed UNSIGNED. writeLong() masks with
        0xFFFFFFFFFFFFFFFF, and on the 32-bit python2 build shipped for AIX that
        mask returns the wrong value for a negative long, which would put wrong
        bytes on the wire. HashUtil.hash64go() already returns unsigned.
        """
        try:
            if self.client is None:
                return False

            out = DataOutputX()
            out.writeByte(net.NETSRC_AGENT_ONEWAY)
            out.writeByte(net.NETSRC_AGENT_VERSION)
            out.writeLong(pcode)
            out.writeLong(licenseHash)
            if hasattr(b, 'getvalue'):
                out.writeIntBytes(b.getvalue())
            else:
                out.writeIntBytes(b)
            self.client.sendall(out.toByteArray())

            global lastSuccessfulSend, sentCount
            lastSuccessfulSend = datetime.now()
            sentCount += 1
            return True
        except Exception, e:
            # Same treatment as Send(): a failed write means the link is bad, so
            # close and let the reconnect path take over, and log the cause so a
            # data gap can be traced back here.
            logging_util.error(
                "[TcpSession] SendOneway failed pcode=%s, closing session: cause=%s type=%s errno=%s err=%s" %
                (pcode, _classifyConnErr(e), e.__class__.__name__, getattr(e, 'errno', None), e))
            self.Close()

    def __send(self, code, b):
        if self.client is None :
            return False

        secu = secure.GetSecurityMaster()
        secuSession = secure.GetSecuritySession()
        out = DataOutputX()
        out.writeByte(net.NETSRC_AGENT_JAVA_EMBED)
        out.writeByte(code)
        out.writeLong(secu.pcode)
        out.writeInt(secu.oid)
        out.writeInt(secuSession.transferKey)
        if hasattr(b, 'getvalue'):
            out.writeIntBytes(b.getvalue())
        else:
            out.writeIntBytes(b)
        sendbuf = out.toByteArray()
        self.client.sendall(sendbuf)
        return True

    def Close(self):
        if self.client is not None:
            self.client.close()
        self.client = None

    def Read(self):
        if not self.isOpen():
            return empty

        read_start = datetime.now()
        step = 'src'
        code = None
        pcode = None
        oid = None
        transfer_key = None
        data_len = -1
        try:
            self.in_.readByte()
            step = 'code'
            code = self.in_.readByte()
            step = 'pcode'
            pcode = self.in_.readLong()
            step = 'oid'
            oid = self.in_.readInt()
            step = 'transferKey'
            transfer_key = self.in_.readInt()
            step = 'data'
            data = self.in_.readIntBytes(READ_MAX)
            data_len = -1 if data is None else len(data)
        except Exception, e:
            elapsed_ms = int((datetime.now() - read_start).total_seconds() * 1000)
            logging_util.error(
                "[TcpSession] Read failed step=%s elapsed=%dms code=%s pcode=%s oid=%s trkey=%s err=%s" %
                (step, elapsed_ms, code, pcode, oid, transfer_key, e))
            raise

        if data is None :
            logging_util.error("[TcpSession] Read got None data (server closed connection / EOF) code=%s" % code)
            return empty

        secu = secure.GetSecurityMaster()
        if pcode != secu.pcode or oid != secu.oid :
            logging_util.error(
                "[TcpSession] Read pcode/oid mismatch recv(pcode=%s,oid=%s) expect(pcode=%s,oid=%s) code=%s len=%d" %
                (pcode, oid, secu.pcode, secu.oid, code, data_len))
            return empty

        self.lastReceived = datetime.now()
        return TcpReturn(code= code, data= data, transfer_key= transfer_key)

class TcpReturn:
    def __init__(self, code=None, data=None, transfer_key = None):
        self.code = code
        self.data = data
        self.transferKey = transfer_key

def connect(s):
    attempt = 0
    connect_start = datetime.now()
    while True:
        while not s.open():
            attempt += 1
            elapsed = int((datetime.now() - connect_start).total_seconds())
            host = s.hosts[s.dest] if (s.hosts and s.dest < len(s.hosts)) else "?"
            logging_util.error(
                "[TcpSession] Session Open Error attempt=%d elapsed=%ds host=%s" %
                (attempt, elapsed, host))
            time.sleep(3)
        now = dateutil.now()
        if not s.Send(net.NET_TIME_SYNC, DataOutputX.toBytesLong(now)):
            logging_util.error("[TcpSession] TimeSync Error, closing and retrying")
            s.Close()
            time.sleep(3)
            continue
        break

def WaitForConnection():
    while True:
        tcp = GetTcpSession()
        if tcp.isOpen() :
            break

        time.sleep(1)

empty = TcpReturn()
sessionLock = threading.Lock()
session = None
def GetTcpSession():
    global session
    sessionLock.acquire()
    try:
        if session and session.isOpen() and not session.isHostChanged():
            return session
        if session :
            # Session reconnect trace: record the trigger and how long the old
            # session lasted / how stale its last receive was (data-gap invest.)
            now = datetime.now()
            age = (now - session.started).total_seconds() if session.started else -1
            lastRecvAge = (now - session.lastReceived).total_seconds()
            lastSendAge = (now - lastSuccessfulSend).total_seconds() if lastSuccessfulSend else -1
            reason = "hostChanged" if session.isHostChanged() else ("closed" if not session.isOpen() else "unknown")
            logging_util.info(
                "[TcpSession] Tcp Session Reconnect reason=%s sessionAge=%.0fs lastRecvAge=%.0fs lastSendAge=%.0fs sent=%d dest=%d" %
                (reason, age, lastRecvAge, lastSendAge, sentCount, session.dest))
            session.Close()
        session = TcpSession()
        connect(session)
        logging_util.info("[TcpSession] Connection : ", session.hosts)
        return session
    finally:
        sessionLock.release()

def isSessionOK():
    global lastSessionFailReason
    now = datetime.now()
    if now-moduleLoaded <= timedelta(minutes=config.GetConfig().HealthCheckCoolTime) :
        return True

    if not session:
        lastSessionFailReason = "no_session"
        logging_util.error("[TcpSession] isSessionOK fail: no session")
        return False

    if not session.isOpen():
        lastSessionFailReason = "not_open(dest=%d)" % session.dest
        logging_util.error("[TcpSession] isSessionOK fail: session not open dest=%d" % session.dest)
        return False

    if not session.started:
        lastSessionFailReason = "not_started"
        logging_util.error("[TcpSession] isSessionOK fail: session not started")
        return False

    if session.started and now-session.started > timedelta(minutes=config.GetConfig().HealthCheckCoolTime):
        staleLimit = config.GetConfig().TimeSyncInterval*10
        lastRecvAge = (now-session.lastReceived).total_seconds()
        if lastRecvAge >= staleLimit:
            lastSessionFailReason = "no_data_recv_%ds(limit=%ds,dest=%d)" % (lastRecvAge, staleLimit, session.dest)
            logging_util.error(
                "[TcpSession] isSessionOK fail: no data received for %.0fs (limit=%ss) dest=%d" %
                (lastRecvAge, staleLimit, session.dest))
            return False

        # G4: send-side stall detection. The receive-staleness check above lags
        # up to TimeSyncInterval*10 (~5 min) and only proves the RECEIVE path;
        # this fails faster and names the outbound stall explicitly. Opt-in
        # (net.send.stale.limit.sec=0 disables) so it does not change restart
        # cadence unless enabled for a data-gap investigation.
        sendStaleLimit = _intConf("NetSendStaleLimit", 0)
        if sendStaleLimit > 0:
            lastSendAge = (now-lastSuccessfulSend).total_seconds() if lastSuccessfulSend else -1
            if lastSuccessfulSend is None or lastSendAge >= sendStaleLimit:
                lastSessionFailReason = "no_send_%ds(limit=%ds,dest=%d)" % (lastSendAge, sendStaleLimit, session.dest)
                logging_util.error(
                    "[TcpSession] isSessionOK fail: no successful send for %.0fs (limit=%ss) dest=%d" %
                    (lastSendAge, sendStaleLimit, session.dest))
                return False

    return True

def net_test(connect_timeout=5, response_timeout=3):
    secu = secure.GetSecurityMaster()
    secu.WaitForInitFor(30)

    if secu.pcode == 0:
        return "invalid license error"

    conf = config.GetConfig()
    hosts = conf.WhatapHost.split('/')
    if hosts is None or len(hosts) == 0:
        return "whatap host config error"

    port = int(conf.WhatapPort)
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

    connected = False
    for host in hosts:
        try:
            client.connect((host.strip(), port))
            connected = True
            break
        except Exception, e:
            logging_util.error("[NetTest] connect failed to %s:%s - %s" % (host, port, e))
            continue

    if not connected:
        client.close()
        return "connection failed - host: %s, port: %s" % (conf.WhatapHost, port)

    test_session = TcpSession()
    test_session.client = client
    try:
        test_session.hosts = hosts
        myIP = client.getsockname()[0]
        secu.DecideAgentOnameOid(myIP)

        keyReset = test_session.keyReset()
        client.sendall(keyReset)
        test_session.in_ = DataInputX(client)
        data = test_session.readKeyReset(test_session.in_)
        if data is None or len(data) < 1:
            return "key reset failed"
        secure.UpdateNetCypherKey(data)

        now = dateutil.now()
        if not test_session.Send(net.NET_TIME_SYNC, DataOutputX.toBytesLong(now)):
            return "send failed - host: %s, port: %s" % (conf.WhatapHost, port)

        is_response_received = [False]

        def read_response():
            try:
                client.settimeout(response_timeout)
                out = test_session.Read()
                if out and out.code == net.NET_TIME_SYNC:
                    is_response_received[0] = True
            except Exception, e:
                logging_util.error("[NetTest] read response error: %s" % e)

        reader = threading.Thread(target=read_response)
        reader.setDaemon(True)
        reader.start()
        reader.join(response_timeout + 1)

        if not is_response_received[0]:
            return "received no response - host: %s, port: %s" % (conf.WhatapHost, port)

        return None

    except Exception, e:
        return "net test error: %s" % str(e)
    finally:
        test_session.Close()
