"""
Configuration checks - env vars, config values, and gitignore.

Tests:
    test_no_getenv_defaults                     No default values in os.getenv()
    test_no_unused_config_values                All Config class attributes must be used
    test_gitignore_exists                       .gitignore file must exist
    test_gitignore_contains_required_entries    Standard entries in .gitignore
    test_env_file_has_required_vars             ENV variable required in .env
    test_no_unused_env_vars                     All .env variables must be referenced
"""

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

from .base import CheckBase


@dataclass
class ConfigChecks(CheckBase):
    """Configuration and environment validation."""

    config_file: str = "config.py"

    # Infrastructure env vars (excluded from unused check, read by InfraConfig)
    infra_env_vars: set[str] = field(
        default_factory=lambda: {
            "DATABASE_URL",
            "KAFKA_BOOTSTRAP_SERVERS",
            "GRAFANA_URL",
            "LOKI_URL",
            "MIMIR_URL",
            "REDIS_URL",
        }
    )

    # Additional env vars that must be in .env (ENV is always required)
    additional_required_env_vars: set[str] = field(default_factory=set)

    # Required gitignore entries
    required_gitignore_entries: set[str] = field(
        default_factory=lambda: {
            "__pycache__/",
            "*.py[cod]",
            "*$py.class",
            "*.so",
            ".Python",
            "venv/",
            ".venv/",
            "env/",
            "ENV/",
            ".vscode/",
            ".idea/",
            "*.swp",
            "*.swo",
            ".env",
            "*.bak",
            ".pytest_cache/",
            ".coverage",
            "htmlcov/",
            "*.log",
            ".DS_Store",
            "Thumbs.db",
            "node_modules/",
        }
    )

    @property
    def required_env_vars(self) -> set[str]:
        """ENV is always required, plus any additional vars."""
        return {"ENV"} | self.additional_required_env_vars

    def parse_env_file(self) -> dict[str, str]:
        """Parse .env file and return dict of var_name -> value."""
        env_file = self.project_root / ".env"
        if not env_file.exists():
            return {}
        env_vars = {}
        for line in env_file.read_text().splitlines():
            line = line.strip()
            if not line or line.startswith("#"):
                continue
            if "=" in line:
                key, value = line.split("=", 1)
                env_vars[key.strip()] = value.strip()
        return env_vars

    def validate_getenv_defaults(self) -> dict:
        """Check for os.getenv() calls with default values."""
        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.Call):
                    if (
                        isinstance(node.func, ast.Attribute)
                        and node.func.attr == "getenv"
                        and isinstance(node.func.value, ast.Name)
                        and node.func.value.id == "os"
                    ):
                        has_default = len(node.args) > 1 or any(
                            kw.arg == "default" for kw in node.keywords
                        )
                        if has_default:
                            var_name = (
                                node.args[0].value
                                if isinstance(node.args[0], ast.Constant)
                                else "?"
                            )
                            result["violations"].append(
                                f'{rel_path}:{node.lineno}: os.getenv("{var_name}", ...)'
                            )

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

    def validate_unused_config_values(self) -> dict:
        """Check that all Config class attributes are used somewhere."""
        result = {"valid": True, "violations": []}

        config_path = self.project_root / self.config_file
        if not config_path.exists():
            return result

        content = config_path.read_text()
        tree = self.parse_ast(content)
        if not tree:
            return result

        # Extract Config class attributes (including nested classes)
        attributes: dict[str, set[str]] = {}
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef) and node.name == "Config":
                attributes["Config"] = set()
                for item in node.body:
                    if isinstance(item, ast.Assign):
                        for target in item.targets:
                            if isinstance(target, ast.Name):
                                attributes["Config"].add(target.id)
                    elif isinstance(item, ast.AnnAssign) and isinstance(
                        item.target, ast.Name
                    ):
                        attributes["Config"].add(item.target.id)
                    elif isinstance(item, ast.ClassDef):
                        nested_name = f"Config.{item.name}"
                        attributes[nested_name] = set()
                        for nested_item in item.body:
                            if isinstance(nested_item, ast.Assign):
                                for target in nested_item.targets:
                                    if isinstance(target, ast.Name):
                                        attributes[nested_name].add(target.id)
                            elif isinstance(nested_item, ast.AnnAssign) and isinstance(
                                nested_item.target, ast.Name
                            ):
                                attributes[nested_name].add(nested_item.target.id)

        if not attributes:
            return result

        def find_references(class_path: str, attr: str) -> bool:
            """Check if a config attribute is referenced anywhere."""
            patterns = [
                rf"{class_path}\.{attr}\b",
                (
                    rf"\.{class_path.split('.')[-1]}\.{attr}\b"
                    if "." in class_path
                    else rf"\.Config\.{attr}\b"
                ),
                rf"\bself\.{attr}\b",
            ]
            if class_path == "Config":
                patterns.append(rf"\bconfig\.{attr}\b")

            for py_file in self.get_python_files():
                file_content = py_file.read_text()
                for pattern in patterns:
                    if re.search(pattern, file_content):
                        return True
            return False

        for class_path, attrs in attributes.items():
            for attr in attrs:
                if attr.startswith("_"):
                    continue
                if not find_references(class_path, attr):
                    result["violations"].append(f"{class_path}.{attr}")

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

    def validate_gitignore_exists(self) -> dict:
        """Check that .gitignore file exists."""
        result = {"valid": True, "error": None}
        gitignore = self.project_root / ".gitignore"
        if not gitignore.exists():
            result["valid"] = False
            result["error"] = ".gitignore file is missing"
        return result

    def validate_gitignore_entries(self) -> dict:
        """Check that .gitignore contains required entries."""
        result = {"valid": True, "missing": []}

        if not self.required_gitignore_entries:
            return result

        gitignore = self.project_root / ".gitignore"
        if not gitignore.exists():
            return result  # Covered by validate_gitignore_exists

        content = gitignore.read_text()
        entries = set()
        for line in content.splitlines():
            line = line.strip()
            if line and not line.startswith("#"):
                entries.add(line)

        result["missing"] = list(self.required_gitignore_entries - entries)
        result["valid"] = len(result["missing"]) == 0
        return result

    def validate_env_file_required_vars(self) -> dict:
        """Check that .env contains required variables."""
        result = {"valid": True, "missing": [], "error": None}

        if not self.required_env_vars:
            return result

        env_vars = self.parse_env_file()
        if not env_vars:
            result["valid"] = False
            result["error"] = ".env file is missing or empty"
            return result

        result["missing"] = list(self.required_env_vars - set(env_vars.keys()))
        result["valid"] = len(result["missing"]) == 0
        return result

    def validate_unused_env_vars(self) -> dict:
        """Check that all .env variables are used somewhere."""
        result = {"valid": True, "unused": []}

        env_vars = self.parse_env_file()
        if not env_vars:
            return result

        # Find references in codebase
        referenced = set()
        for py_file in self.get_python_files():
            content = py_file.read_text()
            for match in re.finditer(r'os\.getenv\s*\(\s*["\']([^"\']+)["\']', content):
                referenced.add(match.group(1))
            for match in re.finditer(
                r'os\.environ\s*[\[\.].*?["\']([^"\']+)["\']', content
            ):
                referenced.add(match.group(1))

        # Exclude infra vars and required vars
        excluded = self.infra_env_vars | self.required_env_vars
        result["unused"] = list(set(env_vars.keys()) - referenced - excluded)
        result["valid"] = len(result["unused"]) == 0
        return result

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

        class ConfigTests:
            """Configuration validation tests."""

            def test_no_getenv_defaults(self):
                """No file should pass defaults to os.getenv()."""
                if "test_no_getenv_defaults" in checks.skip_tests:
                    return

                result = checks.validate_getenv_defaults()
                assert result["valid"], (
                    f"os.getenv() calls with defaults:\n"
                    f"  {chr(10).join(result['violations'])}\n\n"
                    f"Remove defaults - env vars should fail fast if not set."
                )

            def test_no_unused_config_values(self):
                """All Config values should be used somewhere in the codebase."""
                if "test_no_unused_config_values" in checks.skip_tests:
                    return

                result = checks.validate_unused_config_values()
                assert result["valid"], (
                    f"Unused Config values (not referenced in codebase):\n"
                    f"  {chr(10).join(sorted(result['violations']))}\n\n"
                    f"Remove unused config values or start using them."
                )

            def test_gitignore_exists(self):
                """.gitignore file must exist."""
                if "test_gitignore_exists" in checks.skip_tests:
                    return

                result = checks.validate_gitignore_exists()
                assert result["valid"], result["error"]

            def test_gitignore_contains_required_entries(self):
                """All required entries must be in .gitignore."""
                if "test_gitignore_contains_required_entries" in checks.skip_tests:
                    return
                if not checks.required_gitignore_entries:
                    return

                result = checks.validate_gitignore_entries()
                assert result["valid"], (
                    f"Missing required entries in .gitignore:\n"
                    f"  {chr(10).join(sorted(result['missing']))}\n\n"
                    f"Add these entries to .gitignore."
                )

            def test_env_file_has_required_vars(self):
                """.env file must contain required environment variables."""
                if "test_env_file_has_required_vars" in checks.skip_tests:
                    return
                if not checks.required_env_vars:
                    return

                result = checks.validate_env_file_required_vars()
                if result["error"]:
                    assert False, result["error"]
                assert result["valid"], (
                    f"Missing required variables in .env:\n"
                    f"  {chr(10).join(sorted(result['missing']))}\n\n"
                    f"Add these variables to .env."
                )

            def test_no_unused_env_vars(self):
                """All .env variables should be used somewhere in the codebase."""
                if "test_no_unused_env_vars" in checks.skip_tests:
                    return

                result = checks.validate_unused_env_vars()
                assert result["valid"], (
                    f"Unused .env variables (not referenced in codebase):\n"
                    f"  {chr(10).join(sorted(result['unused']))}\n\n"
                    f"Remove unused variables from .env or start using them."
                )

        return ConfigTests
