from abc import ABC, abstractmethod
from collections.abc import Hashable
from dataclasses import dataclass
from pathlib import Path
from typing import Any, ClassVar

from rich.markup import escape

from cognite_toolkit._cdf_tk.constants import HINT_LEAD_TEXT

from .base import SeverityLevel, ToolkitWarning


@dataclass(frozen=True)
class FileReadWarning(ToolkitWarning, ABC):
    severity: ClassVar[SeverityLevel]
    filepath: Path

    def group_key(self) -> tuple[Any, ...]:
        return (self.filepath,)

    def group_header(self) -> str:
        return f"    In File {self.filepath.as_posix()!r}"

    def __str__(self) -> str:
        return self.get_message()


@dataclass(frozen=True)
class FileExistsWarning(FileReadWarning):
    severity = SeverityLevel.MEDIUM
    extra_message: str = ""

    def get_message(self) -> str:
        msg = f"{type(self).__name__}: The file {self.filepath} already exists."
        if self.extra_message:
            msg += f" {self.extra_message}"
        return msg


@dataclass(frozen=True)
class IdentifiedResourceFileReadWarning(FileReadWarning, ABC):
    id_value: str
    id_name: str


@dataclass(frozen=True)
class YAMLFileWarning(FileReadWarning, ABC):
    def __post_init__(self) -> None:
        if self.filepath.suffix not in {".yaml", ".yml"}:
            raise ValueError(f"Expected a YAML file, got {self.filepath.suffix}.")


@dataclass(frozen=True)
class YAMLFileWithElementWarning(YAMLFileWarning, ABC):
    # None is a dictionary, number is a list
    element_no: int | None
    path: tuple[str | int, ...]

    @property
    def _location(self) -> str:
        if self.element_no is not None:
            value = f" in entry {self.element_no} "
        else:
            value = ""
        if len(self.path) <= 1:
            return f"{value}"
        else:
            return f"{value} in section {self.path!r}"


@dataclass(frozen=True)
class DuplicatedItemWarning(YAMLFileWarning):
    severity = SeverityLevel.MEDIUM
    identifier: Hashable
    first_location: Path

    def get_message(self) -> str:
        return (
            f"{type(self).__name__}: Duplicated item with identifier "
            f"{self.identifier!r} first seen in {self.first_location.name!r}."
        )


@dataclass(frozen=True)
class UnknownResourceTypeWarning(YAMLFileWarning):
    severity = SeverityLevel.MEDIUM

    suggestion: str | None

    def get_message(self) -> str:
        msg = f"{type(self).__name__}: File {self.filepath.as_posix()!r}."
        if self.suggestion:
            msg += f"\n[blue]HINT: [/blue]{self.suggestion}"
        return msg


@dataclass(frozen=True)
class UnusedParameterWarning(YAMLFileWithElementWarning):
    severity = SeverityLevel.LOW
    actual: str

    def get_message(self) -> str:
        message = f"{type(self).__name__}: Parameter {self.actual!r} is not used{self._location}."
        if self.actual == "dataSetId" or self.actual == "data_set_id":
            message += f" {HINT_LEAD_TEXT}The parameter 'dataSetExternalId' should be used instead."
        return message


@dataclass(frozen=True)
class UnresolvedVariableWarning(FileReadWarning):
    severity = SeverityLevel.HIGH
    variable: str

    def get_message(self) -> str:
        return f"{type(self).__name__}: Variable {self.variable!r} is not resolved in {self.filepath}."


@dataclass(frozen=True)
class ResourceMissingIdentifierWarning(YAMLFileWarning):
    severity: ClassVar[SeverityLevel] = SeverityLevel.HIGH
    message: ClassVar[str] = "The resource is missing an identifier:"
    resource: str
    ext_id_type: str

    def get_message(self) -> str:
        message = f"The {self.resource} {self.filepath} is missing the {self.ext_id_type} field(s)."
        return message


@dataclass(frozen=True)
class NamingConventionWarning(YAMLFileWarning):
    severity: ClassVar[SeverityLevel] = SeverityLevel.LOW
    message: ClassVar[str] = "The naming convention is not followed:"
    resource: str
    ext_id_type: str
    external_id: str

    @property
    @abstractmethod
    def recommendation(self) -> str:
        raise NotImplementedError()

    def get_message(self) -> str:
        message = (
            f"The {self.ext_id_type} identifier [bold]{self.external_id!r}[/bold] of the resource {self.resource} "
            f"does not follow the recommended naming convention {self.recommendation}"
        )
        return message


