import datetime
import getpass
import json
import logging
import os
import platform
import shutil
import subprocess

import requests

# Developer settings
GITHUB_API_TOKEN = os.getenv("GITHUB_PAT", "")
headers = {"Authorization": "Bearer " + GITHUB_API_TOKEN}
CUSTOM_HEADERS_ENABLED = False
DEBUGGING = False
TIME_FORMAT = "%Y-%m-%d %H:%M:%S"

# ANISETTE-SERVER
ANISETTE_HOST = "127.0.0.1"
ANISETTE_PORT = 6969

# ARCH
ARCH = platform.machine()
if ARCH == "armv7l":
    ARCH = "armv7"
NETMUXD_AVAILABLE_ARCHS = ("x86_64", "aarch64", "armv7")
NETMUXD_IS_AVAILABLE = ARCH in NETMUXD_AVAILABLE_ARCHS
Netmuxd_is_on = True if NETMUXD_IS_AVAILABLE else False


# DIRECTORY
CURRENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
RESOURCE_DIRECTORY = os.path.join(CURRENT_DIRECTORY, "resource")

# VERSIONS
"""
Versions will be fetch with FetchVersion()
"""
Latest_AltServer_Version = ""
Latest_AltStore_Version = ""
Latest_Netmuxd_Version = ""
Latest_Anisette_Server_Version = ""
Latest_Script_Version = ""

# PATH AND URL
"""
URL value default is "" , value will be update with FetchVersion()
"""
VERSION_JSON_PATH = os.path.join(CURRENT_DIRECTORY, "version.json")

ALTSERVER_PATH = os.path.join(RESOURCE_DIRECTORY, "AltServer")
Altserver_URL = ""

ALTSTORE_PATH = os.path.join(RESOURCE_DIRECTORY, "AltStore.ipa")
AltStore_URL = ""

NETMUXD_PATH = os.path.join(RESOURCE_DIRECTORY, "netmuxd")
Netmuxd_URL = ""

ANISETTE_SERVER_PATH = os.path.join(RESOURCE_DIRECTORY, "anisette-server")
Anisette_Server_URL = ""

SCRIPT_PATH = os.path.join(CURRENT_DIRECTORY, "main.py")
SCRIPT_URL = (
    "https://raw.githubusercontent.com/dreth/AltServer-Linux-PyScript/rewrite/main.py"
)

# UPDATABLE BOOLS
"""
Default is false , value will be update with CheckUpdate()
"""
AltServer_Is_Updatable = False
AltStore_Is_Updatable = False
Nermuxd_Is_Updatable = False
Anisette_Server_Is_Updatable = False
Script_Is_Updatable = False


Version_Fetched = False


def getAnswer(text):
    try:
        return input(text)
    except KeyboardInterrupt:
        logging.info("\nCtrl+C pressed, aborting")
        exit(0)


def DebugPrint(msg):
    now = datetime.datetime.now().strftime(TIME_FORMAT)
    if DEBUGGING:
        logging.info(f"[DEBUG] {now}\n== {msg} ==")


