#!/usr/bin/python3
import os
import pwd
import shutil
import subprocess
import sys


def try_to_run_command(command, shell=False):
    try:
        cmd = subprocess.run(command, shell=shell)
        if cmd.returncode == 0:
            print(f'Выполнено: {command}')
        else:
            print(f'Возникла ошибка. Вывод команды: {cmd.stdout.decode()}')
    except Exception as e:
        print(f'Команда: {command}, во время выполнения возникло исключение: {e}')


def install_packages_for_building():
    """
    Установка всех пакетов, необходимых для локальной сборки
    """
    try_to_run_command(
        [
            'sudo', 'dnf', 'in', '-y', 'git-core', 'rpmdevtools', 'rpm-mk-build-deps', 'abf-console-client',
            'basesystem-build'
        ]
    )


def replace_current_with_remote():
    cmd = subprocess.run('git reset --hard && git pull', shell=True, capture_output=True)
    try:
        if cmd.returncode == 0:
            print('Состояние обновлено из удалённого репозитория.')
        else:
            print(f'Не удалось обновить состояние из удалённого репозитория. Вывод: {cmd.stdout.decode()}')
    except Exception as e:
        print(f'Во время выполнения команды возникла ошибка: {e}')


def install_spec_build_requires(spec_file='', remove_rpm=True):
    """
    Установка зависимостей spec-файла.
    :param remove_rpm: если True, удалить rpm-файл для установки зависимостей после установки
    :param spec_file: спек
    :return: -
    """
    if not spec_file:
        print('Укажите spec-файл.')
        return
    else:
        try_to_run_command(f'mbd {spec_file} && sudo dnf in --disablerepo "*i686*" -y *build-deps*.rpm'
                           f'{" && rm -f *build-deps*.rpm" if remove_rpm else ""}',
                           shell=True)


def remove_spec_build_requires(spec_file=''):
    """
    Удаление зависимостей spec-файла
    :param spec_file: спек
    :return: -
    """
    if not spec_file:
        print('Укажите spec-файл.')
        return
    name = subprocess.run(
        ["rpmspec", "-q", "--srpm", "--qf", "%{name}", spec_file], capture_output=True
    ).stdout.decode().strip()

    try_to_run_command(['sudo', 'dnf', 'rm', '-y', f'{name}-build-deps'])


def build_package(spec_file='', install_required=False, no_delete=False):
    """
    Сборка пакета
    :param no_delete: если False, то *.src.rpm и всё указанное в .abf.yml не удаляется после сборки
    :param spec_file: спек для сборки
    :param install_required: если True, то автоматическая установка пакета после сборки
    :return: -
    """
    if not spec_file:
        print('Укажите spec-файл.')
        return
    install_rpm_command = (' && find . -maxdepth 1 -name "*.rpm" ! -name "*build-deps*" ! -name "*.src.rpm" '
                           '-exec sudo dnf in -y {} \;') if install_required else ''

    command_to_run = (f'spectool --define "_sourcedir $PWD" -g {spec_file} && rm -f .abf.yml && abf put -n && abf rpmbuild'
                      f'{install_rpm_command}')
    try_to_run_command(command_to_run, shell=True)
    if not no_delete:
        delete_garbage_command = 'rm -rf BUILD BUILDROOT *.src.rpm '
        try:
            yml_contents = open(os.path.join(os.getcwd(), '.abf.yml'), 'r').read()
            for file in os.listdir(os.getcwd()):
                if file in yml_contents:
                    delete_garbage_command += f'{file} '
        except FileNotFoundError:
            pass
        try_to_run_command(delete_garbage_command, shell=True)


