import itertools

from collections import OrderedDict
from typing import Any
from typing import Dict
from typing import Iterable
from typing import List
from typing import Union

from commitizen.cz import exceptions
from commitizen.cz.base import BaseCommitizen
from commitizen.cz.base import BaseConfig
from commitizen.defaults import Questions
from config import INCREMENT
from config import CommitType
from config import CzEspressifConfig
from example import build_example
from jinja2 import PackageLoader


def required_validator(answer: str, msg: str = '') -> str:
    if not answer:
        raise exceptions.AnswerRequiredError(msg)
    return answer


def multiple_line_breaker(answer: str, sep: str = '|') -> str:
    return '\n'.join(line.strip() for line in answer.split(sep) if line)


def parse_scope(text: str) -> str:
    if not text:
        return ''
    scope = text.strip().split()
    if len(scope) == 1:
        return scope[0]
    return '-'.join(scope)


def parse_subject(text: str) -> str:
    if isinstance(text, str):
        text = text.strip('.').strip()
    return required_validator(text, msg='Subject is required.')


class CzPluginEspressif(BaseCommitizen):  # pylint: disable=abstract-method
    template = 'CHANGELOG.md.j2'
    template_loader = PackageLoader('czespressif', 'templates')

    def __init__(self, config: BaseConfig):
        super().__init__(config)
        self.cze_config = CzEspressifConfig(config.settings)

    @property
    def known_types(self) -> Dict[str, CommitType]:
        return dict(
            itertools.chain(
                ((t.type, t) for t in self.cze_config.known_types),
            )
        )

    @property
    def re_known_types(self) -> str:
        return '|'.join(t.regex or k for k, t in self.known_types.items())

    @property
    def changelog_pattern(self) -> str:
        re_known_types = '|'.join(k for k, t in self.known_types.items())
        return rf'\A({re_known_types})(\(.+\))?(!)?'

    @property
    def change_type_map(self) -> Dict[str, str]:
        return dict(
            itertools.chain(
                ((t.type, f'{t.emoji + " " if self.cze_config.use_emoji and t.emoji else ""}{t.heading}') for t in self.cze_config.known_types if t.heading),
            )
        )

    @property
    def change_type_order(self) -> List[str]:
        return [
            f'{commit_type.emoji} {commit_type.heading}' if self.cze_config.use_emoji and commit_type.emoji else commit_type.heading
            for commit_type in self.cze_config.known_types
            if commit_type.changelog and commit_type.heading
        ]

    @property
    def bump_pattern(self) -> str:
        """Regex to extract information from commit (subject and body)"""
        re_types = '|'.join(t.regex or k for k, t in self.known_types.items() if t.bump)
        return rf'^((({re_types})(\(.+\))?(!)?)|\w+!):'

    @property
    def bump_map(self) -> Dict[str, INCREMENT]:
        """
        Mapping the extracted information to a SemVer increment type (MAJOR, MINOR, PATCH)
        """
        return OrderedDict(
            (
                (r'^.+!$', 'MAJOR'),
                *((rf'^{t.regex or k}', t.bump) for k, t in self.known_types.items() if t.bump),
            )
        )

    @property
    def bump_map_major_version_zero(self) -> Dict[str, INCREMENT]:
        return OrderedDict((pattern, increment.replace('MAJOR', 'MINOR')) for pattern, increment in self.bump_map.items())

    @property
    def commit_parser(self) -> str:
        return (
            rf'(?P<change_type>{self.re_known_types})'
            r'(?:\((?P<scope>[^()\r\n]*)\))?'  # Optional scope
            r'(?P<breaking>!)?:\s'
            r'(?P<message>.*)'  # Required message
            r'(?:\n{2,}(?P<body>.*))?'  # Optional body
            r'(?:\n{2,}(?P<footer>.*))?'  # Optional footer
        )

    def questions(self) -> Questions:
        """Questions regarding the commit message."""
        questions: List[Dict[str, Any]] = [
            {
                'type': 'list',
                'name': 'type',
                'message': 'Select the type of change you are committing',
                'choices': [
                    {
                        'value': t.type,
                        'name': f'{t.emoji + " " if self.cze_config.use_emoji and t.emoji else ""}{t.type}: {t.description}',
                    }
                    for t in self.cze_config.known_types
                    if t.question
                ],
            },
            {
                'type': 'input',
                'name': 'scope',
                'message': 'Scope. Define Could be anything specifying place of the commit change (wifi, bt, core):\n',
                'filter': parse_scope,
            },
            {
                'type': 'input',
                'name': 'subject',
                'filter': parse_subject,
                'message': 'Subject. Concise description of the changes. Imperative, lower case and no period:\n',
            },
            {
                'type': 'input',
                'name': 'body',
                'message': 'Provide additional contextual information - commit message BODY: (press [enter] to skip)\n',
                'filter': multiple_line_breaker,
            },
            {
                'type': 'confirm',
                'name': 'is_breaking_change',
                'message': 'Is this a BREAKING CHANGE?\n',
                'default': False,
            },
            {
                'when': lambda x: x['is_breaking_change'],
                'type': 'input',
                'name': 'breaking_change',
                'message': 'Breaking changes. Details the breakage:\n',
                'filter': multiple_line_breaker,
            },
            {
                'type': 'input',
                'name': 'footer',
                'message': 'Footer. Information about Breaking Changes and reference issues that this commit impacts: (press [enter] to skip)\n',
                'filter': multiple_line_breaker,
            },
        ]
        return questions

    def message(self, answers: Dict[str, Any]) -> str:
        prefix = answers['type']
        scope = answers['scope']
        subject = answers['subject']
        body = answers['body']
        is_breaking_change = answers['is_breaking_change']
        breaking_change = answers.get('breaking_change')
        footer = answers.get('footer', '')
        extra = ''

        if scope:
            scope = f'({scope})'

        if is_breaking_change and not breaking_change:
            scope += '!'

        if body:
            body = f'\n\n{body}'

        if is_breaking_change and breaking_change:
            footer = f'BREAKING CHANGE: {breaking_change}\n{footer}'

        if footer:
            footer = f'\n\n{footer}'

        message = f'{prefix}{scope}: {subject}{extra}{body}{footer}'

        return message

    def changelog_message_builder_hook(self, parsed_message: Dict[str, Any], commit: Any) -> Union[Dict[str, Any], Iterable[Dict[str, Any]], None]:  # pylint: disable=unused-argument
        # Remap breaking changes type
        if parsed_message.get('breaking'):
            parsed_message['change_type'] = 'BREAKING CHANGE'

        # Get the full list of types to include from pyproject.toml
        included_in_changelog = self.cze_config.settings.get('types_in_changelog', [])

        # If the list is empty (i.e., not set), include all default types
        if not included_in_changelog:
            commit_type = self.known_types.get(parsed_message['change_type'])
            if commit_type and commit_type.changelog:
                return parsed_message
            return None

        # Filter out all commit types not explicitly included in `types_in_changelog`
        if parsed_message['change_type'] not in included_in_changelog:
            return None
        return parsed_message

    def changelog_hook(self, full: str, partial: Union[str, None]) -> str:
        """Process resulting changelog to keep 1 empty line at the end of the file."""
        changelog = partial or full
        return changelog.rstrip() + '\n'

    def example(self) -> str:
        examples = build_example(types=self.cze_config.types, extra_types=self.cze_config.extra_types)
        return examples

    def schema(self) -> str:
        return '<type>(<scope>): <subject>\n' '<BLANK LINE>\n' '<body>\n' '<BLANK LINE>\n' '(BREAKING CHANGE: <breaking changes>)*\n' '(<footers>)*'

    def schema_pattern(self) -> str:
        pattern = (
            r'(?s)'  # To explicitly make . match new line
            rf'({self.re_known_types})'  # type
            r'(\(\S+\))?!?:'  # scope
            r'( [^\n\r]+)'  # subject
            r'((\n\n.*)|(\s*))?$'
        )
        return pattern

    @property
    def template_extras(self) -> Dict[str, Any]:
        return {'config': self.cze_config, 'settings': self.cze_config.settings}

    def info(self) -> str:
        return (
            'This is a custom Commitizen plugin for Espressif Systems. \n'
            'It is mainly used to generate consistent and beautiful changelog with minimum settings from target project. \n'
            'Can be used for creating commit messages, that passes Espressif standards. \n'
            'Customization from target project is possible, check this plugin GitHub repo https://github.com/espressif/cz-plugin-espressif\n\n'
            'More about internal standards, commit rules, and guidelines can be found in the https://github.com/espressif/standards.'
        )


discover_this = CzPluginEspressif  # pylint: disable=invalid-name
