import datetime
import json
import logging
import signal
import subprocess
from json import JSONDecodeError
from pathlib import Path
from time import sleep

import rich
from tqdm import tqdm

from abst.config import default_creds_path


class Bastion:
    shell: bool = False
    connected: bool = False
    active_tunnel: subprocess.Popen = None
    response: dict = None

    @classmethod
    def get_bastion_state(cls) -> dict:
        bastion_id = cls.response["data"]["id"]
        get_state_arg_str = f"oci bastion session get --session-id {bastion_id}"
        res = subprocess.check_output(get_state_arg_str.split())
        try:
            return json.loads(res)
        except JSONDecodeError:
            rich.print(f"Failed to decode json: '{res}'")

    @classmethod
    def kill_bastion(cls, a=None, b=None, c=None):
        print("Killing Bastion SSH Tunnel")
        try:
            cls.active_tunnel.send_signal(signal.SIGTERM)
            sess_id = cls.response["data"]["id"]
            print("Cleaning")
            out = cls.delete_bastion_session(sess_id, Bastion.shell)
            rich.print(out)
            print("Removed Session")
        except Exception:
            print("Looks like Bastion is already deleted")
        finally:
            exit(0)

    @classmethod
    def delete_bastion_session(cls, sess_id, shell):
        print("Removing Bastion session")
        bastion_kill_arg_str = f"oci bastion session delete --session-id {sess_id}  --force"
        out = subprocess.check_output(bastion_kill_arg_str.split(), shell=shell).decode("utf-8")
        return out

    @classmethod
    def create_bastion(cls, shell: bool = False):
        with tqdm(7) as progress_bar:
            Bastion.shell = shell
            print("Loading Credentials")
            creds = None

            creds = cls.handle_creds_load(creds)
            progress_bar.update()

            host, ip, port, res = cls.create_bastion_session_wrapper(creds, shell)
            progress_bar.update()

            bid, response = cls.load_response(res)
            progress_bar.update()

            if bid is None:
                rich.print(f"Failed to Create Bastion with response '{response}'")
                return
            else:
                rich.print(f"Created Session with id '{bid}'")

            progress_bar.update()

            cls.wait_for_prepared()

            progress_bar.update()

            sleep(1)  # Precaution

            progress_bar.update()

            ssh_tunnel_arg_str = cls.run_ssh_tunnel_wrapper(bid, host, ip, port, shell)

            progress_bar.update()


        while status := (sdata := cls.get_bastion_state()["data"])["lifecycle-state"] == "ACTIVE":
            deleted = cls.connect_till_deleted(sdata, ssh_tunnel_arg_str, status, shell)

            if deleted:
                print("Bastion Session got deleted")
                return

        print("SSH Tunnel Terminated")
        Bastion.kill_bastion()

    @classmethod
    def run_ssh_tunnel_wrapper(cls, bid, host, ip, port, shell):
        print("Bastion initialized")
        print("Initializing SSH Tunnel")
        ssh_tunnel_arg_str = f"ssh  -N -L {port}:{ip}:{port} -p 22 {bid}@{host} -v"
        cls.run_ssh_tunnel(ssh_tunnel_arg_str, shell)
        return ssh_tunnel_arg_str

    @classmethod
    def wait_for_prepared(cls):
        print("Waiting for Bastion to initialize")
        while cls.get_bastion_state()["data"]["lifecycle-state"] != "ACTIVE":
            sleep(1)

    @classmethod
    def load_response(cls, res):
        response = None
        try:
            cls.response = response = json.loads(res)
        except JSONDecodeError:
            print(f"Failed to decode json: {res}")
        bid = response.get("data", None).get("id", None)
        return bid, response

    @classmethod
    def create_bastion_session_wrapper(cls, creds, shell):
        bastion_id, host, ip, name, port, ssh_path, ttl = cls.extract_creds(creds)
        res = cls.create_bastion_session(bastion_id, ip, name, port, ssh_path, ttl, shell)
        return host, ip, port, res

    @classmethod
    def handle_creds_load(cls, creds):
        try:
            creds = cls.load_creds_json()
        except FileNotFoundError:
            td = cls.generate_sample_dict()

            creds_path = cls.write_creds_json(td)

            rich.print(
                f"Sample credentials generated, please fill 'creds.json' in {creds_path} with "
                f"your credentials for this to work")
            exit(1)
        return creds

    @classmethod
    def load_creds_json(cls) -> dict:
        with open(str(default_creds_path), "r") as f:
            creds = json.load(f)
            if "delete_this" in creds.keys():
                raise FileNotFoundError()
        return creds

    @classmethod
    def write_creds_json(cls, td: dict):
        creds_path = default_creds_path
        if not creds_path.exists():
            with open(str(creds_path), "w") as f:
                json.dump(td, f, indent=4)
        return creds_path

    @classmethod
    def connect_till_deleted(cls, sdata, ssh_tunnel_arg_str, status, shell):
        """
        Will connect to session and reconnect every time it disconnect until session is deleted
        :param shell: If use shell environment (can have different impacts on MAC and LINUX)
        :param sdata:
        :param ssh_tunnel_arg_str:
        :param status:
        :return:
        """
        for i in range(1, 3):
            if not status:
                rich.print(f"Trying another time {i}/3")
                status = cls.run_ssh_tunnel(ssh_tunnel_arg_str, shell)
            else:
                break
        if not cls.connected and status:
            rich.print(f"Checking for idle termination, Bastion Active? {status}")
            created_time = datetime.datetime.fromisoformat(sdata["time-created"]).astimezone(
                datetime.timezone.utc)
            now_time = datetime.datetime.now(datetime.timezone.utc)
            ttl_now = now_time - created_time
            ttl_max = sdata["session-ttl-in-seconds"]
            delta = ttl_max - ttl_now.seconds
            if delta > 0:
                print(
                    f"Current session {delta} seconds remaining ({ttl_now}) "
                    f"reconnecting")
                cls.run_ssh_tunnel(ssh_tunnel_arg_str, shell)
                return False
            else:
                print("Bastion Session TTL run out, please start create new one")
                return True

    @classmethod
    def create_bastion_session(cls, bastion_id, ip, name, port, ssh_path, ttl, shell):
        bastion_arg_str = f"oci bastion session create-port-forwarding " \
                          f"--bastion-id {bastion_id} " \
                          f"--display-name {name} " \
                          f"--ssh-public-key-file {ssh_path} " \
                          f"--key-type PUB " \
                          f"--target-private-ip {ip} " \
                          f"--target-port {port}" \
                          f" --session-ttl {ttl}"
        print("Creating Session")
        res = subprocess.check_output(bastion_arg_str.split(), shell=shell)
        return res

    @classmethod
    def extract_creds(cls, creds):
        host = creds["host"]
        bastion_id = creds["bastion-id"]
        name = creds["default-name"]
        ssh_path = str(Path(creds["ssh-pub-path"]).resolve().absolute())
        ip = creds["target-ip"]
        port = creds["target-port"]
        ttl = creds["ttl"]
        return bastion_id, host, ip, name, port, ssh_path, ttl

    @classmethod
    def generate_sample_dict(cls):
        td = dict()
        td["delete_this"] = "Delete This Key after you Fill this file with your credentials"
        td["host"] = "host.bastion.us-phoenix-1.oci.oraclecloud.com"
        td["bastion-id"] = "ocid1.bastion......"
        td["default-name"] = "My super cool name"
        td["ssh-pub-path"] = "~/.ssh/id_rsa.pub"
        td["target-ip"] = "0.0.0.0"
        td["target-port"] = "22"
        td["ttl"] = "1800"
        return td

    @classmethod
    def run_ssh_tunnel(cls, ssh_tunnel_arg_str, shell):
        """

        :param ssh_tunnel_arg_str: String for ssh tunnel creation
        :param shell: If use shell environment (can have different impacts on MAC and LINUX)
        :return:
        """
        p = subprocess.Popen(ssh_tunnel_arg_str.split(), stdout=subprocess.PIPE,
                             stderr=subprocess.STDOUT, shell=shell)
        cls.active_tunnel = p
        while not p.poll():
            line = p.stdout.readline().decode("utf-8").replace("\r", "").replace("", "")
            if "Permission denied" in line:
                cls.connected = False
                return False
            if "pledge: network" in line:
                print("Success !")
                rich.print(f"SSH Tunnel Running from {datetime.datetime.now()}")
                cls.connected = True
            if line:
                logging.debug(line)
        cls.connected = False
        return True
