#!/home/autismbot1/ze_runner/venv/bin/python3
import os
import sys
import subprocess
import atexit
import pwd
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
import random
import signal
import socket
import codecs
import json
import datetime
import time
import glob

restart_time = datetime.datetime.now() + datetime.timedelta(hours=3)
block_connection = False
the_undesired_crash_counter = 0 #hate this monkey solution
shoot_distance = 450.0
shoot_burst_cycles = 4
enemy_chase_distance = 450.0
team_follow_distance = 800.0
infect_chase_distance = 300.0
momentum_hold_seconds = 1.4
min_forward_wait = 6
max_forward_wait = 12
last_forward_time = 0.0

whoami = subprocess.getoutput("whoami")
with open(f'/home/{whoami}/ze_runner/config.json') as jsonfile:
    data_ports = json.load(jsonfile)
looptestPath = f"/home/{whoami}/.steam/debian-installation/steamapps/common/Counter-Strike Source/cstrike/cfg/looptest.cfg"
chatmsg = ""

def overwrite_file_access():
    #setting defaults with setfacl for the download directory does not work seemingly
    subprocess.Popen(["chmod", "-R", "g+rwx", f"/home/{whoami}/.steam/debian-installation/steamapps/common/Counter-Strike Source/cstrike/download"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).communicate()

def writeCfgInput(Input_user):
    with open(looptestPath, 'w') as f:
        f.write(Input_user)
    #print("wrote to file: ", Input_user)
    if "connect" in Input_user or "retry;" in Input_user:
        #give extra time for disconnect, retry and connect commands
        time.sleep(1.0)
    else:
        time.sleep(0.35)
    open(looptestPath, 'w').close() #clearing file.

def close_observer(observer):
    observer.stop()
    observer.join() #supposedly needed to clear resources again.

def clean_up_files():
    #deleting POSIX shared memory objects, as long as one process has them open they exist. THis is to prevent /dev/shm from being full
    #due to steam child processes.
    #even with steam turned offline might there be chromium web browsers left over from steam who still hold processes open.
    #only kind of potential issues from this is steam cloud being out of sync, which is really fucking irrelevant.
    for pattern in ["/tmp/steam*", "/tmp/dbus*", "/tmp/pressure*", "/tmp/tigervnc*", "/tmp/dumps*", "/dev/shm/u100*"]:
        for path in glob.glob(pattern):
            subprocess.run(["rm", "-rf", path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    #users should only have permissions to delete their own files probably

    #check if any bz2 files left over.
    for f in glob.glob(f"/home/{whoami}/.steam/steam/steamapps/common/Counter-Strike Source/cstrike/download/**/*.bz2", recursive=True):
        file_size = None
        while True:
            time.sleep(10)
            if not os.path.exists(f):
                break  # finished downloading, bz2 file is removed.
            cur_file_size = os.path.getsize(f)
            if file_size == cur_file_size:
                # delete the bz2 file if its not progressing downloading
                subprocess.run(["rm", "-rf", f], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                break
            file_size = cur_file_size

    #clean up the game cache as it otherwise just keeps growing in gigabyte size
    subprocess.run(["rm", "-rf", f"/home/{whoami}/.steam/steam/steamapps/common/Counter-Strike Source/cstrike/cache"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

def exit_handler():
    print('reached exithandler')
    #securing the looptest.cfg wont be stuck accidently with commands
    kill_owned_process("pidof cstrike_linux64")
    kill_owned_process("pidof xterm")

    clean_up_files()
    subprocess.getoutput(f"pkill -9 -u {whoami}")

def bot_process_movement(input_line):
    try:
        dist_target = input_line[input_line.index("dist_target:") + len("dist_target:"):input_line.index("enemy_distance")]
        enemy_distance = input_line[input_line.index("enemy_distance:") + len("enemy_distance:"):input_line.index("targeteam:")]
        targeteam = input_line[input_line.index("targeteam:") + len("targeteam:"):input_line.index("state:")]
        state = input_line[input_line.index("state:") + len("state:"):]

        state = int(state.strip())
        dist_target = float(dist_target)
        enemy_distance = float(enemy_distance)
        targeteam = int(targeteam)
    except (ValueError, IndexError):
        # Ignore malformed movement packets instead of crashing the main loop.
        return

    global last_forward_time
    strInput = "cl_minmodels 1; wait 2; "

    if targeteam == 3:
        strInput = "cl_minmodels 1; wait 2; "

    def forward_burst(wait_ticks):
        return f"+forward; wait {wait_ticks}; -forward; "

    is_aggressive_state = state >= 3 or state == 1 or state == 8
    far_from_team = dist_target > team_follow_distance
    enemy_close = enemy_distance <= enemy_chase_distance
    enemy_very_close = enemy_distance <= infect_chase_distance
    very_close_to_team = dist_target < (team_follow_distance * 0.35)
    momentum_active = time.time() - last_forward_time < momentum_hold_seconds

    # Decide one primary forward burst to avoid conflicting movement bursts in a single packet.
    forward_ticks = 0
    if far_from_team:
        # Strong regroup push when separated from teammates.
        forward_ticks = random.randint(min_forward_wait, max_forward_wait + 2)
    elif enemy_close:
        # Controlled chase pressure when enemy is in range.
        forward_ticks = random.randint(min_forward_wait - 1, max_forward_wait - 1)
    elif is_aggressive_state or momentum_active:
        # Keep motion alive without overcommitting.
        forward_ticks = 2

    # Prevent overshooting teammates when no nearby threat.
    if very_close_to_team and not enemy_close:
        forward_ticks = min(forward_ticks, 2)

    if forward_ticks > 0:
        strInput += forward_burst(forward_ticks)
        last_forward_time = time.time()
    else:
        strInput += "wait 2;"

    if enemy_very_close:
        # Close-range commit with a short firing hold.
        strInput += "+attack; wait 16; -attack; wait 4; "

    if dist_target <= team_follow_distance and not enemy_very_close:
        # Lighter, steadier strafes while maintaining formation.
        strafe_pulses = 1 if very_close_to_team else 2
        strInput = strinput_append(strInput, strafe_pulses)
    if enemy_distance <= shoot_distance:
        for _ in range(shoot_burst_cycles):
            strInput += "+attack; wait 12; -attack; wait 4; "
    #print('strInput final:', strInput)
    writeCfgInput(strInput)

def strinput_append(strInput, nth):
    if nth <= 0:
        return strInput

    direction_left = random.choice([True, False])
    for _ in range(nth):
        # Keep direction for a pulse to reduce jitter, occasionally switch.
        if direction_left:
            strInput += f" +moveleft; wait {random.randint(10, 16)}; -moveleft; "
        else:
            strInput += f" +moveright; wait {random.randint(10, 16)}; -moveright; "
        if random.random() < 0.30:
            direction_left = not direction_left
    return strInput

def kill_user_owned_pid(pid):
    #print('pid: ', pid, ' killed')
    pid = int(pid.strip())
    os.kill(pid, signal.SIGKILL)
    time.sleep(10)

def return_user_owned_pid(pidof):
    owner_pid = subprocess.getoutput([f"{pidof}"])
    if owner_pid:
        for pid in owner_pid.split(" "):
            username = subprocess.getoutput([f"""ps -o user= -p {pid}"""])
            if username == whoami:
                return pid
    return None

def kill_owned_process(pidof):
    pid = return_user_owned_pid(pidof)
    while pid:
        kill_user_owned_pid(pid)
        pid = return_user_owned_pid(pidof)

def restart_sdl_and_steam():
    #subprocess.getoutput(["screen -XS XTERM quit"])
    kill_owned_process("pidof cstrike_linux64")
    kill_owned_process("pidof xterm")

    subprocess.getoutput([f'vncserver -kill']) #only displays vncservers for the specific user.
    subprocess.getoutput([f'vncserver -list -cleanstale']) #cleans stale sessions by removing files from disk
    time.sleep(5)

    cmd = f'vncserver -localhost no -geometry 1x1 -depth 24'
    #cmd = f'vncserver -localhost no -geometry 800x800 -depth 24'
    #print(f'cmd: {cmd}')
    subprocess.getoutput(cmd)
    print('reached .bashrc executing steam and variables')

def bot_connect(data):
    #use whatever ip you want here to connect with
    str1 = ""
    if "connect to ze" == data:
        str1 = f"connect {data_ports['server_ip_port_ze']};"
    elif "connect to ze2" == data:
        str1 = f"connect {data_ports['server_ip_port_ze2']};" #wait 15000;
    writeCfgInput(str1)

def cpulimit_pid_of_game():
    # ' > /dev/null' redirects stdout to /dev/null
    # '2>&1' redirects stderr to the same place as stdout
    pid = return_user_owned_pid("pidof cstrike_linux64")
    if pid is None:
        return
    cmd = f"cpulimit --pid={pid} --limit=25 --background > /dev/null 2>&1"
    #cmd = f"cpulimit --pid={pid} --limit=55 --background > /dev/null 2>&1"
    subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()[0]

def my_file_created_function(event_path):
    #print(f"New file created: {event_path}")
    if event_path.startswith("/tmp/source_engine") and event_path.endswith(".lock"):
        subprocess.run(["rm", "-rf", event_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        return
    if not event_path.lower().endswith(".bsp.bz2"):
        return

    global block_connection
    block_connection = True
    overwrite_file_access() #set chmod once for the bsp bz2 file.
    file_size = None

    while True:
        if not os.path.exists(event_path):
            print(f'finished the bz2 file {event_path}')
            break

        owner = pwd.getpwuid(os.stat(event_path).st_uid).pw_name
        if whoami == owner:  # we are the user that owns the file on disk
            cur_file_size = os.path.getsize(event_path)
            if file_size == cur_file_size:
                print(f"Deleting {event_path} due to being stuck on download.")
                subprocess.run(["rm", "-rf", event_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                continue
            file_size = cur_file_size
        else:
            writeCfgInput("disconnect;")
        time.sleep(10)

    overwrite_file_access() #set chmod for the actual bsp file
    global the_undesired_crash_counter
    the_undesired_crash_counter = 0
    block_connection = False

class NewFileHandler(FileSystemEventHandler):
    def on_created(self, event):
        my_file_created_function(event.src_path)

def handle_temp_files():
    #mostly its just downloading maps that we will be doing
    root_dir = f"/tmp"
    event_handler = NewFileHandler()
    observer = Observer()
    # Schedule the observer to watch the root directory and its subdirectories recursively
    observer.schedule(event_handler, root_dir, recursive=True)
    observer.start()
    return observer

def handle_bz2_files():
    #mostly its just downloading maps that we will be doing
    root_dir = f"/home/{whoami}/.steam/debian-installation/steamapps/common/Counter-Strike Source/cstrike/download"
    event_handler = NewFileHandler()
    observer = Observer()
    # Schedule the observer to watch the root directory and its subdirectories recursively
    observer.schedule(event_handler, root_dir, recursive=True)
    observer.start()
    return observer

def bot_process_surf(input_line):
    # Placeholder to avoid crashes until surfing behavior is implemented.
    return

if __name__ == '__main__':
    atexit.register(exit_handler)
    local_port = data_ports['udp_port']
    external_port_messages = data_ports['chat_external_port']
    buffer_size = 4096 #potentially not large enough?

    clean_up_files()
    observer = handle_bz2_files()
    observer1 = handle_temp_files()

    maps_folder_size = subprocess.Popen(["du", "-sh", f"/home/{whoami}/.steam/debian-installation/steamapps/common/Counter-Strike Source/cstrike/download/maps"], stdout=subprocess.PIPE).communicate()[0].decode().split("\t")[0]
    #deleting when maps folder larger than some GB
    if maps_folder_size.endswith("G"):
        maps_folder_size = float(maps_folder_size[:-1])
        if maps_folder_size > 40.0:
            for f in glob.glob(f"/home/{whoami}/.steam/debian-installation/steamapps/common/Counter-Strike Source/cstrike/download/maps/*"):
                subprocess.run(["rm", "-rf", f], stdout=subprocess.PIPE)

    restart_sdl_and_steam()
    is_bot_connected_to_ze2 = False

    print('preparing to launch game....')
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(("", local_port))
    sock.settimeout(5.0)
    messager_name = ""

    #give the game some time to launch
    fail_safe = 5
    while return_user_owned_pid("pidof cstrike_linux64") is None:
        #just a fail safe to prevent being stuck forever.
        if fail_safe <= 0:
            sys.exit(1)
        time.sleep(10)
        print("launching game...")
        fail_safe -= 1
    time.sleep(10)
    cpulimit_pid_of_game()

    try:
        while True:
            try:
                #print("socket waiting for recvfrom")
                data, addr = sock.recvfrom(buffer_size)
            except socket.timeout:
                continue
            data = codecs.decode(data, "utf-8", "ignore")
            ip = addr[0]
            port = addr[1]
            #print('port: ', port, " ip: ", ip)
            #print('data: ', data)
            if not data:
                continue

            #print("ip: ", ip, " port: ", port)
            if ip == data_ports['discord_bot_ip'] and port == external_port_messages:
                if messager_name in data:
                    messager_name = ""
                response_msg = f"""say {messager_name} {data}"""
                print("remote UDP packet response_msg: ", response_msg)
                writeCfgInput(response_msg)
            if ip != data_ports['ovh_ip']:
                continue
            elif data == "rtv":
                response_msg = "say rtv"
                writeCfgInput(response_msg)
            elif data == "bot kicked server full":
                print('bot kicked server full: ', datetime.datetime.now().time())
            elif "autismo connected to ze" == data:
                print('Bot connected to ze!')
                is_bot_connected_to_ze2 = False
                the_undesired_crash_counter = 0
            elif "not connected to ze2" == data:
                is_bot_connected_to_ze2 = False
            elif "autismo connected to ze2" == data:
                print('Bot connected to ze2!')
                the_undesired_crash_counter = 0
                is_bot_connected_to_ze2 = True
            elif "connect to ze" == data or ("connect to ze2" == data and not is_bot_connected_to_ze2):
                if datetime.datetime.now() >= restart_time or return_user_owned_pid("pidof cstrike_linux64") is None or the_undesired_crash_counter >= 5:
                    #it already finished downloading the contnet, it just cant connect. probably stuck. shitty solution welp
                    kill_owned_process("pidof cstrike_linux64")
                    sock.close()
                    close_observer(observer)
                    close_observer(observer1)
                    print('exiting after running the game for several hours or game being stuck on map change.')
                    sys.exit(1)
                elif not block_connection: #need this check to prevent connection spam during content downloading.
                    print('data: ', data, ' the_undesired_crash_counter: ', the_undesired_crash_counter)
                    the_undesired_crash_counter += 1
                    bot_connect(data)
            elif "clientmessage:" in data:
                messager_name = data.split("clientmessage:", 1)[1].split(f" {data_ports['magic_secret']}")[0]
                databyte_send_message = messager_name + data.split(f"{data_ports['magic_secret']}")[1]
                sock.sendto(databyte_send_message.encode(), (data_ports["discord_bot_ip"], external_port_messages))
                #print('databyte_send_message: ', databyte_send_message)
            elif data.startswith("dist_target:"):
                bot_process_movement(data)
            elif data.startswith("surfing:"):
                bot_process_surf(data)
            elif data.startswith("hull info:"):
                hull_info = data[data.index("hull info:") + len("hull info:"):]
                strInput = ""
                if hull_info == "jump":
                    strInput += "+jump; wait 5; -jump; +duck; wait 50; -duck; wait 5; "
                elif hull_info == "crouch":
                    strInput += "+duck; wait 50; -duck; wait 5; "
                writeCfgInput(strInput)
    finally:
        sock.close()
        close_observer(observer)
        close_observer(observer1)
