import hashlib
import json
from typing import Any
from typing import Dict
from typing import Iterable
from typing import Mapping
from typing import Sequence

from semgrep.constants import RuleSeverity
from semgrep.error import SemgrepError
from semgrep.formatter.base import BaseFormatter
from semgrep.rule import Rule
from semgrep.rule_match import RuleMatch


def _to_gitlab_severity(semgrep_severity: RuleSeverity) -> str:
    # Todo: Semgrep states currently don't map super well to Gitlab schema.
    conversion_table: Dict[RuleSeverity, str] = {
        RuleSeverity.INFO: "Info",
        RuleSeverity.WARNING: "Medium",
        RuleSeverity.ERROR: "High",
    }
    return conversion_table.get(semgrep_severity, "Unknown")


def _construct_semgrep_rule_url(rule_id: str) -> str:
    # this is a hack to fix name -> registry disagreement
    components = rule_id.split(".")
    result = []
    for chunk in components:
        if chunk not in result:
            result.append(chunk)
    rule_name = ".".join(result)
    return f"https://semgrep.dev/r/{rule_name}"


class GitlabSastFormatter(BaseFormatter):
    def _format_rule_match(self, rule_match: RuleMatch) -> Mapping[str, Any]:
        # create UUID from sha256 hash
        return {
            "id": str(rule_match.uuid),
            "category": "sast",
            # CVE is a required field from Gitlab schema.
            # It also is part of the determination for uniqueness
            # of a detected secret
            # /regardless/ of differentiated ID. See issue 262648.
            # https://gitlab.com/gitlab-org/gitlab/-/issues/262648
            # Gitlab themselves mock a CVE for findings that lack a CVE
            # Format: path:hash-of-file-path:check_id
            "cve": "{}:{}:{}".format(
                rule_match.path,
                hashlib.sha256(bytes(rule_match.path)).hexdigest(),
                rule_match.rule_id,
            ),
            "message": rule_match.message,
            "severity": _to_gitlab_severity(rule_match.severity),
            # Semgrep is designed to be a low-FP tool by design.
            # Does hard-coding confidence make sense here?
            "confidence": "High",
            "scanner": {
                "id": "semgrep",
                "name": "Semgrep",
                "vendor": {"name": "Semgrep"},
            },
            "location": {
                "file": str(rule_match.path),
                # Gitlab only uses line identifiers
                "start_line": rule_match.start.line,
                "end_line": rule_match.end.line,
                "dependency": {"package": {}},
            },
            "identifiers": [
                {
                    "type": "semgrep_type",
                    "name": f"Semgrep - {rule_match.rule_id}",
                    "value": rule_match.rule_id,
                    "url": _construct_semgrep_rule_url(rule_match.rule_id),
                }
            ],
        }

    def format(
        self,
        rules: Iterable[Rule],
        rule_matches: Iterable[RuleMatch],
        semgrep_structured_errors: Sequence[SemgrepError],
        extra: Mapping[str, Any],
    ) -> str:
        """
        Format matches in GitLab SAST report compliant JSON.

        - Written based on:
            https://github.com/returntocorp/semgrep-action/blob/678eff1a4269ed04b76631771688c8be860ec4e9/src/semgrep_agent/findings.py#L137-L165
        - Docs:
            https://docs.gitlab.com/ee/user/application_security/sast/#reports-json-format
        - Schema:
            https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json
        """
        output_dict = {
            "$schema": "https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json",
            "version": "2.0",
            "vulnerabilities": [
                self._format_rule_match(rule_match)
                for rule_match in rule_matches
                if rule_match.severity != RuleSeverity.INVENTORY
            ],
        }

        return json.dumps(output_dict, sort_keys=True, indent=2)
