# TODO: jem, zam - 2023_04_14: Discuss if we can move more functionality to domain?
import json
import re
import subprocess as sub
from abc import ABC, abstractmethod
from pathlib import Path
from os import environ

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):
        stream = sub.Popen(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 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 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