from typing import Callable, Dict
from inspect import signature
from sys import argv, stderr, stdout
from subprocess import CalledProcessError, Popen
from select import select
from pty import openpty
from os import close, read


class Bakery:
    class Target:
        def __init__(self, func: Callable[[], bool]):
            self.func = func
            self.docs = (func.__doc__ or "no documentation").strip()

        def __repr__(self):
            return f"Target({self.func}/{self.docs})"

    builtin_functions = [
        "help",
    ]

    def __init__(self):
        self.targets: Dict[str, Bakery.Target] = {}
        self.__default: str = "help"

        def help() -> bool:
            """autogenerated help command"""
            print(f"usage: bake (target [optional])")
            print("targets:")

            longest_name = len(max(self.targets.keys(), key=len))
            for name, target in self.targets.items():
                default = "(default)"
                if name != self.__default:
                    default = " " * len(default)
                print(default, name.ljust(longest_name), "-", target.docs)

            return True

        self.targets.setdefault("help", Bakery.Target(help))

    def target(self, f: Callable[[], bool]):
        f_signat = signature(f)

        # ensure correct number of parameters
        f_params = f_signat.parameters
        assert len(f_params) == 0, "targets cannot have arguments"

        # ensure correct return type
        f_return = f_signat.return_annotation
        assert f_return is bool, "targets must return bool"

        # ensure target name is not builtin function
        assert (
            f.__name__ not in Bakery.builtin_functions
        ), f"target name: {f.__name__} is taken by a built in function"

        # set target and set default if not set
        self.targets.setdefault(f.__name__, Bakery.Target(f))
        if not self.__default:
            self.__default = f.__name__

    def default(self, name):
        assert name in self.targets.keys(), "default target must be a valid target"
        self.__default = name

    def run(self, name):
        target = self.targets.get(name, None)
        assert target, f"invalid target name: {name}"
        if not target.func():
            print(f"target: {target.func.__name__} failed")
            exit(1)

    def _run_and_print(self, command: str, check: bool) -> str:
        print(f"$ {command}", file=stderr)

        parent_fd, child_fd = openpty()
        p = Popen(
            command,
            shell=True,
            stdin=child_fd,
            stdout=child_fd,
            stderr=child_fd,
            close_fds=True,
        )
        close(child_fd)

        buffer = bytearray()
        try:
            while True:
                r, _, _ = select([parent_fd], [], [])
                if parent_fd in r:
                    data = read(parent_fd, 1024)
                    if not data:
                        break
                    buffer.extend(data)
                    stdout.buffer.write(data)
                    stdout.buffer.flush()
        finally:
            close(parent_fd)

        ret = p.wait()
        if check and ret != 0:
            raise CalledProcessError(ret, command)
        return buffer.decode("utf-8", errors="replace").rstrip()

    def shell_pass(self, command: str) -> str:
        try:
            return self._run_and_print(command, check=False)
        except CalledProcessError:
            return ""

    def shell(self, command: str) -> str:
        return self._run_and_print(command, check=False)

    def shell_strict(self, command: str) -> str:
        try:
            return self._run_and_print(command, check=True)
        except CalledProcessError as ex:
            exit(ex.returncode)

    def compile(self):
        match len(argv) - 1:
            case 0:  # run the default target
                self.run(self.__default)
            case 1:  # run the user defined target
                self.run(argv[1])
            case _:
                print(f"usage: {argv[0]} [target]")
                print("targets:")
                print(self.targets)
                exit(1)

    def __repr__(self):
        return str(self.targets)