def version_up(spec_file='', specified_version=''):
    """
    Обновляет версию в spec-файле со сбросом релиза во все единицы
    :param spec_file: спек
    :param specified_version: если укзаана, то версия заменяется на указанную, если нет, повышается на 1
    :return: -
    """
    if not spec_file:
        print('Укажите spec-файл.')
        return
    version_updated = False
    try:
        with open(spec_file, 'r') as inp:
            lines = inp.readlines()
        for i in range(len(lines)):
            line = lines[i]
            if line.startswith('Version'):
                # чтобы полностью сохранить структуру строки
                line = line.strip()
                # Если версия не указана, просто повысить её на 1
                if not specified_version:
                    indx = -1
                    while line[indx].isdigit():
                        indx -= 1
                    if indx != -1:
                        # Повышается число после последней точки
                        number = int(line[indx + 1:]) + 1
                        line = line[:indx + 1] + str(number) + '\n'
                        lines[i] = line
                        print('Версия повышена.')
                        version_updated = True
                        break
                    else:
                        print('Строка с версией имеет некорректный формат.')
                        break
                # Если версия указана, нужно заменить ту, которая есть, на указанную
                else:
                    indx = 0
                    while indx < len(line) and not line[indx].isdigit():
                        indx += 1
                    if indx < len(line):
                        line = line[:indx] + specified_version + '\n'
                        lines[i] = line
                        print('Версия повышена.')
                        version_updated = True
                        break
                    else:
                        print('Строка с версией имеет некорректный формат.')
                        break
        else:
            print('Строка с версией не найдена.')
        # Если версия обновлена, нужно сбросить релиз во все единицы
        if version_updated:
            for i in range(len(lines)):
                line = lines[i]
                if line.startswith('Release'):
                    line = line.strip()
                    indx = 0
                    while indx < len(line) and not line[indx].isdigit():
                        indx += 1
                    if indx < len(line):
                        release_list = line[indx:].split('.')
                        for k in range(len(release_list)):
                            if release_list[k].isdigit():
                                release_list[k] = '1'
                        line = line[:indx] + '.'.join(release_list) + '\n'
                        lines[i] = line
                        print('Релиз обновлён.')
                        break
                    else:
                        print('Строка с релизом имеет некорректный формат.')
                        break
            while not lines[-1].strip():
                lines = lines[:-1]
            with open(spec_file, 'w') as out:
                print(*lines, sep='', file=out)

    except Exception as e:
        print(f'Возникло исключение: {e}')


def commit_update(spec_file='', commit=''):
    """
    Обновляет коммит в спеке, где указан %define commit
    :param spec_file: спек
    :param commit: коммит (40 символов)
    :return: -
    """
    if not spec_file:
        print('Не указан spec-файл.')
        return
    elif not commit or len(commit) != 40:
        print('Укажите полный хэш коммита. '
              'Синтаксис: pirpm commit-update [file.spec] c9ecd3c39cf004fd777c6caaaa1f502e1e8475f0')
        return
    else:
        try:
            commit_updated, release_updated = False, False
            with open(spec_file, 'r') as inp:
                lines = inp.readlines()
            for i in range(len(lines)):
                line = lines[i]
                if line.startswith('%define commit') and not commit_updated:
                    line = line.strip().split()[:-1]
                    line.append(commit)
                    line = ' '.join(line) + '\n'
                    lines[i] = line
                    commit_updated = True
                elif line.startswith('Release') and not release_updated:
                    release = line.strip().split()[-1]
                    index_to_replace = line.index(release)
                    macros = '%' + release.split('%')[-1].split('}')[0] + '}'
                    release_list = release.split('.')
                    macros_index = 0
                    for j in range(len(release_list)):
                        if macros in release_list[j]:
                            macros_index = j
                            break
                    # повышение релиза
                    release_list[macros_index - 1] = f'{int(release_list[macros_index - 1]) + 1}'
                    # сброс всего, что справа, в 1
                    for j in range(macros_index + 1, len(release_list)):
                        release_list[j] = '1'
                    release = '.'.join(release_list)
                    line = line[:index_to_replace] + release + '\n'
                    lines[i] = line
                    release_updated = True
            if commit_updated and release_updated:
                while not lines[-1].strip():
                    lines = lines[:-1]
                print(*lines, sep='', file=open(spec_file, 'w'))
                print('Коммит обновлён.')
                if '.abf.yml' in os.listdir(os.getcwd()):
                    get_archive_cmd = subprocess.run('spectool --define "_sourcedir $PWD" -g spec_file', shell=True)
                    if get_archive_cmd.returncode != 0:
                        print('Не удалось получить архив с исходным кодом для обновления .abf.yml')
                    else:
                        os.remove(os.path.join(os.getcwd(), '.abf.yml'))
                        create_link_cmd = subprocess.run(['abf', 'put'])
                        if create_link_cmd.returncode == 0:
                            print('Ссылка на архив в .abf.yml обновлена.')
                        else:
                            print('Не удалось загрузить архив с исходным кодом на abf.')
            else:
                print('Не удалось обновить коммит.')

        except Exception as e:
            print(f'Возникло исключение: {e}')


