import argparse
from dataclasses import dataclass
from pathlib import Path
import re


SUCCESS_CHAR = '\u2714'
FAIL_CHAR = '\u2716'

CVAT_VERSION_PATTERN = r'VERSION\s*=\s*\((\d+),\s*(\d*),\s*(\d+),\s*[\',\"](\w+)[\',\"],\s*(\d+)\)'
COMPOSE_VERSION_PATTERN = r'(\$\{CVAT_VERSION:-)([\w.]+)(\})'
HELM_VERSION_PATTERN = r'(^    image: cvat/(?:ui|server)\n    tag: )([\w.]+)'

@dataclass()
class Version:
    major: int = 0
    minor: int = 0
    patch: int = 0
    prerelease: str = ''
    prerelease_number: int = 0

    def __str__(self) -> str:
        return f'{self.major}.{self.minor}.{self.patch}-{self.prerelease}.{self.prerelease_number}'

    def cvat_repr(self):
        return f"({self.major}, {self.minor}, {self.patch}, '{self.prerelease}', {self.prerelease_number})"

    def compose_repr(self):
        return f'v{self.major}.{self.minor}.{self.patch}'

    def increment_prerelease_number(self) -> None:
        self.prerelease_number += 1

    def increment_prerelease(self) -> None:
        flow = ('alpha', 'beta', 'rc', 'final')
        idx = flow.index(self.prerelease)
        if idx == len(flow) - 1:
            raise ValueError(f"Cannot increment current '{self.prerelease}' prerelease version")

        self.prerelease = flow[idx + 1]
        self._set_default_prerelease_number()

    def set_prerelease(self, value: str) -> None:
        values = ('alpha', 'beta', 'rc', 'final')
        if value not in values:
            raise ValueError(f'{value} is a wrong, must be one of {values}')

        self.prerelease = value
        self._set_default_prerelease_number()

    def increment_patch(self) -> None:
        self.patch += 1
        self._set_default_prerelease()

    def increment_minor(self) -> None:
        self.minor += 1
        self._set_default_patch()

    def increment_major(self) -> None:
        self.major += 1
        self._set_default_minor()

    def _set_default_prerelease_number(self) -> None:
        self.prerelease_number = 0

    def _set_default_prerelease(self) -> None:
        self.prerelease = 'alpha'
        self._set_default_prerelease_number()

    def _set_default_patch(self) -> None:
        self.patch = 0
        self._set_default_prerelease()

    def _set_default_minor(self) -> None:
        self.minor = 0
        self._set_default_patch()

def update_compose_config(new_version: Version) -> None:
    compose_file = get_compose_filename()
    with open(compose_file, 'r') as fp:
        compose_text = fp.read()

    if new_version.prerelease == 'final':
        new_version_repr = new_version.compose_repr()
    else:
        new_version_repr = 'dev'

    match = re.search(COMPOSE_VERSION_PATTERN, compose_text)
    if not match:
        raise RuntimeError('Cannot match version pattern')

    if match[2] != new_version_repr:
        compose_text = re.sub(COMPOSE_VERSION_PATTERN, f'\\g<1>{new_version_repr}\\g<3>', compose_text)
        with open(compose_file, 'w') as fp:
            fp.write(compose_text)

        print(f'{SUCCESS_CHAR} {compose_file} was updated. {match[2]} -> {new_version_repr}\n')

    else:
        print(f'{SUCCESS_CHAR} {compose_file} no need to update.')

def update_cvat_version(old_version: str, new_version: Version) -> None:
    version_file = get_cvat_version_filename()
    with open(version_file, 'r') as fp:
        version_text = fp.read()

    new_version_str = f'VERSION = {new_version.cvat_repr()}'
    version_text = version_text.replace(old_version, new_version_str)

    with open(version_file, 'w') as fp:
        fp.write(version_text)

    print(f'{SUCCESS_CHAR} {version_file} was updated. {old_version} -> {new_version_str}\n')

