import json
import re
import subprocess as sub
from abc import ABC, abstractmethod
from pathlib import Path
from os import environ

# TODO: jem, zam - 2023_04_18: Discuss if we can move more functionality to domain?
class FileHandler(ABC):

    @classmethod
    def from_file_path(cls, file_path):
        config_file_type = file_path.suffix
        match config_file_type:
            case '.json':
                file_handler = JsonFileHandler()
            case '.gradle':
                file_handler = GradleFileHandler()
            case '.clj':
                file_handler = ClojureFileHandler()
            case '.py':
                file_handler = PythonFileHandler()
            case _:
                raise Exception(
                    f'The file type "{config_file_type}" is not implemented')
        # TODO: Attribute is only set in classmethod. Should this be initialized outside of this class?
        file_handler.config_file_path = file_path
        file_handler.config_file_type = config_file_type
        return file_handler

    @abstractmethod
    def parse(self) -> tuple[list[int], bool]:
        pass

    @abstractmethod
    def write(self, version_string):
        pass


class JsonFileHandler(FileHandler):

    def parse(self) -> tuple[list[int], bool]:
        with open(self.config_file_path, 'r') as json_file:
            json_version = json.load(json_file)['version']
            is_snapshot = False
            if '-SNAPSHOT' in json_version:
                is_snapshot = True
                json_version = json_version.replace('-SNAPSHOT', '')
            version = [int(x) for x in json_version.split('.')]
            return version, is_snapshot

    def write(self, version_string):
        with open(self.config_file_path, 'r+') as json_file:
            json_data = json.load(json_file)
            json_data['version'] = version_string
            json_file.seek(0)
            json.dump(json_data, json_file, indent=4)
            json_file.truncate()


class GradleFileHandler(FileHandler):

    def parse(self) -> tuple[list[int], bool]:
        with open(self.config_file_path, 'r') as gradle_file:
            contents = gradle_file.read()
            version_line = re.search("\nversion = .*", contents)
            exception = Exception("Version not found in gradle file")
            if version_line is None:
                raise exception

            version_line = version_line.group()
            version_string = re.search(
                '[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?', version_line)
            if version_string is None:
                raise exception

            version_string = version_string.group()
            is_snapshot = False
            if '-SNAPSHOT' in version_string:
                is_snapshot = True
                version_string = version_string.replace('-SNAPSHOT', '')

            version = [int(x) for x in version_string.split('.')]

            return version, is_snapshot

    def write(self, version_string):
        with open(self.config_file_path, 'r+') as gradle_file:
            contents = gradle_file.read()
            version_substitute = re.sub(
                '\nversion = "[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?"', f'\nversion = "{version_string}"', contents)
            gradle_file.seek(0)
            gradle_file.write(version_substitute)
            gradle_file.truncate()


class PythonFileHandler(FileHandler):

    def parse(self) -> tuple[list[int], bool]:
        with open(self.config_file_path, 'r') as python_file:
            contents = python_file.read()
            version_line = re.search("\nversion = .*\n", contents)
            exception = Exception("Version not found in gradle file")
            if version_line is None:
                raise exception

            version_line = version_line.group()
            version_string = re.search(
                '[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?', version_line)
            if version_string is None:
                raise exception

            version_string = version_string.group()
            is_snapshot = False
            if '-SNAPSHOT' in version_string:
                is_snapshot = True
                version_string = version_string.replace('-SNAPSHOT', '')

            version = [int(x) for x in version_string.split('.')]

            return version, is_snapshot

    def write(self, version_string):
        with open(self.config_file_path, 'r+') as python_file:
            contents = python_file.read()
            version_substitute = re.sub(
                '\nversion = "[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?"', f'\nversion = "{version_string}"', contents)
            python_file.seek(0)
            python_file.write(version_substitute)
            python_file.truncate()