def create_edit(spec_file='', edit_folder=''):
    """
    Скачивает и распаковывает исходники в ~/pirpm-edit/имя_пакета
    :param edit_folder: по умолчанию ~/pirpm-edit
    :return: -
    """
    if not spec_file:
        print('Не указан spec-файл.')
        return
    if not edit_folder:
        print('Не определена папка для распаковки исходного кода.')
        return
    current_folder = os.getcwd()
    try:
        os.mkdir(edit_folder)
    except FileExistsError:
        pass
    current_pkg_edit_folder = os.path.join(edit_folder, os.path.basename(current_folder))
    if os.path.isdir(current_pkg_edit_folder):
        if input(
                f'Папка {current_pkg_edit_folder} уже существует. Перезаписать? '
        ).lower() not in 'дy':
            print('Отменено пользователем.')
            return
        shutil.rmtree(current_pkg_edit_folder)
    os.mkdir(current_pkg_edit_folder)
    subprocess.run(
        f'spectool --define "_sourcedir $PWD" -g {spec_file} && tar -xzf $(ls *.tar.gz | head -1) --strip-components=1 '
        f'-C {current_pkg_edit_folder} && rm -f $(ls *.tar.gz | head -1)',
        shell=True
    )
    shutil.copytree(current_folder, current_pkg_edit_folder, dirs_exist_ok=True)
    print(f'Текущий проект скопирован для правки в {current_pkg_edit_folder}')
    os.chdir(current_pkg_edit_folder)
    subprocess.run('git init && git add . && git commit -m "init"', shell=True)
    os.chdir(current_folder)


def create_patch(spec_file='', edit_folder=''):
    """
    Создаёт патч из изменений в ~/pirpm-edit/имя_пакета по сравнению с текущей папкой и записывает его в спек
    :param spec_file: спек
    :param edit_folder: по умолчанию ~/pirpm-edit
    :return: -
    """
    if not spec_file:
        print('Укажите spec-файл.')
        return
    elif not os.path.isdir(f'{edit_folder}/{os.path.basename(os.getcwd())}'):
        print('Не найдена папка с отредактированным проектом.')
        return
    else:
        current_folder = os.getcwd()
        current_pkg_edit_folder = os.path.join(edit_folder, os.path.basename(current_folder))
        # Поиск номера первого патча, которого нет в папке с проектом
        current_patches = [i for i in os.listdir(current_folder) if i.endswith('.patch')]
        patch_number = 0
        patch_number_in_patches = True
        while patch_number_in_patches:
            patch_number += 1
            # 0001
            patch_number_string = str(patch_number).zfill(4)
            patch_number_in_patches = False
            for i in current_patches:
                if i.startswith(patch_number_string):
                    patch_number_in_patches = True
                    break
        patch_filename = f'{patch_number_string}-pirpm.patch'
        cmd = subprocess.run(
            f'cd {current_pkg_edit_folder} && git add . && git commit -m "pirpm" && '
            f'git format-patch -n1 && cp 0001-pirpm.patch {current_folder}/new_pirpm.patch && cd {current_folder}',
            shell=True)
        if cmd.returncode == 0:
            print('Патч создан и скопирован в папку с проектом.')
        else:
            print('При создании и копировании патча возникла ошибка.')
            return
        os.rename('new_pirpm.patch', patch_filename)
        print(f'Патч переименован в {patch_filename}.')
        # if cmd.returncode != 0:
        #     print('Во время создания патча возникла ошибка.')
        #     sys.exit(0)
        if os.path.getsize(patch_filename) == 0:
            print('Изменения не найдены. Патч будет удалён.')
            os.remove(patch_filename)
            return
        index_to_insert_patch = 0
        with open(spec_file, 'r') as inp:
            lines = inp.readlines()
        for i in range(len(lines)):
            # вставить патч нужно после всех источников и патчей, если они есть
            if lines[i].startswith('Source') or lines[i].startswith('Patch'):
                index_to_insert_patch = i + 1
        if index_to_insert_patch == 0:
            print('В spec-файле не найдены источники, проверьте его на корректность.')
            sys.exit(0)
        lines.insert(index_to_insert_patch, f'Patch{patch_number_string}:\t{patch_filename}\n')
        while not lines[-1].strip():
            lines = lines[:-1]
        with open(spec_file, 'w') as out:
            print(*lines, sep='', file=out)
        print(f'Патч {patch_filename} создан и записан в spec-файл.')
    if not edit_folder:
        print('Не определена папка для распаковки исходного кода.')
        return


