# Copyright (c) 2026 MergeGuide, Inc. All rights reserved.
# SPDX-License-Identifier: Proprietary
"""Custom Rule DSL for advanced policy definitions.

This module provides a domain-specific language for defining complex policy rules
that go beyond simple regex pattern matching. The DSL supports:

- Named capture groups that become variables
- Built-in functions (entropy, length, count, etc.)
- Multi-pattern rules with AND/OR/NOT logic
- Template messages with variable interpolation
- Multiline pattern matching

DSL Syntax:
    The DSL uses YAML configuration with special fields:

    ```yaml
    rules:
      - id: "detect-weak-secret"
        type: dsl
        patterns:
          - name: secret_value
            pattern: '(?:api_key|secret)\\s*=\\s*["\\'](<?value>[^"\\']*)["\\']'
        where: 'entropy(value) < 3.0 or length(value) < 8'
        message: 'Weak secret detected: "${value}" has low entropy'
    ```

Supported Functions:
    - entropy(str): Shannon entropy of a string
    - length(str): Length of a string
    - count(str, substr): Count occurrences of substr in str
    - lower(str): Convert to lowercase
    - upper(str): Convert to uppercase
    - starts_with(str, prefix): Check if str starts with prefix
    - ends_with(str, suffix): Check if str ends with suffix
    - contains(str, substr): Check if str contains substr
    - matches(str, pattern): Check if str matches regex pattern
    - is_numeric(str): Check if str is numeric
    - is_alpha(str): Check if str is alphabetic
    - is_alnum(str): Check if str is alphanumeric

Example Usage:
    >>> from mergeguide.engine.dsl import DSLRule, DSLEngine
    >>> rule = DSLRule(
    ...     id="weak-password",
    ...     name="Weak Password Detection",
    ...     patterns=[
    ...         PatternDefinition(
    ...             name="pwd",
    ...             pattern=r'password\\s*=\\s*["\\'](<?value>[^"\\']*)["\\']',
    ...         )
    ...     ],
    ...     where="entropy(value) < 3.5",
    ...     message='Weak password: "${value}"',
    ... )
    >>> engine = DSLEngine([rule])
    >>> results = engine.check_content('password = "abc123"')
"""

from __future__ import annotations

import math
import re
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any

from .rules import RuleMatch, RuleResult, Severity


def _entropy(s: str) -> float:
    """Calculate Shannon entropy of a string.

    Higher entropy indicates more randomness/complexity.
    Typical values:
    - Low entropy (<3.0): Simple patterns like "password", "123456"
    - Medium entropy (3.0-4.0): Mixed but predictable patterns
    - High entropy (>4.0): Random or complex strings

    Args:
        s: Input string

    Returns:
        Shannon entropy value (bits per character)
    """
    if not s:
        return 0.0

    # Count character frequencies
    freq: dict[str, int] = {}
    for char in s:
        freq[char] = freq.get(char, 0) + 1

    # Calculate entropy
    length = len(s)
    entropy = 0.0
    for count in freq.values():
        probability = count / length
        entropy -= probability * math.log2(probability)

    return entropy


def _length(s: str) -> int:
    """Return length of string."""
    return len(s)


def _count(s: str, substr: str) -> int:
    """Count occurrences of substr in s."""
    return s.count(substr)


def _lower(s: str) -> str:
    """Convert string to lowercase."""
    return s.lower()


def _upper(s: str) -> str:
    """Convert string to uppercase."""
    return s.upper()


def _starts_with(s: str, prefix: str) -> bool:
    """Check if string starts with prefix."""
    return s.startswith(prefix)


def _ends_with(s: str, suffix: str) -> bool:
    """Check if string ends with suffix."""
    return s.endswith(suffix)


def _contains(s: str, substr: str) -> bool:
    """Check if string contains substring."""
    return substr in s


def _matches(s: str, pattern: str) -> bool:
    """Check if string matches regex pattern."""
    try:
        return bool(re.search(pattern, s))
    except re.error:
        return False


def _is_numeric(s: str) -> bool:
    """Check if string is numeric."""
    return s.isdigit()


def _is_alpha(s: str) -> bool:
    """Check if string is alphabetic."""
    return s.isalpha()


def _is_alnum(s: str) -> bool:
    """Check if string is alphanumeric."""
    return s.isalnum()


