"""Translates Mypy's output into GitHub's error/warning annotation syntax.

See: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions

This first is run with Mypy's output piped in, to collect messages in
mypy_annotate.dat. After all platforms run, we run this again, which prints the
messages in GitHub's format but with cross-platform failures deduplicated.
"""

from __future__ import annotations

import argparse
import pickle
import re
import sys

import attrs

# Example: 'package/filename.py:42:1:46:3: error: Type error here [code]'
report_re = re.compile(
    r"""
    ([^:]+):  # Filename (anything but ":")
    ([0-9]+):  # Line number (start)
    (?:([0-9]+):  # Optional column number
      (?:([0-9]+):([0-9]+):)?  # then also optionally, 2 more numbers for end columns
    )?
    \s*(error|warn|note):  # Kind, prefixed with space
    (.+)  # Message
    """,
    re.VERBOSE,
)

mypy_to_github = {
    "error": "error",
    "warn": "warning",
    "note": "notice",
}


@attrs.frozen(kw_only=True)
class Result:
    """Accumulated results, used as a dict key to deduplicate."""

    filename: str
    start_line: int
    kind: str
    message: str
    start_col: int | None = None
    end_line: int | None = None
    end_col: int | None = None


def process_line(line: str) -> Result | None:
    if match := report_re.fullmatch(line.rstrip()):
        filename, st_line, st_col, end_line, end_col, kind, message = match.groups()
        return Result(
            filename=filename,
            start_line=int(st_line),
            start_col=int(st_col) if st_col is not None else None,
            end_line=int(end_line) if end_line is not None else None,
            end_col=int(end_col) if end_col is not None else None,
            kind=mypy_to_github[kind],
            message=message,
        )
    else:
        return None


def export(results: dict[Result, list[str]]) -> None:
    """Display the collected results."""
    for res, platforms in results.items():
        print(f"::{res.kind} file={res.filename},line={res.start_line},", end="")
        if res.start_col is not None:
            print(f"col={res.start_col},", end="")
            if res.end_col is not None and res.end_line is not None:
                print(f"endLine={res.end_line},endColumn={res.end_col},", end="")
                message = f"({res.start_line}:{res.start_col} - {res.end_line}:{res.end_col}):{res.message}"
            else:
                message = f"({res.start_line}:{res.start_col}):{res.message}"
        else:
            message = f"{res.start_line}:{res.message}"
        print(f"title=Mypy-{'+'.join(platforms)}::{res.filename}:{message}")


def main(argv: list[str]) -> None:
    """Look for error messages, and convert the format."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--dumpfile",
        help="File to write pickled messages to.",
        required=True,
    )
    parser.add_argument(
        "--platform",
        help="OS name, if set Mypy should be piped to stdin.",
        default=None,
    )
    cmd_line = parser.parse_args(argv)

    results: dict[Result, list[str]]
    try:
        with open(cmd_line.dumpfile, "rb") as f:
            results = pickle.load(f)
    except (FileNotFoundError, pickle.UnpicklingError):
        # If we fail to load, assume it's an old result.
        results = {}

    if cmd_line.platform is None:
        # Write out the results.
        export(results)
    else:
        platform: str = cmd_line.platform
        for line in sys.stdin:
            parsed = process_line(line)
            if parsed is not None:
                try:
                    results[parsed].append(platform)
                except KeyError:
                    results[parsed] = [platform]
            sys.stdout.write(line)
        with open(cmd_line.dumpfile, "wb") as f:
            pickle.dump(results, f)


if __name__ == "__main__":
    main(sys.argv[1:])