def update_helm_version(new_version: Version) -> None:
    helm_values_file = get_helm_version_filename()
    with open(helm_values_file, 'r') as fp:
        helm_values_text = fp.read()

    if new_version.prerelease == 'final':
        new_version_repr = new_version.compose_repr()
    else:
        new_version_repr = 'dev'

    match = re.search(HELM_VERSION_PATTERN, helm_values_text, re.M)
    if not match:
        raise RuntimeError('Cannot match version pattern')

    if match[2] != new_version_repr:
        helm_values_text = re.sub(HELM_VERSION_PATTERN, f'\\g<1>{new_version_repr}', helm_values_text, 2, re.M)
        with open(helm_values_file, 'w') as fp:
            fp.write(helm_values_text)

        print(f'{SUCCESS_CHAR} {helm_values_file} was updated. {match[2]} -> {new_version_repr}\n')

    else:
        print(f'{SUCCESS_CHAR} {helm_values_file} no need to update.')


def verify_input(version_types: dict, args: dict) -> None:
    versions_to_bump = [args[v_type] for v_type in version_types]
    i = iter(versions_to_bump)
    if not any(i):
        raise ValueError(f'One of {list(version_types)} options must be specified')
    if any(i):
        raise ValueError(f'Only one of {list(version_types)} options accepted')

def get_cvat_version_filename() -> Path:
    return Path(__file__).resolve().parents[2] / 'cvat' / '__init__.py'

def get_compose_filename() -> Path:
    return Path(__file__).resolve().parents[2] / 'docker-compose.yml'

def get_helm_version_filename() -> Path:
    return Path(__file__).resolve().parents[2] / 'helm-chart' / 'values.yaml'

def get_current_version() -> 'tuple[str, Version]':
    version_file = get_cvat_version_filename()

    with open(version_file, 'r') as fp:
        version_text = fp.read()

    match = re.search(CVAT_VERSION_PATTERN, version_text)
    if not match:
        raise RuntimeError(f'Failed to find version in {version_file}')

    version = Version(int(match[1]), int(match[2]), int(match[3]), match[4], int(match[5]))
    return match[0], version

def main() -> None:
    version_types = {
        'major': {
            'action': 'store_true',
            'help': 'Increment the existing major version by 1',
        },
        'minor': {
            'action': 'store_true',
            'help': 'Increment the existing minor version by 1',
        },
        'patch': {
            'action': 'store_true',
            'help': 'Increment the existing patch version by 1',
        },
        'prerelease': {
            'nargs': '?',
            'const': 'increment',
            'help': '''Increment prerelease version alpha->beta->rc->final,
                    Also it's possible to pass value explicitly''',
        },
        'prerelease_number': {
            'action': 'store_true',
            'help': 'Increment prerelease number by 1',
        },
    }

    parser = argparse.ArgumentParser(description='Bump CVAT version')

    for v, v_config in version_types.items():
        parser.add_argument(f'--{v}', **v_config)

    parser.add_argument('--current', '--show-current',
        action='store_true', help='Display current version')

    args = parser.parse_args()

    version_str, version = get_current_version()

    if args.current:
        print(version)
        return

    try:
        verify_input(version_types, vars(args))
    except ValueError as e:
        print(f'{FAIL_CHAR} ERROR: {e}\n')
        parser.print_help()
        return

    if args.prerelease_number:
        version.increment_prerelease_number()

    elif args.prerelease:
        if args.prerelease  == 'increment':
            version.increment_prerelease()
        else:
            version.set_prerelease(args.prerelease)

    elif args.patch:
        version.increment_patch()

    elif args.minor:
        version.increment_minor()

    elif args.major:
        version.increment_major()

    print(f'{SUCCESS_CHAR} Bump version to {version}\n')

    update_cvat_version(version_str, version)
    update_compose_config(version)
    update_helm_version(version)

if __name__ == '__main__':
    main()