# Built-in function registry
BUILTIN_FUNCTIONS: dict[str, Callable[..., Any]] = {
    "entropy": _entropy,
    "length": _length,
    "len": _length,  # Alias
    "count": _count,
    "lower": _lower,
    "upper": _upper,
    "starts_with": _starts_with,
    "ends_with": _ends_with,
    "contains": _contains,
    "matches": _matches,
    "is_numeric": _is_numeric,
    "is_alpha": _is_alpha,
    "is_alnum": _is_alnum,
}


class DSLError(Exception):
    """Exception raised for DSL-related errors."""

    pass


@dataclass
class PatternDefinition:
    """Defines a named pattern for capturing values.

    Attributes:
        name: Name of the pattern (used as variable prefix)
        pattern: Regex pattern with named capture groups
        flags: Regex flags string (i=case-insensitive, m=multiline, s=dotall)
        multiline: If True, pattern can match across line boundaries
    """

    name: str
    pattern: str
    flags: str = ""
    multiline: bool = False

    def __post_init__(self) -> None:
        """Compile the pattern after initialization."""
        regex_flags = 0
        if "i" in self.flags:
            regex_flags |= re.IGNORECASE
        if "m" in self.flags:
            regex_flags |= re.MULTILINE
        if "s" in self.flags:
            regex_flags |= re.DOTALL
        if self.multiline:
            regex_flags |= re.MULTILINE | re.DOTALL

        try:
            self._compiled = re.compile(self.pattern, regex_flags)
        except re.error as e:
            raise DSLError(f"Invalid pattern '{self.pattern}': {e}") from e

    @property
    def compiled(self) -> re.Pattern[str]:
        """Get the compiled regex pattern."""
        return self._compiled


@dataclass
class DSLRule:
    """A DSL-based rule for advanced pattern matching.

    Attributes:
        id: Unique rule identifier
        name: Human-readable rule name
        patterns: List of pattern definitions
        where: Optional condition expression using captured variables
        message: Violation message (supports ${var} interpolation)
        severity: Severity level (error, warning, info)
        remediation: How to fix the violation
        combine: How to combine multiple patterns ("all", "any", "sequence")
    """

    id: str
    name: str
    patterns: list[PatternDefinition]
    message: str
    where: str = ""
    severity: Severity = Severity.WARNING
    remediation: str = ""
    combine: str = "all"  # "all", "any", or "sequence"
    languages: list[str] = field(default_factory=lambda: ["*"])

    def __post_init__(self) -> None:
        """Validate rule after initialization."""
        if not self.patterns:
            raise DSLError(f"Rule '{self.id}' must have at least one pattern")
        if self.combine not in ("all", "any", "sequence"):
            raise DSLError(f"Invalid combine mode: {self.combine}")


