# Copyright (c) 2021-2025 Arista Networks, Inc.
# Use of this source code is governed by the MIT license
# that can be found in the LICENSE file.
"""rule.py - Base class for all the lint rules with functions for matching line and text based rule."""

from __future__ import annotations

import json
from abc import ABC, abstractmethod
from typing import Any, ClassVar, Literal

from rich.text import Text

from j2lint.linter.error import JinjaLinterError, LinterError


class Rule(ABC):
    """Abstract rule class which acts as a base class for rules with regex match functions."""

    rule_id: ClassVar[str]
    short_description: ClassVar[str]
    description: ClassVar[str]
    severity: ClassVar[Literal[None, "LOW", "MEDIUM", "HIGH"]]

    def __init__(
        self,
        *,
        ignore: bool = False,
        warn: list[Any] | None = None,
        origin: str = "BUILT-IN",
    ) -> None:
        self.ignore = ignore
        self.warn = warn if warn is not None else []
        self.origin = origin

    def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:  # noqa: ANN401
        """Override the way a subclass of Rule is instantiated."""
        super().__init_subclass__(**kwargs)
        # Mandatory class attributes
        mandatory_attributes = [
            "rule_id",  # Like S0
            "description",
            "short_description",
            "severity",
        ]
        for attr in mandatory_attributes:
            if not hasattr(cls, attr):
                msg = f"Class {cls} is missing required class attribute {attr}"
                raise NotImplementedError(msg)

        if cls.severity not in [None, "LOW", "MEDIUM", "HIGH"]:
            msg = f"Rule {cls.rule_id}: severity must be in [None, 'LOW', 'MEDIUM', 'HIGH'], {cls.severity} was provided"
            raise JinjaLinterError(msg)

    def __repr__(self) -> str:
        """Return a representation of a Rule object."""
        return f"{self.rule_id}: {self.description}"

    def to_rich(self) -> Text:
        """Return a rich representation of the rule.

        Examples
        --------
        ```
        S0 Jinja syntax should be correct (jinja-syntax-error)
        ```

        Where `S0` is in red and `(jinja-syntax-error)` in blue
        """
        res = Text()
        res.append(f"{self.rule_id} ", "red")
        res.append(self.description)
        res.append(f" ({self.short_description})", "blue")
        return res

    def to_json(self) -> str:
        """Return a json representation of the rule."""
        return json.dumps(
            {
                "rule_id": self.rule_id,
                "short_description": self.short_description,
                "description": self.description,
                "severity": self.severity,
                "origin": self.origin,
            }
        )

    @abstractmethod
    def checktext(self, filename: str, text: str) -> list[LinterError]:
        """Check the rule against a full file content.

        This method is expected to be overrididen by child classes.

        Parameters
        ----------
        filename
            The filename to which the line belongs.
        text
            Entire text content of the file

        Returns
        -------
        list[LinterError]
            The list of LinterError generated by this rule.
        """

    @abstractmethod
    def checkline(self, filename: str, line: str, line_no: int) -> list[LinterError]:
        """Check the rule against a full file content.

        This method is expected to be overrididen by child classes.

        Parameters
        ----------
        filename
            The filename to which the line belongs.
        line
            The content of the line to check.
        line_no:
            The line number.

        Returns
        -------
        list[LinterError]
            The list of LinterError generated by this rule.
        """

    def checkrule(self, filename: str, text: str) -> list[LinterError]:
        """Check the string text against the current rule by calling either the checkline or checktext method depending on which one is implemented.

        Parameters
        ----------
        filename
            The filename to which the line belongs.
        text
            Entire text content of the file

        Returns
        -------
        list[LinterError]
            The list of LinterError generated by this rule.
        """
        errors: list[LinterError] = []

        try:
            # First try with checktext
            results = self.checktext(filename, text)
            errors.extend(results)

        except NotImplementedError:
            # checkline it is
            for index, line in enumerate(text.split("\n")):
                # TODO: parsing jinja2 templates .. lines starting with `#` should probably still be parsed somewhow as these are not comments.
                if line.lstrip().startswith("#"):
                    continue

                results = self.checkline(filename, line, line_no=index + 1)
                errors.extend(results)
        return errors
