"""Jinja2 extensions."""
from __future__ import annotations

from collections.abc import Iterable, Mapping
from decimal import Decimal
from typing import TYPE_CHECKING, Any
import re

from jinja2.ext import Extension

if TYPE_CHECKING:
    from aiohttp import ClientSession
    import jinja2

__all__ = ('GithubAPIExtension', 'ParseMarkdownBadgeExtension', 'ShellExtension',
           'ToPythonExtension')


def topython(  # noqa: PLR0911
        obj: Any, *, convert_strings: bool = True, list_to_tuple: bool = False) -> Any:
    data: Any
    if isinstance(obj, str):
        if convert_strings:
            if re.match(r'^true|false$', obj, re.IGNORECASE):
                return obj.lower() == 'true'
            if obj.isdigit():
                return int(obj)
        fixed = obj.replace('\\', r'\\').replace("'", r"\'")
        return f"'{fixed}'"
    if isinstance(obj, float | int | bool | Decimal | None):
        return str(obj)
    if isinstance(obj, list) and not list_to_tuple:
        data = [topython(x, list_to_tuple=list_to_tuple) for x in obj]
        return f'[{", ".join(data)}]'
    if isinstance(obj, Mapping):
        data = {
            str(k).replace('\\', r'\\').replace("'", r"\'"):
                topython(v, list_to_tuple=list_to_tuple)
            for k, v in obj.items()
        }
        val = ', '.join(f"'{k}': {v}" for k, v in data.items())
        return f'{{{val}}}'
    if isinstance(obj, tuple):
        if not obj:
            return '()'
        data = tuple(topython(x, list_to_tuple=list_to_tuple) for x in obj)
        start = f'({", ".join(data)}'
        if len(data) == 1:
            start += ','
        return f'{start})'
    if isinstance(obj, set):
        data = {topython(x, list_to_tuple=list_to_tuple) for x in obj}
        return f'{{{", ".join(sorted(data))}}}'
    if isinstance(obj, Iterable):
        data = [topython(x, list_to_tuple=list_to_tuple) for x in obj]
        if list_to_tuple:
            if not data:
                return '()'
            return f'({", ".join(data)})' if len(data) > 1 else f'({data[0]},)'
        return f'[{", ".join(data)}]'
    return obj


_MD_BADGE_RE = re.compile(r'^\[!\[(.+?)]\((.+?)\)]$')
_HEREDOC_START = re.compile(r'<<-?\s*\\?[\'"]?(\w+)[\'"]?\s*$')


def _shell_indent(text: str, width: int = 4) -> str:
    prefix = ' ' * width
    lines = text.split('\n')
    result: list[str] = []
    heredoc_end: str | None = None
    for line in lines:
        if heredoc_end is not None:
            result.append(line)
            if line.strip() == heredoc_end:
                heredoc_end = None
        else:
            result.append(prefix + line if line.strip() else line)
            m = _HEREDOC_START.search(line)
            if m:
                heredoc_end = m.group(1)
    return '\n'.join(result)


def _parse_md_badge(anchor: str) -> dict[str, str]:
    """Parse a Markdown badge anchor ``[![alt](image_url)]`` into ``{alt, image}``."""
    m = _MD_BADGE_RE.match(anchor)
    return {'alt': m.group(1), 'image': m.group(2)} if m else {'alt': '', 'image': ''}


class ParseMarkdownBadgeExtension(Extension):
    """Extension that exports the ``parse_md_badge`` :py:class:`~jinja2.Environment` filter."""
    def __init__(self, environment: jinja2.Environment) -> None:
        super().__init__(environment)
        environment.filters['parse_md_badge'] = _parse_md_badge


class ShellExtension(Extension):
    """Extension that exports the ``shell_indent`` :py:class:`~jinja2.Environment` filter."""
    def __init__(self, environment: jinja2.Environment) -> None:
        super().__init__(environment)
        environment.filters['shell_indent'] = _shell_indent


class ToPythonExtension(Extension):
    """Extension that exports the ``topython`` :py:class:`~jinja2.Environment` filter."""
    def __init__(self, environment: jinja2.Environment) -> None:
        super().__init__(environment)
        environment.filters['topython'] = topython


class GithubAPIExtension(Extension):
    """Extension for GitHub API calls."""
    def __init__(self, environment: jinja2.Environment) -> None:
        from .utils.versions import get_github_release_latest_tag  # noqa: PLC0415

        super().__init__(environment)

        async def _github_latest_action_tag(owner: str, repo: str) -> str:
            session: ClientSession | None = environment.globals.get(
                '_aiohttp_session')  # type: ignore[assignment]
            if session is None:
                msg = 'No aiohttp session available for GitHub API calls'
                raise RuntimeError(msg)
            return await get_github_release_latest_tag(session,
                                                       owner,
                                                       repo,
                                                       actions=True,
                                                       skip_releases=True,
                                                       allow_suffixes=False)

        environment.globals['github_latest_action_tag'] = _github_latest_action_tag