def FetchVersion() -> dict:
    logging.info("Fetching version ...")

    global \
        Latest_AltServer_Version, \
        Latest_AltStore_Version, \
        Latest_Anisette_Server_Version, \
        Latest_Netmuxd_Version, \
        Latest_Script_Version

    AltStore_Response = requests.get(
        "https://cdn.altstore.io/file/altstore/apps.json"
    ).json()["apps"][0]["versions"][0]
    Latest_AltStore_Version = AltStore_Response["version"]

    Latest_AltServer_Version = requests.get(
        "https://api.github.com/repos/NyaMisty/AltServer-Linux/releases/latest",
        headers=headers if CUSTOM_HEADERS_ENABLED else "",
    ).json()["tag_name"]
    Latest_Netmuxd_Version = requests.get(
        "https://api.github.com/repos/jkcoxson/netmuxd/releases/latest",
        headers=headers if CUSTOM_HEADERS_ENABLED else "",
    ).json()["tag_name"]
    Latest_Anisette_Server_Version = requests.get(
        "https://api.github.com/repos/Dadoum/Provision/releases/latest",
        headers=headers if CUSTOM_HEADERS_ENABLED else "",
    ).json()["tag_name"]
    Latest_Script_Version = requests.get(
        "https://api.github.com/repos/dreth/AltServer-Linux-PyScript/releases/latest",
        headers=headers if CUSTOM_HEADERS_ENABLED else "",
    ).json()["tag_name"]

    global \
        Altserver_URL, \
        AltStore_URL, \
        Netmuxd_URL, \
        Anisette_Server_URL, \
        Version_Fetched

    Altserver_URL = f"https://github.com/NyaMisty/AltServer-Linux/releases/download/{Latest_AltServer_Version}/AltServer-{ARCH}"
    Netmuxd_URL = f"https://github.com/jkcoxson/netmuxd/releases/download/{Latest_Netmuxd_Version}/{ARCH}-linux-netmuxd"
    Anisette_Server_URL = f"https://github.com/Dadoum/Provision/releases/download/{Latest_Anisette_Server_Version}/anisette-server-{ARCH}"
    AltStore_URL = AltStore_Response["downloadURL"]

    Version_Fetched = True

    logging.info("Done")
    json_data = {
        "AltServer": Latest_AltServer_Version,
        "AltStore": Latest_AltStore_Version,
        "Netmuxd": Latest_Netmuxd_Version,
        "Anisette-Server": Latest_Anisette_Server_Version,
        "Script": Latest_Script_Version,
    }
    DebugPrint(json_data)
    return json_data


def CheckResource():
    resource_list = (
        os.listdir(RESOURCE_DIRECTORY) if os.path.exists(RESOURCE_DIRECTORY) else []
    )
    Resource_Missed = not all(
        resource in resource_list
        for resource in ["AltServer", "anisette-server", "AltStore.ipa", "netmuxd"]
    )
    latest_version_json = {}

    DebugPrint(f"RESOURCE_MISSED : {Resource_Missed}")

    if not os.path.exists(VERSION_JSON_PATH):
        logging.info("version.json not exists")
        latest_version_json: dict = FetchVersion()
        DebugPrint(latest_version_json)
        with open(VERSION_JSON_PATH, "w") as outfile:
            json.dump(latest_version_json, outfile)
        # Remove all executable binaries to get new binaries
        if os.path.exists(RESOURCE_DIRECTORY):
            shutil.rmtree(RESOURCE_DIRECTORY)

    if Resource_Missed and not Version_Fetched:
        latest_version_json: dict = FetchVersion()

    global current_version_json
    current_version_json = json.load(open(VERSION_JSON_PATH))
    # Resource dir
    if not os.path.exists(RESOURCE_DIRECTORY):
        logging.info("Creating 'resource' directory")
        os.mkdir(RESOURCE_DIRECTORY)
    # AltServer
    if not os.path.exists(ALTSERVER_PATH):
        logging.info(f"Downloading Altserver {Latest_AltServer_Version}")
        DebugPrint(Altserver_URL)
        response = requests.get(Altserver_URL)
        open(ALTSERVER_PATH, "wb").write(response.content)
        current_version_json["AltServer"] = Latest_AltServer_Version
    # AltStore
    if not os.path.exists(ALTSTORE_PATH):
        logging.info(f"Downloading AltStore ipa {Latest_AltStore_Version}")
        DebugPrint(AltStore_URL)
        response = requests.get(AltStore_URL)
        open(ALTSTORE_PATH, "wb").write(response.content)
        current_version_json["AltStore"] = Latest_AltStore_Version
    # Netmuxd
    if not os.path.exists(NETMUXD_PATH) and NETMUXD_IS_AVAILABLE:
        logging.info(f"Downloading netmuxd {Latest_Netmuxd_Version}")
        DebugPrint(Netmuxd_URL)
        response = requests.get(Netmuxd_URL)
        open(NETMUXD_PATH, "wb").write(response.content)
        current_version_json["Netmuxd"] = Latest_Netmuxd_Version
    # Anisette-Server
    if not os.path.exists(ANISETTE_SERVER_PATH):
        logging.info(f"Downloading anisette-server {Latest_Anisette_Server_Version}")
        DebugPrint(Anisette_Server_URL)
        response = requests.get(Anisette_Server_URL)
        open(ANISETTE_SERVER_PATH, "wb").write(response.content)
        current_version_json["Anisette-Server"] = Latest_Anisette_Server_Version

    # Write updated json data into version.json
    with open(VERSION_JSON_PATH, "w") as outfile:
        json.dump(current_version_json, outfile)

    if not os.access(ALTSERVER_PATH, os.X_OK):
        logging.info("Setting AltServer exec permission")
        os.chmod(ALTSERVER_PATH, 0o755)
    if os.path.exists(NETMUXD_PATH) and not os.access(NETMUXD_PATH, os.X_OK):
        logging.info("Setting netmuxd exec permission")
        os.chmod(NETMUXD_PATH, 0o755)
    if not os.access(ANISETTE_SERVER_PATH, os.X_OK):
        logging.info("Setting anisette-server permission")
        os.chmod(ANISETTE_SERVER_PATH, 0o755)

    DebugPrint(subprocess.getoutput(f"ls -al {RESOURCE_DIRECTORY}"))