class DSLExpressionEvaluator:
    """Evaluates DSL expressions with variables and functions.

    The evaluator supports a safe subset of expressions including:
    - Comparison operators: ==, !=, <, >, <=, >=
    - Logical operators: and, or, not
    - Function calls: entropy(), length(), etc.
    - Variable access from captured groups

    Security:
        Does NOT use Python's eval(). Parses and evaluates safely.
    """

    # Token patterns for expression parsing
    TOKEN_PATTERNS = [
        (r"\s+", "SKIP"),
        (r"and\b", "AND"),
        (r"or\b", "OR"),
        (r"not\b", "NOT"),
        (r"true\b", "TRUE"),
        (r"false\b", "FALSE"),
        (r"==", "EQ"),
        (r"!=", "NE"),
        (r"<=", "LE"),
        (r">=", "GE"),
        (r"<", "LT"),
        (r">", "GT"),
        (r"\(", "LPAREN"),
        (r"\)", "RPAREN"),
        (r",", "COMMA"),
        (r'"[^"]*"', "STRING_DOUBLE"),
        (r"'[^']*'", "STRING_SINGLE"),
        (r"-?\d+\.?\d*", "NUMBER"),
        (r"[a-zA-Z_][a-zA-Z0-9_]*", "IDENTIFIER"),
    ]

    def __init__(self) -> None:
        """Initialize the expression evaluator."""
        # Build token regex with unique group names
        patterns = []
        self._token_types: list[str] = []
        for i, (pattern, token_type) in enumerate(self.TOKEN_PATTERNS):
            group_name = f"T{i}"
            patterns.append(f"(?P<{group_name}>{pattern})")
            self._token_types.append(token_type)
        self._token_regex = re.compile("|".join(patterns))

    def evaluate(self, expression: str, variables: dict[str, Any]) -> bool:
        """Evaluate an expression with given variables.

        Args:
            expression: The expression to evaluate
            variables: Variables available for the expression

        Returns:
            Boolean result of the expression

        Raises:
            DSLError: If the expression is invalid
        """
        if not expression or not expression.strip():
            return True  # Empty expression means always match

        # Prevent excessive input that could cause slow regex matching
        if len(expression) > 10_000:
            raise DSLError(f"Expression too long ({len(expression)} chars, max 10000)")

        try:
            tokens = self._tokenize(expression)
            result = self._parse_expression(tokens, variables)
            return bool(result)
        except Exception as e:
            raise DSLError(f"Invalid expression: {expression}. Error: {e}") from e

    def _tokenize(self, expression: str) -> list[tuple[str, Any]]:
        """Tokenize the expression."""
        tokens = []
        pos = 0

        while pos < len(expression):
            match = self._token_regex.match(expression, pos)
            if not match:
                raise DSLError(f"Invalid character at position {pos}: {expression[pos]}")

            group_name = match.lastgroup
            if group_name is None:
                raise DSLError(f"No group matched at position {pos}")

            group_idx = int(group_name[1:])
            token_type = self._token_types[group_idx]
            token_value = match.group()

            if token_type != "SKIP":
                if token_type in ("STRING_DOUBLE", "STRING_SINGLE"):
                    token_value = token_value[1:-1]
                    token_type = "STRING"
                elif token_type == "NUMBER":
                    token_value = float(token_value) if "." in token_value else int(token_value)
                elif token_type == "TRUE":
                    token_value = True
                elif token_type == "FALSE":
                    token_value = False

                tokens.append((token_type, token_value))

            pos = match.end()

        return tokens

    def _parse_expression(self, tokens: list[tuple[str, Any]], variables: dict[str, Any]) -> Any:
        """Parse and evaluate expression."""
        self._pos = 0
        self._tokens = tokens
        self._variables = variables
        return self._parse_or()

    def _current_token(self) -> tuple[str, Any] | None:
        """Get current token."""
        if self._pos < len(self._tokens):
            return self._tokens[self._pos]
        return None

    def _consume(self, expected_type: str | None = None) -> tuple[str, Any]:
        """Consume current token."""
        token = self._current_token()
        if token is None:
            raise DSLError("Unexpected end of expression")
        if expected_type and token[0] != expected_type:
            raise DSLError(f"Expected {expected_type}, got {token[0]}")
        self._pos += 1
        return token

    def _parse_or(self) -> Any:
        """Parse OR expression."""
        left = self._parse_and()
        while self._current_token() and self._current_token()[0] == "OR":
            self._consume("OR")
            right = self._parse_and()
            left = left or right
        return left

    def _parse_and(self) -> Any:
        """Parse AND expression."""
        left = self._parse_not()
        while self._current_token() and self._current_token()[0] == "AND":
            self._consume("AND")
            right = self._parse_not()
            left = left and right
        return left

    def _parse_not(self) -> Any:
        """Parse NOT expression."""
        if self._current_token() and self._current_token()[0] == "NOT":
            self._consume("NOT")
            return not self._parse_not()
        return self._parse_comparison()

    def _parse_comparison(self) -> Any:
        """Parse comparison expression."""
        left = self._parse_primary()

        token = self._current_token()
        if token:
            op = token[0]
            if op in ("EQ", "NE", "LT", "GT", "LE", "GE"):
                self._consume()
                right = self._parse_primary()
                if op == "EQ":
                    return left == right
                elif op == "NE":
                    return left != right
                elif op == "LT":
                    return left < right
                elif op == "GT":
                    return left > right
                elif op == "LE":
                    return left <= right
                elif op == "GE":
                    return left >= right

        return left

    def _parse_primary(self) -> Any:
        """Parse primary expression (literals, identifiers, function calls)."""
        token = self._current_token()

        if token is None:
            raise DSLError("Unexpected end of expression")

        if token[0] == "LPAREN":
            self._consume("LPAREN")
            result = self._parse_or()
            self._consume("RPAREN")
            return result

        if token[0] == "STRING":
            self._consume()
            return token[1]

        if token[0] == "NUMBER":
            self._consume()
            return token[1]

        if token[0] in ("TRUE", "FALSE"):
            self._consume()
            return token[1]

        if token[0] == "IDENTIFIER":
            name = token[1]
            self._consume()

            # Check if it's a function call
            if self._current_token() and self._current_token()[0] == "LPAREN":
                return self._parse_function_call(name)

            # Otherwise it's a variable
            return self._resolve_variable(name)

        raise DSLError(f"Unexpected token: {token}")

    def _parse_function_call(self, name: str) -> Any:
        """Parse and execute a function call."""
        if name not in BUILTIN_FUNCTIONS:
            raise DSLError(f"Unknown function: {name}")

        self._consume("LPAREN")
        args = []

        while self._current_token() and self._current_token()[0] != "RPAREN":
            args.append(self._parse_or())
            if self._current_token() and self._current_token()[0] == "COMMA":
                self._consume("COMMA")

        self._consume("RPAREN")

        func = BUILTIN_FUNCTIONS[name]
        try:
            return func(*args)
        except TypeError as e:
            raise DSLError(f"Function {name} call failed: {e}") from e

    def _resolve_variable(self, name: str) -> Any:
        """Resolve a variable from the variables dict."""
        if name in self._variables:
            return self._variables[name]
        return ""  # Unknown variable returns empty string


