"""Collect code tags and output for review in a single location."""

from collections import defaultdict
from pathlib import Path
from typing import Dict, List, Pattern, Sequence

import attr
from beartype import beartype
from loguru import logger

from ..file_helpers import read_lines
from ..log_helpers import log_fun
from .base import debug_task
from .doit_globals import DG, DoitTask

SKIP_PHRASE = 'calcipy:skip_tags'
"""String that indicates the file should be excluded from the tag search."""


@attr.s(auto_attribs=True)
class _CodeTag:  # noqa: H601
    """Code Tag (FIXME,TODO,etc) with contextual information."""  # noqa: T100,T101

    lineno: int
    tag: str
    text: str


@attr.s(auto_attribs=True)
class _Tags:  # noqa: H601
    """Collection of code tags with additional contextual information."""

    path_source: Path
    code_tags: List[_CodeTag]


@beartype
def _search_lines(
    lines: List[str], regex_compiled: Pattern[str],
    skip_phrase: str = 'calcipy:skip_tags',
) -> List[_CodeTag]:
    """Search lines of text for matches to the compiled regular expression.

    Args:
        lines: lines of text as list
        regex_compiled: compiled regular expression. Expected to have matching groups `(tag, text)`
        skip_phrase: skip file if string is found in final two lines. Default is `SKIP_PHRASE`

    Returns:
        List[_CodeTag]: list of all code tags found in lines

    """
    if skip_phrase in '\n'.join(lines[-2:]):
        return []

    comments = []
    for lineno, line in enumerate(lines):
        match = regex_compiled.search(line)
        if match:
            mg = match.groupdict()
            comments.append(_CodeTag(lineno + 1, tag=mg['tag'], text=mg['text']))
    return comments


@beartype
def _search_files(paths_source: Sequence[Path], regex_compiled: Pattern[str]) -> List[_Tags]:
    """Collect matches from multiple files.

    Args:
        paths_source: list of source files to parse
        regex_compiled: compiled regular expression. Expected to have matching groups `(tag, text)`

    Returns:
        List[_Tags]: list of all code tags found in files

    """
    matches = []
    for path_source in paths_source:
        lines = []
        try:
            lines = read_lines(path_source)
        except UnicodeDecodeError as err:
            logger.warning(f'Could not parse: {path_source}', err=err)

        comments = _search_lines(lines, regex_compiled)
        if comments:
            matches.append(_Tags(path_source, comments))

    return matches


@beartype
def _format_report(base_dir: Path, code_tags: List[_Tags]) -> str:  # noqa: CCR001
    """Pretty-format the code tags by file and line number.

    Args:
        base_dir: base directory relative to the searched files
        code_tags: list of all code tags found in files

    Returns:
        str: pretty-formatted text

    """
    output = ''
    counter: Dict[str, int] = defaultdict(lambda: 0)
    for comments in sorted(code_tags, key=lambda tc: tc.path_source, reverse=False):
        output += f'- {comments.path_source.relative_to(base_dir).as_posix()}\n'
        for comment in comments.code_tags:
            output += f'    - line {comment.lineno:>3} {comment.tag:>7}: {comment.text}\n'
            counter[comment.tag] += 1
        output += '\n'
    logger.debug('counter={counter}', counter=counter)

    sorted_counter = {tag: counter[tag] for tag in DG.ct.tags if tag in counter}
    logger.debug('sorted_counter={sorted_counter}', sorted_counter=sorted_counter)
    formatted_summary = ', '.join(f'{tag} ({count})' for tag, count in sorted_counter.items())
    if formatted_summary:
        output += f'Found code tags for {formatted_summary}\n'
    return output


@log_fun
@beartype
def _write_code_tag_file(path_tag_summary: Path) -> None:
    """Create the code tag summary file.

    Args:
        path_tag_summary: Path to the output file

    """
    header = f'# Task Summary\n\nAuto-Generated by `{DG.meta.pkg_name}`'
    regex_compiled = DG.ct.compile_issue_regex()
    matches = _search_files(DG.meta.paths, regex_compiled)
    report = _format_report(DG.meta.path_project, matches).strip()
    if report:
        path_tag_summary.write_text(f'{header}\n\n{report}\n\n<!-- {SKIP_PHRASE} -->\n')
    elif path_tag_summary.is_file():
        path_tag_summary.unlink()


@beartype
def task_collect_code_tags() -> DoitTask:
    """Create a summary file with all of the found code tags.

    Returns:
        DoitTask: doit task

    """
    return debug_task([(_write_code_tag_file, (DG.ct.path_code_tag_summary,))])