def CheckNetworkConnection() -> bool:
    try:
        requests.get("http://google.com")
        return True
    except Exception as e:
        logging.exception(e)
        return False


def CheckUpdate() -> bool:
    if not Version_Fetched:
        FetchVersion()
    with open(VERSION_JSON_PATH, "r") as openfile:
        json_data = json.load(openfile)

    Current_Script_Version = json_data["Script"]
    Current_AltServer_Version = json_data["AltServer"]
    Current_AltStore_Version = json_data["AltStore"]
    Current_Netmuxd_Version = json_data["Netmuxd"]
    Current_Anisette_Server_Version = json_data["Anisette-Server"]

    global \
        Script_Is_Updatable, \
        AltServer_Is_Updatable, \
        AltStore_Is_Updatable, \
        Nermuxd_Is_Updatable, \
        Anisette_Server_Is_Updatable
    # script
    if Latest_Script_Version != Current_Script_Version:
        Script_Is_Updatable = True
        logging.info(
            f"Script is updatable , current ver : {Current_Script_Version} , latest ver : {Latest_Script_Version}"
        )
    # altserver
    if Latest_AltServer_Version != Current_AltServer_Version:
        AltServer_Is_Updatable = True
        logging.info(
            f"AltServer is updatable , current ver : {Current_AltServer_Version} , latest ver : {Latest_AltServer_Version}"
        )
    # altstore
    if Latest_AltStore_Version != Current_AltStore_Version:
        AltStore_Is_Updatable = True
        logging.info(
            f"AltStore is updatable , current ver : {Current_AltStore_Version} , latest ver : {Latest_AltStore_Version}"
        )
    # netmuxd
    if Latest_Netmuxd_Version != Current_Netmuxd_Version:
        Nermuxd_Is_Updatable = True
        logging.info(
            f"Netmuxd is updatable , current ver : {Current_Netmuxd_Version} , latest ver : {Latest_Netmuxd_Version}"
        )
    # anisette server
    if Latest_Anisette_Server_Version != Current_Anisette_Server_Version:
        Anisette_Server_Is_Updatable = True
        logging.info(
            f"Anisette-Server is updatable , current ver : {Current_Anisette_Server_Version} , latest ver : {Latest_Anisette_Server_Version}"
        )

    return (
        AltServer_Is_Updatable
        or AltStore_Is_Updatable
        or Nermuxd_Is_Updatable
        or Anisette_Server_Is_Updatable
        or Script_Is_Updatable
    )


def RemoveOutdatedResource():
    if AltServer_Is_Updatable:
        os.remove(ALTSERVER_PATH)
    if AltStore_Is_Updatable:
        os.remove(ALTSTORE_PATH)
    if Anisette_Server_Is_Updatable:
        os.remove(ANISETTE_SERVER_PATH)
    if Nermuxd_Is_Updatable:
        os.remove(NETMUXD_PATH)


