import logging
from net import macho_marshal
from net.macho_marshal import MachoObject, BoundObject, RawStream, UniStr, long_type
from net.packets import (placebo_crypto_hash, 
    HANDSHAKE_CODE_BYTES, ITEM_DESCRIPTOR_RAW, SLASH_USERERROR_BYTES,
    INV_COLS_11, make_dbrow, get_filetime_now, build_call_rsp, build_error_rsp,
    build_session_change_notification, extract_service_name
)
import db

logger = logging.getLogger("dystropia")

# Static return tables for fast O(1) lookup
NONE_METHODS = {
    "GetOldStationData", "set_item_validation_enabled", "set_item_failure_enabled",
    "SendClientStats", "LogClientCall", "BeanDelivery", "GroupBeanDelivery",
    "get_client_campaign_state", "Ping", "GetCharacterLockType", "LogClientStats",
    "LogString", "LogPlayerRequestedDisconnect", "UpdateClientAchievmentsAndCounters",
    "ProcessCharacterLogon", "get_season_data_for_character",
    "get_air_npe_state", "UpdateLocalizationQAWrap", "SetSessionCheck", "AddFolder",
    "GetContractForShip", "GetMember", "GetMyCharacterRankInfo", "GetCorporationWarFactionID",
    "get_next_remote_change_time", "SetCharacterDescription", "GetCynoJammerState", "GetInfrastructureHubInfo",
}

EMPTY_DICT_METHODS = {
    "GetAverageMarketPrices", "GetCharacterLoadSlot", "GetWars", "get_modified_systems",
    "GetCharacterSettings", "GetMyDockableStructures", "GetTestingConfiguration",
    "GetMultiCharactersTrainingSlots", "GetAvailableTurretSlots", "GetCohortsForUser",
    "GetAvailableNonDiminishingInjections", "GetBoosters", "GetMyExpertSystems",
    "GetEnabledCosmetics", "GetCorporateContacts", "GetAllianceContacts",
    "GetAllInterferenceBands", "GetWarsByOwnerID", "GetFactionWarsForWarFactionID",
    "GetMyApplications", "GetApplications", "GetDevelopmentIndicesForSystem",
}

EMPTY_LIST_METHODS = {
    "GetBlackListedPlanets", "GetMyApparel", "GetRedeemTokens", "GetSkillChangesForISIS",
    "GetMyCorporationsOffices", "GetEventList", "GetAllVisibleCampaigns", "get_categories",
    "get_offers", "GetGuests", "GetSkillHistory", "GetAllNotifications",
    "CheckAndSendNotifications", "GetStandingTransactions", "GetLicencedSkins",
    "GetLicensedSkins", "GetLicencedSkinsForShipType", "GetLicensedSkinsForShipType",
    "GetSecurityStatusTransactions", "GetStationSolarSystemsByOwner",
    "GetCorpAllowedEnlistmentFactions", "GetCorpInfo", "GetSovStructuresInfoForLocalSolarSystem",
}

EPIC_AGENTS = [
    [3019415, 7, 24, 4, 60008494, 1000084, 500003, 30002187, 30002187, 5, 1, 20],
    [3019396, 7, 24, 4, 60003760, 1000020, 500001, 30000142, 30000142, 1, 0, 20],
    [3019412, 7, 24, 4, 60011866, 1000111, 500004, 30002659, 30002659, 7, 1, 20],
    [3019411, 7, 24, 4, 60004588, 1000051, 500002, 30002510, 30002510, 4, 1, 20],
    [3019467, 7, 24, 3, 60013762, 1000124, 500011, 30000903, 30000903, 4, 1, 20],
    [3019465, 7, 24, 3, 60005251, 1000057, 500011, 30000903, 30000903, 4, 1, 20],
    [3019466, 7, 24, 3, 60013762, 1000124, 500011, 30000903, 30000903, 4, 0, 20],
    [3019460, 7, 24, 3, 60009364, 1000091, 500011, 30000903, 30000903, 5, 1, 20],
    [3019463, 7, 24, 3, 60014437, 1000127, 500010, 30001269, 30001269, 1, 1, 20],
    [3019462, 7, 24, 3, 60010600, 1000107, 500010, 30001269, 30001269, 7, 0, 20],
    [3019464, 7, 24, 3, 60014437, 1000127, 500010, 30001269, 30001269, 1, 0, 20],
    [3019457, 7, 24, 3, 60000964, 1000009, 500010, 30001269, 30001269, 1, 1, 20],
    [3019356, 7, 24, 1, 60011893, 1000130, 500016, 30002718, 30002718, 7, 0, 20],
]

