#!/usr/bin/env python3

import os
import re
import subprocess
import sys

# Support dashes in command names
COMMAND_TO_METHOD = {
    "ts-node": "ts_node",
    "check-types": "check_types",
    "bash-completions": "bash_completions",
    "plantuml-to-png": "plantuml_to_png",
}
METHOD_TO_COMMAND = {method: command for command, method in COMMAND_TO_METHOD.items()}

WAIT_FOR_PROCESS = "wait_for_process"


class DevHelper:
    def __init__(self):
        self.last_exit_status = 0

    # Core logic
    def call(self, *args, **kwargs):
        if not args:
            return self.compose(*args, **kwargs)

        command, rest = args[0], args[1:]
        method_name = COMMAND_TO_METHOD.get(command, command)
        method = getattr(self, method_name, None)
        if callable(method) and not method_name.startswith("_") and method_name != "call":
            return method(*rest, **kwargs)
        return self.compose(*args, **kwargs)

    def compose(self, *args, **kwargs):
        command = self._compose_command(*args, **kwargs)
        if not kwargs.get("silent"):
            print(f"Running: {command}")

        if kwargs.get("execution_mode") == WAIT_FOR_PROCESS:
            self._wait_for_process_with_logging(command)
        else:
            os.execv("/bin/sh", ["/bin/sh", "-c", command])

    # Primary command wrappers
    def build(self, *args, **kwargs):
        self.compose("build", *args, **kwargs)

    def compile(self, *args, **kwargs):
        self.run("api", "npm", "run", "build", execution_mode=WAIT_FOR_PROCESS)
        if self.last_exit_status != 0:
            sys.exit(self.last_exit_status)
        self.run("web", "npm", "run", "build")

    def up(self, *args, **kwargs):
        self.compose("up", "--remove-orphans", *args, **kwargs)

    def down(self, *args, **kwargs):
        self.compose("down", "--remove-orphans", *args, **kwargs)

    def logs(self, *args, **kwargs):
        self.compose("logs", "-f", *args, **kwargs)

    def run(self, *args, **kwargs):
        self.compose("run", "--rm", *args, **kwargs)

    def ps(self, *args, **kwargs):
        self.compose("ps", *args, **kwargs)

    # Custom helpers
    def api(self, *args, **kwargs):
        self.run("api", *args, **kwargs)

    def web(self, *args, **kwargs):
        self.run("web", *args, **kwargs)

    def check_types(self, *args, **kwargs):
        self.run("api", "npm", "run", "check-types", *args, **kwargs)

    def test(self, *args, **kwargs):
        service = args[0] if args else None
        if service == "api":
            self.test_api(*args[1:], **kwargs)
        elif service == "web":
            self.test_web(*args[1:], **kwargs)
        else:
            self.test_api(*args, **kwargs)

    def test_api(self, *args, **kwargs):
        args = self._reformat_project_relative_path_filter_for_vitest(list(args), "api/")
        self.run("test_api", "npm", "run", "test", *args, **kwargs)

    def test_web(self, *args, **kwargs):
        args = self._reformat_project_relative_path_filter_for_vitest(list(args), "web/")
        self.run("test_web", "npm", "run", "test", *args, **kwargs)

    def sqlcmd(self, *args, **kwargs):
        db_host = os.environ.get("DB_HOST", "localhost")
        db_user = os.environ.get("DB_USER", "sa")
        db_pass = os.environ.get("DB_PASS", "1m5ecure!")
        db_name = os.environ.get("DB_NAME", "YHSI")
        self.compose(
            "exec",
            "db",
            "/opt/mssql-tools/bin/sqlcmd",
            "-U",
            db_user,
            "-P",
            db_pass,
            "-H",
            db_host,
            "-d",
            db_name,
            "-I",  # enable quoted identifiers, e.g. "table"."column"
            *args,
            **kwargs,
        )

    def db(self, *args, **kwargs):
        self.compose("exec", "db", *args, **kwargs)

    def debug(self, *args, **kwargs):
        container_id = self._container_id("api")
        print("Waiting for breakpoint to trigger...")
        print("'ctrl-c' to exit.")
        command = f"docker attach --detach-keys ctrl-c {container_id}"
        print(f"Running: {command}")
        os.execv("/bin/sh", ["/bin/sh", "-c", command])

    def npm(self, *args, **kwargs):
        self.run("api", "npm", *args, **kwargs)

    def ts_node(self, *args, **kwargs):
        self.run("api", "npm", "run", "ts-node", *args, **kwargs)

    def knex(self, *args, **kwargs):
        if sys.platform.startswith("linux"):
            self.run("api", "npm", "run", "knex", *args, execution_mode=WAIT_FOR_PROCESS, **kwargs)

            file_or_directory = os.path.join(self._project_root(), "api/src/db/migrations")
            if not self._take_over_needed(file_or_directory):
                sys.exit(0)

            self.ownit(file_or_directory)
        else:
            self.run("api", "npm", "run", "knex", *args, **kwargs)

    def migrate(self, *args, **kwargs):
        action = args[0] if args else None
        self.knex(f"migrate:{action}", *args[1:], **kwargs)

    def seed(self, *args, **kwargs):
        action = args[0] if args else None
        self.knex(f"seed:{action}", *args[1:], **kwargs)

    def ownit(self, *args, **kwargs):
        file_or_directory = args[0] if args else None
        if file_or_directory is None:
            raise ValueError("Must provide a file or directory path.")

        if sys.platform.startswith("linux"):
            print(f"Take ownership of the file or directory? {file_or_directory}")
            command = f"sudo chown -R {self._user_id()}:{self._group_id()} {file_or_directory}"
            os.execv("/bin/sh", ["/bin/sh", "-c", command])
        else:
            raise NotImplementedError(f"Not implemented for platform {sys.platform}")

    def plantuml_to_png(self, *args, **kwargs):
        args = list(args)
        if not args:
            raise ValueError("Must provide a file path.")
        file_path = args.pop()

        png_path = re.sub(r"\.(wsd|pu|puml|plantuml|uml)$", ".png", file_path)

        command = f"curl {' '.join(args)} --data-binary @'{file_path}' http://localhost:9999/png > '{png_path}'"

        print(f"Running: {command}")
        os.execv("/bin/sh", ["/bin/sh", "-c", command])

    def bash_completions(self, *args, **kwargs):
        completions = sorted(
            METHOD_TO_COMMAND.get(name, name)
            for name in vars(DevHelper)
            if not name.startswith("_") and name != "call" and callable(getattr(DevHelper, name))
        )
        print(" ".join(completions))

    # Private helpers
    def _wait_for_process_with_logging(self, command):
        process = subprocess.Popen(
            ["/bin/sh", "-c", f"{command} 2>&1"],
            stdout=subprocess.PIPE,
            text=True,
        )
        for line in process.stdout:
            print(line, end="")
        process.wait()
        self.last_exit_status = process.returncode

    def _container_id(self, container_name, *args, **kwargs):
        command = self._compose_command("ps", "-q", container_name, *args, **kwargs)
        print(f"Running: {command}")
        result = subprocess.run(["/bin/sh", "-c", command], stdout=subprocess.PIPE, text=True)
        container_id = result.stdout.strip()
        print(f"Container id is: {container_id}")
        return container_id

    def _service_running(self, container_name):
        self.ps("-q", "--status=running", execution_mode=WAIT_FOR_PROCESS, silent=True)
        return self.last_exit_status == 0

    def _compose_command(self, *args, **kwargs):
        environment = kwargs.get("environment", "development")
        return f"cd {self._project_root()} && docker compose -f docker-compose.{environment}.yml {' '.join(args)}"

    def _project_root(self):
        return os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))

    def _take_over_needed(self, file_or_directory):
        result = subprocess.run(
            ["/bin/sh", "-c", f"find {file_or_directory} -not -user {self._user_id()} -print -quit | grep -q ."],
        )
        return result.returncode == 0

    def _user_id(self):
        if not sys.platform.startswith("linux"):
            raise NotImplementedError(f"Not implemented for platform {sys.platform}")
        return subprocess.run(["id", "-u"], stdout=subprocess.PIPE, text=True).stdout.strip()

    def _group_id(self):
        if not sys.platform.startswith("linux"):
            raise NotImplementedError(f"Not implemented for platform {sys.platform}")
        return subprocess.run(["id", "-g"], stdout=subprocess.PIPE, text=True).stdout.strip()

    def _reformat_project_relative_path_filter_for_vitest(self, args, prefix):
        if args and args[0].startswith(prefix):
            src_path_prefix = f"{prefix}src/"

            if args[0].startswith(src_path_prefix):
                # TODO: handle other file types
                args[0] = re.sub(r"\.ts$", ".test.ts", re.sub(f"^{re.escape(src_path_prefix)}", "tests/", args[0]))
            else:
                args[0] = re.sub(f"^{re.escape(prefix)}", "", args[0])

            print("Reformatted path filter from project relative to service relative for vitest.")

        return args


if __name__ == "__main__":
    DevHelper().call(*sys.argv[1:])