def Update():
    if CheckUpdate():
        # Remove outdated resource
        logging.info("Removing outdated resource ...")
        RemoveOutdatedResource()
        # Update script
        if Script_Is_Updatable:
            logging.info(
                f"Downloading the lastest script [{Latest_Script_Version}] ..."
            )
            response = requests.get(
                "https://raw.githubusercontent.com/dreth/AltServer-Linux-PyScript/rewrite/main.py"
            )
            open(SCRIPT_PATH, "wb").write(response.content)
            current_version_json["Script"] = Latest_Script_Version
            with open(VERSION_JSON_PATH, "w") as outfile:
                json.dump(current_version_json, outfile)

        logging.info(
            "\n\nUpdate done\nYou can find update log in https://github.com/dreth/AltServer-Linux-PyScript/releases\nScript requires restart to apply updates\nUse `e` option to exit the script\n\n"
        )
    else:
        logging.info("All resources and script are up to dated :)")


class AnisetteServer:
    def __init__(self, host=ANISETTE_HOST, port=ANISETTE_PORT):
        self.host = host
        self.port = port
        os.environ["ALTSERVER_ANISETTE_SERVER"] = f"http://{host}:{port}"
        DebugPrint(os.environ["ALTSERVER_ANISETTE_SERVER"])
        DebugPrint(f"{ANISETTE_SERVER_PATH} -n {host} -p {port}")
        self.server = subprocess.Popen(
            f"{ANISETTE_SERVER_PATH} -n {host} -p {port}",
            shell=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.STDOUT,
        )

    def kill(self):
        logging.info(subprocess.getoutput("killall anisette-server"))


class AltServerDaemon:
    def __init__(self):
        self.start()

    def start(self):
        self.altserver = subprocess.Popen(
            ALTSERVER_PATH, shell=True
        )  # ,env=os.environ)

    def kill(self):
        logging.info(subprocess.getoutput("killall AltServer"))

    def restart(self):
        self.kill()
        self.start()


class Netmuxd:
    def __init__(self):
        if Netmuxd_is_on:
            if subprocess.getoutput("echo $(pidof usbmuxd)") != "":
                logging.info(subprocess.getoutput("kill -9 $(pidof usbmuxd)"))
            self.start()

    def start(self):
        self.netmuxd = subprocess.Popen(f"-b {NETMUXD_PATH}", shell=True)

    def kill(self):
        logging.info(subprocess.getoutput("killall netmuxd"))

    def switchWiFi(self):
        global Netmuxd_is_on
        Netmuxd_is_on = True
        DebugPrint(f"NETMUXD : {Netmuxd_is_on}")
        logging.info(subprocess.getoutput("kill -9 $(pidof usbmuxd)"))
        self.kill()
        self.start()

    def switchTether(self):
        global Netmuxd_is_on
        Netmuxd_is_on = False
        DebugPrint(f"NETMUXD : {Netmuxd_is_on}")
        logging.info(subprocess.getoutput("usbmuxd"))
        self.kill()


class iDevice:
    def __init__(self, name, UDID):
        self.name = name
        self.UDID = UDID


class DeviceManager:
    def __init__(self, devices=[]):
        self.devices = devices

    def getDevices(self) -> list[iDevice]:
        DebugPrint(f"NETMUXD : {Netmuxd_is_on}")
        self.devices = []
        udids = (
            subprocess.getoutput("idevice_id -n").split("\n")
            if Netmuxd_is_on
            else subprocess.getoutput("idevice_id -l").split("\n")
        )
        DebugPrint(udids)
        if udids == [""]:
            logging.info("No devices found")
        else:
            for udid in udids:
                name = (
                    subprocess.getoutput(f"ideviceinfo -n -u {udid} -k DeviceName")
                    if Netmuxd_is_on
                    else subprocess.getoutput(f"ideviceinfo -u {udid} -k DeviceName")
                )
                d = iDevice(name=name, UDID=udid)
                self.devices.append(d)
        return self.devices