def _build_gm_corp_row():
    return 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": [
            216121397, UniStr("Game Masters"), UniStr("GMS"), 90000001, 90000001,
            60014842, 0.0, 0.0, 0.0, 434243723, 500, 375,
            UniStr("https://support.eveonline.com"),
            UniStr("This is CCP's official Game Master corporation. Note that GMs can only be reached through support tickets, to contact us press F12 and open our help center.  In-game EVE-mails will not be answered."),
            None, None, None, None, None, None, None, 0, None, None, 1, 0
        ]
    })

def _build_ccp_alliance_row():
    return 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"), 216121397, 216121397,
            90000001, 0,
            UniStr("This is the official CCP alliance. It does not accept member corporations beyond the three official CCP corporations."),
            UniStr("http://www.eveonline.com"), 127307526000000000, 0, 1, None
        ]
    })

def _build_implants_dict():
    return {
        1: MachoObject("utillib.KeyVal", {"itemID": 990000001, "typeID": 10217, "implantness": 1}),
        2: MachoObject("utillib.KeyVal", {"itemID": 990000002, "typeID": 10209, "implantness": 2}),
        3: MachoObject("utillib.KeyVal", {"itemID": 990000003, "typeID": 10222, "implantness": 3}),
        4: MachoObject("utillib.KeyVal", {"itemID": 990000004, "typeID": 10213, "implantness": 4}),
        5: MachoObject("utillib.KeyVal", {"itemID": 990000005, "typeID": 10226, "implantness": 5}),
        6: MachoObject("utillib.KeyVal", {"itemID": 990000006, "typeID": 33329, "implantness": 6}),
    }

