"""
Style checks - naming conventions, type hints, docstrings, constants.

Tests:
    test_naming_conventions     snake_case, PascalCase, UPPER_SNAKE_CASE
    test_type_hints_required    Public functions must have type hints
    test_public_docstrings      Public classes/functions must have docstrings
    test_constants_in_config    ALL_CAPS literal constants must be in config.py
"""

import ast
from dataclasses import dataclass

from .base import CheckBase


@dataclass
class StyleChecks(CheckBase):
    """Code style validation."""

    config_file: str = "config.py"

    def validate_naming_conventions(self) -> dict:
        """Check naming conventions: snake_case, PascalCase, UPPER_SNAKE_CASE."""
        result = {"valid": True, "violations": []}

        def is_snake_case(name: str) -> bool:
            return name == name.lower()

        def is_pascal_case(name: str) -> bool:
            return name[0].isupper() and "_" not in name

        def is_upper_snake_case(name: str) -> bool:
            return name.isupper() or (name == name.upper() and "_" in name)

        for py_file in self.get_python_files():
            rel_path = self.get_relative_path(py_file)
            content = py_file.read_text()
            tree = self.parse_ast(content)
            if not tree:
                continue

            for node in ast.walk(tree):
                # Check class names (should be PascalCase)
                if isinstance(node, ast.ClassDef):
                    if not is_pascal_case(node.name) and not node.name.startswith("_"):
                        result["violations"].append(
                            f"{rel_path}:{node.lineno} - class '{node.name}' should be PascalCase"
                        )

                # Check function names (should be snake_case)
                elif isinstance(node, ast.FunctionDef) or isinstance(
                    node, ast.AsyncFunctionDef
                ):
                    if not node.name.startswith("_") and not is_snake_case(node.name):
                        # Allow test methods and special methods
                        if not node.name.startswith(
                            "test"
                        ) and not node.name.startswith("__"):
                            result["violations"].append(
                                f"{rel_path}:{node.lineno} - function '{node.name}' should be snake_case"
                            )

                # Check module-level constants (UPPER_SNAKE_CASE for assignments)
                elif isinstance(node, ast.Assign):
                    if hasattr(node, "col_offset") and node.col_offset == 0:
                        for target in node.targets:
                            if isinstance(target, ast.Name):
                                name = target.id
                                if name.isupper() or (
                                    name[0].isupper() and "_" in name
                                ):
                                    if not is_upper_snake_case(name):
                                        result["violations"].append(
                                            f"{rel_path}:{node.lineno} - constant '{name}' should be UPPER_SNAKE_CASE"
                                        )

        result["valid"] = len(result["violations"]) == 0
        return result

    def validate_type_hints(self) -> dict:
        """Check that public functions have type hints."""
        result = {"valid": True, "violations": []}

        for py_file in self.get_python_files():
            rel_path = self.get_relative_path(py_file)
            content = py_file.read_text()
            tree = self.parse_ast(content)
            if not tree:
                continue

            for node in ast.walk(tree):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    # Skip private/magic methods and test functions
                    if node.name.startswith("_") or node.name.startswith("test"):
                        continue

                    # Check return type annotation
                    if node.returns is None:
                        result["violations"].append(
                            f"{rel_path}:{node.lineno} - '{node.name}' missing return type hint"
                        )

                    # Check argument type annotations (skip self/cls)
                    for arg in node.args.args:
                        if arg.arg in ("self", "cls"):
                            continue
                        if arg.annotation is None:
                            result["violations"].append(
                                f"{rel_path}:{node.lineno} - '{node.name}' param '{arg.arg}' missing type hint"
                            )

        result["valid"] = len(result["violations"]) == 0
        return result

    def validate_public_docstrings(self) -> dict:
        """Check that public classes and functions have docstrings."""
        result = {"valid": True, "violations": []}

        for py_file in self.get_python_files():
            rel_path = self.get_relative_path(py_file)
            content = py_file.read_text()
            tree = self.parse_ast(content)
            if not tree:
                continue

            for node in ast.walk(tree):
                # Check classes
                if isinstance(node, ast.ClassDef):
                    if not node.name.startswith("_"):
                        if not ast.get_docstring(node):
                            result["violations"].append(
                                f"{rel_path}:{node.lineno} - class '{node.name}' missing docstring"
                            )

                # Check functions
                elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    # Skip private, magic, and test methods
                    if node.name.startswith("_") or node.name.startswith("test"):
                        continue
                    if not ast.get_docstring(node):
                        result["violations"].append(
                            f"{rel_path}:{node.lineno} - function '{node.name}' missing docstring"
                        )

        result["valid"] = len(result["violations"]) == 0
        return result

    def validate_constants_in_config(self) -> dict:
        """Check that module-level literal constants are in config.py."""
        result = {"valid": True, "violations": []}

        def is_all_caps(name: str) -> bool:
            return name.isupper() and not name.startswith("_")

        def is_literal_value(node: ast.AST) -> bool:
            if isinstance(node, ast.Constant):
                return True
            if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
                return all(is_literal_value(elt) for elt in node.elts)
            if isinstance(node, ast.Dict):
                keys_ok = all(is_literal_value(k) for k in node.keys if k is not None)
                vals_ok = all(is_literal_value(v) for v in node.values)
                return keys_ok and vals_ok
            if isinstance(node, ast.UnaryOp) and isinstance(
                node.op, (ast.UAdd, ast.USub)
            ):
                return is_literal_value(node.operand)
            if isinstance(node, ast.BinOp):
                return is_literal_value(node.left) and is_literal_value(node.right)
            return False

        for py_file in self.get_python_files():
            rel_path = self.get_relative_path(py_file)

            # Skip config file itself
            if rel_path == self.config_file:
                continue

            # Skip test files
            if rel_path.startswith(f"{self.test_dir}/"):
                continue

            content = py_file.read_text()
            tree = self.parse_ast(content)
            if not tree:
                continue

            # Only check top-level assignments (not inside classes/functions)
            for node in tree.body:
                if isinstance(node, ast.Assign):
                    for target in node.targets:
                        if isinstance(target, ast.Name) and is_all_caps(target.id):
                            if is_literal_value(node.value):
                                result["violations"].append(
                                    f"{rel_path}:{node.lineno} - {target.id} = {ast.unparse(node.value)[:50]}"
                                )
                elif isinstance(node, ast.AnnAssign):
                    if isinstance(node.target, ast.Name) and is_all_caps(
                        node.target.id
                    ):
                        if node.value and is_literal_value(node.value):
                            result["violations"].append(
                                f"{rel_path}:{node.lineno} - {node.target.id} = {ast.unparse(node.value)[:50]}"
                            )

        result["valid"] = len(result["violations"]) == 0
        return result

    def create_test_class(self):
        """Create pytest test class for style checks."""
        checks = self

        class StyleTests:
            """Code style validation tests."""

            def test_naming_conventions(self):
                """Enforce naming conventions: snake_case, PascalCase, UPPER_SNAKE_CASE."""
                if "test_naming_conventions" in checks.skip_tests:
                    return

                result = checks.validate_naming_conventions()
                violations = result["violations"]
                assert result["valid"], (
                    f"Naming convention violations:\n"
                    f"  {chr(10).join(violations[:20])}\n"
                    f"{'  ... and more' if len(violations) > 20 else ''}\n\n"
                    f"Use: snake_case (functions/vars), PascalCase (classes), UPPER_SNAKE_CASE (constants)"
                )

            def test_type_hints_required(self):
                """Public functions should have type hints."""
                if "test_type_hints_required" in checks.skip_tests:
                    return

                result = checks.validate_type_hints()
                violations = result["violations"]
                assert result["valid"], (
                    f"Missing type hints:\n"
                    f"  {chr(10).join(violations[:20])}\n"
                    f"{'  ... and more' if len(violations) > 20 else ''}\n\n"
                    f"Add type hints to public function signatures."
                )

            def test_public_docstrings(self):
                """Public functions and classes should have docstrings."""
                if "test_public_docstrings" in checks.skip_tests:
                    return

                result = checks.validate_public_docstrings()
                violations = result["violations"]
                assert result["valid"], (
                    f"Missing docstrings:\n"
                    f"  {chr(10).join(violations[:20])}\n"
                    f"{'  ... and more' if len(violations) > 20 else ''}\n\n"
                    f"Add docstrings to public classes and functions."
                )

            def test_constants_in_config(self):
                """Module-level constants with literal values should be in config.py."""
                if "test_constants_in_config" in checks.skip_tests:
                    return

                result = checks.validate_constants_in_config()
                violations = result["violations"]
                assert result["valid"], (
                    f"Module-level constants with literal values should be in {checks.config_file}:\n"
                    f"  {chr(10).join(violations[:20])}\n"
                    f"{'  ... and more' if len(violations) > 20 else ''}\n\n"
                    f"Move these constants to Config class in {checks.config_file}, "
                    f"or use a computed value (e.g., config.some_value)."
                )

        return StyleTests