@dataclass(frozen=True)
class PrefixConventionWarning(NamingConventionWarning):
    prefix: str

    @property
    def recommendation(self) -> str:
        return f"of prefixing with {self.prefix!r}."


@dataclass(frozen=True)
class NamespacingConventionWarning(NamingConventionWarning):
    namespace: str

    @property
    def recommendation(self) -> str:
        return f"of using {self.namespace!r} as separator."


@dataclass(frozen=True)
class CaseTypoWarning(UnusedParameterWarning):
    expected: str

    def get_message(self) -> str:
        return f"{type(self).__name__}: Got {self.actual!r}. Did you mean {self.expected!r}?{self._location}."


@dataclass(frozen=True)
class MissingRequiredParameterWarning(YAMLFileWithElementWarning):
    severity: ClassVar[SeverityLevel] = SeverityLevel.HIGH
    expected: str

    def get_message(self) -> str:
        return f"{type(self).__name__}: Missing required parameter {self.expected!r}{self._location}."


@dataclass(frozen=True)
class MissingRequiredIdentifierWarning(YAMLFileWithElementWarning):
    expected: tuple[str, ...]

    def get_message(self) -> str:
        return f"{type(self).__name__}: Missing required identifier {self.expected!r}{self._location}."


@dataclass(frozen=True)
class MissingReferencedWarning(YAMLFileWithElementWarning):
    message: str

    def get_message(self) -> str:
        return f"{type(self).__name__}: {self.message}."


@dataclass(frozen=True)
class TemplateVariableWarning(IdentifiedResourceFileReadWarning):
    path: str

    def get_message(self) -> str:
        return f"{type(self).__name__}: Variable {self.id_name!r} has value {self.id_value!r} in file: {self.filepath.name}. Did you forget to change it?"


@dataclass(frozen=True)
class DataSetMissingWarning(IdentifiedResourceFileReadWarning):
    severity = SeverityLevel.MEDIUM
    resource_name: str

    def get_message(self) -> str:
        # Avoid circular import
        from cognite_toolkit._cdf_tk.cruds import TransformationCRUD

        if self.filepath.parent.name == TransformationCRUD.folder_name:
            return f"{type(self).__name__}: It is recommended to use a data set if source or destination can be scoped with a data set. If not, ignore this warning."
        else:
            return f"{type(self).__name__}: It is recommended that you set dataSetExternalId for {self.resource_name}. This is missing in {self.filepath.name}. Did you forget to add it?"


@dataclass(frozen=True)
class SourceFileModifiedWarning(FileReadWarning):
    severity = SeverityLevel.ERROR

    def get_message(self) -> str:
        message = (
            f"{type(self).__name__}: The source file {self.filepath} has been modified since the last build. "
            "Please rebuild the project."
        )
        return message


@dataclass(frozen=True)
class MissingFileWarning(FileReadWarning):
    severity = SeverityLevel.MEDIUM
    attempted_check: str

    def get_message(self) -> str:
        message = f"{type(self).__name__}: The file {self.filepath} is missing. Cannot verify {self.attempted_check}."
        return message


@dataclass(frozen=True)
class EnvironmentVariableMissingWarning(FileReadWarning):
    severity = SeverityLevel.HIGH
    variables: frozenset[str]
    identifiers: frozenset[Hashable] | None = None

    def get_message(self) -> str:
        from cognite_toolkit._cdf_tk.utils import humanize_collection

        suffix = "s are" if len(self.variables) > 1 else " is"
        variables = humanize_collection(self.variables, sort=True, bind_word="and")
        return f"The environment variable{suffix} missing: {variables}"


@dataclass(frozen=True)
class StreamlitRequirementsWarning(FileReadWarning):
    severity: ClassVar[SeverityLevel] = SeverityLevel.MEDIUM
    dependencies: str | list[str]

    def get_message(self) -> str:
        if isinstance(self.dependencies, str):
            return f"Missing recommended dependencies in requirements.txt: {self.dependencies}"
        return f"Missing dependencies in requirements.txt: {', '.join(self.dependencies)}"


@dataclass(frozen=True)
class ResourceFormatWarning(FileReadWarning):
    severity: ClassVar[SeverityLevel] = SeverityLevel.HIGH
    errors: tuple[str, ...]
    text: str | None = None

    def get_message(self) -> str:
        sep = "\n     * "
        errors = sep.join(map(escape, self.errors))
        s = "s" if len(self.errors) > 1 else ""
        text = self.text or ""
        return f"{type(self).__name__} {text}{len(self.errors)} error{s}:{sep}{errors}"