class DSLEngine:
    """Engine for executing DSL-based rules.

    The DSLEngine manages a collection of DSL rules and provides methods
    to check content against all rules.
    """

    def __init__(self, rules: list[DSLRule] | None = None) -> None:
        """Initialize the DSL engine.

        Args:
            rules: Initial list of DSL rules
        """
        self.rules = rules or []
        self._evaluator = DSLExpressionEvaluator()

    def add_rule(self, rule: DSLRule) -> None:
        """Add a rule to the engine."""
        self.rules.append(rule)

    def check_content(
        self,
        content: str,
        file_path: str = "",
        language: str = "*",
    ) -> list[RuleResult]:
        """Check content against all DSL rules.

        Args:
            content: The content to check
            file_path: Optional file path for context
            language: Language for rule filtering

        Returns:
            List of RuleResult for each rule
        """
        results = []

        for rule in self.rules:
            # Check if rule applies to this language
            if "*" not in rule.languages and language.lower() not in [
                lang.lower() for lang in rule.languages
            ]:
                continue

            result = self._check_rule(rule, content, file_path)
            results.append(result)

        return results

    def _check_rule(self, rule: DSLRule, content: str, file_path: str) -> RuleResult:
        """Check a single rule against content.

        Args:
            rule: The DSL rule to check
            content: Content to check
            file_path: File path for context

        Returns:
            RuleResult with matches
        """
        matches: list[RuleMatch] = []

        if rule.combine == "all":
            # All patterns must match
            all_pattern_matches = self._find_all_pattern_matches(rule, content)
            if all_pattern_matches:
                matches = self._create_matches(rule, content, all_pattern_matches)
        elif rule.combine == "any":
            # Any pattern can match
            for pattern_def in rule.patterns:
                pattern_matches = self._find_pattern_matches(pattern_def, content)
                for match_data in pattern_matches:
                    if self._check_where_condition(rule.where, match_data):
                        match = self._create_match(rule, content, match_data, pattern_def)
                        matches.append(match)
        elif rule.combine == "sequence":
            # Patterns must match in sequence
            sequence_matches = self._find_sequence_matches(rule, content)
            if sequence_matches:
                matches = self._create_matches(rule, content, sequence_matches)

        return RuleResult(
            rule_id=rule.id,
            passed=len(matches) == 0,
            matches=matches,
            message=rule.message,
            severity=rule.severity,
        )

    def _find_pattern_matches(
        self,
        pattern_def: PatternDefinition,
        content: str,
    ) -> list[dict[str, Any]]:
        """Find all matches for a pattern definition.

        Args:
            pattern_def: Pattern to search for
            content: Content to search in

        Returns:
            List of match data dicts with captured groups and position info
        """
        matches = []

        if pattern_def.multiline:
            # Search entire content at once
            for match in pattern_def.compiled.finditer(content):
                match_data = self._extract_match_data(match, content, pattern_def.name)
                matches.append(match_data)
        else:
            # Search line by line
            lines = content.split("\n")
            for line_no, line in enumerate(lines, start=1):
                for match in pattern_def.compiled.finditer(line):
                    match_data = self._extract_match_data(
                        match, line, pattern_def.name, line_no=line_no
                    )
                    matches.append(match_data)

        return matches

    def _extract_match_data(
        self,
        match: re.Match[str],
        content: str,
        pattern_name: str,
        line_no: int | None = None,
    ) -> dict[str, Any]:
        """Extract data from a regex match.

        Args:
            match: The regex match object
            content: Content that was searched
            pattern_name: Name of the pattern
            line_no: Line number (if searching line by line)

        Returns:
            Dict with captured groups, position, and metadata
        """
        data: dict[str, Any] = {
            "_match": match.group(),
            "_start": match.start(),
            "_end": match.end(),
            "_pattern": pattern_name,
        }

        # Add named capture groups
        for name, value in match.groupdict().items():
            if value is not None:
                data[name] = value

        # Calculate line number if not provided (multiline mode)
        if line_no is None:
            line_no = content[: match.start()].count("\n") + 1
            # Get the line content
            lines = content.split("\n")
            line_content = lines[line_no - 1] if line_no <= len(lines) else ""
            # Calculate column within the line
            line_start = content.rfind("\n", 0, match.start()) + 1
            column_start = match.start() - line_start + 1
            column_end = match.end() - line_start + 1
        else:
            line_content = content
            column_start = match.start() + 1
            column_end = match.end() + 1

        data["_line_no"] = line_no
        data["_line_content"] = line_content
        data["_column_start"] = column_start
        data["_column_end"] = column_end

        return data

    def _find_all_pattern_matches(
        self,
        rule: DSLRule,
        content: str,
    ) -> list[dict[str, Any]] | None:
        """Find matches where ALL patterns match.

        Returns matches from the first pattern that pass the where condition,
        but only if all patterns have at least one match.

        Args:
            rule: The DSL rule
            content: Content to search

        Returns:
            List of match data if all patterns match, None otherwise
        """
        # First, check that all patterns have at least one match
        all_matches: list[list[dict[str, Any]]] = []
        for pattern_def in rule.patterns:
            pattern_matches = self._find_pattern_matches(pattern_def, content)
            if not pattern_matches:
                return None  # A pattern didn't match, rule doesn't apply
            all_matches.append(pattern_matches)

        # Combine variables from all pattern matches and filter by where
        result_matches = []
        for match_data in all_matches[0]:  # Use first pattern's matches as base
            # Merge variables from all patterns (first match from each)
            combined = dict(match_data)
            for other_matches in all_matches[1:]:
                if other_matches:
                    for key, value in other_matches[0].items():
                        if not key.startswith("_"):
                            combined[key] = value

            if self._check_where_condition(rule.where, combined):
                result_matches.append(combined)

        return result_matches if result_matches else None

    def _find_sequence_matches(
        self,
        rule: DSLRule,
        content: str,
    ) -> list[dict[str, Any]] | None:
        """Find matches where patterns match in sequence.

        Args:
            rule: The DSL rule
            content: Content to search

        Returns:
            List of match data if patterns match in sequence, None otherwise
        """
        # Find matches for each pattern
        pattern_matches: list[list[dict[str, Any]]] = []
        for pattern_def in rule.patterns:
            matches = self._find_pattern_matches(pattern_def, content)
            if not matches:
                return None
            pattern_matches.append(matches)

        # Check for sequential matches (each pattern's match starts after previous ends)
        result_matches = []
        last_end = 0

        for i, matches in enumerate(pattern_matches):
            # Find a match that starts after the last pattern's end
            found = False
            for match_data in matches:
                if match_data["_start"] >= last_end:
                    if i == 0:  # First pattern, just check where condition
                        if self._check_where_condition(rule.where, match_data):
                            result_matches.append(match_data)
                            last_end = match_data["_end"]
                            found = True
                            break
                    else:  # Subsequent patterns
                        # Combine with previous match data
                        combined = dict(result_matches[-1])
                        for key, value in match_data.items():
                            if not key.startswith("_"):
                                combined[key] = value
                        combined["_line_no"] = match_data["_line_no"]
                        combined["_line_content"] = match_data["_line_content"]
                        combined["_column_start"] = match_data["_column_start"]
                        combined["_column_end"] = match_data["_column_end"]
                        combined["_match"] = match_data["_match"]

                        if self._check_where_condition(rule.where, combined):
                            result_matches[-1] = combined
                            last_end = match_data["_end"]
                            found = True
                            break

            if not found:
                return None

        return result_matches if result_matches else None

    def _check_where_condition(self, where: str, variables: dict[str, Any]) -> bool:
        """Check if a where condition is satisfied.

        Args:
            where: The where expression
            variables: Variables from pattern matches

        Returns:
            True if condition is satisfied or empty
        """
        if not where or not where.strip():
            return True

        # Filter out internal variables for the evaluator
        eval_vars = {k: v for k, v in variables.items() if not k.startswith("_")}
        return self._evaluator.evaluate(where, eval_vars)

    def _create_match(
        self,
        rule: DSLRule,
        content: str,
        match_data: dict[str, Any],
        pattern_def: PatternDefinition,
    ) -> RuleMatch:
        """Create a RuleMatch from match data.

        Args:
            rule: The DSL rule
            content: Original content
            match_data: Match data dict
            pattern_def: Pattern that matched

        Returns:
            RuleMatch instance
        """
        return RuleMatch(
            line_no=match_data["_line_no"],
            column_start=match_data["_column_start"],
            column_end=match_data["_column_end"],
            matched_text=match_data["_match"],
            line_content=match_data["_line_content"],
        )

    def _create_matches(
        self,
        rule: DSLRule,
        content: str,
        match_data_list: list[dict[str, Any]],
    ) -> list[RuleMatch]:
        """Create RuleMatch list from match data list.

        Args:
            rule: The DSL rule
            content: Original content
            match_data_list: List of match data dicts

        Returns:
            List of RuleMatch instances
        """
        matches = []
        for match_data in match_data_list:
            matches.append(
                RuleMatch(
                    line_no=match_data["_line_no"],
                    column_start=match_data["_column_start"],
                    column_end=match_data["_column_end"],
                    matched_text=match_data["_match"],
                    line_content=match_data["_line_content"],
                )
            )
        return matches

    def _interpolate_message(self, message: str, variables: dict[str, Any]) -> str:
        """Interpolate variables into a message template.

        Supports ${var} syntax for variable interpolation.

        Args:
            message: Message template
            variables: Variables to interpolate

        Returns:
            Interpolated message
        """

        def replace_var(match: re.Match[str]) -> str:
            var_name = match.group(1)
            if var_name in variables:
                return str(variables[var_name])
            return match.group(0)  # Keep original if not found

        return re.sub(r"\$\{(\w+)\}", replace_var, message)


