import argparse
import os
import re
from pathlib import Path


class DynamicCompose:
    _COMPOSE_NAME = 'compose.yml'
    _DOCKERFILE_NAME = 'Dockerfile'
    SAFELY_COVER_MARK = ('# This file was automatically generated by DynamicCompose, and can be safely deleted, '
                         'but do not edit it or remove this comment.\n')

    def __init__(self, cwd: str | Path = None, show_command: bool = True, force_cover: bool = False):
        self.show_command = show_command
        self.force_cover = force_cover
        self.cwd = cwd or Path().cwd()

        self.env_file = self.cwd / '.env'
        self.compose_dir = self.cwd / '.compose'
        self.compose_dir.mkdir(exist_ok=True)

        self.compose_content, self.dockerfile_content = '', ''
        self.compose_name, self.dockerfile_name = '', ''

        self.env = self._get_env()
        args, self.extra_args = self._get_args()
        self.env.update(dict(args.e or ()))

    @staticmethod
    def _parse_key_value(arg_string: str):
        key, value = arg_string.strip().split('=', maxsplit=1)
        return key, value

    def _get_env(self):
        env_dict = {}
        if self.env_file.exists():
            with open(self.env_file) as f:
                for line in f.readlines():
                    if line.strip():
                        key, value = self._parse_key_value(line)
                        env_dict[key] = value
        return env_dict

    def _get_args(self) -> tuple[argparse.Namespace, list[str]]:
        parser = argparse.ArgumentParser(description='Dynamic generating the Dockerfile and the compose.yml')
        parser.add_argument('-e', action='append', type=self._parse_key_value,
                            help='The environment variables are used in generating.')
        parsed_args, parsed_extra_args = parser.parse_known_args()
        return parsed_args, parsed_extra_args

    @staticmethod
    def _convert_env_var_ref(template: str) -> str:
        """Replace { to {{, {{ will be seen as literal { in python's f-string"""
        return re.sub(r'\$\{(.+?)}', r'${{\1}}', template)

    def _format(self, template: str, template_file: str | Path, name: str) -> 'DynamicCompose':
        if not template:
            path = Path(template_file or self.compose_dir / name)
            if name == self._COMPOSE_NAME:
                self.compose_name = path.name
            else:
                self.dockerfile_name = path.name
            template = path.read_text(encoding='utf-8')
        if name == self._COMPOSE_NAME:
            self.compose_content = self._convert_env_var_ref(template).format(**self.env)
        else:
            self.dockerfile_content = self._convert_env_var_ref(template).format(**self.env)
        return self

    def format_compose(self, template: str = None, template_file: str | Path = None) -> 'DynamicCompose':
        return self._format(template, template_file, self._COMPOSE_NAME)

    def format_dockerfile(self, template: str = None, template_file: str | Path = None) -> 'DynamicCompose':
        return self._format(template, template_file, self._DOCKERFILE_NAME)

    def _dump(self, target_file: str | Path, content: str) -> 'DynamicCompose':
        if content == self.compose_content:
            name = self._COMPOSE_NAME if not self.compose_name else self.compose_name
            self.compose_name = ''  # clear the name
        else:
            name = self._DOCKERFILE_NAME if not self.dockerfile_name else self.dockerfile_name
            self.dockerfile_name = ''  # clear the name

        path = Path(target_file or self.cwd / name)
        if path.exists():
            old_content = path.read_text(encoding='utf-8').strip()
            if not old_content.startswith(self.SAFELY_COVER_MARK):
                raise ValueError(f'"{path}" may not have been generated by this program.'
                                 f'You should check and delete it manually then re-run this program.')
        path.write_text(self.SAFELY_COVER_MARK + content, encoding='utf-8')
        return self

    def dump_compose(self, target_file: str | Path = None) -> 'DynamicCompose':
        return self._dump(target_file, self.compose_content)

    def dump_dockerfile(self, target_file: str | Path = None) -> 'DynamicCompose':
        return self._dump(target_file, self.dockerfile_content)

    def fd_compose(self, template: str = None, template_file: str | Path = None,
                   target_file: str | Path = None) -> 'DynamicCompose':
        self.format_compose(template, template_file)
        self.dump_compose(target_file)
        return self

    def fd_dockerfile(self, template: str = None, template_file: str | Path = None,
                      target_file: str | Path = None) -> 'DynamicCompose':
        self.format_dockerfile(template, template_file)
        self.dump_dockerfile(target_file)
        return self

    def fd_all(self) -> 'DynamicCompose':
        self.fd_compose()
        dockerfiles = [self.compose_dir / f for f in os.listdir(self.compose_dir)
                       if f.startswith(self._DOCKERFILE_NAME)]
        for dockerfile in dockerfiles:
            self.fd_dockerfile(template_file=dockerfile)
        return self

    def up(self, with_extra_args: bool = True) -> int:
        command = f'docker compose up'
        if with_extra_args:
            command = f'{command} {" ".join(self.extra_args)}'
        if self.show_command:
            print(command)
        return os.system(command)
