import argparse
import configparser
import json
import os
import platform
import re
import subprocess
import sys
from typing import List, Any, Tuple, Optional, Dict, Pattern, Set

import tomli
import yaml

from ul_py_tool.commands.cmd import Cmd
from ul_py_tool.commands.conf import MAX_LINE_LEN, THIS_LIB_PATH, PYENV_PY_VERSION, SUPPORTED_PYTHON_VERSIONS, MYPY_CONFIG, PIPFILE, PRE_PUSH_DEST, \
    PRE_COMMIT_DEST, UNDER_CI_JOB, PY_TYPED, SOURCE_PYPI, SOURCE_NERO_GITLAB, SOURCE_NERO_GITLAB_HOST, SOURCE_NERO_GITLAB_URL, PIPFILE_LOCK
from ul_py_tool.utils.arg_files_glob import arg_files_glob, arg_file_glob_compile_files, arg_files_print, arg_file_info
from ul_py_tool.utils.arg_str2bool import arg_str2bool
from ul_py_tool.utils.colors import FG_RED, NC, FG_YELLOW, FG_GRAY
from ul_py_tool.utils.pipfile import Pipfile
from ul_py_tool.utils.step import StepError, Stepper, Step
from ul_py_tool.utils.write_stdout import write_stdout


class CmdLint(Cmd):
    setup_file_lists: List[List[str]]
    fix: bool
    py_file_lists: List[List[str]]
    py_file_ignore_lists: List[List[str]]
    # docker_registry_tpl: str
    dockerfiles_lists: List[List[str]]
    dockerfiles_ignore_lists: List[List[str]]
    dockercomposefiles_lists: List[List[str]]
    dockercomposefiles_ignore_lists: List[List[str]]
    helmtemplates_lists: List[List[str]]
    helmtemplates_ignore_lists: List[List[str]]
    yml_file_lists: List[List[str]]
    yml_file_ignored_lists: List[List[str]]
    print_files: int
    check_environment: bool
    check_imports: bool
    exclude_libs: List[str]
    packages_ignored: List[str]
    py_init_dir_ignore_lists: List[List[str]]
    packages_ignored_in_setup: List[str]
    check_hooks: bool
    check_setup_packages: bool
    check_nero_packages_is_fresh: bool
    check_packages: bool

    @staticmethod
    def add_parser_args(parser: argparse.ArgumentParser) -> None:
        parser.add_argument('--fix', dest='fix', type=arg_str2bool, default=False)
        parser.add_argument('--check-env', dest='check_environment', type=arg_str2bool, default=False)
        parser.add_argument('--check-hooks', dest='check_hooks', type=arg_str2bool, required=False, default=not UNDER_CI_JOB)
        parser.add_argument('--check-imports', dest='check_imports', type=arg_str2bool, default=True, required=False)
        parser.add_argument('--check-packages', dest='check_packages', type=arg_str2bool, default=not UNDER_CI_JOB, required=False)
        parser.add_argument('--check-setup-packages', dest='check_setup_packages', type=arg_str2bool, default=not UNDER_CI_JOB, required=False)
        parser.add_argument('--check-nero-packages-is-fresh', dest='check_nero_packages_is_fresh', type=arg_str2bool, default=not UNDER_CI_JOB, required=False)
        parser.add_argument('--ignore-setup-packages', dest='packages_ignored_in_setup', type=str, default=[], nargs='+', required=False)
        parser.add_argument('--ignore-packages', dest='packages_ignored', type=str, default=[], nargs='+', required=False)

        parser.add_argument('--exclude-import', dest='exclude_libs', type=str, default=[], nargs='+', required=False)

        parser.add_argument('--print-files', dest='print_files', type=int, required=False, default=10)

        #  parser.add_argument('--docker-registry-tpl', dest='docker_registry_tpl', type=str, required=False, default='gitlab.neroelectronics.by:5050/unic-lab')

        parser.add_argument('--dockerfiles-ignore', dest='dockerfiles_ignore_lists', nargs='+', type=arg_files_glob(ignore_absent=True), required=False, default=[])
        parser.add_argument('--dockerfiles', dest='dockerfiles_lists', nargs='+', type=arg_files_glob(), required=False, default=[
            arg_files_glob(ignore_absent=True)('Dockerfile'),
            arg_files_glob(ignore_absent=True)('**/Dockerfile'),
        ])

        parser.add_argument('--dockercomposefiles-ignore', dest='dockercomposefiles_ignore_lists', nargs='+', type=arg_files_glob(ignore_absent=True), required=False, default=[])
        parser.add_argument('--dockercomposefiles', dest='dockercomposefiles_lists', nargs='+', type=arg_files_glob(), required=False, default=[
            arg_files_glob(ignore_absent=True)('**/*.docker-compose.yml'),
            arg_files_glob(ignore_absent=True)('**/docker-compose.*.yml'),
            arg_files_glob(ignore_absent=True)('**/docker-compose.yml'),
            arg_files_glob(ignore_absent=True)('*.docker-compose.yml'),
            arg_files_glob(ignore_absent=True)('docker-compose.*.yml'),
            arg_files_glob(ignore_absent=True)('docker-compose.yml'),
        ])

        parser.add_argument('--helmtemplatefiles-ignore', dest='helmtemplates_ignore_lists', nargs='+', type=arg_files_glob(ignore_absent=True), required=False, default=[])
        parser.add_argument('--helmtemplatefiles', dest='helmtemplates_lists', nargs='+', type=arg_files_glob(), required=False, default=[
            arg_files_glob(ignore_absent=True)('.helm/templates/**/*.yml'),
            arg_files_glob(ignore_absent=True)('.helm/templates/*.yml'),
        ])

        parser.add_argument('--setup-files', dest='setup_file_lists', nargs='+', type=arg_files_glob(ignore_absent=True), required=False, default=[
            arg_files_glob(ignore_absent=True)('setup.py'),
        ])

        parser.add_argument('--py-init-dir-ignore', dest='py_init_dir_ignore_lists', nargs='+', type=arg_files_glob(ignore_absent=True, dir=True), required=False, default=[
            arg_files_glob(ignore_absent=True, dir=True)('__init__.py'),
        ])

        parser.add_argument('--py-files-ignore', dest='py_file_ignore_lists', nargs='+', type=arg_files_glob(ignore_absent=True), required=False, default=[
            arg_files_glob(ignore_absent=True)('**/.*/**/*.py'),
        ])
        parser.add_argument('--py-files', dest='py_file_lists', nargs='+', type=arg_files_glob(), required=False, default=[
            arg_files_glob(ignore_absent=True)('**/*.py'),
        ])

        parser.add_argument('--yml-files-ignore', dest='yml_file_ignored_lists', nargs='+', type=arg_files_glob(ignore_absent=True), required=False, default=[
        ])
        parser.add_argument('--yml-files', dest='yml_file_lists', nargs='+', type=arg_files_glob(), required=False, default=[
            arg_files_glob(ignore_absent=True)('*.yml'),
            arg_files_glob(ignore_absent=True)('**/*.yml'),
        ])

    def _check_type_ignore_file(self, files: List[str]) -> None:
        ingore_re = re.compile(r'^\s*#\s*type\s*:\s*ignore\s*')
        noqa_re = re.compile(r'^\s*#\s*noqa\s*$')
        flake8_noqa_re = re.compile(r'^\s*#\s*flake8\s*:\s*noqa\s*$')
        type_ignored_files = []
        noqa_files = []
        for file in files:
            with open(file, 'rt') as f:
                for line in f.readlines():
                    if ingore_re.match(line) is not None:
                        type_ignored_files.append(file)
                        break
                    if noqa_re.match(line) is not None:
                        noqa_files.append(file)
                        break
                    if flake8_noqa_re.match(line) is not None:
                        noqa_files.append(file)
                        break
        if len(type_ignored_files) > 0 or len(noqa_files) > 0:
            for ign_file in type_ignored_files:
                write_stdout(f'{arg_file_info(ign_file)} :: has "# type: ignore" for HOLE FILE. please explicitly ignore this file from linting recipe')
            for ign_file in noqa_files:
                write_stdout(f'{arg_file_info(ign_file)} :: has "# noqa" or "# flake8: noqa" for HOLE FILE. please explicitly ignore this file from linting recipe')
            raise StepError()

    def _check_isort(self, result: 'subprocess.CompletedProcess[bytes]') -> None:
        lines_count = 0
        for line in result.stderr.decode('utf-8').split('\n'):
            line = line.strip()
            if not line:
                continue
            lines_count += 1
            write_stdout(line.replace('ERROR: ', '').replace(' Imports ', f':0 {FG_RED}Imports ') + NC)
        write_stdout(f'{lines_count} errors')
        if lines_count > 0:
            raise StepError()

    def _step_check_setup_files(self) -> bool:
        setup_files, ignored_files = arg_file_glob_compile_files(self.setup_file_lists)

        if len(setup_files) == 0:
            return False

        arg_files_print(self.print_files, setup_files, print_total=False, name='setup files')
        for setup_file in setup_files:
            with open(setup_file, 'rt') as f:
                setup_file_content = f.read()
                if PY_TYPED not in setup_file_content:
                    raise StepError(f'"py.typed" must be specified in {setup_file}. please add "py.typed" EMPTY file to repo and specify it in setup.py')
        return True

    def _check_imports(self, py_files: List[str]) -> bool:
        excl_libs = set(self.exclude_libs or [])
        libs = {i for i in {'flask', 'requests', 'flask_limiter', 'werkzeug'} if i not in excl_libs}

        if len(libs) == 0:
            return False

        write_stdout(f'Checking libs: {", ".join(libs)}')
        regs: Dict[str, List[Pattern[str]]] = {}
        for lib in libs:
            regs[lib] = [
                re.compile(rf'^\s*import\s+{lib}\s+'),
                re.compile(rf'^\s*import\s+{lib}\.'),
                re.compile(rf'^\s*from\s+{lib}\s+'),
                re.compile(rf'^\s*from\s+{lib}\.'),
            ]

        errors: Dict[str, List[str]] = {}
        for py_file in py_files:
            with open(py_file, 'rt') as f:
                for i, line in enumerate(f.readlines()):
                    for lib, reg_list in regs.items():
                        for reg in reg_list:
                            for m in reg.findall(line):
                                if py_file not in errors:
                                    errors[py_file] = []
                                errors[py_file].append((
                                    f'{py_file}:{i + 1}: '
                                    f'{FG_RED}RESTRICTED{NC} import of "{FG_RED}{lib}{NC}" in line #{i} '
                                    f'::    {line.replace(m, f"{FG_YELLOW}{m.strip()}{NC}").strip()}'
                                ))
        if len(errors.keys()) > 0:
            for _1, err_list in errors.items():
                for err in err_list:
                    write_stdout(err)
            raise StepError()
        return True

    def _step_check_gitignore(self) -> bool:
        gitignore = os.path.join(os.getcwd(), '.gitignore')
        if not os.path.isfile(gitignore):
            write_stdout(f'file "{gitignore}" was not found ')
            raise StepError()
        with open(gitignore, 'rt') as f:
            for line in f.readlines():
                if PYENV_PY_VERSION in line:
                    raise StepError(f'found {PYENV_PY_VERSION} in version. REMOVE IT FROM .gitignore')
        return True

    def _step_check_python_version(self) -> None:
        python_version_fp = os.path.join(os.getcwd(), PYENV_PY_VERSION)
        if not os.path.exists(python_version_fp):
            raise StepError(f'file ./{PYENV_PY_VERSION}:0 not found. please use pyenv!')
        with open(python_version_fp, 'rt') as f:
            version_string = f.read().strip()
        m = re.compile(r'^\d+\.\d+\.\d+$').findall(version_string)
        if len(m) != 1:
            raise StepError(f'invalid format of python version. {version_string} was given')

        version: str = m[0]
        minor_version = ".".join(version.split('.')[:2])
        if not any(version.startswith(v) for v in SUPPORTED_PYTHON_VERSIONS):
            raise StepError(f'unsupported python version. MUST BE {" or ".join(SUPPORTED_PYTHON_VERSIONS)}. {version} was given')
        if not version.startswith('.'.join(platform.python_version_tuple()[:2])):
            write_stdout(f'unsupported version of python to run this script. must be {minor_version}. {sys.version} was ran')

        mypy_config_fp = os.path.join(os.getcwd(), MYPY_CONFIG)
        if os.path.exists(mypy_config_fp):
            config = configparser.ConfigParser()
            config.read(mypy_config_fp)
            mypy_section = config['mypy']
            if not mypy_section:
                raise StepError(f'[mypy] section must be specified in {mypy_config_fp}')
            mypy_py_version = mypy_section.get('python_version', None)
            if not mypy_py_version:
                raise StepError(f'python_version must be specified in {mypy_config_fp}')
            if not mypy_py_version.startswith(minor_version):
                raise StepError(f'python_version in {MYPY_CONFIG} is not compatible with version in {PYENV_PY_VERSION}. must be {minor_version}')

        pipfile = os.path.join(os.getcwd(), PIPFILE)
        if not os.path.exists(pipfile):
            raise StepError(f'file {pipfile} must be specified')

        with open(pipfile, 'rt') as f:
            pipfile_content = tomli.loads(f.read())

        pipfile_requires = pipfile_content.get('requires', None)
        if pipfile_requires is None:
            raise StepError(f'[requires] section must be specified in {PIPFILE}')

        pipfile_py_version: Optional[str] = pipfile_requires.get('python_version', None)
        if pipfile_py_version is None:
            raise StepError(f'python_version must be specified in {PIPFILE}')

        if not pipfile_py_version.startswith(minor_version):
            raise StepError(f'python_version in {PIPFILE} not compatible with version in {PYENV_PY_VERSION}. must be {minor_version}')

    def _step_check_hooks(self) -> bool:
        if not self.check_hooks:
            return False

        with open(PRE_COMMIT_DEST, 'rt') as f:
            content = f.read()
            if 'pipenv run lint' not in content:
                raise StepError('"pipenv run lint" must be in pre-commit hook. please do "ulpytool install"')

        with open(PRE_PUSH_DEST, 'rt') as f:
            content = f.read()
            if 'pipenv run test' not in content:
                raise StepError('"pipenv run test" must be in pre-push hook. please do "ulpytool install"')
            if 'pipenv run lint' not in content:
                raise StepError('"pipenv run lint" must be in pre-push hook. please do "ulpytool install"')
        return True

    def _check_docker_image(self, file: str, stp: Step, image: Optional[str], *, line: int = 0, err_prefix: str = '') -> None:
        if image is None:
            return
        if len(image) == 0:
            return
        if 'WERF_' in image:
            return
        # if self.docker_registry_tpl not in image:
            # stp.add_error_in_file(file, f'{err_prefix} invalid image. must starts from "{self.docker_registry_tpl}". "{image}" was given', line=line)

    def _step_check_docker_compose_files(self, stp: Step) -> bool:
        dockercomposefiles, ignored_files = arg_file_glob_compile_files(self.dockercomposefiles_lists, self.dockercomposefiles_ignore_lists)
        if len(dockercomposefiles) == 0:
            return False
        arg_files_print(self.print_files, dockercomposefiles, ignored_files=ignored_files, name='docker-compose files')
        for dcf in dockercomposefiles:
            with open(dcf, 'rt') as f:
                content = yaml.load(f.read(), yaml.SafeLoader)
            if content.get('version') != '3.8':
                stp.add_error_in_file(dcf, 'invalid specification version. must be 3.8')
            if 'services' not in content:
                stp.add_error_in_file(dcf, 'no services found')
                continue
            for service_name, sv in content['services'].items():
                self._check_docker_image(dcf, stp, sv.get('image', None), err_prefix=f'service "{FG_RED}{service_name}{NC}" has ')
        return True

    def _step_check_dockerfiles(self, stp: Step) -> bool:
        dockerfiles, ignored_files = arg_file_glob_compile_files(self.dockerfiles_lists, self.dockerfiles_ignore_lists)
        if len(dockerfiles) == 0:
            return False
        arg_files_print(self.print_files, dockerfiles, ignored_files=ignored_files, name='Dockerfiles')
        for df in dockerfiles:
            with open(df, 'rt') as f:
                df_lines = f.readlines()
            for li, lc in enumerate(df_lines):
                lc = lc.strip()
                if lc.startswith('FROM'):
                    res = re.split(r'\s+', lc)
                    self._check_docker_image(df, stp, res[1], line=li + 1)
                if lc.startswith('RUN'):
                    if 'pipenv' in lc and 'install' in lc:
                        valid_pipenv_install_str = 'RUN pipenv install --dev --system --deploy --ignore-pipfile'
                        if lc != valid_pipenv_install_str:
                            stp.add_error_in_file(df, f'invalid command of packages installation from Pipfile. must be "{valid_pipenv_install_str}". "{lc}" was given', line=li + 1)
        return True

    def _find_helm_spec(self, spec: Any, prop: str, *, _context: str = '') -> List[Tuple[Any, str]]:
        res: List[Tuple[Any, str]] = []
        if isinstance(spec, dict):
            if prop in spec:
                res.append((spec[prop], _context[1:] if _context.startswith('.') else _context))
            for k, v in spec.items():
                res = [*res, *self._find_helm_spec(v, prop, _context=f'{_context}.{k}')]
        if isinstance(spec, list):
            for i, v in enumerate(spec):
                res = [*res, *self._find_helm_spec(v, prop, _context=f'{_context}.{i}')]
        return res

    def check_helm(self, stp: Step) -> bool:
        helm_files, ignored_files = arg_file_glob_compile_files(self.helmtemplates_lists, self.helmtemplates_ignore_lists)
        if len(helm_files) == 0:
            return False
        for f_name in helm_files:
            with open(f_name, 'rt') as f:
                contents = ['']
                for line in f.read().split('\n'):
                    if line.startswith('#'):
                        continue
                    line = re.sub(r'\{\{[^}]+\}\}', '', line)
                    if len(line.strip()) == 0:
                        continue
                    if line == '---':
                        if len(contents[-1]) > 0:
                            contents.append('')
                        continue
                    contents[-1] += f'\n{line}'
                yml_contents = [yaml.load(content, yaml.SafeLoader) for content in contents]
                for yml_content in yml_contents:
                    for image in self._find_helm_spec(yml_content, 'image'):
                        self._check_docker_image(f_name, stp, image[0], err_prefix=f'{image[1]} :: ')
        return True

    def _step_check_mypy_config(self) -> bool:
        mypy_config = os.path.join(os.getcwd(), MYPY_CONFIG)
        if not os.path.exists(mypy_config):
            return False

        config = configparser.ConfigParser()
        config.read(mypy_config)
        mypy_section = config['mypy']
        if not mypy_section:
            raise StepError(f'[mypy] section must be specified in {mypy_config}')

        # important_plugins = ['pydantic.mypy']
        # for plug in important_plugins:
        #     if plug not in mypy_section.get('plugins', '').lower():
        #         raise StepError(f'{arg_file_info(MYPY_CONFIG)} :: plugins must contains "{plug}"')
        if mypy_section.get('ignore_missing_imports', '').lower() != "false":
            raise StepError(f'{arg_file_info(MYPY_CONFIG)} :: ignore_missing_imports must be False. "{mypy_section["ignore_missing_imports"]}" was given')
        if mypy_section.get('follow_imports', '').lower() != "error":
            raise StepError(f'{arg_file_info(MYPY_CONFIG)} :: follow_imports must be error. "{mypy_section["follow_imports"]}" was given')
        if mypy_section.get('follow_imports_for_stubs', '').lower() != "true":
            raise StepError(f'{arg_file_info(MYPY_CONFIG)} :: follow_imports_for_stubs must be True. "{mypy_section["follow_imports_for_stubs"]}" was given')

        return True

    @property
    def _ignored_packages(self) -> Set[str]:
        res = {'wheel', 'setuptools'}
        for p in self.packages_ignored:
            res.add(p.strip().lower())
        return res

    @property
    def _packages_ignored_in_setup(self) -> Set[str]:
        res = set()
        for p in self.packages_ignored_in_setup:
            res.add(p.strip().lower())
        return res

    def _step_check_pipfile(self, stp: Step) -> bool:
        err_pref = f'{arg_file_info(PIPFILE, line=1)} ::'
        pipfile = os.path.join(os.getcwd(), PIPFILE)
        pipfile_lock = os.path.join(os.getcwd(), PIPFILE_LOCK)

        if not os.path.exists(pipfile):
            raise StepError(f'{err_pref} must be specified')

        if not os.path.exists(pipfile_lock):
            raise StepError(f'{err_pref} was not locked. please make "pipenv install"')

        try:
            with open(pipfile_lock, 'rt') as f:
                pipfile_lock_cntnt: Dict[str, Any] = json.load(f)
            ppfile = Pipfile.load(pipfile)
        except Exception as e:  # noqa: B902
            raise StepError(f'{err_pref} {e}')

        if len(ppfile.sources) == 0:
            raise StepError(f'{err_pref} has no sources')

        # valid_pypi_source = 'https://pypi.org/simple'
        # if ppfile.default_source.url != valid_pypi_source:
        #     stp.add_error_in_file(pipfile, f'has invalid default source url. must be "{valid_pypi_source}". {ppfile.default_source.url} was given')

        if not ppfile.default_source.verify_ssl:
            stp.add_error_in_file(pipfile, f'default source must be verified with ssl. please set "verify_ssl = true" for source "{ppfile.default_source.name}"')

        if ppfile.default_source.name != SOURCE_PYPI:
            stp.add_error_in_file(pipfile, f'default source must have name "{SOURCE_PYPI}". "{ppfile.default_source.name}" was given')

        for source in ppfile.sources:
            if SOURCE_NERO_GITLAB_HOST not in source.url:
                continue
            if source.name != SOURCE_NERO_GITLAB:
                stp.add_error_in_file(pipfile, f'please rename source "{FG_RED}{source.name}{NC}" to {SOURCE_NERO_GITLAB}')
            if source.url != SOURCE_NERO_GITLAB_URL:
                stp.add_error_in_file(pipfile, f'ALL GITLAB packages must be stored in one project. url must be "{SOURCE_NERO_GITLAB_URL}"')
            if source.verify_ssl:
                stp.add_error_in_file(pipfile, f'source "{source.name}" must have "verify_ssl = false"')

        if self.check_hooks:
            for command in {'test', 'lint'}:
                if ppfile.scripts.get(command, None) is None:
                    stp.add_error_in_file(pipfile, f'script "{FG_RED}{command}{NC}" is not specified')
                else:
                    if not ppfile.scripts[command].command:
                        stp.add_error_in_file(pipfile, f'script "{FG_RED}{command}{NC}" has empty command')

        setup_files, _1 = arg_file_glob_compile_files(self.setup_file_lists)

        main_setup_file_content = ''
        for sf in setup_files:
            if os.path.dirname(sf) == os.getcwd():
                with open(sf, 'rt') as f:
                    main_setup_file_content = f.read()
                break

        installed_packages = Pipfile.freeze()
        for pack in ppfile.all_packages.values():
            if pack.name in self._ignored_packages or not self.check_packages:
                continue

            assert pack.source is not None

            if pack.semver is None and pack.source.name == SOURCE_NERO_GITLAB:
                stp.add_error_in_file(
                    pipfile,
                    f'package "{FG_RED}{pack.name}{NC}" form "{SOURCE_NERO_GITLAB}" must have version with valid semver formant. '
                    f'IT IS THE RULE OF COMPANY. {pack.version} was given',
                )

            if pack.version_operator != '==':
                stp.add_error_in_file(
                    pipfile,
                    f'package "{FG_RED}{pack.name}{NC}" has invalid operator for version. must be "==". '
                    f'"{pack.version_operator}" was given in version "{pack.version}"',
                )

            if pack.source.name == SOURCE_NERO_GITLAB:
                if pack.is_dev:
                    stp.add_error_in_file(pipfile, f'package "{FG_RED}{pack.name}{NC}" could not be in the dev-package!')

                if len(pack.available_versions) > 0:
                    msg = f'package "{FG_RED}{pack.name}{NC}" is not fresh. version={FG_RED}{pack.semver}{NC}. ' \
                        f'versions for upgrading: {", ".join(str(v) for v in pack.available_versions)}'

                    if pack.name == 'ulpytool':
                        stp.add_error(msg)
                    else:
                        stp.add_warn(msg)

            if pack.name not in installed_packages:
                stp.add_error_in_file(pipfile, f'package "{FG_RED}{pack.name}{NC}" not installed')
            else:
                if installed_packages[pack.name].clean_version != pack.clean_version:
                    stp.add_error_in_file(
                        pipfile,
                        f'package "{FG_RED}{pack.name}{NC}" installed version is invalid. '
                        f'must be "{pack.clean_version}". '
                        f'{installed_packages[pack.name].clean_version} was given',
                    )

            if pack.is_dev:
                ps = pipfile_lock_cntnt['develop']
            else:
                ps = pipfile_lock_cntnt['default']
            if pack.name not in ps:
                stp.add_error_in_file(pipfile, f'package "{FG_RED}{pack.name}{NC}" not in lock. please make "pipenv install{" --dev" if pack.is_dev else ""}"')
            else:
                p = ps[pack.name]
                if p['version'] != pack.version:
                    stp.add_error_in_file(
                        pipfile,
                        f'package "{FG_RED}{pack.name}{NC}" has invalid version. '
                        f'must be {FG_RED}"{pack.version}"{NC}. '
                        f'"{p["version"]}" was given',
                    )

            if main_setup_file_content and pack.name not in self._packages_ignored_in_setup and not pack.is_dev and pack.source.name != SOURCE_NERO_GITLAB:
                if pack.pip_representation not in main_setup_file_content and pack.pip_representation.replace('==', '>=') not in main_setup_file_content:
                    stp.add_error_in_file(pipfile, f'package "{FG_RED}{pack.pip_representation}{NC}" not in {arg_file_info(setup_files[0])}')
        return True

    def _chk_ymllinter(self, result: 'subprocess.CompletedProcess[bytes]') -> None:
        form_re = re.compile(r'^(.+?):(\d+):(\d+):\s+\[([^]]+)]\s+([^(]+)\(([^)]+)\)\s*$')
        errors: List[Tuple[str, str, str, str, str, str]] = []
        max_len_of_file = 0
        for lc in result.stdout.decode().split('\n'):
            m = form_re.findall(lc.strip())
            for mr in m:
                if len(mr) == 6:
                    file, line, col, lvl, msg, code = mr
                    max_len_of_file = max(max_len_of_file, len(file))
                    errors.append(mr)
        for file, line, col, lvl, msg, code in errors:
            write_stdout(f'{arg_file_info(os.path.abspath(file), line=int(line), col=int(col)): <{max_len_of_file + 10}} :: {FG_RED}{lvl}{NC} :: {msg} :: {FG_GRAY}({code}){NC}')
        if len(errors) == 0 and result.returncode != 0:
            write_stdout(result.stdout.decode())
        result.check_returncode()

    def _check_init_files(self, py_files: List[str], stp: Step) -> None:
        uniq_init_files = set()
        cwd = os.path.abspath(os.getcwd())
        for f in py_files:
            d = os.path.dirname(f)
            if d != cwd:
                uniq_init_files.add(os.path.join(d, '__init__.py'))

        init_files, ignored_inits = arg_file_glob_compile_files([list(uniq_init_files)], self.py_init_dir_ignore_lists)
        for init in init_files:
            if not os.path.exists(init):
                stp.add_error_in_file(init, 'is not exists')
            elif not os.path.isfile(init):
                stp.add_error_in_file(init, 'is not a file')

        for init in init_files:
            if not os.path.exists(init):
                continue
            with open(init, 'rt') as init_f:
                if len(init_f.read().strip()) > 0:
                    stp.add_warn(f'init file "{init}" is not empty')

    def run(self) -> None:
        stepper = Stepper()

        with stepper.step('CHECK PYTHON VERSION'):
            self._step_check_python_version()

        has_py_files = self._check_py_files(stepper)

        with stepper.step('CHECK HOOKS', print_error=True) as stp:
            if not self._step_check_hooks():
                stp.skip()

        yml_files, ignored_yml_files = arg_file_glob_compile_files(self.yml_file_lists, self.yml_file_ignored_lists)
        with stepper.step('CHECK YAML FILES') as stp:
            if len(yml_files) == 0:
                stp.skip()
            else:
                arg_files_print(self.print_files, yml_files, ignored_files=ignored_yml_files)
                stp.run_cmd([
                    'yamllint',
                    '--config-file', os.path.join(THIS_LIB_PATH, 'conf', 'yamlint.yml'),
                    '--format', 'parsable',
                    '--strict',
                    *yml_files,
                ], chk=self._chk_ymllinter, print_std=False)

        with stepper.step('CHECK HELM-TEMPLATES') as stp:
            if not self.check_helm(stp):
                stp.skip()

        with stepper.step('CHECK PIPFILE') as stp:
            if not self._step_check_pipfile(stp):
                stp.skip()

        with stepper.step('CHECK DOCKER COMPOSE FILES') as stp:
            if not self._step_check_docker_compose_files(stp):
                stp.skip()

        with stepper.step('CHECK SETUP FILES') as stp:
            if not self._step_check_setup_files():
                stp.skip()

        with stepper.step('CHECK gitignore') as stp:
            if not self._step_check_gitignore():
                stp.skip()

        with stepper.step('CHECK DOCKERFILES') as stp:
            if not self._step_check_dockerfiles(stp):
                stp.skip()

        if not has_py_files:
            raise StepError('no python files found for linting')

    def _check_py_files(self, stepper: Stepper) -> bool:
        py_files, ignored_files = arg_file_glob_compile_files(self.py_file_lists, self.py_file_ignore_lists)
        write_stdout('')
        arg_files_print(self.print_files, py_files, ignored_files=ignored_files, name='python files')

        local_mypy_conf = os.path.join(os.getcwd(), 'mypy.ini')
        mypy_conf = local_mypy_conf if os.path.isfile(local_mypy_conf) else os.path.join(THIS_LIB_PATH, "conf", "mypy.ini")

        with stepper.step('CHECK __init__.py FILES') as stp:
            if not len(py_files):
                stp.skip()
            else:
                self._check_init_files(py_files, stp)

        with stepper.step('TYPE IGNORE FILES') as stp:
            if not len(py_files):
                stp.skip()
            else:
                self._check_type_ignore_file(py_files)

        with stepper.step('CHECK MYPY CONFIG') as stp:
            if not len(py_files):
                stp.skip()
            else:
                if not self._step_check_mypy_config():
                    stp.skip()

        with stepper.step('MYPY') as stp:
            if not len(py_files):
                stp.skip()
            else:
                stp.run_cmd([
                    'mypy',
                    '--follow-imports=normal',
                    f'--config-file={mypy_conf}',
                    '--namespace-packages',
                    '--explicit-package-bases',
                    *py_files,
                ])

        with stepper.step('LINTING RUFF') as stp:
            if not len(py_files):
                stp.skip()
            else:
                stp.run_cmd([
                    'ruff',
                    'check',
                    *(('--fix', ) if self.fix else tuple()),
                    '--cache-dir',
                    os.path.join(os.getcwd(), ".tmp", "ruff_cache"),
                    f'--line-length={MAX_LINE_LEN}',
                    *py_files,
                ])

        with stepper.step('LINT IMPORTS') as stp:
            if not len(py_files):
                stp.skip()
            else:
                if not self.check_imports:
                    stp.skip()
                elif not self._check_imports(py_files):
                    stp.skip()

        return bool(py_files)
