import os
import sys
import json
import time
import socket
import binascii
import threading
import psycopg2
import psycopg2.extras

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)

from net import macho_marshal
from net.macho_marshal import MachoObject, BoundObject, SubStream, RawStream, UniStr, long_type

CONFIG_PATH = os.path.join(BASE_DIR, "config", "server_config.json")
LOG_DIR     = os.path.join(BASE_DIR, "logs")

HANDSHAKE_CODE_HEX = "630000000000000000010000004000000073040000006900005328000000002800000000280000000028000000002800000000730b0000003c68616e647368616b653e74080000003c6d6f64756c653e010000007300000000"

ITEM_DESCRIPTOR_HEX = (
    "7e000000002214030214626c75652e4442526f7744657363726970746f722514092c11270614"
    "2c114a06032c113f06032c112c06142c1306666c6167494406022c114106032c112306032c11"
    "0a06032c111705810015022c1309737461636b73697a6502266576652e636f6d6d6f6e2e7363"
    "726970742e7379732e6576654366672e537461636b53697a652c114702266576652e636f6d6d"
    "6f6e2e7363726970742e7379732e6576654366672e53696e676c65746f6e2d2d"
)
ITEM_DESCRIPTOR_RAW = binascii.unhexlify(ITEM_DESCRIPTOR_HEX)

def load_config():
    with open(CONFIG_PATH, "r") as f:
        return json.load(f)

def get_db_conn(cfg):
    db_cfg = cfg.get("db", {})
    try:
        return psycopg2.connect(
            dbname=db_cfg.get("dbname", "ericgw_dystropia"),
            user=db_cfg.get("user", "ericgw_eve"),
            password=db_cfg.get("password", "DystropiaEve2026_db"),
            host=db_cfg.get("host", "127.0.0.1"),
            port=db_cfg.get("port", 5432)
        )
    except Exception:
        return psycopg2.connect(dbname=db_cfg.get("dbname", "ericgw_dystropia"), user="postgres")

def log_msg(msg):
    ts = time.strftime("%Y-%m-%d %H:%M:%S")
    line = "[%s] %s" % (ts, msg)
    print(line)
    sys.stdout.flush()
    if not os.path.exists(LOG_DIR):
        os.makedirs(LOG_DIR)
    with open(os.path.join(LOG_DIR, "machonet_server.log"), "a") as lf:
        lf.write(line + "\n")

def placebo_crypto_hash(*args):
    raw = macho_marshal.dumps(args)
    return str(binascii.crc_hqx(raw, 0))

INV_DBROW_DESC = MachoObject(
    "blue.DBRowDescriptor",
    {},
    args=(((
        ("itemID", 20),
        ("typeID", 3),
        ("ownerID", 3),
        ("locationID", 20),
        ("flagID", 2),
        ("quantity", 3),
        ("groupID", 3),
        ("categoryID", 3),
        ("customInfo", 129),
    ),),),
    new_style=True,
)

SHIP_INV_OIDS = set()
INV_COLS_9_GODMA = ["itemID", "typeID", "ownerID", "locationID", "flagID", "singleton", "groupID", "categoryID", "customInfo"]
INV_COLS_11 = ["itemID", "typeID", "ownerID", "locationID", "flagID", "quantity", "groupID", "categoryID", "customInfo", "stacksize", "singleton"]

def make_dbrow(line9):
    return MachoObject("carbon.common.script.sys.row.Row", {"header": INV_COLS_9_GODMA, "line": list(line9)})

def get_filetime_now():
    return long_type((time.time() + 11644473600.0) * 10000000.0)

def datetime_to_filetime(dt):
    if dt is None:
        return get_filetime_now()
    return long_type((dt.timestamp() + 11644473600.0) * 10000000.0)

def db_authenticate_or_create_user(cfg, raw_username, pw_hash):
    uname = raw_username.val if isinstance(raw_username, UniStr) else str(raw_username)
    uname = uname.strip()
    if not uname:
        return None
    conn = get_db_conn(cfg)
    try:
        with conn:
            with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
                cur.execute("SELECT * FROM users WHERE LOWER(user_name) = LOWER(%s)", (uname,))
                user = cur.fetchone()
                if not user:
                    log_msg("[Auth] Unknown user_name=%r — account does not exist!" % (uname,))
                    return None
                log_msg("[DB] Loaded existing user %r (user_id=%d) from PostgreSQL" % (user["user_name"], user["user_id"]))
                return dict(user)
    finally:
        conn.close()

def db_verify_or_set_password(cfg, user_id, candidate_hash):
    if not candidate_hash:
        return False
    raw_bytes = candidate_hash.encode("latin1") if isinstance(candidate_hash, str) else bytes(candidate_hash)
    conn = get_db_conn(cfg)
    try:
        with conn:
            with conn.cursor() as cur:
                cur.execute("SELECT password_hash FROM users WHERE user_id = %s", (user_id,))
                row = cur.fetchone()
                stored = bytes(row[0]) if (row and row[0] is not None) else None
                if stored is None:
                    cur.execute("UPDATE users SET password_hash = %s WHERE user_id = %s", (psycopg2.Binary(raw_bytes), user_id))
                    log_msg("[Auth] TOFU: Initialized and locked password_hash (%d bytes) for user_id=%d!" % (len(raw_bytes), user_id))
                    return True
                match = (stored == raw_bytes)
                log_msg("[Auth] Password verification for user_id=%d: %s" % (user_id, "SUCCESS" if match else "FAILED"))
                return match
    finally:
        conn.close()