def dispatch_rpc(session, service_name, method_name, call_args, call_oid=None):
    """Evaluates an RPC method (either direct or piggybacked on MachoBindObject)."""
    if method_name in NONE_METHODS:
        return None
    if method_name in EMPTY_DICT_METHODS:
        return {}
    if method_name in EMPTY_LIST_METHODS:
        return []

    cfg = session.cfg
    uid = session.user_id
    cid = session.char_id

    if method_name == "GetServiceInfo":
        pub_addr = str(cfg.get("public_address", "dystropia.com:26000"))
        chg = {
            "countryCode": (None, "US"),
            "languageID": (None, "EN"),
            "userid": (None, int(uid)),
            "userType": (None, int(session.user_type)),
            "role": (None, long_type(session.role)),
            "address": (None, pub_addr),
        }
        sess_pkt = build_session_change_notification(uid, session.client_id, chg, [-1])
        macho_marshal.send_packet(session.conn, macho_marshal.dumps(sess_pkt))
        logger.info("[>] Sent TQ-style SessionChangeNotification (userid=%d)", uid)
        return {}

    elif method_name in ("is_air_npe_enabled", "AmIBanned", "is_item_failure_enabled", "GetCharOmegaDowngradeStatus", "can_character_play_the_tutorial"):
        return False
    elif method_name == "is_item_validation_enabled":
        return True
    elif method_name in ("GetCloneGrade", "GetPaperdollState", "MachoResolveObject"):
        return 1
    elif method_name in ("GetItemKillCountPlayer", "GetKillsRecent"):
        return 0
    elif method_name == "GetInsurancePrice":
        return 0.0
    elif method_name in ("GetCharacterSecurityStatus", "GetMySecurityStatus"):
        return 5.0
    elif method_name == "GetConnectedClusterName":
        return UniStr("DYSTROPIA")
    elif method_name == "GetItemDescriptor":
        return RawStream(ITEM_DESCRIPTOR_RAW)
    elif method_name in ("GetTime", "GetDirtTimestamp"):
        return get_filetime_now()
    elif method_name in ("BeanCount", "SendClientStackTraceAlert"):
        return (1, "B")
    elif method_name == "GetLogModeForError":
        return ({}, [])
    elif method_name in ("GetMyPaperDollData", "GetPaperDollData"):
        return db.build_paperdoll_dna_keyval(cid)
    elif method_name == "GetPaperDollDataBatch":
        req_ids = call_args[0] if (call_args and isinstance(call_args[0], (list, tuple))) else [cid]
        return {int(r_id): db.build_paperdoll_dna_keyval(int(r_id)) for r_id in req_ids}
    elif method_name == "GetPaperDollPortraitDataFor":
        target_cid = int(call_args[0]) if call_args and isinstance(call_args[0], int) else cid
        pkv = db.build_portrait_data_keyval(target_cid)
        return ([pkv], pkv)
    elif method_name in ("UpdateExistingCharacterFull", "UpdateExistingCharacterLimited"):
        d_info = call_args[1] if len(call_args) > 1 else None
        p_info = call_args[2] if len(call_args) > 2 else None
        db.save_paperdoll_from_rpc(cid, d_info, p_info)
        return None
    elif method_name == "GetCharacterSelectionData":
        return db.db_get_character_selection_data(cfg, uid)
    elif method_name == "GetCharacterToSelect":
        _, _, char_rows, _ = db.db_get_character_selection_data(cfg, uid)
        return [char_rows[0]] if char_rows else []
    elif method_name == "SelectCharacterID":
        cid_sel = int(call_args[0]) if call_args else cid
        db.db_load_character_into_session(session, cid_sel)
        chg = {
            "charid": (None, session.char_id),
            "corpid": (None, session.corp_id),
            "allianceid": (None, session.alliance_id),
            "stationid": (None, session.station_id),
            "stationid2": (None, session.station_id),
            "locationid": (None, session.station_id),
            "solarsystemid2": (None, session.solarsystem_id),
            "constellationid": (None, session.constellation_id),
            "regionid": (None, session.region_id),
            "shipid": (None, session.ship_id),
            "hqID": (None, session.station_id),
            "baseID": (None, session.station_id),
            "corprole": (None, 0),
            "rolesAtAll": (None, 0),
            "rolesAtHQ": (None, 0),
            "rolesAtBase": (None, 0),
            "rolesAtOther": (None, 0),
            "genderID": (None, session.gender),
            "raceID": (None, session.race_id),
            "bloodlineID": (None, session.bloodline_id),
            "ancestryID": (None, session.ancestry_id),
        }
        scn = build_session_change_notification(uid, session.client_id, chg, [1])
        macho_marshal.send_packet(session.conn, macho_marshal.dumps(scn))
        logger.info("[>] Sent Character SessionChangeNotification for %d (%s)", session.char_id, session.char_name)
        return None
    elif method_name == "GetMultiOwnersEx":
        req_ids = call_args[0] if call_args else [cid]
        return db.db_get_multi_owners(cfg, req_ids)
    elif method_name == "GetMultiLocationsEx":
        req_ids = call_args[0] if call_args else []
        return db.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]
        return (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]
        return (header, rows)
    elif method_name == "GetSecurityModifiedSystems":
        return MachoObject("eve.common.script.sys.rowset.Rowset", {
            "header": ["solarSystemID", "security", "modifiedSecurity"], "lines": []
        })
    elif method_name == "get_active_campaign":
        return (None, None, None, None)
    elif method_name in ("GetSkills", "GetAllSkills"):
        skills_dict, _, _ = db.db_get_character_skills(session)
        return skills_dict
    elif method_name == "GetFreeSkillPoints":
        _, free_sp, _ = db.db_get_character_skills(session)
        return free_sp
    elif method_name == "GetSkillPoints":
        _, _, total_sp = db.db_get_character_skills(session)
        return total_sp
    elif method_name in ("GetSkillQueueAndFreePoints", "GetSkillQueue"):
        return ([], 0)
    elif method_name == "GetAttributes":
        return {164: 27, 165: 27, 166: 27, 167: 27, 168: 27}
    elif method_name == "GetRespecInfo":
        return MachoObject("utillib.KeyVal", {"nextTimedRespec": get_filetime_now(), "freeRespecs": 3})
    elif method_name == "GetImplants":
        return _build_implants_dict()
    elif method_name == "GetCompletedAchievementsAndClientEventCount":
        return {"completed": {}, "eventCount": {}, "completedDict": {}, "eventDict": {}, "taskIds": {}, "counterValues": {}}
    elif method_name == "initialize_tutorial_status":
        return ({}, {})
    elif method_name in ("GetMyJournalDetails", "GetMedalsReceived"):
        return ([], [])
    elif method_name == "GetFightersForShip":
        return ([], [], {})
    elif method_name == "GetMyActiveBookmarks":
        return ([], [], [])
    elif method_name == "GetLoginInfo":
        return MachoObject("utillib.KeyVal", {
            "assignedToMe": [], "needsAttention": [], "inProgress": [],
            "numOutstandingContractsNonCorp": 0, "numOutstandingContractsForCorp": 0,
            "numContractsRequiringAttention": 0, "numCorpContractsRequiringAttention": 0
        })
    elif method_name == "GetPublicInfo":
        if (call_args and call_args[0] == 216121397) or service_name == "corpmgr":
            return MachoObject("utillib.KeyVal", {
                "corporationID": 216121397, "corporationName": UniStr("Game Masters"), "tickerName": UniStr("GMS"),
                "ceoID": 90000001, "creatorID": 90000001, "stationID": 60014842, "taxRate": 0.0,
                "loyaltyPointTaxRate": 0.0, "minCorporationTaxAmount": 0.0, "allianceID": 434243723,
                "shares": 500, "memberCount": 375, "url": UniStr("https://support.eveonline.com"),
                "description": UniStr("This is CCP's official Game Master corporation. Note that GMs can only be reached through support tickets, to contact us press F12 and open our help center.  In-game EVE-mails will not be answered."),
                "deleted": 0, "allowWar": 1, "isRecruiting": 0, "warFactionID": None, "militiaFactionID": None,
                "shape1": None, "shape2": None, "shape3": None, "color1": None, "color2": None, "color3": None, "typeface": None
            })
        return MachoObject("utillib.KeyVal", {
            "corporationID": session.corp_id, "allianceID": session.alliance_id,
            "raceID": session.race_id, "bloodlineID": session.bloodline_id, "ancestryID": session.ancestry_id,
            "gender": session.gender, "characterName": UniStr(session.char_name),
            "createDateTime": get_filetime_now(), "schoolID": 17,
            "description": UniStr("CCP Game Master"), "deleted": 0, "shares": 500,
            "militiaFactionID": None, "warFactionID": None
        })
    elif method_name == "GetPublicInfo3":
        return 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."), session.corp_id, session.race_id, session.bloodline_id, session.ancestry_id, 5.0, session.gender, UniStr(session.char_name), get_filetime_now(), 17, None]]
        })
    elif method_name == "GetInfoWindowDataForChar":
        return MachoObject("utillib.KeyVal", {
            "corpID": session.corp_id, "allianceID": session.alliance_id,
            "title": UniStr("Game Master"), "militiaFactionID": None, "warFactionID": None
        })
    elif method_name == "GetAssetWorth":
        return (1000000000.0, 0)
    elif method_name == "get_home_station":
        return MachoObject("utillib.KeyVal", {"id": session.station_id, "type_id": 56, "solar_system_id": session.solarsystem_id, "is_fallback": False})
    elif method_name == "get_home_station_candidates":
        return [MachoObject("utillib.KeyVal", {"id": session.station_id, "type_id": 56, "solar_system_id": session.solarsystem_id, "is_current_station": True, "is_school_hq": True, "errors": set()})]
    elif method_name == "GetCharacterDescription":
        return UniStr("CCP Game Master - Dystropia Server")
    elif method_name == "GetCloneState":
        return MachoObject("utillib.KeyVal", {"clones": [], "implants": [], "timeLastJump": None})
    elif method_name == "GetAggressionSettings":
        return MachoObject("crimewatch.corp_aggression.settings.AggressionSettings", {"_enableAfter": 0, "_disableAfter": None})
    elif method_name == "GetEveOwners":
        return MachoObject("eve.common.script.sys.rowset.Rowset", {
            "header": ["ownerID", "ownerName", "typeID", "gender", "ownerNameID"],
            "lines": [
                [session.char_id, UniStr(session.char_name), 1373, session.gender, None],
                [216121397, UniStr("Game Masters"), 2, 0, None],
                [434243723, UniStr("C C P Alliance"), 16159, 0, None],
            ]
        })
    elif method_name == "GetEmployementRecordAndCharacterTransfers":
        emp_rs = MachoObject("eve.common.script.sys.rowset.Rowset", {
            "header": ["corporationID", "startDate", "deleted"],
            "lines": [[session.corp_id, 128974960800000000, 0]]
        })
        return (emp_rs, [])
    elif method_name in ("GetAllianceMembers", "GetEmploymentRecord"):
        return MachoObject("eve.common.script.sys.rowset.Rowset", {
            "header": ["corporationID", "startDate", "deleted"],
            "lines": [[session.corp_id, 128974960800000000, 0]]
        })
    elif method_name == "GetAlliancePublicInfo":
        return MachoObject("utillib.KeyVal", {
            "allianceID": 434243723, "allianceName": UniStr("C C P Alliance"), "shortName": UniStr("C C P"),
            "executorCorpID": 216121397, "creatorCorpID": 216121397, "creatorCharID": 90000001,
            "dictatorial": 0, "description": UniStr("This is the official CCP alliance. It does not accept member corporations beyond the three official CCP corporations."),
            "url": UniStr("http://www.eveonline.com"), "startDate": 127307526000000000, "deleted": 0,
            "allowWar": 1, "currentCapital": None, "warFactionID": None, "currentPrimeHour": 0,
            "newPrimeHour": 0, "newPrimeHourValidAfter": 0
        })
    elif method_name == "GetCorporation":
        return _build_gm_corp_row()
    elif method_name == "GetAlliance":
        return _build_ccp_alliance_row()
    elif method_name == "GetLimits":
        return {"maxBlueprintResults": 25000}
    elif method_name == "GetStationItemBits":
        return (1000173, session.station_id, 1, 56)
    elif method_name == "GetStation":
        return MachoObject("utillib.KeyVal", {
            "stationID": session.station_id, "security": 1.0, "dockingCostPerVolume": 0.0,
            "maxShipVolumeDockable": 50000000.0, "officeRentalCost": 10000, "operationID": 1,
            "stationTypeID": 56, "corporationID": 1000173, "ownerID": 1000173,
            "solarSystemID": session.solarsystem_id, "solarsystemID": session.solarsystem_id,
            "constellationID": session.constellation_id, "regionID": session.region_id,
            "stationName": UniStr("Polaris II - Polaris Corporation Plantation"),
            "x": 65561272320.0, "y": 9169920000.0, "z": 95195504640.0,
            "reprocessingEfficiency": 0.5, "reprocessingStationsTake": 0.025,
            "reprocessingHangarFlag": 4, "orbitID": 40023745, "serviceMask": 26,
            "hangarGraphicID": 20, "stationServices": []
        })
    elif method_name == "GetStationInfo":
        return 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": [[session.station_id, 1.0, 0.0, 50000000.0, 10000, 1, 56, 1000173, session.solarsystem_id, session.constellation_id, session.region_id, UniStr("Polaris II - Polaris Corporation Plantation"), 65561272320.0, 9169920000.0, 95195504640.0, 0.5, 0.025, 4, 40023745]]
        })
    elif method_name == "GetSelfInvItem":
        ctx = session.get_bound_context(call_oid)
        if ctx.get("type") == "ship_inv":
            return make_dbrow([session.ship_id, session.ship_type_id, cid, session.station_id, 4, -1, 237, 6, ""])
        return make_dbrow([session.station_id, 56, 1000173, session.solarsystem_id, 0, -1, 15, 3, ""])
    elif method_name == "GetItem":
        item_id = call_args[0] if call_args else session.ship_id
        if item_id == cid:
            line = [cid, 1373, uid, session.station_id, 57, -1, 1, 1, ""]
        else:
            line = [session.ship_id, session.ship_type_id, cid, session.station_id, 4, -1, 237, 6, ""]
        return make_dbrow(line)
    elif method_name == "List":
        ctx = session.get_bound_context(call_oid)
        if ctx.get("type") == "ship_inv":
            return MachoObject("eve.common.script.sys.rowset.Rowset", {"header": INV_COLS_11, "lines": []})
        return MachoObject("eve.common.script.sys.rowset.Rowset", {
            "header": INV_COLS_11,
            "lines": [
                [session.ship_id, session.ship_type_id, cid, session.station_id, 4, -1, 237, 6, "", 1, 1],
                [100000002, 87573, cid, session.station_id, 4, -1, 1089, 30, "", 1, 1],
                [100000003, 87573, cid, cid, 3, -1, 1089, 30, "", 1, 1],
                [100000004, 92564, cid, cid, 3, -1, 1089, 30, "", 1, 1],
                [100000005, 88186, cid, session.station_id, 4, -1, 1089, 30, "", 1, 1],
                [100000006, 88186, cid, cid, 3, -1, 1089, 30, "", 1, 1],
            ]
        })
    elif method_name == "GetAllInfo":
        ref = get_filetime_now()
        c_it = make_dbrow([cid, 1373, uid, session.station_id, 57, -1, 1, 1, ""])
        s_it = make_dbrow([session.ship_id, session.ship_type_id, cid, session.station_id, 4, -1, 237, 6, ""])
        c_inf = MachoObject("utillib.KeyVal", {"itemID": cid, "invItem": c_it, "implants": [], "boosters": [], "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": session.ship_id, "invItem": s_it, "activeEffects": {}, "attributes": {}, "time": ref, "wallclockTime": ref, "description": ""})
        return MachoObject("utillib.KeyVal", {
            "structureInfo": None, "shipInfo": {session.ship_id: s_inf},
            "charInfo": ({cid: c_inf}, (1, [], [], [])),
            "shipModifiedCharAttribs": None, "systemWideEffectsOnShip": None,
            "shipState": ({}, {}, {}, {})
        })
    elif method_name == "GetClientStates":
        return (((0, None), (0, None), (0, None), (0, None), (0, None)), {}, ([], []), 0)
    elif method_name == "GetTimers":
        return (None, None, None)
    elif method_name == "GetLocalOccupationState":
        return (session.solarsystem_id, None)
    elif method_name == "GetOutputForClientSolarSystem":
        return (1.0, 1.0)
    elif method_name == "GetSettingsInfo":
        return (HANDSHAKE_CODE_BYTES, 0)
    elif method_name == "Hostname":
        return "127.0.0.1"
    elif method_name == "GetDeprecatedPrefsFallback":
        return "127.0.0.1"
    elif method_name == "GetNPCNPCStandings":
        return MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["fromID", "toID", "standing"], "lines": []})
    elif method_name in ("GetCharStandings", "GetCorpStandings") or "Standings" in str(method_name):
        return MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["fromID", "standing"], "lines": []})
    elif method_name == "GetInitialState":
        return MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["contactID", "online"], "lines": []})
    elif method_name == "GetContactList":
        if service_name == "charMgr":
            empty_rs = MachoObject("eve.common.script.sys.rowset.Rowset", {"header": ["contactID", "inWatchlist", "relationshipID", "labelMask"], "lines": []})
            return MachoObject("utillib.KeyVal", {"addresses": empty_rs, "blocked": empty_rs})
        return {}
    elif method_name == "GetAgents":
        return MachoObject("eve.common.script.sys.rowset.Rowset", {
            "header": ["agentID", "agentTypeID", "divisionID", "level", "stationID", "corporationID", "factionID", "solarsystemID", "solarSystemID", "bloodlineID", "gender", "quality"],
            "lines": EPIC_AGENTS
        })

    logger.warning("[!] Unhandled RPC method %r (service=%r) -> returning None", method_name, service_name)
    return None