class InstallationManager:
    def __init__(self):
        pass

    def selectDevice(self, devices: list[iDevice]):
        for i in range(len(devices)):
            logging.info(f"[{i}] : {devices[i].name} , {devices[i].UDID}")
        try:
            index = int(getAnswer("Enter the index of the device for installation : "))
            self.selectedDevice = devices[index]
        except Exception as e:
            logging.info(f"Invalid index: {e}")
            self.selectedDevice = None

    def getAccount(self):
        ac = getAnswer("Enter your Apple ID : ")
        self.account = ac

    def getPassword(self):
        pd = getpass.getpass("Enter password of the Apple ID : ")
        self.password = pd

    def selectFile(self):
        answer = getAnswer(
            "Do you want to install AltStore ? (y/n) [n for select your own iPA] : "
        ).lower()
        if answer == "n":
            filePath = getAnswer("Enter the absolute path of the file : ")
            if filePath != "":
                self.filePath = filePath
            else:
                self.filePath = None
                logging.info("No file path entered")
        else:
            self.filePath = ALTSTORE_PATH

    def run(self):
        subprocess.run(
            f"{ALTSERVER_PATH} -u {self.selectedDevice.UDID} -a '{self.account}' -p '{self.password}' {self.filePath}",
            shell=True,
        )

    def getInfo(self) -> str:
        return [self.selectedDevice.name, self.account, self.password, self.filePath]


def main():
    if CheckNetworkConnection() is False:
        logging.info("Please connect to network and re-run the script")
        exit(1)
    CheckResource()
    CheckUpdate()
    anisetteserver = AnisetteServer()
    netmuxd = Netmuxd()
    altserverdaemon = AltServerDaemon()
    device_manager = DeviceManager()
    installaion_manager = InstallationManager()
    logging.info(HELP_MSG)
    option = getAnswer("Enter OPTION to continue : ").lower()

    if option == "i":
        devices = device_manager.getDevices()
        if len(devices) == 0:
            logging.error("No devices found")
            exit(1)
        installaion_manager.selectDevice(devices=devices)
        if installaion_manager.selectedDevice is None:
            logging.error("No device selected")
            exit(1)
        installaion_manager.getAccount()
        installaion_manager.getPassword()
        installaion_manager.selectFile()
        if installaion_manager.filePath is None:
            logging.error("No file selected")
            exit(1)
        DebugPrint(installaion_manager.getInfo())
        installaion_manager.run()

    elif option == "w":
        if NETMUXD_IS_AVAILABLE:
            if not Netmuxd_is_on:
                netmuxd.switchWiFi()
                altserverdaemon.restart()
        else:
            logging.info(f"Netmuxd does not support your architecture : {ARCH}")

    elif option == "t":
        if Netmuxd_is_on:
            netmuxd.switchTether()
            altserverdaemon.restart()

    elif option == "e":
        altserverdaemon.kill()
        anisetteserver.kill()
        if Netmuxd_is_on:
            netmuxd.kill()
        exit(0)

    elif option == "h":
        logging.info(HELP_MSG)

    elif option == "p":
        devices = device_manager.getDevices()
        for d in devices:
            logging.info(f"{d.name} , {d.UDID}")

    elif option == "u":
        Update()

    else:
        logging.info("Invalid option")


HELP_MSG = """
#####################################
#  Welcome to the AltServer script  #
#####################################

ScriptUsage: [OPTION]

OPTIONS
    i, --Install AltStore or ipa files
        Install AltStore or ipa files to your device
    w, --Switch to wifi Daemode mode (Default using it after launch)
        Switch and restart to wifi Daemode mode to refresh apps or AltStore
    t, --Switch to usb tethered Daemode mode
        Switch and restart to usb tethered Daemode mode to refresh apps or AltStore
    e, --Exit
        Exit script
    h, --Help
        Show this message
    p, --Pair
        Show paired devices
    u, --Update
        Update this script
        
For more information: 
https://github.com/dreth/AltServer-Linux-PyScript

FORKED FROM:
https://github.com/powenn/AltServer-Linux-PyScript

"""

if __name__ == "__main__":
    DebugPrint("Script Start")
    DebugPrint(f"RUNNING AT {CURRENT_DIRECTORY} , RESOURCE_DIR : {RESOURCE_DIRECTORY}")
    DebugPrint(f"ARCH : {ARCH} , NETMUXD_AVAILABLE : {NETMUXD_IS_AVAILABLE}")
    main()