def db_get_character_selection_data(cfg, user_id):
    now_ft = get_filetime_now()
    day_ticks = long_type(864000000000)
    hour_ticks = long_type(36000000000)

    conn = get_db_conn(cfg)
    try:
        with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute("SELECT * FROM users WHERE user_id = %s", (user_id,))
            u = cur.fetchone()
            cur.execute("SELECT * FROM characters WHERE user_id = %s ORDER BY character_id", (user_id,))
            chars = cur.fetchall()
    finally:
        conn.close()

    uname = u["user_name"] if u else "gm_eric"
    slots = int(u["character_slots"]) if u else 3

    user_row = MachoObject("utillib.KeyVal", {
        "characterSlots": slots,
        "maxCharacterSlots": slots,
        "userName": UniStr(uname),
        "creationDate": now_ft - day_ticks * 365,
        "subscriptionEndTime": now_ft + day_ticks * 365
    })

    char_rows = []
    for c in chars:
        log_msg("[DB] Serving character %r (ID=%d, Balance=%s ISK, Ship=%d)" % (
            c["character_name"], c["character_id"], c["balance"], c["ship_type_id"]
        ))
        char_rows.append(MachoObject("utillib.KeyVal", {
            "characterID": int(c["character_id"]),
            "characterName": UniStr(c["character_name"]),
            "deletePrepareDateTime": None,
            "gender": int(c["gender"]),
            "accessoryID": None,
            "beardID": None,
            "costumeID": None,
            "decoID": None,
            "eyebrowsID": None,
            "eyesID": None,
            "hairID": None,
            "lipstickID": None,
            "makeupID": None,
            "skinID": None,
            "backgroundID": None,
            "lightID": None,
            "headMorph1": None,
            "headMorph2": None,
            "bloodlineID": int(c["bloodline_id"]),
            "raceID": int(c["race_id"]),
            "ancestryID": int(c["ancestry_id"]),
            "schoolID": int(c["school_id"]),
            "departmentID": int(c["department_id"]),
            "fieldID": int(c["field_id"]),
            "specialityID": int(c["speciality_id"]),
            "SolarSystemID": int(c["solar_system_id"]),
            "solarSystemID": int(c["solar_system_id"]),
            "stationID": int(c["station_id"]),
            "locationID": int(c["location_id"]),
            "corporationID": int(c["corporation_id"]),
            "allianceID": c["alliance_id"],
            "factionID": c["faction_id"],
            "createDateTime": datetime_to_filetime(c.get("created_at")) - day_ticks * 100,
            "logoffDate": now_ft - hour_ticks,
            "securityStatus": float(c["security_status"]),
            "balance": float(c["balance"]),
            "balanceChange": 0.0,
            "skillPoints": int(c["skill_points"]),
            "shipTypeID": int(c["ship_type_id"]),
            "unreadMailCount": 0,
            "unallocatedSP": 0,
            "paperdollState": 0,
            "finishedSkills": 0,
            "skillTypeID": None,
            "toLevel": None,
            "trainingStartTime": None,
            "trainingEndTime": None,
            "queueEndTime": None,
            "finishSP": None,
            "trainedSP": None,
            "lockTypeID": None,
            "petitionMessage": None
        }))

    return ([user_row], (None, None), char_rows, [])

def db_get_multi_owners(cfg, req_ids):
    conn = get_db_conn(cfg)
    try:
        with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute("SELECT character_id, character_name, gender FROM characters WHERE character_id = ANY(%s)", (list(req_ids),))
            found = {int(r["character_id"]): r for r in cur.fetchall()}
    finally:
        conn.close()

    header = ["ownerID", "ownerName", "typeID", "gender", "ownerNameID"]
    rows = []
    for oid in req_ids:
        if oid in found:
            rows.append([oid, UniStr(found[oid]["character_name"]), 1383, int(found[oid]["gender"]), None])
        else:
            rows.append([oid, UniStr("Game Masters"), 2, 0, None] if oid == 109299958 else ([oid, UniStr("C C P Alliance"), 16159, 0, None] if oid == 434243723 else [oid, UniStr("Owner %s" % oid), 1383, 1, None]))
    return (header, rows)

def db_get_multi_locations(cfg, req_ids):
    conn = get_db_conn(cfg)
    try:
        with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
            cur.execute("SELECT station_id, station_name, solar_system_id, solar_system_name FROM characters")
            loc_map = {}
            for r in cur.fetchall():
                loc_map[int(r["station_id"])] = (r["station_name"], int(r["solar_system_id"]))
                loc_map[int(r["solar_system_id"])] = (r["solar_system_name"], int(r["solar_system_id"]))
    finally:
        conn.close()

    header = ["locationID", "locationName", "solarSystemID", "x", "y", "z", "locationNameID"]
    rows = []
    for lid in req_ids:
        if lid in loc_map:
            lname, sys_id = loc_map[lid]
            rows.append([lid, UniStr(lname), sys_id, 0.0, 0.0, 0.0, None])
        else:
            rows.append([lid, UniStr("Dodixie IX - Moon 20 - Federation Navy Assembly Plant"), 30002659, 0.0, 0.0, 0.0, None])
    return (header, rows)

def extract_service_name(addr_obj):
    st = addr_obj.state
    if st[0] == 8:
        return st[1]
    return st[2]

def build_call_rsp(req_obj, return_value, client_id=10001, oob_dict=None):
    req_state  = req_obj.state
    req_source = req_state[1]
    req_dest   = req_state[2]
    user_id    = req_state[3]
    svc_name   = extract_service_name(req_dest)

    if isinstance(svc_name, str) and svc_name.startswith("<StrTableRef:"):
        svc_name = None

    if req_dest.state[0] == 8:
        rsp_source = MachoObject(
            "carbon.common.script.net.machoNetAddress.MachoAddress",
            (8, svc_name, None)
        )
    else:
        rsp_source = MachoObject(
            "carbon.common.script.net.machoNetAddress.MachoAddress",
            (1, 1, svc_name, None)
        )

    rsp_dest = MachoObject(
        "carbon.common.script.net.machoNetAddress.MachoAddress",
        (2, client_id, req_source.state[2], None)
    )
    rsp_state = (
        7,
        rsp_source,
        rsp_dest,
        user_id,
        (SubStream(return_value),),
        None,
        oob_dict,
        req_state[7],
        None, None, None, None, None, None
    )
    return MachoObject("carbon.common.script.net.machoNetPacket.CallRsp", rsp_state)