class ClojureFileHandler(FileHandler):

    def parse(self) -> tuple[list[int], bool]:
        with open(self.config_file_path, 'r') as clj_file:
            contents = clj_file.read()
            version_line = re.search("^\\(defproject .*\n", contents)
            exception = Exception("Version not found in clj file")
            if version_line is None:
                raise exception

            version_line = version_line.group()
            version_string = re.search(
                '[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?', version_line)
            if version_string is None:
                raise exception

            version_string = version_string.group()
            is_snapshot = False
            if '-SNAPSHOT' in version_string:
                is_snapshot = True
                version_string = version_string.replace('-SNAPSHOT', '')

            version = [int(x) for x in version_string.split('.')]

            return version, is_snapshot

    def write(self, version_string):
        with open(self.config_file_path, 'r+') as clj_file:
            clj_first = clj_file.readline()
            clj_rest = clj_file.read()
            version_substitute = re.sub(
                '[0-9]*\\.[0-9]*\\.[0-9]*(-SNAPSHOT)?', f'"{version_string}"\n', clj_first)
            clj_file.seek(0)
            clj_file.write(version_substitute)
            clj_file.write(clj_rest)
            clj_file.truncate()


class SystemApi():

    def __init__(self):
        self.stdout = [""]
        self.stderr = [""]

    def run(self, args: list[str]):
        sanitized_args = []
        for arg in args:
            str_arg = str(arg)
            if str_arg is not "":
                sanitized_args.append(str_arg.replace("\n", ""))
        stream = sub.Popen(sanitized_args,
                           stdout=sub.PIPE,
                           stderr=sub.PIPE,
                           text=True,
                           encoding="UTF-8")
        self.stdout = stream.stdout.readlines()
        self.stderr = stream.stderr.readlines()

    def run_checked(self, *args):
        self.run(args)

        if len(self.stderr) > 0:
            raise Exception(f"Command failed with: {self.stderr}")


class GitApi():

    def __init__(self):
        self.system_api = SystemApi()

    def get_latest_n_commits(self, n: int):
        self.system_api.run_checked(
            'git', 'log', '--oneline', '--format="%s %b"', f'-n {n}')
        return self.system_api.stdout

    def get_latest_commit(self):
        output = self.get_latest_n_commits(1)
        return " ".join(output)

    def tag_annotated(self, annotation: str, message: str, count: int):
        self.system_api.run_checked(
            'git', 'tag', '-a', annotation, '-m', message, f'HEAD~{count}')
        return self.system_api.stdout

    def tag_annotated_second_last(self, annotation: str, message:str):
        self.tag_annotated(annotation, message, 1)
        return self.system_api.stdout

    def get_latest_tag(self):
        self.system_api.run_checked('git', 'describe', '--tags', '--abbrev=0')
        return self.system_api.stdout

    def get_current_branch(self):
        self.system_api.run_checked('git', 'branch', '--show-current')
        return ''.join(self.system_api.stdout).rstrip()

    def init(self, default_branch: str = "main"):
        self.system_api.run_checked('git', 'init', '-b', default_branch)
    
    def set_user_config(self, email: str, name: str):
        self.system_api.run_checked('git', 'config', 'user.email', email)
        self.system_api.run_checked('git', 'config', 'user.name', name)

    def add_file(self, file_path: Path):
        self.system_api.run_checked('git', 'add', file_path)
        return self.system_api.stdout

    def add_remote(self, origin: str, url: str):
        self.system_api.run_checked('git', 'remote', 'add', origin, url)
        return self.system_api.stdout

    def commit(self, commit_message: str):
        self.system_api.run_checked(
            'git', 'commit', '-m', commit_message)
        return self.system_api.stdout

    def push(self):
        self.system_api.run_checked('git', 'push')
        return self.system_api.stdout

    def checkout(self, branch: str):
        self.system_api.run_checked('git', 'checkout', branch)
        return self.system_api.stdout

class EnvironmentApi():

    def __init__(self):
        self.environ = environ

    def get(self, key):
        return self.environ.get(key)
    
    def set(self, key, value):
        self.environ[key] = value