# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import re

import structlog

from code_review_bot import Level, Reliability
from code_review_bot.tasks.clang_tidy import ClangTidyIssue, ClangTidyTask

logger = structlog.get_logger(__name__)


ISSUE_MARKDOWN = """
#### Private Static Analysis {level}

- **Message**: {message}
- **Location**: {location}
- **Clang check**: {check}
- **in an expanded Macro**: {expanded_macro}

{notes}"""

ISSUE_NOTE_MARKDOWN = """
- **Note**: {message}
- **Location**: {location}

```
{body}
```
"""

ERROR_MARKDOWN = """
**Message**: ```{message}```
**Location**: {location}
"""

CLANG_MACRO_DETECTION = re.compile(r"^expanded from macro")

BUILD_HELP_MSG = """For private static analysis, please see [our private docs in Mana](https://mana.mozilla.org/wiki/pages/viewpage.action?pageId=130909687), if you cannot access this resource, ask your reviewer to help you resolve the issue."""


class ExternalTidyIssue(ClangTidyIssue):
    """
    An issue reported by source-test-clang-external
    """

    def is_build_error(self):
        return False

    def as_error(self):
        assert self.is_build_error(), "ExternalTidyIssue is not a build error."

        return ERROR_MARKDOWN.format(
            message=self.message, location=f"{self.path}:{self.line}"
        )

    def is_expanded_macro(self):
        """
        Is the issue only found in an expanded macro ?
        """
        if not self.notes:
            return False

        # Only consider first note
        note = self.notes[0]
        return CLANG_MACRO_DETECTION.match(note.message) is not None

    def as_text(self):
        """
        Build the text body published on reporters
        """
        message = self.message
        if len(message) > 0:
            message = message[0].capitalize() + message[1:]
        body = f"{self.level.name}: {message} [external-tidy: {self.check}]"

        # Always add body as it's been cleaned up
        if self.reason:
            body += f"\n{self.reason}"
        return body

    def as_markdown_for_phab(self):
        # skip not in patch or not publishable
        if not self.revision.contains(self) or not self.is_publishable():
            return ""

        return ISSUE_MARKDOWN.format(
            level=self.level.value,
            message=self.message,
            location=f"{self.path}:{self.line}:{self.column}",
            check=self.check,
            expanded_macro="yes" if self.is_expanded_macro() else "no",
            notes="\n".join(
                [
                    ISSUE_NOTE_MARKDOWN.format(
                        message=n.message,
                        location=f"{n.path}:{n.line}:{n.column}",
                        body=n.body,
                    )
                    for n in self.notes
                ]
            ),
        )

    def as_markdown(self):
        return ISSUE_MARKDOWN.format(
            level=self.level.value,
            message=self.message,
            location=f"{self.path}:{self.line}:{self.column}",
            reason=self.reason,
            check=self.check,
            in_patch="yes" if self.revision.contains(self) else "no",
            publishable_check="yes" if self.has_publishable_check() else "no",
            publishable="yes" if self.is_publishable() else "no",
            expanded_macro="yes" if self.is_expanded_macro() else "no",
            reliability=self.reliability.value,
            notes="\n".join(
                [
                    ISSUE_NOTE_MARKDOWN.format(
                        message=n.message,
                        location=f"{n.path}:{n.line}:{n.column}",
                        body=n.body,
                    )
                    for n in self.notes
                ]
            ),
        )


class ExternalTidyTask(ClangTidyTask):
    """
    Support issues from source-test clang-external tasks
    """

    # Note this is currently in fact using the same file name as the
    # normal clang tidy check, but this does NOT pose a problem
    # as artifact names are separated into individual folders per task id.
    artifacts = ["public/code-review/clang-tidy.json"]

    @property
    def display_name(self):
        return "private static analysis"

    def build_help_message(self, files):
        return BUILD_HELP_MSG

    def parse_issues(self, artifacts, revision):
        issues = [
            ExternalTidyIssue(
                analyzer=self,
                revision=revision,
                path=path,
                line=warning["line"],
                column=warning["column"],
                check=warning["flag"],
                level=Level(warning.get("type", "warning")),
                message=warning["message"],
                reliability=Reliability(warning["reliability"])
                if "reliability" in warning
                else Reliability.Unknown,
                reason=warning.get("reason"),
                publish=warning.get("publish")
                and warning["flag"].startswith("mozilla-civet-"),
            )
            for artifact in artifacts.values()
            for path, items in artifact["files"].items()
            for warning in items["warnings"]
        ]
        return issues