def show_help():
    """
    Вывод справки. Выводится при любом некорректном вызове утилиты.
    """
    print(
        'Команды утилиты pirpm:\n'
        'pirpm install-packages-for-building - установка всех необходимых пакетов для сборки пакетов;\n'
        'pirpm replace-current-with-remote - замена текущего состояния проекта на состояние в удалённом репозитории;\n'
        'pirpm install-spec-build-requires [package.spec] [-r] - установка сборочных зависимостей указанного spec-файла и опциональное удаление полученного rpm-файла;\n'
        'pirpm remove-spec-build-requires [package.spec] - удаление сборочных зависимостей указанного spec-файла и удаление полученного rpm-файла;\n'
        'pirpm build-package [package.spec] [-i] [--no-delete] - выкачивание исходников, создание ссылки на abf, сборка пакета из указанного spec-файла и опциональная установка пакета после сборки; --no-delete не удаляет файлы, созданные для сборки;\n'
        'pirpm version-up [package.spec] [version] - повышение версии в указанном spec-файле;\n'
        'pirpm commit-update [package.spec] 712f35... - замена коммита и повышение релиза в указанном spec-файле;\n'
        'pirpm create-edit - копирование текущего проекта в ~/pirpm-edit для правки и последующего создания патча;\n'
        'pirpm create-patch - создание патча из ~/pirpm-edit и его запись в spec-файл;\n'
        'pirpm help - вывод данной справки.'
    )


home_folder = pwd.getpwnam(os.environ.get('SUDO_USER') or os.environ.get('USER')).pw_dir
edit_folder = os.path.join(home_folder, 'pirpm-edit')

argv = sys.argv

if len(argv) < 2:
    show_help()
    sys.exit(0)

specs_in_cwd = [i for i in os.listdir(os.getcwd()) if i.endswith('.spec')]
# spec-файл во всех командах, где используется, стоит под 2 индексом.
# Если его не указать, но в текущей папке только один спек, имеет смысл дописать его автоматически.
if (len(argv) < 3 or not argv[2].endswith('.spec')) and argv[1] not in (
        'install-packages-for-building',
        'replace-current-with-remote',
        'help',
        '--help'
):
    if len(specs_in_cwd) == 1:
        argv.insert(2, specs_in_cwd[0])
        print(f'Не указан spec-файл. Найден в текущей папке: {specs_in_cwd[0]}')
    elif len(specs_in_cwd) == 0:
        print(f'Не указан spec-файл. В текущей папке также не найдено ни одного spec-файла. Утилита завершит работу.')
        sys.exit(0)
    else:
        print(f'Не указан spec-файл. В текущей папке найдено более одного spec-файла. Утилита завершит работу.')
        sys.exit(0)

if argv[1] == 'build-package' and len(specs_in_cwd) > 1:
    print(f'В текущей папке найдено более одного spec-файла. abf не поддерживает сборку пакетов в такой конфигурации. '
          f'Утилита завершит работу.')
    sys.exit(0)

if argv[1] == 'install-packages-for-building':
    install_packages_for_building()

elif argv[1] == 'replace-current-with-remote':
    replace_current_with_remote()

elif argv[1] == 'install-spec-build-requires':
    remove_rpm = True if '-r' in argv else False
    if '-r' in argv:
        argv.remove('-r')
    install_spec_build_requires(spec_file=argv[2], remove_rpm=remove_rpm)

elif argv[1] == 'remove-spec-build-requires':
    remove_spec_build_requires(spec_file=argv[2])

elif argv[1] == 'build-package':
    install_required = True if '-i' in argv else False
    if '-i' in argv:
        argv.remove('-i')
    no_delete = True if '--no-delete' in argv else False
    if '--no-delete' in argv:
        argv.remove('--no-delete')
    build_package(spec_file=argv[2], install_required=install_required, no_delete=no_delete)

elif argv[1] == 'version-up':
    version_up(spec_file=argv[2], specified_version=argv[3] if len(argv) > 3 else '')

elif argv[1] == 'commit-update':
    if len(argv) < 4:
        print('Укажите полный хэш коммита. '
              'Синтаксис: pirpm commit-update [file.spec] c9ecd3c39cf004fd777c6caaaa1f502e1e8475f0')
        sys.exit(0)
    commit_update(spec_file=argv[2], commit=argv[3])

elif argv[1] == 'create-edit':
    create_edit(spec_file=argv[2], edit_folder=edit_folder)

elif argv[1] == 'create-patch':
    create_patch(spec_file=argv[2], edit_folder=edit_folder)

elif argv[1] in ('help', '--help'):
    show_help()

else:
    print(f'Команда {argv[1]} не найдена.')
    show_help()
