"""Svelte validator using tree-sitter AST parsing.

This validator provides production-ready validation for Svelte (.svelte) files,
supporting script blocks with JavaScript/TypeScript, reactive statements, props,
stores, event handlers, and all Svelte language features.

Supports file extension: .svelte
"""

from maid_runner.validators.base_validator import BaseValidator


class SvelteValidator(BaseValidator):
    """Validates Svelte files using tree-sitter AST parsing.

    Features:
    - Accurate AST-based parsing (not regex)
    - Script block extraction and parsing
    - TypeScript and JavaScript support
    - Svelte-specific patterns (reactive statements, props, stores)
    """

    def __init__(self):
        """Initialize Svelte parser and JavaScript/TypeScript parsers."""
        from tree_sitter import Language, Parser
        import tree_sitter_svelte as ts_svelte
        import tree_sitter_typescript as ts_ts

        # Initialize Svelte language and parser (for parsing .svelte files)
        self.svelte_language = Language(ts_svelte.language())
        self.svelte_parser = Parser(self.svelte_language)

        # Aliases for tests that expect 'parser' and 'language' attributes
        self.parser = self.svelte_parser
        self.language = self.svelte_language

        # Initialize TypeScript and JavaScript parsers (for script content)
        self.ts_language = Language(ts_ts.language_typescript())
        self.ts_parser = Parser(self.ts_language)

        self.js_language = Language(ts_ts.language_typescript())  # JS uses TS parser
        self.js_parser = Parser(self.js_language)

    def supports_file(self, file_path: str) -> bool:
        """Check if file is a Svelte file.

        Args:
            file_path: Path to the file

        Returns:
            True if file extension is .svelte
        """
        return file_path.endswith(".svelte")

    def collect_artifacts(self, file_path: str, validation_mode: str) -> dict:
        """Collect artifacts from Svelte file.

        Args:
            file_path: Path to the Svelte file
            validation_mode: "implementation" or "behavioral"

        Returns:
            Dictionary containing found artifacts
        """
        tree, source_code = self._parse_svelte_file(file_path)

        if validation_mode == "implementation":
            return self._collect_implementation_artifacts(tree, source_code)
        else:
            return self._collect_behavioral_artifacts(tree, source_code)

    def _parse_svelte_file(self, file_path: str):
        """Parse Svelte file and return AST tree and source code.

        Args:
            file_path: Path to the Svelte file

        Returns:
            Tuple of (tree, source_code) where source_code is bytes
        """
        with open(file_path, "rb") as f:
            source_code = f.read()

        # Parse the entire Svelte file to extract script content
        svelte_tree = self.svelte_parser.parse(source_code)

        # Extract script content and determine language
        script_content = self._extract_script_content(svelte_tree, source_code)
        script_lang = self._get_language_for_file(file_path)

        if script_content:
            # Parse script content with appropriate parser
            parser = self.ts_parser if script_lang == "typescript" else self.js_parser
            script_tree = parser.parse(script_content.encode("utf-8"))
            return script_tree, script_content.encode("utf-8")
        else:
            # No script content - return empty tree
            empty_code = b""
            return self.js_parser.parse(empty_code), empty_code

    def _extract_script_content(self, svelte_tree, source_code: bytes) -> str:
        """Extract script content from Svelte AST.

        Args:
            svelte_tree: Parsed Svelte AST tree
            source_code: Source code as bytes

        Returns:
            Script content as string (or empty string if no script)
        """
        script_contents = []

        def _visit(node):
            if node.type == "script_element":
                # Find the raw_text node which contains the script content
                for child in node.children:
                    if child.type == "raw_text":
                        content = self._get_node_text(child, source_code)
                        script_contents.append(content)

        self._traverse_tree(svelte_tree.root_node, _visit)

        # Combine all script blocks (supports multiple <script> tags)
        return "\n".join(script_contents)

    def _get_language_for_file(self, file_path: str) -> str:
        """Determine script language from file content.

        Reads the file to check for <script lang="ts"> or similar.

        Args:
            file_path: Path to the Svelte file

        Returns:
            'typescript' or 'javascript'
        """
        try:
            with open(file_path, "r", encoding="utf-8") as f:
                content = f.read()
                # Check for TypeScript script tag
                if '<script lang="ts"' in content or "<script lang='ts'" in content:
                    return "typescript"
        except Exception:
            pass
        return "javascript"

    def _collect_implementation_artifacts(self, tree, source_code: bytes) -> dict:
        """Collect implementation artifacts (definitions).

        Args:
            tree: Parsed AST tree
            source_code: Source code as bytes

        Returns:
            Dictionary with found classes, interfaces, functions, etc.
        """
        # Extract functions and classes from script
        functions = self._extract_functions(tree, source_code)
        classes = self._extract_classes(tree, source_code)

        # Extract methods from classes
        methods = {}
        class_bases = {}

        def _visit(node):
            if node.type in ("class_declaration", "abstract_class_declaration"):
                class_name = self._get_class_name_from_node(node, source_code)
                if class_name:
                    class_methods = self._find_class_methods(node, source_code)
                    if class_methods:
                        methods[class_name] = class_methods
                    bases = self._extract_class_bases(node, source_code)
                    if bases:
                        class_bases[class_name] = bases

        self._traverse_tree(tree.root_node, _visit)

        # Extract behavioral artifacts too (for implementation mode)
        used_classes = self._extract_class_usage(tree, source_code)
        used_functions = self._extract_function_calls(tree, source_code)
        used_methods = self._extract_method_calls(tree, source_code)

        return {
            "found_classes": classes,
            "found_functions": functions,
            "found_methods": methods,
            "found_class_bases": class_bases,
            "found_attributes": {},
            "variable_to_class": {},
            "found_function_types": {},
            "found_method_types": {},
            "used_classes": used_classes,
            "used_functions": used_functions,
            "used_methods": used_methods,
            "used_arguments": {},
        }

    def _collect_behavioral_artifacts(self, tree, source_code: bytes) -> dict:
        """Collect behavioral artifacts (usage).

        Args:
            tree: Parsed AST tree
            source_code: Source code as bytes

        Returns:
            Dictionary with class usage, function calls, method calls, and used arguments
        """
        return {
            "used_classes": self._extract_class_usage(tree, source_code),
            "used_functions": self._extract_function_calls(tree, source_code),
            "used_methods": self._extract_method_calls(tree, source_code),
            "used_arguments": set(),  # Svelte argument tracking not yet implemented
        }

    def _traverse_tree(self, node, callback):
        """Recursively traverse AST nodes.

        Args:
            node: Current AST node
            callback: Function to call for each node
        """
        callback(node)
        for child in node.children:
            self._traverse_tree(child, callback)

    def _get_node_text(self, node, source_code: bytes) -> str:
        """Extract text from AST node.

        Args:
            node: AST node
            source_code: Source code as bytes

        Returns:
            Text content of the node
        """
        return source_code[node.start_byte : node.end_byte].decode("utf-8")

    def _extract_functions(self, tree, source_code: bytes) -> dict:
        """Extract function declarations with their parameters.

        Args:
            tree: Parsed AST tree
            source_code: Source code as bytes

        Returns:
            Dictionary mapping function names to parameter lists
        """
        functions = {}

        def _visit(node):
            # Handle function declarations
            if node.type in ("function_declaration", "function_signature"):
                name = None
                params = []

                for child in node.children:
                    if child.type == "identifier":
                        name = self._get_node_text(child, source_code)
                    elif child.type == "formal_parameters":
                        params = self._extract_parameters(child, source_code)

                if name:
                    functions[name] = params

        self._traverse_tree(tree.root_node, _visit)

        # Also extract arrow functions
        arrow_functions = self._extract_arrow_functions(tree, source_code)
        functions.update(arrow_functions)

        return functions

    def _extract_arrow_functions(self, tree, source_code: bytes) -> dict:
        """Extract arrow function declarations.

        Args:
            tree: Parsed AST tree
            source_code: Source code as bytes

        Returns:
            Dictionary mapping arrow function names to parameter lists
        """
        functions = {}

        def _visit(node):
            # Handle variable declarations with arrow functions
            if node.type == "lexical_declaration":
                for child in node.children:
                    if child.type == "variable_declarator":
                        name = None
                        params = []

                        for subchild in child.children:
                            if subchild.type == "identifier":
                                name = self._get_node_text(subchild, source_code)
                            elif subchild.type == "arrow_function":
                                for arrow_child in subchild.children:
                                    if arrow_child.type == "formal_parameters":
                                        params = self._extract_parameters(
                                            arrow_child, source_code
                                        )
                                    elif arrow_child.type == "identifier":
                                        # Single parameter without parentheses
                                        param_name = self._get_node_text(
                                            arrow_child, source_code
                                        )
                                        params = [{"name": param_name}]

                        if name and any(
                            subchild.type == "arrow_function"
                            for subchild in child.children
                        ):
                            functions[name] = params

        self._traverse_tree(tree.root_node, _visit)
        return functions

    def _extract_parameters(self, params_node, source_code: bytes) -> list:
        """Extract parameter names and types from formal_parameters node.

        Args:
            params_node: formal_parameters AST node
            source_code: Source code as bytes

        Returns:
            List of parameter dicts with 'name' and optionally 'type' keys
        """
        params = []

        for child in params_node.children:
            if child.type == "required_parameter":
                # Find the identifier
                param_name = None
                for subchild in child.children:
                    if subchild.type == "identifier":
                        param_name = self._get_node_text(subchild, source_code)
                        break

                if param_name:
                    # Extract type annotation if present
                    type_annotation = self._find_type_annotation_in_node(
                        child, source_code
                    )
                    if type_annotation:
                        params.append({"name": param_name, "type": type_annotation})
                    else:
                        params.append({"name": param_name})
            elif child.type == "optional_parameter":
                param_name = None
                for subchild in child.children:
                    if subchild.type == "identifier":
                        param_name = self._get_node_text(subchild, source_code)
                        break

                if param_name:
                    type_annotation = self._find_type_annotation_in_node(
                        child, source_code
                    )
                    if type_annotation:
                        params.append({"name": param_name, "type": type_annotation})
                    else:
                        params.append({"name": param_name})

        return params

    def _find_type_annotation_in_node(self, param_node, source_code: bytes) -> str:
        """Find and extract type annotation from a parameter node.

        Args:
            param_node: Parameter AST node
            source_code: Source code as bytes

        Returns:
            Type annotation string or empty string if not found
        """
        for child in param_node.children:
            if child.type == "type_annotation":
                # type_annotation node contains colon and the actual type
                for type_child in child.children:
                    if type_child.type != ":":
                        return self._get_node_text(type_child, source_code)
        return ""

    def _extract_classes(self, tree, source_code: bytes) -> set:
        """Extract class and interface names from AST.

        Args:
            tree: Parsed AST tree
            source_code: Source code as bytes

        Returns:
            Set of class/interface names
        """
        classes = set()

        def _visit(node):
            if node.type in (
                "class_declaration",
                "abstract_class_declaration",
                "interface_declaration",
            ):
                for child in node.children:
                    if child.type in ("type_identifier", "identifier"):
                        name = self._get_node_text(child, source_code)
                        classes.add(name)
                        break

        self._traverse_tree(tree.root_node, _visit)
        return classes

    def _extract_class_bases(self, class_node, source_code: bytes) -> list:
        """Extract base classes from class declaration.

        Args:
            class_node: Class declaration AST node
            source_code: Source code as bytes

        Returns:
            List of base class names
        """
        bases = []

        for child in class_node.children:
            if child.type == "class_heritage":
                for heritage_child in child.children:
                    if heritage_child.type == "extends_clause":
                        for extends_child in heritage_child.children:
                            if extends_child.type in ("identifier", "type_identifier"):
                                bases.append(
                                    self._get_node_text(extends_child, source_code)
                                )

        return bases

    def _get_class_name_from_node(self, node, source_code: bytes) -> str:
        """Extract class name from class declaration node.

        Args:
            node: Class declaration node
            source_code: Source code as bytes

        Returns:
            Class name or empty string
        """
        for child in node.children:
            if child.type == "type_identifier":
                return self._get_node_text(child, source_code)
        return ""

    def _find_class_methods(self, class_node, source_code: bytes) -> dict:
        """Find all methods in a class.

        Args:
            class_node: Class declaration node
            source_code: Source code as bytes

        Returns:
            Dictionary mapping method names to parameter lists
        """
        methods = {}

        for child in class_node.children:
            if child.type == "class_body":
                for body_child in child.children:
                    if body_child.type in (
                        "method_definition",
                        "public_field_definition",
                        "abstract_method_signature",
                    ):
                        method_name = None
                        params = []

                        for method_child in body_child.children:
                            if method_child.type in (
                                "property_identifier",
                                "identifier",
                            ):
                                method_name = self._get_node_text(
                                    method_child, source_code
                                )
                            elif method_child.type == "formal_parameters":
                                params = self._extract_parameters(
                                    method_child, source_code
                                )

                        # Skip constructors
                        if method_name and method_name != "constructor":
                            methods[method_name] = params

        return methods

    def _extract_function_calls(self, tree, source_code: bytes) -> set:
        """Extract function calls.

        Args:
            tree: Parsed AST tree
            source_code: Source code as bytes

        Returns:
            Set of function names being called
        """
        function_calls = set()

        def _visit(node):
            if node.type == "call_expression":
                for child in node.children:
                    if child.type == "identifier":
                        function_calls.add(self._get_node_text(child, source_code))
                        break

        self._traverse_tree(tree.root_node, _visit)
        return function_calls

    def _extract_class_usage(self, tree, source_code: bytes) -> set:
        """Extract class instantiations (new ClassName).

        Args:
            tree: Parsed AST tree
            source_code: Source code as bytes

        Returns:
            Set of class names being instantiated
        """
        class_usage = set()

        def _visit(node):
            if node.type == "new_expression":
                for child in node.children:
                    if child.type == "identifier":
                        class_usage.add(self._get_node_text(child, source_code))
                        break
                    elif child.type == "call_expression":
                        # Handle new ClassName()
                        for call_child in child.children:
                            if call_child.type == "identifier":
                                class_usage.add(
                                    self._get_node_text(call_child, source_code)
                                )
                                break

        self._traverse_tree(tree.root_node, _visit)
        return class_usage

    def _extract_method_calls(self, tree, source_code: bytes) -> dict:
        """Extract method calls (object.method()).

        Args:
            tree: Parsed AST tree
            source_code: Source code as bytes

        Returns:
            Dictionary mapping object names to sets of method names
        """
        method_calls = {}

        def _visit(node):
            if node.type == "call_expression":
                for child in node.children:
                    if child.type == "member_expression":
                        obj_name = None
                        method_name = None

                        for member_child in child.children:
                            if (
                                member_child.type in ("identifier", "this")
                                and obj_name is None
                            ):
                                obj_name = self._get_node_text(
                                    member_child, source_code
                                )
                            elif member_child.type == "property_identifier":
                                method_name = self._get_node_text(
                                    member_child, source_code
                                )

                        if obj_name and method_name:
                            if obj_name not in method_calls:
                                method_calls[obj_name] = set()
                            method_calls[obj_name].add(method_name)

        self._traverse_tree(tree.root_node, _visit)
        return method_calls
