"""
Import checks - module encapsulation and import patterns.

Tests:
    test_no_dotenv_imports          Only config_file imports dotenv
    test_no_direct_infra_env_access Use config for infrastructure env vars
    test_no_direct_submodule_imports External imports through __init__.py only
    test_no_all_in_submodules       __all__ only in __init__.py files
    test_no_unused_exports          All __init__.py exports must be used externally
"""

import ast
import re
from dataclasses import dataclass, field
from pathlib import Path

from .base import CheckBase


@dataclass
class ImportChecks(CheckBase):
    """Import pattern and encapsulation validation."""

    config_file: str = "config.py"

    # Infrastructure env vars that should come from config (not os.getenv)
    infra_env_vars: set[str] = field(
        default_factory=lambda: {
            "DATABASE_URL",
            "KAFKA_BOOTSTRAP_SERVERS",
            "GRAFANA_URL",
            "LOKI_URL",
            "MIMIR_URL",
            "REDIS_URL",
        }
    )

    # Packages EXCLUDED from encapsulation (test_dir is always excluded)
    unencapsulated_packages: set[str] = field(default_factory=set)

    @property
    def all_packages(self) -> set[str]:
        """Discover all top-level packages (directories with __init__.py)."""
        packages = set()
        for init_file in self.project_root.rglob("__init__.py"):
            if any(skip in init_file.parts for skip in self.skip_dirs):
                continue
            package_dir = init_file.parent
            rel_path = package_dir.relative_to(self.project_root)
            if len(rel_path.parts) == 1:
                packages.add(str(rel_path))
        return packages

    @property
    def encapsulated_packages(self) -> set[str]:
        """All packages except unencapsulated ones (including test_dir)."""
        return self.all_packages - self.unencapsulated_packages - {self.test_dir}

    def validate_dotenv_imports(self) -> dict:
        """Check that only config_file imports dotenv."""
        result = {"valid": True, "violations": []}
        module_name = "dotenv"

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

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

            for node in ast.walk(tree):
                if isinstance(node, ast.Import):
                    for alias in node.names:
                        if alias.name == module_name:
                            result["violations"].append(rel_path)
                            break
                elif isinstance(node, ast.ImportFrom):
                    if node.module == module_name:
                        result["violations"].append(rel_path)
                        break

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

    def validate_infra_env_access(self) -> dict:
        """Check that infra env vars are accessed via config, not os.getenv."""
        result = {"valid": True, "violations": []}

        if not self.infra_env_vars:
            return result

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

            for var in self.infra_env_vars:
                patterns = [
                    rf'os\.getenv\s*\(\s*["\']{ var}["\']',
                    rf'os\.environ\s*\[\s*["\']{ var}["\']',
                ]
                for pattern in patterns:
                    if re.search(pattern, content):
                        result["violations"].append(f"{rel_path}: {var}")
                        break

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

    def validate_submodule_imports(self) -> dict:
        """Check that external imports go through package __init__.py."""
        result = {"valid": True, "violations": []}

        if not self.encapsulated_packages:
            return result

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

            if rel_path.endswith("__init__.py"):
                continue

            all_unencapsulated = self.unencapsulated_packages | {self.test_dir}
            in_unencapsulated = any(
                rel_path.startswith(f"{pkg}/") for pkg in all_unencapsulated
            )
            if in_unencapsulated:
                continue

            file_package = None
            for pkg in self.encapsulated_packages:
                if rel_path.startswith(f"{pkg}/"):
                    file_package = pkg
                    break

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

            for node in ast.walk(tree):
                if isinstance(node, ast.ImportFrom) and node.module:
                    for pkg in self.encapsulated_packages:
                        if node.module.startswith(f"{pkg}."):
                            if file_package == pkg:
                                continue
                            result["violations"].append(
                                f"{rel_path}:{node.lineno} - imports from {node.module}"
                            )

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

    def validate_all_in_submodules(self) -> dict:
        """Check that __all__ only appears in __init__.py files."""
        result = {"valid": True, "violations": []}

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

            if rel_path.endswith("__init__.py"):
                continue
            if rel_path.startswith(f"{self.test_dir}/"):
                continue

            content = py_file.read_text()
            if re.search(r"^__all__\s*=", content, re.MULTILINE):
                result["violations"].append(rel_path)

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

    def validate_unused_exports(self) -> dict:
        """Check that all exports in __init__.py are used externally."""
        result = {"valid": True, "violations": []}

        if not self.encapsulated_packages:
            return result

        def get_exports(init_file: Path) -> set[str]:
            exports = set()
            content = init_file.read_text()
            tree = self.parse_ast(content)
            if not tree:
                return exports

            for node in ast.walk(tree):
                if isinstance(node, ast.ImportFrom):
                    for alias in node.names:
                        name = alias.asname if alias.asname else alias.name
                        exports.add(name)
                elif isinstance(node, ast.Import):
                    for alias in node.names:
                        name = alias.asname if alias.asname else alias.name
                        exports.add(name)
                elif isinstance(node, ast.Assign):
                    for target in node.targets:
                        if isinstance(target, ast.Name):
                            exports.add(target.id)
            return exports

        def get_all_imports(package_name: str) -> dict[str, list[str]]:
            imports: dict[str, list[str]] = {}
            for py_file in self.get_python_files():
                rel_path = self.get_relative_path(py_file)
                if rel_path.startswith(f"{package_name}/"):
                    continue

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

                for node in ast.walk(tree):
                    if isinstance(node, ast.ImportFrom):
                        if node.module == package_name:
                            for alias in node.names:
                                symbol = alias.name
                                if symbol not in imports:
                                    imports[symbol] = []
                                imports[symbol].append(rel_path)
            return imports

        for package_name in self.encapsulated_packages:
            init_file = self.project_root / package_name / "__init__.py"
            if not init_file.exists():
                continue

            exports = get_exports(init_file)
            imports = get_all_imports(package_name)

            for symbol in sorted(exports):
                if symbol.startswith("_"):
                    continue
                if symbol not in imports:
                    result["violations"].append(f"{package_name}: {symbol}")

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

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

        class ImportTests:
            """Import pattern validation tests."""

            def test_no_dotenv_imports(self):
                """Only config_file should import dotenv."""
                if "test_no_dotenv_imports" in checks.skip_tests:
                    return

                result = checks.validate_dotenv_imports()
                assert result["valid"], (
                    f"Files importing dotenv (only {checks.config_file} should):\n"
                    f"  {chr(10).join(result['violations'])}\n\n"
                    f"Use 'from config import config' instead."
                )

            def test_no_direct_infra_env_access(self):
                """Files should use config module instead of os.getenv for infra vars."""
                if "test_no_direct_infra_env_access" in checks.skip_tests:
                    return
                if not checks.infra_env_vars:
                    return

                result = checks.validate_infra_env_access()
                assert result["valid"], (
                    f"Files accessing infrastructure env vars directly:\n"
                    f"  {chr(10).join(result['violations'])}\n\n"
                    f"Use 'from config import config' instead."
                )

            def test_no_direct_submodule_imports(self):
                """External imports should go through package __init__.py."""
                if "test_no_direct_submodule_imports" in checks.skip_tests:
                    return
                if not checks.encapsulated_packages:
                    return

                result = checks.validate_submodule_imports()
                assert result["valid"], (
                    f"Direct submodule imports found (use package __init__.py instead):\n"
                    f"  {chr(10).join(result['violations'])}"
                )

            def test_no_all_in_submodules(self):
                """__all__ should only appear in __init__.py files."""
                if "test_no_all_in_submodules" in checks.skip_tests:
                    return

                result = checks.validate_all_in_submodules()
                assert result["valid"], (
                    f"Files with __all__ (should only be in __init__.py):\n"
                    f"  {chr(10).join(result['violations'])}\n\n"
                    f"Remove __all__ from submodules. The public API is controlled by __init__.py."
                )

            def test_no_unused_exports(self):
                """Every export in __init__.py should be used externally."""
                if "test_no_unused_exports" in checks.skip_tests:
                    return
                if not checks.encapsulated_packages:
                    return

                result = checks.validate_unused_exports()
                assert result["valid"], (
                    f"Unused exports in __init__.py (not imported externally):\n"
                    f"  {chr(10).join(result['violations'])}\n\n"
                    f"Remove unused exports from __init__.py or start using them."
                )

        return ImportTests
