import copy
import json 
import yaml
import asyncio
import subprocess
try:
    import pty
except Exception as ex:
    print("You are not able to run a terminal. See error:")
    print(ex)
import os

class ComposeFileManager:
    def __init__(self, compose_path="docker-compose.yml",event_handler=None):
        self.compose_path = compose_path
        self._load()
        self.running_instances = {}
        self.event_handler = event_handler 

    def _load(self):
        try:
            with open(self.compose_path) as f:
                self.compose = yaml.safe_load(f) or {}
        except FileNotFoundError:
            self.compose = {"services": {}}

    def _save(self):
        with open(self.compose_path, "w") as f:
            yaml.dump(self.compose, f, default_flow_style=False)

    def list_instances(self):
        return list(self.compose.get("services", {}).keys())

    async def _create_readers(self, container_name):
        instance = await self.get_instance(container_name)
        if not instance:
            return False 
        proc = self.running_instances.get(container_name)
        if not proc:
            return False
    
        async def reader(event_handler,stream):
            loop = asyncio.get_event_loop()
            while True:
                line = await loop.run_in_executor(None,os.read,stream,1024)
                if not line:
                    break
                await event_handler(container_name,"stdout",line)
            await self.stop(container_name)
        asyncio.create_task(reader(self.event_handler,proc['master']))



    def create_instance(
        self,
        name,
        image,
        command=None,
        cpus=None,
        memory=None,
        ports=None,
        volumes=None,
    ):
        service = {
            "image": image,
        }
        service["command"] = command or "tail -f /dev/null"
        if cpus or memory:
            service["deploy"] = {"resources": {"limits": {}}}
            if cpus:
                service["deploy"]["resources"]["limits"]["cpus"] = str(cpus)
            if memory:
                service["deploy"]["resources"]["limits"]["memory"] = str(memory)
        if ports:
            service["ports"] = [
                f"{host}:{container}" for container, host in ports.items()
            ]
        if volumes:
            service["volumes"] = volumes

        self.compose.setdefault("services", {})[name] = service
        self._save()

    def remove_instance(self, name):
        if name in self.compose.get("services", {}):
            del self.compose["services"][name]
            self._save()

    async def get_instance(self, name):
        instance = self.compose.get("services", {}).get(name)
        if not instance:
            return None
        instance = json.loads(json.dumps(instance,default=str))
        instance['status'] = await self.get_instance_status(name)
        return instance 

    def duplicate_instance(self, name, new_name):
        orig = self.get_instance(name)
        if not orig:
            raise ValueError(f"No such instance: {name}")
        self.compose["services"][new_name] = copy.deepcopy(orig)
        self._save()

    def update_instance(self, name, **kwargs):
        service = self.get_instance(name)
        if not service:
            raise ValueError(f"No such instance: {name}")
        for k, v in kwargs.items():
            if v is not None:
                service[k] = v
        self.compose["services"][name] = service
        self._save()

    async def get_instance_status(self, name):
        """Asynchronously check the status of a docker-compose service instance."""
        if name not in self.list_instances():
            return "error"
        proc = await asyncio.create_subprocess_exec(
            "docker", "compose", "-f", self.compose_path, "ps", "--services", "--filter", f"status=running",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout, _ = await proc.communicate()
        running_services = stdout.decode().split()
        print(running_services)
        return "running" if name in running_services else "stopped"

    async def write_stdin(self, name, data):
        await self.event_handler(name, "stdin", data)
        proc = self.running_instances.get(name)
        if not proc:
            return False 
        try:
            os.write(proc['master'], data.encode())
            return True
        except Exception as ex:
            print(ex)
            await self.stop(name)
        return False 

    async def stop(self, name):
        """Asynchronously stop a container by doing 'docker compose stop [name]'."""
        if name not in self.list_instances():
            return False 
        status = await self.get_instance_status(name)
        if status != "running":
            return True 
        
        proc = await asyncio.create_subprocess_exec(
            "docker", "compose", "-f", self.compose_path, "stop", name,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        if name in self.running_instances:
            del self.running_instances[name]
        stdout, stderr = await proc.communicate()
        if proc.returncode != 0:
            raise RuntimeError(f"Failed to stop {name}: {stderr.decode()}")
        if stdout:
            await self.event_handler(name,"stdout",stdout)
            return stdout.decode(errors="ignore")

        await self.event_handler(name,"stdout",stderr)
        return stderr.decode(errors="ignore")

    async def start(self, name):
        """Asynchronously start a container by doing 'docker compose up -d [name]'."""
        if name not in self.list_instances():
            return False 
        
        status = await self.get_instance_status(name)
        if name in self.running_instances and status == "running" and self.running_instances.get(name) and self.running_instances.get(name).get('proc').returncode == None:
            return True 
        elif name in self.running_instances:
            del self.running_instances[name]
        
        proc = await asyncio.create_subprocess_exec(
            "docker", "compose", "-f", self.compose_path, "up", name, "-d",
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )
        stdout,stderr = await proc.communicate()
        if proc.returncode != 0:
            print(f"Failed to start {name}: {stderr.decode(errors='ignore')}")
            return False 
        master, slave = pty.openpty()
        proc = await asyncio.create_subprocess_exec(
            "docker", "compose", "-f", self.compose_path, "exec", name, "/bin/bash",
            stdin=slave,
            stdout=slave,
            stderr=slave,
        )
        proc = {'proc': proc, 'master': master, 'slave': slave}
        self.running_instances[name] = proc
        await self._create_readers(name)
        return True