def build_ping_rsp(ping_req_obj, client_id=10001, oob_dict=None):
    req_state  = ping_req_obj.state
    req_source = req_state[1]
    user_id    = req_state[3]
    times_list = req_state[4][0] if isinstance(req_state[4], tuple) and len(req_state[4]) > 0 and isinstance(req_state[4][0], list) else []
    now_ft     = get_filetime_now()
    times_list.append((now_ft, now_ft, "proxy::handle_message"))
    times_list.append((now_ft, now_ft, "proxy::writing"))
    times_list.append((now_ft, now_ft, UniStr("server::handle_message")))
    times_list.append((now_ft, now_ft, UniStr("server::turnaround")))
    times_list.append((now_ft, now_ft, "proxy::handle_message"))
    times_list.append((now_ft, now_ft, "proxy::writing"))

    rsp_source = MachoObject(
        "carbon.common.script.net.machoNetAddress.MachoAddress",
        (8, None, None)
    )
    rsp_dest = MachoObject(
        "carbon.common.script.net.machoNetAddress.MachoAddress",
        (2, client_id, req_source.state[2], None)
    )
    rsp_state = (
        21,
        rsp_source,
        rsp_dest,
        user_id,
        (times_list,),
        None,
        None,
        req_state[7],
        None, None, None, None, None, None
    )
    return MachoObject("carbon.common.script.net.machoNetPacket.PingRsp", rsp_state)

def build_session_change_notification(cfg, user_id=1000001, client_id=10001, sid=1000001, role=7131450362141085952, user_type=30):
    src_addr = MachoObject(
        "carbon.common.script.net.machoNetAddress.MachoAddress",
        (1, 1, None, None)
    )
    dst_addr = MachoObject(
        "carbon.common.script.net.machoNetAddress.MachoAddress",
        (2, client_id, None, None)
    )
    pub_addr = str(cfg.get("public_address", "dystropia.com:26000"))
    change_dict = {
        "countryCode": (None, "US"),
        "languageID": (None, "EN"),
        "userid": (None, int(user_id)),
        "userType": (None, int(user_type)),
        "role": (None, long_type(role)),
        "address": (None, pub_addr)
    }
    pkt_state = (
        16,
        src_addr,
        dst_addr,
        int(user_id),
        (sid, (0, change_dict), [-1]),
        None,
        None,
        None,
        None, None, None, None, None, None
    )
    return MachoObject("carbon.common.script.net.machoNetPacket.SessionChangeNotification", pkt_state)