def handle_call_req(session, decoded):
    """Processes a top-level MachoNet CallReq packet and sends the appropriate response."""
    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
    call_oid = sub_tuple[0] if isinstance(sub_tuple[0], str) else None
    method_name = sub_tuple[1]
    call_args = sub_tuple[2] if len(sub_tuple) > 2 else ()

    logger.info("[*] RPC CallReq: service=%r, method=%r, args=%r, callID=%r",
                service_name, method_name, call_args, call_id)

    # 1. Direct Object-Binding RPCs
    if method_name in ("GetMySkillHandler", "GetInventory", "GetInventoryFromId"):
        target_id = call_args[0] if call_args else None
        obj_type = "ship_inv" if (method_name == "GetInventoryFromId" and target_id == session.ship_id) else "station_inv"
        oid = session.allocate_oid(obj_type=obj_type, item_id=target_id)
        ref = get_filetime_now()
        pkt = build_call_rsp(decoded, BoundObject(oid, ref), session.client_id, {"OID+": {oid: ref}})
        macho_marshal.send_packet(session.conn, macho_marshal.dumps(pkt))
        logger.info("[>] Sent %s BoundObject oid=%s (type=%s)", method_name, oid, obj_type)
        return

    # 2. MachoBindObject with optional piggybacked call
    if method_name == "MachoBindObject":
        bind_param = call_args[0] if call_args else None
        oid = session.allocate_oid(obj_type="service_bind", item_id=bind_param)
        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
        pb_args = pb[1] if (pb and isinstance(pb, tuple) and len(pb) > 1 and isinstance(pb[1], tuple)) else ()

        if pb_name in ("GetInventory", "GetInventoryFromId"):
            inv_id = pb_args[0] if pb_args else None
            inv_type = "ship_inv" if (pb_name == "GetInventoryFromId" and inv_id == session.ship_id) else "station_inv"
            oid2 = session.allocate_oid(obj_type=inv_type, item_id=inv_id)
            oids_map[oid2] = ref
            pb_ret = BoundObject(oid2, ref)
        elif pb_name:
            pb_ret = dispatch_rpc(session, service_name, pb_name, pb_args, oid)
        else:
            pb_ret = None

        pkt = build_call_rsp(decoded, (BoundObject(oid, ref), pb_ret), session.client_id, {"OID+": oids_map})
        macho_marshal.send_packet(session.conn, macho_marshal.dumps(pkt))
        logger.info("[>] Sent MachoBindObject CallRsp oids=%r (pb=%r)", list(oids_map.keys()), pb_name)
        return

    # 3. Slash Commands
    if method_name == "SlashCmd":
        raw_cmd = str(call_args[0]).strip() if call_args else "/"
        logger.info("[*] SlashCmd invoked: %r", raw_cmd)
        if raw_cmd == "/":
            err_pkt = build_error_rsp(decoded, SLASH_USERERROR_BYTES, session.client_id)
            macho_marshal.send_packet(session.conn, macho_marshal.dumps(err_pkt))
            logger.info("[>] Sent SlashCmd('/') ErrorResponse command list")
            return
        rsp_val = UniStr("Executed slash command: %s" % raw_cmd)
    else:
        # 4. Standard RPC Dispatch
        rsp_val = dispatch_rpc(session, service_name, method_name, call_args, call_oid)

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