# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
# https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2022 Maxwell G <gotmax@e.email>

"""
Validate that collections tag their releases in their respective git repositories
"""
import asyncio
import os
import re
import sys
import typing as t

import asyncio_pool  # type: ignore[import]

from antsibull_core.logging import log

from antsibull_core import app_context
from antsibull_core.dependency_files import DepsFile
from antsibull_core.yaml import store_yaml_file, load_yaml_file

from antsibull.collection_meta import CollectionMetadata, CollectionsMetadata

TAG_MATCHER: t.Pattern[str] = re.compile(r'^.*refs/tags/(.*)$')
TAG_VERSION_REGEX: t.Pattern[str] = re.compile(r'^v?(.*)$')
mlog = log.fields(mod=__name__)


def validate_tags_file_command() -> int:
    """
    Ensure that collection versions in an Ansible release are tagged in
    collections' respective git repositories. This validates the tag file
    generated by the 'validate-tags' subcommand.
    """
    app_ctx = app_context.app_ctx.get()
    tag_data = load_yaml_file(app_ctx.extra['tags_file'])
    return _print_validation_errors(tag_data)


def validate_tags_command() -> int:
    """
    Ensure that collection versions in an Ansible release are tagged in
    collections' respective git repositories.
    This retrieves a list of tags from the repository of each collection,
    optionally saves the retrieved tag data to a file,
    and then verifies the data.
    """
    app_ctx = app_context.app_ctx.get()
    tag_data = asyncio.run(
        get_collections_tags(
            app_ctx.extra['data_dir'], app_ctx.extra['deps_file']
        )
    )
    if app_ctx.extra['output']:
        store_yaml_file(app_ctx.extra['output'], tag_data)
    return _print_validation_errors(tag_data)


def _print_validation_errors(
    tag_data: t.Dict[str, t.Dict[str, t.Optional[str]]]
) -> int:
    """
    This takes the tag_data and prints any validation errors to stderr.
    """
    errors = validate_tags(tag_data)
    if not errors:
        return 0
    for error in errors:
        print(error, file=sys.stderr)
    return 1


##############
# Library code
##############


def validate_tags(
    tag_data: t.Dict[str, t.Dict[str, t.Optional[str]]]
) -> t.List[str]:
    """
    Validate that each collection in tag_data has a repository and a tag
    associated with it. Return a list of validation errors.
    """
    errors = []
    for name, data in tag_data.items():
        if not data['repository']:
            errors.append(
                f"{name}'s repository is not specified at all in collection-meta.yaml"
            )
            continue
        if not data['tag']:
            errors.append(
                f"{name} {data['version']} is not tagged in "
                f"{data['repository']}"
            )
    return errors


async def get_collections_tags(
    data_dir: str, deps_filename: str
) -> t.Dict[str, t.Dict[str, t.Optional[str]]]:
    """
    Iterate over the collections in a CollectionsMetadata file,
    retrieve their tags, and return a dictionary
    of collection names mapped to dictionaries containing
    each collection's 'version' and 'repository' from a DepsFile
    and the 'tag' that matches.
    """
    lib_ctx = app_context.lib_ctx.get()

    deps_filename = os.path.join(data_dir, deps_filename)
    deps_data = DepsFile(deps_filename).parse()
    meta_data = CollectionsMetadata(data_dir)

    async with asyncio_pool.AioPool(size=lib_ctx.thread_max) as pool:
        collection_tags = {}
        for name, data in meta_data.data.items():
            collection_tags[name] = pool.spawn_n(
                _get_collection_tags(deps_data.deps[name], data, name)
            )
        collection_tags = {
            name: await data for name, data in collection_tags.items()
        }
        return collection_tags


async def _get_collection_tags(
    version: str, meta_data: CollectionMetadata, name: str
) -> t.Dict[str, t.Optional[str]]:
    flog = mlog.fields(func='_get_collection_tags')
    repository = meta_data.repository
    data: t.Dict[str, t.Optional[str]] = dict(
        version=version, repository=repository, tag=None
    )
    if meta_data.collection_directory:
        data['collection_directory'] = meta_data.collection_directory
    if not repository:
        flog.debug("'repository' is None. Exitting...")
        return data
    tag_version_regex: t.Optional[t.Pattern[str]] = None
    if meta_data.tag_version_regex:
        try:
            tag_version_regex = re.compile(meta_data.tag_version_regex)
        except re.error as err:
            flog.fields(err=err, collection=name).error(
                f'{tag_version_regex} is an invalid regex'
            )
            return data
    async for tag in _get_tags(repository):
        if _normalize_tag(tag, tag_version_regex) == version:
            data['tag'] = tag
            break
    return data


async def _get_tags(repository) -> t.AsyncGenerator[str, None]:
    flog = mlog.fields(func='_get_tags')
    args = (
        'git',
        'ls-remote',
        '--refs',
        '--tags',
        repository,
    )
    flog.debug(f'Running {args}')
    proc = await asyncio.create_subprocess_exec(
        *args,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
        # This makes it so git doesn't ask for a password when a repository
        # is inaccessible.
        env={'GIT_TERMINAL_PROMPT': '0'},
    )
    stdout, stderr = await proc.communicate()
    flog.fields(stderr=stderr, returncode=proc.returncode).debug(
        'Ran git ls-remote'
    )
    if proc.returncode != 0:
        flog.error(f'Failed to fetch tags for {repository}')
        return
    tags: t.List[str] = stdout.decode('utf-8').splitlines()
    if not tags:
        flog.warning(f'{repository} does not have any tags')
        return
    for tag in tags:
        match = TAG_MATCHER.match(tag)
        if match:
            yield match.group(1)
        else:
            flog.debug(f'git ls-remote output line skipped: {tag}')


def _normalize_tag(
    tag: str, regex: t.Optional[t.Pattern[str]]
) -> t.Optional[str]:
    regex = regex or TAG_VERSION_REGEX
    if match := regex.match(tag):
        return match.group(1)
    return tag