def handle_client(conn, addr, cfg):
    log_msg("[+] Connection accepted from %s:%d" % addr)
    conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    conn.settimeout(300.0)

    current_user = {
        "user_id": 1000001,
        "user_name": "gm_eric",
        "role": 7131450362141085952,
        "user_type": 30,
        "client_id": 10001
    }

    release_tag = "%s@%s" % (str(cfg["boot_codename"]), str(cfg["boot_region"]))
    llve_7tuple = (
        int(cfg["handshake_secret"]),
        int(cfg["macho_version"]),
        int(cfg["user_count"]),
        float(cfg["boot_version"]),
        int(cfg["boot_build"]),
        release_tag,
        None
    )

    try:
        payload = macho_marshal.dumps(llve_7tuple)
        macho_marshal.send_packet(conn, payload)
        log_msg("[>] Sent Server LLVE 7-Tuple: %r" % (llve_7tuple,))

        import itertools
        for step in itertools.count(1):
            pkt = macho_marshal.recv_packet(conn)
            if pkt is None:
                log_msg("[*] Client closed socket cleanly after step %d." % (step - 1))
                break

            decoded = macho_marshal.loads(pkt)
            log_msg("[<] Step %d - Received %d bytes from client: %r" % (step, len(pkt), decoded))

            if isinstance(decoded, tuple) and len(decoded) == 2 and decoded[1] == "QC":
                log_msg("[>] Replying to Queue Check ('QC') with queue position 0")
                macho_marshal.send_packet(conn, macho_marshal.dumps(0))
                log_msg("[>] Pre-sending fresh Server LLVE 7-Tuple for Connect reuse")
                macho_marshal.send_packet(conn, macho_marshal.dumps(llve_7tuple))

            elif isinstance(decoded, tuple) and len(decoded) == 3 and decoded[1] == "VK":
                log_msg("[*] Received VIP Key ('VK') for username hash: %r" % (decoded[2],))

            elif isinstance(decoded, tuple) and len(decoded) == 2 and decoded[0] == "placebo":
                log_msg("[>] Received Client Crypto Context ('placebo') -> Sending 'OK CC'")
                macho_marshal.send_packet(conn, macho_marshal.dumps("OK CC"))

            elif isinstance(decoded, tuple) and len(decoded) == 2 and isinstance(decoded[1], dict) and len(decoded[0]) == 64:
                client_challenge, req_dict = decoded
                raw_uname = req_dict.get("user_name", "gm_eric")
                pw_hash = req_dict.get("user_password_hash")
                log_msg("[*] Received Client Credentials for user_name=%r!" % (raw_uname,))

                db_user = db_authenticate_or_create_user(cfg, raw_uname, pw_hash)
                if not db_user:
                    log_msg("[!] Rejecting login at Step 6 for unknown user_name=%r!" % (raw_uname,))
                    macho_marshal.send_packet(conn, binascii.unhexlify("7e010000032214030239636172626f6e2e636f6d6d6f6e2e7363726970742e6e65742e475053457863657074696f6e732e4750535472616e73706f7274436c6f73656425530f4c6f67696e417574684661696c6564160b2e06636c69656e7413066f726967696e1600130a726561736f6e417267732f0890fbabcdc265dd011305636c6f636b0813116c6f676765644f6e55736572436f756e7413036363701306726567696f6e1b011306726561736f6e0ac3f5285c8f023840130776657273696f6e04eeb2370013056275696c6401130a726561736f6e436f646513065632342e30311308636f64656e616d6505f00111872d2d01000000"))
                    time.sleep(0.25)
                    break
                current_user["user_id"] = int(db_user["user_id"])
                current_user["user_name"] = str(db_user["user_name"])
                current_user["role"] = long_type(db_user["role"])
                current_user["user_type"] = int(db_user["user_type"])
                current_user["pw_hash_step4"] = pw_hash

                if pw_hash and not db_verify_or_set_password(cfg, current_user["user_id"], pw_hash):
                    log_msg("[!] Rejecting login at Step 6 for user_id=%d due to invalid password!" % current_user["user_id"])
                    macho_marshal.send_packet(conn, binascii.unhexlify("7e010000032214030239636172626f6e2e636f6d6d6f6e2e7363726970742e6e65742e475053457863657074696f6e732e4750535472616e73706f7274436c6f73656425530f4c6f67696e417574684661696c6564160b2e06636c69656e7413066f726967696e1600130a726561736f6e417267732f0890fbabcdc265dd011305636c6f636b0813116c6f676765644f6e55736572436f756e7413036363701306726567696f6e1b011306726561736f6e0ac3f5285c8f023840130776657273696f6e04eeb2370013056275696c6401130a726561736f6e436f646513065632342e30311308636f64656e616d6505f00111872d2d01000000"))
                    time.sleep(0.25)
                    break

                log_msg("[>] Sending passwordVersion = 2")
                macho_marshal.send_packet(conn, macho_marshal.dumps(2))

                server_challenge = b"\x00" * 64
                signed_func = (binascii.unhexlify(HANDSHAKE_CODE_HEX), 0)
                challenge_hash = placebo_crypto_hash(client_challenge)
                resp_dict = {
                    "challenge_responsehash": challenge_hash,
                    "macho_version": int(cfg["macho_version"]),
                    "boot_version": float(cfg["boot_version"]),
                    "boot_build": int(cfg["boot_build"]),
                    "boot_codename": str(cfg["boot_codename"]),
                    "boot_region": str(cfg["boot_region"]),
                    "cluster_usercount": int(cfg["user_count"]),
                    "proxy_nodeid": 1,
                    "user_logonqueueposition": 1,
                    "config_vals": {"imageserverurl": "https://images.evetech.net/"}
                }
                ch_tuple = (server_challenge, signed_func, {}, resp_dict)
                log_msg("[>] Sending Server Challenge 4-Tuple (challenge_responsehash=%s)" % challenge_hash)
                macho_marshal.send_packet(conn, macho_marshal.dumps(ch_tuple))

            elif isinstance(decoded, tuple) and len(decoded) == 3 and decoded[0] == "55087":
                log_msg("[*] Received Valid Client Challenge-Response! %r" % (decoded,))
                uid = current_user["user_id"]
                cid = current_user["client_id"]
                cand_hash = current_user.get("pw_hash_step4")
                for item in (decoded[1], decoded[2]):
                    if isinstance(item, (bytes, str)) and item != "55087" and len(item) >= 16:
                        cand_hash = item
                        break
                if not db_verify_or_set_password(cfg, uid, cand_hash):
                    log_msg("[!] Rejecting login at Step 7 for user_id=%d due to invalid password!" % uid)
                    macho_marshal.send_packet(conn, binascii.unhexlify("7e010000032214030239636172626f6e2e636f6d6d6f6e2e7363726970742e6e65742e475053457863657074696f6e732e4750535472616e73706f7274436c6f73656425530f4c6f67696e417574684661696c6564160b2e06636c69656e7413066f726967696e1600130a726561736f6e417267732f0890fbabcdc265dd011305636c6f636b0813116c6f676765644f6e55736572436f756e7413036363701306726567696f6e1b011306726561736f6e0ac3f5285c8f023840130776657273696f6e04eeb2370013056275696c6401130a726561736f6e436f646513065632342e30311308636f64656e616d6505f00111872d2d01000000"))
                    time.sleep(0.25)
                    break
                pub_addr = str(cfg.get("public_address", "dystropia.com:26000"))
                ack_dict = {
                    "sessionID": uid,
                    "user_clientid": cid,
                    "session_init": {
                        "userid": uid,
                        "role": current_user["role"],
                        "userType": current_user["user_type"],
                        "address": pub_addr,
                        "languageID": "EN",
                        "countryCode": "US"
                    },
                    "access_token": None,
                    "computer_hash": "dystropia_sandbox",
                    "client_hash": None
                }
                log_msg("[>] Sending Final Server Challenge-Response ACK for user_id=%d!" % uid)
                macho_marshal.send_packet(conn, macho_marshal.dumps(ack_dict))

            elif isinstance(decoded, MachoObject) and "PingReq" in decoded.class_name:
                ping_rsp = build_ping_rsp(decoded, current_user["client_id"])
                macho_marshal.send_packet(conn, macho_marshal.dumps(ping_rsp))
                log_msg("[>] Sent PingRsp for PingReq")

            elif isinstance(decoded, MachoObject) and "CallReq" in decoded.class_name:
                req_state = decoded.state
                service_name = extract_service_name(req_state[2])
                call_id = req_state[1].state[2]
                sub_tuple = req_state[4][0][1].value
                method_name = sub_tuple[1]
                call_args = sub_tuple[2] if len(sub_tuple) > 2 else ()
                log_msg("[*] RPC CallReq: service=%r, method=%r, args=%r, callID=%r" % (
                    service_name, method_name, call_args, call_id
                ))

                uid = current_user["user_id"]
                cid = current_user["client_id"]

                if method_name == "GetServiceInfo":
                    sess_pkt = build_session_change_notification(
                        cfg, user_id=uid, client_id=cid, sid=uid,
                        role=current_user["role"], user_type=current_user["user_type"]
                    )
                    macho_marshal.send_packet(conn, macho_marshal.dumps(sess_pkt))
                    log_msg("[>] Sent TQ-style SessionChangeNotification (userid=%d)" % uid)
                    rsp_val = {}
                elif method_name == "GetBlackListedPlanets":
                    rsp_val = []
                elif method_name == "GetOldStationData":
                    rsp_val = None
                elif method_name == "GetAverageMarketPrices":
                    rsp_val = {}
                elif method_name == "is_air_npe_enabled":
                    rsp_val = False
                elif method_name == "GetConnectedClusterName":
                    rsp_val = UniStr("DYSTROPIA")
                elif method_name == "GetItemDescriptor":
                    rsp_val = RawStream(ITEM_DESCRIPTOR_RAW)
                elif method_name == "GetTime":
                    rsp_val = get_filetime_now()
                elif method_name == "AmIBanned":
                    rsp_val = False
                elif method_name == "GetCloneGrade":
                    rsp_val = 1
                elif method_name == "BeanCount":
                    rsp_val = (1, "B")
                elif method_name == "GetLogModeForError":
                    rsp_val = ({}, [])
                elif method_name == "is_item_validation_enabled":
                    rsp_val = True
                elif method_name == "is_item_failure_enabled":
                    rsp_val = False
                elif method_name in ("set_item_validation_enabled", "set_item_failure_enabled"):
                    rsp_val = None
                elif method_name in ("SendClientStats", "LogClientCall", "SendClientStackTraceAlert", "BeanDelivery", "GroupBeanDelivery"):
                    rsp_val = (1, "B") if method_name == "SendClientStackTraceAlert" else None
                elif method_name == "GetCharacterSelectionData":
                    rsp_val = db_get_character_selection_data(cfg, uid)
                elif method_name == "GetMultiOwnersEx":
                    req_ids = call_args[0] if call_args else [90000001]
                    rsp_val = db_get_multi_owners(cfg, req_ids)
                elif method_name == "GetMultiLocationsEx":
                    req_ids = call_args[0] if call_args else []
                    rsp_val = db_get_multi_locations(cfg, req_ids)
                elif method_name == "GetMultiCorpTickerNamesEx":
                    req_ids = call_args[0] if call_args else []
                    header = ["corporationID", "tickerName", "shape1", "shape2", "shape3", "color1", "color2", "color3"]
                    rows = [[c_id, UniStr("GMS"), None, None, None, None, None, None] for c_id in req_ids]
                    rsp_val = (header, rows)
                elif method_name == "GetMultiAllianceShortNamesEx":
                    req_ids = call_args[0] if call_args else []
                    header = ["allianceID", "shortName"]
                    rows = [[a_id, UniStr("C C P")] for a_id in req_ids]
                    rsp_val = (header, rows)
                elif method_name == "GetSecurityModifiedSystems":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {
                        "header": ["solarSystemID", "security", "modifiedSecurity"],
                        "lines": []
                    })
                elif method_name == "get_active_campaign":
                    rsp_val = (None, None, None, None)
                elif method_name == "get_client_campaign_state":
                    rsp_val = None
                elif method_name == "GetRedeemTokens":
                    rsp_val = []
                elif method_name == "GetCharOmegaDowngradeStatus":
                    rsp_val = False
                elif method_name in ("Ping", "GetCharacterLockType", "LogClientStats", "LogString", "LogPlayerRequestedDisconnect", "UpdateClientAchievmentsAndCounters", "ProcessCharacterLogon", "GetDeprecatedPrefsFallback", "get_season_data_for_character", "get_air_npe_state", "UpdateLocalizationQAWrap", "SetSessionCheck", "AddFolder", "GetPaperDollPortraitDataFor"):
                    rsp_val = None
                elif method_name == "MachoResolveObject":
                    rsp_val = 1
                elif method_name in ("GetCharacterLoadSlot", "GetSkills", "GetWars", "get_modified_systems", "GetCharacterSettings", "GetMyDockableStructures", "GetTestingConfiguration", "GetMultiCharactersTrainingSlots", "GetPaperDollDataBatch", "GetAvailableTurretSlots", "GetCohortsForUser", "GetAvailableNonDiminishingInjections", "GetBoosters", "GetEnabledCosmetics", "GetCorporateContacts", "GetAllianceContacts"):
                    rsp_val = {}
                elif method_name == "GetCompletedAchievementsAndClientEventCount":
                    rsp_val = {"completed": {}, "eventCount": {}, "completedDict": {}, "eventDict": {}, "taskIds": {}, "counterValues": {}}
                elif method_name == "initialize_tutorial_status":
                    rsp_val = ({}, {})
                elif method_name in ("GetSkillQueueAndFreePoints", "GetSkillQueue"):
                    rsp_val = ([], 0)
                elif method_name == "GetMyJournalDetails":
                    rsp_val = ([], [])
                elif method_name == "GetFightersForShip":
                    rsp_val = ([], [], {})
                elif method_name == "GetMyActiveBookmarks":
                    rsp_val = ([], [], [])
                elif method_name in ("GetMyCorporationsOffices", "GetEventList", "GetAllVisibleCampaigns", "get_categories", "get_offers", "GetGuests", "GetSkillHistory", "GetAllNotifications", "CheckAndSendNotifications"):
                    rsp_val = []
                elif method_name == "GetLoginInfo":
                    rsp_val = MachoObject("utillib.KeyVal", {"assignedToMe": [], "needsAttention": [], "inProgress": [], "numOutstandingContractsNonCorp": 0, "numOutstandingContractsForCorp": 0, "numContractsRequiringAttention": 0, "numCorpContractsRequiringAttention": 0})
                elif False:
                    rsp_val = []
                elif method_name == "GetPublicInfo":
                    if (call_args and call_args[0] == 109299958) or extract_service_name(decoded.state[2]) == "corpmgr":
                        rsp_val = MachoObject("utillib.KeyVal", {"corporationID": 109299958, "corporationName": UniStr("Game Masters"), "tickerName": UniStr("GMS"), "ceoID": 90000001, "creatorID": 90000001, "stationID": 60011866, "taxRate": 0.0, "loyaltyPointTaxRate": 0.0, "minCorporationTaxAmount": 0.0, "allianceID": 434243723, "shares": 500, "memberCount": 375, "url": UniStr("https://support.eveonline.com"), "description": UniStr("Member of the CCP Customer Support Team."), "deleted": 0, "allowWar": 1, "isRecruiting": 0, "warFactionID": None, "militiaFactionID": None, "shape1": None, "shape2": None, "shape3": None, "color1": None, "color2": None, "color3": None, "typeface": None})
                    else:
                        rsp_val = MachoObject("utillib.KeyVal", {"corporationID": 109299958, "allianceID": 434243723, "raceID": 8, "bloodlineID": 7, "ancestryID": 19, "gender": 1, "characterName": UniStr("GM Eric"), "createDateTime": get_filetime_now(), "schoolID": 17, "description": UniStr("CCP Game Master"), "deleted": 0, "shares": 500, "militiaFactionID": None, "warFactionID": None})
                elif method_name == "GetPublicInfo3":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["bounty", "title", "startDateTime", "description", "corporationID", "raceID", "bloodlineID", "ancestryID", "securityStatus", "gender", "characterName", "createDateTime", "schoolID", "militiaFactionID"], "lines": [[0.0, UniStr("Game Master"), get_filetime_now(), UniStr("Member of the CCP Customer Support Team."), 109299958, 8, 7, 19, 5.0, 1, UniStr("GM Eric"), get_filetime_now(), 17, None]]})
                elif method_name == "GetInfoWindowDataForChar":
                    rsp_val = MachoObject("utillib.KeyVal", {"corpID": 109299958, "allianceID": 434243723, "title": UniStr("Game Master"), "militiaFactionID": None, "warFactionID": None})
                elif method_name == "GetCharacterSecurityStatus":
                    rsp_val = 5.0
                elif method_name == "GetMedalsReceived":
                    rsp_val = ([], [])
                elif method_name in ("GetStationSolarSystemsByOwner", "GetCorpAllowedEnlistmentFactions", "GetCorpInfo"):
                    rsp_val = []
                elif method_name in ("GetWarsByOwnerID", "GetFactionWarsForWarFactionID", "GetMyApplications", "GetApplications"):
                    rsp_val = {}
                elif method_name == "GetAggressionSettings":
                    rsp_val = MachoObject("crimewatch.corp_aggression.settings.AggressionSettings", {"_enableAfter": 0, "_disableAfter": None})
                elif method_name == "GetEveOwners":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["ownerID", "ownerName", "typeID", "gender", "ownerNameID"], "lines": [[90000001, UniStr("GM Eric"), 1373, 1, None]]})
                elif method_name == "GetEmployementRecordAndCharacterTransfers":
                    emp_rs = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["corporationID", "startDate", "deleted"], "lines": [[109299958, 128974960800000000, 0]]})
                    rsp_val = (emp_rs, [])
                elif method_name == "GetAllianceMembers":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["corporationID", "startDate", "deleted"], "lines": [[109299958, 128974960800000000, 0]]})
                elif method_name == "GetEmploymentRecord":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["corporationID", "startDate", "deleted"], "lines": [[109299958, get_filetime_now(), 0]]})
                elif method_name == "GetAlliancePublicInfo":
                    rsp_val = MachoObject("utillib.KeyVal", {"allianceID": 434243723, "allianceName": UniStr("C C P Alliance"), "shortName": UniStr("C C P"), "executorCorpID": 109299958, "creatorCorpID": 109299958, "creatorCharID": 90000001, "dictatorial": 0, "description": UniStr("Official CCP alliance."), "url": UniStr("https://www.eveonline.com"), "startDate": 128974960800000000, "deleted": 0, "allowWar": 1, "currentCapital": None, "warFactionID": None, "currentPrimeHour": 0, "newPrimeHour": 0, "newPrimeHourValidAfter": 0})
                elif method_name in ("GetItemKillCountPlayer", "GetKillsRecent"):
                    rsp_val = 0
                elif method_name == "GetCorporation":
                    rsp_val = MachoObject("carbon.common.script.sys.row.Row",{"header":["corporationID","corporationName","tickerName","ceoID","creatorID","stationID","taxRate","loyaltyPointTaxRate","minCorporationTaxAmount","allianceID","shares","memberCount","url","description","shape1","shape2","shape3","color1","color2","color3","typeface","isRecruiting","warFactionID","militiaFactionID","allowWar","deleted"],"line":[109299958,UniStr("Game Masters"),UniStr("GMS"),90000001,90000001,60011866,0.0,0.0,0.0,434243723,500,375,UniStr("https://support.eveonline.com"),UniStr("Member of the CCP Customer Support Team."),None,None,None,None,None,None,None,0,None,None,1,0]})
                elif method_name == "GetAlliance":
                    rsp_val = MachoObject("carbon.common.script.sys.row.Row",{"header":["allianceID","allianceName","shortName","executorCorpID","creatorCorpID","creatorCharID","dictatorial","description","url","startDate","deleted","allowWar","warFactionID"],"line":[434243723,UniStr("C C P Alliance"),UniStr("C C P"),109299958,109299958,90000001,0,UniStr("Official CCP alliance."),UniStr("https://www.eveonline.com"),128974960800000000,0,1,None]})
                elif method_name == "GetLimits":
                    rsp_val = {"maxBlueprintResults": 25000}
                elif method_name == "GetInsurancePrice":
                    rsp_val = 0.0
                elif method_name == "GetDirtTimestamp":
                    rsp_val = get_filetime_now()
                elif method_name == "GetStationItemBits":
                    rsp_val = (1000120, 60011866, 26, 3867)
                elif method_name == "GetStation":
                    rsp_val = MachoObject("utillib.KeyVal", {"stationID": 60011866, "security": 0.9, "dockingCostPerVolume": 0.0, "maxShipVolumeDockable": 50000000.0, "officeRentalCost": 10000, "operationID": 26, "stationTypeID": 3867, "corporationID": 1000120, "ownerID": 1000120, "solarSystemID": 30002659, "solarsystemID": 30002659, "constellationID": 20000389, "regionID": 10000032, "stationName": UniStr("Dodixie IX - Moon 20 - Federation Navy Assembly Plant"), "x": -107303362560.0, "y": -18744975360.0, "z": 436489052160.0, "reprocessingEfficiency": 0.5, "reprocessingStationsTake": 0.05, "reprocessingHangarFlag": 4, "orbitID": 40169171, "serviceMask": 26, "hangarGraphicID": 20, "stationServices": []})
                elif method_name == "GetSelfInvItem":
                    call_oid = sub_tuple[0] if isinstance(sub_tuple[0], str) else None
                    if call_oid in SHIP_INV_OIDS:
                        rsp_val = make_dbrow([100000001, 9860, 90000001, 60011866, 4, -1, 237, 6, ""])
                    else:
                        rsp_val = make_dbrow([60011866, 3867, 1000120, 30002659, 0, -1, 15, 3, ""])
                elif method_name == "GetItem":
                    item_id = call_args[0] if call_args else 100000001
                    line = [90000001, 1373, 1000001, 60011866, 57, -1, 1, 1, ""] if item_id == 90000001 else [100000001, 9860, 90000001, 60011866, 4, -1, 237, 6, ""]
                    rsp_val = make_dbrow(line)
                elif method_name == "List":
                    call_oid = sub_tuple[0] if isinstance(sub_tuple[0], str) else None
                    if call_oid in SHIP_INV_OIDS:
                        rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": INV_COLS_11, "lines": []})
                    else:
                        rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": INV_COLS_11, "lines": [[100000001, 9860, 90000001, 60011866, 4, -1, 237, 6, "", 1, 1]]})
                elif method_name == "GetTimers":
                    rsp_val = (None, None, None)
                elif method_name == "GetLocalOccupationState":
                    rsp_val = (30002659, None)
                elif method_name == "can_character_play_the_tutorial":
                    rsp_val = False
                elif method_name == "GetSettingsInfo":
                    rsp_val = (binascii.unhexlify(HANDSHAKE_CODE_HEX), 1)
                elif method_name == "Hostname":
                    rsp_val = MachoObject("utillib.KeyVal", {"hostname": "localhost", "port": 5222})
                elif method_name == "GetNPCNPCStandings":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["fromID", "toID", "standing"], "lines": []})
                elif method_name in ("GetCharStandings", "GetCorpStandings") or "Standings" in str(method_name):
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["fromID", "standing"], "lines": []})
                elif method_name == "GetInitialState":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["contactID", "online"], "lines": []})
                elif method_name == "GetContactList":
                    if extract_service_name(decoded.state[2]) == "charMgr":
                        empty_rs = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["contactID", "inWatchlist", "relationshipID", "labelMask"], "lines": []})
                        rsp_val = MachoObject("utillib.KeyVal", {"addresses": empty_rs, "blocked": empty_rs})
                    else:
                        rsp_val = {}
                elif method_name == "GetAgents":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["agentID", "agentTypeID", "divisionID", "level", "stationID", "corporationID", "factionID", "solarsystemID", "solarSystemID", "bloodlineID", "gender", "quality"], "lines": []})
                elif method_name == "GetEveOwners":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["ownerID", "ownerName", "typeID", "gender", "ownerNameID"], "lines": [[90000001, UniStr("GM Eric"), 1373, 1, None], [109299958, UniStr("Game Masters"), 2, 0, None], [434243723, UniStr("C C P Alliance"), 16159, 0, None]]})
                elif method_name == "GetStationInfo":
                    rsp_val = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["stationID", "security", "dockingCostPerVolume", "maxShipVolumeDockable", "officeRentalCost", "operationID", "stationTypeID", "corporationID", "solarSystemID", "constellationID", "regionID", "stationName", "x", "y", "z", "reprocessingEfficiency", "reprocessingStationsTake", "reprocessingHangarFlag", "orbitID"], "lines": [[60011866, 0.9, 0.0, 50000000.0, 10000, 26, 3867, 1000120, 30002659, 20000389, 10000032, UniStr("Dodixie IX - Moon 20 - Federation Navy Assembly Plant"), -107303362560.0, -18744975360.0, 436489052160.0, 0.5, 0.05, 4, 40169171]]})
                elif method_name in ("GetMySkillHandler", "GetInventory", "GetInventoryFromId"):
                    oid = "N=1:%d" % (getattr(handle_client, "seq", 100) + 1)
                    handle_client.seq = getattr(handle_client, "seq", 100) + 1
                    ref = get_filetime_now()
                    pkt = build_call_rsp(decoded, BoundObject(oid, ref), cid, {"OID+": {oid: ref}})
                    macho_marshal.send_packet(conn, macho_marshal.dumps(pkt))
                    log_msg("[>] Sent %s BoundObject oid=%s" % (method_name, oid))
                    continue
                elif method_name == "MachoBindObject":
                    oid = "N=1:%d" % (getattr(handle_client, "seq", 100) + 1)
                    handle_client.seq = getattr(handle_client, "seq", 100) + 1
                    ref = get_filetime_now()
                    oids_map = {oid: ref}
                    pb = call_args[1] if len(call_args) > 1 else None
                    pb_name = pb[0] if (pb and isinstance(pb, tuple)) else None
                    if pb_name == "GetCharacterLoadSlot":
                        pb_ret = {}
                    elif pb_name == "GetCorporation":
                        pb_ret = MachoObject("carbon.common.script.sys.row.Row",{"header":["corporationID","corporationName","tickerName","ceoID","creatorID","stationID","taxRate","loyaltyPointTaxRate","minCorporationTaxAmount","allianceID","shares","memberCount","url","description","shape1","shape2","shape3","color1","color2","color3","typeface","isRecruiting","warFactionID","militiaFactionID","allowWar","deleted"],"line":[109299958,UniStr("Game Masters"),UniStr("GMS"),90000001,90000001,60011866,0.0,0.0,0.0,434243723,500,375,UniStr("https://support.eveonline.com"),UniStr("Member of the CCP Customer Support Team."),None,None,None,None,None,None,None,0,None,None,1,0]})
                    elif pb_name == "GetAlliance":
                        pb_ret = MachoObject("carbon.common.script.sys.row.Row",{"header":["allianceID","allianceName","shortName","executorCorpID","creatorCorpID","creatorCharID","dictatorial","description","url","startDate","deleted","allowWar","warFactionID"],"line":[434243723,UniStr("C C P Alliance"),UniStr("C C P"),109299958,109299958,90000001,0,UniStr("Official CCP alliance."),UniStr("https://www.eveonline.com"),128974960800000000,0,1,None]})
                    elif pb_name == "GetInsurancePrice":
                        pb_ret = 0.0
                    elif pb_name == "GetCharacterSecurityStatus":
                        pb_ret = 5.0
                    elif pb_name == "GetContactList":
                        pb_ret = {}
                    elif pb_name == "GetAggressionSettings":
                        pb_ret = MachoObject("crimewatch.corp_aggression.settings.AggressionSettings", {"_enableAfter": 0, "_disableAfter": None})
                    elif pb_name == "GetEveOwners":
                        pb_ret = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["ownerID", "ownerName", "typeID", "gender", "ownerNameID"], "lines": [[90000001, UniStr("GM Eric"), 1373, 1, None]]})
                    elif pb_name == "GetDirtTimestamp":
                        pb_ret = ref
                    elif pb_name == "GetAllInfo":
                        c_it = make_dbrow([90000001, 1373, 1000001, 60011866, 57, -1, 1, 1, ""])
                        s_it = make_dbrow([100000001, 9860, 90000001, 60011866, 4, -1, 237, 6, ""])
                        c_inf = MachoObject("utillib.KeyVal", {"itemID": 90000001, "invItem": c_it, "activeEffects": {}, "attributes": {164: 20.0, 165: 20.0, 166: 20.0, 167: 20.0, 168: 20.0}, "time": ref, "wallclockTime": ref, "description": ""})
                        s_inf = MachoObject("utillib.KeyVal", {"itemID": 100000001, "invItem": s_it, "activeEffects": {}, "attributes": {}, "time": ref, "wallclockTime": ref, "description": ""})
                        pb_ret = MachoObject("utillib.KeyVal", {"structureInfo": None, "shipInfo": {100000001: s_inf}, "charInfo": ({90000001: c_inf}, (1, [], [], [])), "shipModifiedCharAttribs": None, "systemWideEffectsOnShip": None, "shipState": ({}, {}, {}, {})})
                    elif pb_name == "GetClientStates":
                        pb_ret = (((0, None), (0, None), (0, None), (0, None), (0, None)), {}, ([], []), 0)
                    elif pb_name in ("GetInventory", "GetInventoryFromId"):
                        oid2 = "N=1:%d" % (getattr(handle_client, "seq", 100) + 1)
                        handle_client.seq = getattr(handle_client, "seq", 100) + 1
                        oids_map[oid2] = ref
                        if pb_name == "GetInventoryFromId" and len(pb) > 1 and isinstance(pb[1], tuple) and len(pb[1]) > 0 and pb[1][0] == 100000001:
                            SHIP_INV_OIDS.add(oid2)
                        pb_ret = BoundObject(oid2, ref)
                    else:
                        pb_ret = None
                    pkt = build_call_rsp(decoded, (BoundObject(oid, ref), pb_ret), cid, {"OID+": oids_map})
                    macho_marshal.send_packet(conn, macho_marshal.dumps(pkt))
                    log_msg("[>] Sent MachoBindObject CallRsp oids=%r (pb=%r)" % (list(oids_map.keys()), pb_name))
                    continue
                elif method_name == "SelectCharacterID":
                    cid_sel = call_args[0] if call_args else 90000001
                    chg = {"charid":(None,cid_sel),"corpid":(None,109299958),"allianceid":(None,434243723),"stationid":(None,60011866),"stationid2":(None,60011866),"locationid":(None,60011866),"solarsystemid2":(None,30002659),"constellationid":(None,20000389),"regionid":(None,10000032),"shipid":(None,100000001),"hqID":(None,60011866),"baseID":(None,60011866),"corprole":(None,0),"rolesAtAll":(None,0),"rolesAtHQ":(None,0),"rolesAtBase":(None,0),"rolesAtOther":(None,0),"genderID":(None,1),"raceID":(None,8),"bloodlineID":(None,7),"ancestryID":(None,19)}
                    src = MachoObject("carbon.common.script.net.machoNetAddress.MachoAddress", (1, 1, None, None))
                    dst = MachoObject("carbon.common.script.net.machoNetAddress.MachoAddress", (2, cid, None, None))
                    scn = MachoObject("carbon.common.script.net.machoNetPacket.SessionChangeNotification", (16, src, dst, uid, (uid, (0, chg), [1]), None, None, None, None, None, None, None, None, None))
                    macho_marshal.send_packet(conn, macho_marshal.dumps(scn))
                    log_msg("[>] Sent Character SessionChangeNotification for %s" % cid_sel)
                    rsp_val = None
                else:
                    log_msg("[!] Unhandled RPC method %r -> returning None" % method_name)
                    rsp_val = None

                rsp_pkt = build_call_rsp(decoded, rsp_val, cid)
                macho_marshal.send_packet(conn, macho_marshal.dumps(rsp_pkt))
                log_msg("[>] Sent CallRsp for %s (callID=%r)" % (method_name, call_id))

    except socket.timeout:
        log_msg("[*] Connection idle timeout (300s).")
    except Exception as e:
        import traceback
        log_msg("[!] Exception in client handler: %s\n%s" % (e, traceback.format_exc()))
    finally:
        conn.close()
        log_msg("[-] Connection closed for %s:%d\n" % addr)

def main():
    cfg = load_config()
    host = str(cfg["host"])
    port = int(cfg["port"])

    conn = get_db_conn(cfg)
    conn.close()

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((host, port))
    server.listen(10)

    print("=================================================================")
    print(" DYSTROPIA.COM MachoNet + PostgreSQL Server Online (Python %s)" % sys.version.split()[0])
    print(" Database  : PostgreSQL (%s)" % cfg.get("db", {}).get("dbname", "ericgw_dystropia"))
    print(" Listening : %s:%d (Public: %s)" % (host, port, cfg.get("public_address", "dystropia.com:26000")))
    print(" Build     : %d (MachoVersion %d | %s@%s)" % (
        cfg["boot_build"], cfg["macho_version"], cfg["boot_codename"], cfg["boot_region"]
    ))
    print("=================================================================")
    sys.stdout.flush()

    while True:
        conn, addr = server.accept()
        t = threading.Thread(target=handle_client, args=(conn, addr, cfg))
        t.daemon = True
        t.start()

if __name__ == "__main__":
    main()
