# checks/base.py
import os
from abc import ABC, abstractmethod
from enum import IntEnum
from pathlib import Path
from typing import Any, Callable

import tqdm
from pydantic import BaseModel

from ..util import Config, get_logger

logger = get_logger(__name__)


class Severity(IntEnum):
    """Severity levels for issues."""

    INFO = 1
    WARNING = 2
    ERROR = 3


class FileIssue(BaseModel):
    """Represents an issue found in a file."""

    check_name: str
    line: int | None = None
    message: str
    severity: Severity
    # column: int = 0
    # suggestion: str = ""


class FileCheckResult(BaseModel):
    was_checked: bool  # some files may be ignored
    issues: list[FileIssue]


class FileCheckResultBuilder:
    """Simplify building file check result in simpler scenarios."""

    def __init__(self, checker_name: str):
        """Save checker name for results

        Args:
            checker_name (str): The name of the checker.
        """
        self.checker_name = checker_name

    def skipped(self, _reason: str | None = None) -> FileCheckResult:
        """File was skipped and that was intended."""
        return FileCheckResult(was_checked=False, issues=[])

    def passed(self) -> FileCheckResult:
        return FileCheckResult(was_checked=True, issues=[])

    def failed(
        self, message: str | None = None, severity: Severity = Severity.ERROR
    ) -> FileCheckResult:
        return FileCheckResult(
            was_checked=True,
            issues=[
                FileIssue(
                    check_name=self.checker_name,
                    message=message if message is not None else "Check failed",
                    severity=severity,
                )
            ],
        )

    def ambiguous(
        self, reason: str, severity: Severity = Severity.INFO
    ) -> FileCheckResult:
        """No clear result and that was not intended"""
        return FileCheckResult(
            was_checked=False,
            issues=[
                FileIssue(
                    check_name=self.checker_name, message=reason, severity=severity
                )
            ],
        )


class CheckerABC(ABC):
    def __init__(self, config: Config):
        self.config = config
        self.statistics: list[Any] = []  # any info saved during dir check

    @abstractmethod
    def _check_file_impl(self, file_path: Path) -> FileCheckResult:
        """Implement the file checking logic."""
        pass

    def get_name(self) -> str:
        """Get the name of the checker."""
        return self.__class__.__name__

    def check_file(self, file_path: Path) -> FileCheckResult:
        """Check a single file for issues.
        Args:
            file_path (Path): The path to the file to check.

        Returns:
            FileCheckResult: The result of the file check.
        """
        res = self._check_file_impl(file_path)
        self._clear_statistics()
        return res

    def check_directory(self, directory_path: Path) -> dict[Path, FileCheckResult]:
        """Check all files in a directory for issues.
        Args:
            directory_path (Path): The path to the directory to check.

        Returns:
            dict[Path, FileCheckResult]:
                A dictionary mapping file paths to their check results.
        """
        results: dict[Path, FileCheckResult] = {}

        # todo: follow_symlink = True with saving to avoid recursion
        with tqdm.tqdm() as pbar:
            for dirpath, dirnames, filenames in os.walk(
                directory_path, topdown=True, onerror=None, followlinks=False
            ):
                dirnames[:] = [
                    d for d in dirnames if self._filter_dir(Path(dirpath) / d)
                ]
                for file_name in filenames:
                    file_path = Path(dirpath) / file_name
                    if file_path.is_file() and self._filter_file(file_path):
                        pbar.set_description_str(
                            f"{self.get_name()}: checking file {file_name}"
                        )
                        try:
                            results[file_path] = self._check_file_impl(file_path)
                        except Exception as e:
                            logger.warning(f"Failed to check {file_path}: {e}")
                            results[file_path] = FileCheckResult(
                                was_checked=False, issues=[]
                            )
                        pbar.update(1)

        return results

    def _clear_statistics(self) -> None:
        """Clear collected statistics."""
        self.statistics = []

    def _filter_file(self, file_path: Path) -> bool:
        """Process the file during checks."""
        labels = self.config.get_labels(file_path.suffix)
        return ("code" in labels) or ("docs" in labels)

    def _filter_dir(self, dir_path: Path) -> bool:
        """Process the directory during checks."""
        return dir_path.name not in self.config.get_ignored_directories()


class SimpleCheckerABC(ABC):
    @abstractmethod
    def _check_file(self, file_path: Path, checker_config: dict) -> FileCheckResult:
        """Check a single file for issues."""
        pass


class SimpleCheckerAdapter(CheckerABC):
    """Adapter to make a complex checker from a simple one."""

    def __init__(self, config: Config, checker: SimpleCheckerABC):
        super().__init__(config)
        self.checker = checker
        self.checker_config = self.config.get_checker_extra(self.get_name())

    def _check_file_impl(self, file_path: Path) -> FileCheckResult:
        return self.checker._check_file(file_path, self.checker_config)

    def get_name(self) -> str:
        return self.checker.__class__.__name__


class FunctionAdapter(CheckerABC):
    """Deprecated interface to support function checks"""

    def __init__(
        self, config: Config, function: Callable[[Path], bool | None], check_name: str
    ):
        super().__init__(config)
        self.function = function
        self.check_name = check_name

    def _check_file_impl(self, file_path: Path) -> FileCheckResult:
        """Implement the file checking logic."""
        result = self.function(file_path)
        if result is None:
            return FileCheckResult(was_checked=False, issues=[])
        elif result is False:
            return FileCheckResult(
                was_checked=True,
                issues=[
                    FileIssue(
                        check_name=self.check_name,
                        message=f"Check {self.check_name} - failed.",
                        severity=Severity.ERROR,
                    )
                ],
            )
        else:
            return FileCheckResult(was_checked=True, issues=[])

    def get_name(self) -> str:
        return self.check_name