def parse_dsl_rule(data: dict[str, Any]) -> DSLRule:
    """Parse a DSL rule from a dictionary (YAML/JSON).

    Args:
        data: Dictionary containing rule definition

    Returns:
        DSLRule instance

    Raises:
        DSLError: If the rule definition is invalid
    """
    if "id" not in data:
        raise DSLError("DSL rule must have 'id' field")
    if "name" not in data:
        raise DSLError("DSL rule must have 'name' field")
    if "patterns" not in data:
        raise DSLError("DSL rule must have 'patterns' field")
    if "message" not in data:
        raise DSLError("DSL rule must have 'message' field")

    # Parse patterns
    patterns = []
    for p in data["patterns"]:
        if isinstance(p, str):
            # Simple pattern string
            patterns.append(PatternDefinition(name="match", pattern=p))
        elif isinstance(p, dict):
            patterns.append(
                PatternDefinition(
                    name=p.get("name", "match"),
                    pattern=p["pattern"],
                    flags=p.get("flags", ""),
                    multiline=p.get("multiline", False),
                )
            )
        else:
            raise DSLError(f"Invalid pattern definition: {p}")

    # Parse severity
    severity_str = data.get("severity", "warning")
    try:
        severity = Severity(severity_str.lower())
    except ValueError:
        severity = Severity.WARNING

    return DSLRule(
        id=data["id"],
        name=data["name"],
        patterns=patterns,
        message=data["message"],
        where=data.get("where", ""),
        severity=severity,
        remediation=data.get("remediation", ""),
        combine=data.get("combine", "all"),
        languages=data.get("languages", ["*"]),
    )
