import os.path
import time
import warnings
from typing import SupportsFloat, Any, Tuple, Dict

import requests
import json

import gymnasium as gym
from gymnasium.core import ObsType

import utils as U

# from .minecraft_launcher import MinecraftInstance
from .process_monitor import SubprocessMonitor

import wandb, sys, shutil


class VoyagerEnv(gym.Env):
    def __init__(
            self,
            mc_port=None,
            azure_login=None,
            server_host="http://127.0.0.1",
            server_port=3000,
            request_timeout=600,
            log_path="./logs",
            visual_server_port=-1,
            uuid="0000"
    ):
        if not mc_port and not azure_login:
            raise ValueError("Either mc_port or azure_login must be specified")
        if mc_port and azure_login:
            warnings.warn(
                "Both mc_port and mc_login are specified, mc_port will be ignored"
            )
        self.mc_port = mc_port
        self.visual_server_port = visual_server_port
        self.azure_login = azure_login
        self.server = f"{server_host}:{server_port}"
        self.server_port = server_port
        self.request_timeout = request_timeout
        self.log_path = log_path
        self.mineflayer = self.get_mineflayer_process(server_port)
        self.mc_instance = None
        # if azure_login:
        #     self.mc_instance = self.get_mc_instance()
        # else:
        #     self.mc_instance = None
        self.has_reset = False
        self.reset_options = None
        self.connected = False
        self.server_paused = False

        self.run_uuid = uuid

        self.initial_tick = 0


    def get_mineflayer_process(self, server_port):
        U.f_mkdir(self.log_path, "mineflayer")
        # file_path = os.path.abspath(os.path.dirname(__file__))
        return SubprocessMonitor(
            commands=[
                "node",
                "/app/mineflayer/index.js",
                str(server_port),
                str(self.visual_server_port)
            ],
            name="mineflayer",
            ready_match=r"Server started on port (\d+)",
            log_path=U.f_join(self.log_path, "mineflayer"),
        )

    # def get_mc_instance(self):
    #     print("Creating Minecraft server")
    #     U.f_mkdir(self.log_path, "minecraft")
    #     return MinecraftInstance(
    #         **self.azure_login,
    #         mineflayer=self.mineflayer,
    #         log_path=U.f_join(self.log_path, "minecraft"),
    #     )

    def check_process(self):
        if self.mc_instance and not self.mc_instance.is_running:
            print("Starting Minecraft server")
            self.mc_instance.run()
            self.mc_port = self.mc_instance.port
            self.reset_options["port"] = self.mc_instance.port
            print(f"Server started on port {self.reset_options['port']}")
        retry = 0
        while not self.mineflayer.is_running:
            print("Mineflayer process has exited, restarting")
            self.mineflayer.run()
            if not self.mineflayer.is_running:
                if retry > 3:
                    raise RuntimeError("Mineflayer process failed to start")
                else:
                    continue
            print(self.mineflayer.ready_line)
            try:
                res = requests.post(
                    f"{self.server}/start",
                    json=self.reset_options,
                    timeout=self.request_timeout,
                )
                if res.status_code != 200:
                    body = res.json()
                    error_message = body.get("error", str(res))
                    if any(keyword in error_message for keyword in ["Your code", "In your program code:", "in your code"]):
                        print(f"Error in action: {error_message}")
                        return "action_failed"
                    else:
                        print(f"Unknown error in server. Stop experiment. Error: {error_message}")
                        self.mineflayer.stop()
                        return "server_failed"
            except Exception as e:
                print("e:", str(e))
                print("Disconnected" in str(e))
                if "EPIPE" in str(e) or "Disconnected" in str(e):
                    print("Mineflayer process has stopped. Stop experiment.")
                    self.mineflayer.stop()
                    return "server_failed"
                else:
                    print(f"Unknown error in server. Stop experiment. Error: {str(e)}")
                    self.mineflayer.stop()
                    return "server_failed"
            return res.json()

    def step(
            self,
            code: str,
            programs: str = "",
    ) -> Tuple[ObsType, SupportsFloat, bool, bool, Dict[str, Any]]:
        if not self.has_reset:
            raise RuntimeError("Environment has not been reset yet")
        self.check_process()
        self.unpause()
        data = {
            "code": code,
            "programs": programs,
        }
        try:
            res = requests.post(
                f"{self.server}/step", json=data, timeout=self.request_timeout
            )
            if res.status_code != 200:
                body = res.json()
                error_message = body.get("error", str(res))
                if any(keyword in error_message for keyword in ["Your code", "In your program code:", "in your code"]):
                    print(f"Error in action: {error_message}")
                    return "action_failed"
                else:
                    print(f"Unknown error in server. Stop experiment. Error: {error_message}")
                    self.mineflayer.stop()
                    return "server_failed"
        except Exception as e:
            print("e:", str(e))
            print("Disconnected" in str(e))
            if "timeout" in str(e):
                print("Failed experiment because of low level controller issue.")
                return "action_failed_timeout"
            elif "EPIPE" in str(e) or "Disconnected" in str(e):
                print("Mineflayer process has stopped. Stop experiment.")
                self.mineflayer.stop()
                return "server_failed"
            else:
                print(f"Unknown error in server. Stop experiment. Error: {str(e)}")
                self.mineflayer.stop()
                return "server_failed"
        returned_data = res.json()
        self.pause()
        return json.loads(returned_data)
    
    def get_played_tick(self):
        try:
            res = requests.post(f"{self.server}/tick")
            if res.status_code != 200:
                body = res.json()
                error_message = body.get("error", str(res))
                if any(keyword in error_message for keyword in ["Your code", "In your program code:", "in your code"]):
                    print(f"Error in action: {error_message}")
                    return "action_failed"
                else:
                    print(f"Unknown error in server. Stop experiment. Error: {error_message}")
                    self.mineflayer.stop()
                    return "server_failed"
        except Exception as e:
            print("e:", str(e))
            print("Disconnected" in str(e))
            if "EPIPE" in str(e) or "Disconnected" in str(e):
                print("Mineflayer process has stopped. Stop experiment.")
                self.mineflayer.stop()
                return "server_failed"
            else:
                print(f"Unknown error in server. Stop experiment. Error: {str(e)}")
                self.mineflayer.stop()
                return "server_failed"

        tick = int(res.json()["message"])
        return tick
  
    def get_minutes(self):
        tick = self.get_played_tick()
        
        # 1 tick == 0.05 seconds
        # 1 minute == 1200 ticks
        return (tick - self.initial_tick) / 1200
    
    def get_used_ticks(self):
        tick = self.get_played_tick()
        
        # 1 tick == 0.05 seconds
        # 1 minute == 1200 ticks
        return tick - self.initial_tick

    def render(self):
        raise NotImplementedError("render is not implemented")

    def reset(
            self,
            *,
            seed=None,
            options=None,
    ) -> Tuple[ObsType, Dict[str, Any]]:
        if options is None:
            options = {}

        if options.get("inventory", {}) and options.get("mode", "hard") != "hard":
            raise RuntimeError("inventory can only be set when options is hard")

        self.reset_options = {
            "port": self.mc_port,
            "reset": options.get("mode", "peaceful"),
            "inventory": options.get("inventory", {}),
            "equipment": options.get("equipment", []),
            "spread": options.get("spread", False),
            "waitTicks": options.get("wait_ticks", 5),
            "position": options.get("position", None),
        }

        # self.unpause()
        self.mineflayer.stop()
        time.sleep(1)  # wait for mineflayer to exit

        returned_data = self.check_process()
        self.has_reset = True
        self.connected = True
        # All the reset in step will be soft
        self.reset_options["reset"] = "soft"

        self.initial_tick = self.get_played_tick()
        self.initial_tick = int(self.initial_tick)
        self.pause()
        return json.loads(returned_data)

    def close(self):
        self.unpause()
        if self.connected:
            try:
                res = requests.post(f"{self.server}/stop")
                if res.status_code == 400:
                    body = res.json()
                    error_message = body.get("error", str(res))
                    if any(keyword in error_message for keyword in ["Your code", "In your program code:", "in your code"]):
                        print(f"Error in action: {error_message}")
                        return "action_failed"
                    else:
                        print(f"Unknown error in server. Stop experiment. Error: {error_message}")
                        self.mineflayer.stop()
                        return "server_failed"
            except Exception as e:
                print("e:", str(e))
                print("Disconnected" in str(e))
                if "EPIPE" in str(e) or "Disconnected" in str(e):
                    print("Mineflayer process has stopped. Stop experiment.")
                    self.mineflayer.stop()
                    return "server_failed"
                else:
                    print("Mineflayer process cannot find block. Failed experiment because of low level controller issue.")
                    return "action_failed"
        if self.mc_instance:
            self.mc_instance.stop()
        self.mineflayer.stop()
        return not self.connected

    def pause(self):
        if self.mineflayer.is_running and not self.server_paused:
            try:
                res = requests.post(f"{self.server}/pause")
                if res.status_code == 400:
                    body = res.json()
                    error_message = body.get("error", str(res))
                    if any(keyword in error_message for keyword in ["Your code", "In your program code:", "in your code"]):
                        print(f"Error in action: {error_message}")
                        return "action_failed"
                    else:
                        print(f"Unknown error in server. Stop experiment. Error: {error_message}")
                        self.mineflayer.stop()
                        return "server_failed"
            except Exception as e:
                print("e:", str(e))
                print("Disconnected" in str(e))
                if "EPIPE" in str(e) or "Disconnected" in str(e):
                    print("Mineflayer process has stopped. Stop experiment.")
                    self.mineflayer.stop()
                    return "server_failed"
                else:
                    print("Mineflayer process cannot find block. Failed experiment because of low level controller issue.")
                    return "action_failed"
        return self.server_paused

    def unpause(self):
        if self.mineflayer.is_running and self.server_paused:
            try:
                res = requests.post(f"{self.server}/pause")
                if res.status_code == 400:
                    body = res.json()
                    error_message = body.get("error", str(res))
                    if any(keyword in error_message for keyword in ["Your code", "In your program code:", "in your code"]):
                        print(f"Error in action: {error_message}")
                        return "action_failed"
                    else:
                        print(f"Unknown error in server. Stop experiment. Error: {error_message}")
                        self.mineflayer.stop()
                        return "server_failed"
            except Exception as e:
                print("e:", str(e))
                print("Disconnected" in str(e))
                if "EPIPE" in str(e) or "Disconnected" in str(e):
                    print("Mineflayer process has stopped. Stop experiment.")
                    self.mineflayer.stop()
                    return "server_failed"
                else:
                    print("Mineflayer process cannot find block. Failed experiment because of low level controller issue.")
                    return "action_failed"
        return self.server_paused
