"""
HoloViz Param Language Server Protocol - Core Analyzer.

Provides comprehensive analysis of Param-based Python code including:
- Parameter discovery and type inference
- Cross-file inheritance resolution
- External library class introspection
- Real-time type checking and validation
- Bounds and constraint checking

Modular Architecture:
This analyzer uses a modular component architecture for maintainability
and testability:

- parso_utils: AST navigation and parsing utilities
- parameter_extractor: Parameter definition extraction
- validation: Type checking and constraint validation
- external_class_inspector: Runtime introspection of external classes
- inheritance_resolver: Parameter inheritance resolution
- import_resolver: Cross-file import and module resolution

The analyzer orchestrates these components to provide complete IDE support
for Parameterized classes from both local code and external libraries
like Panel, HoloViews, Bokeh, and others.
"""

from __future__ import annotations

import inspect
import logging
import re
from pathlib import Path
from typing import TYPE_CHECKING, Any

import parso

from ._analyzer import parso_utils
from ._analyzer.ast_navigator import ImportHandler, ParameterDetector, SourceAnalyzer
from ._analyzer.external_class_inspector import ExternalClassInspector
from ._analyzer.import_resolver import ImportResolver
from ._analyzer.inheritance_resolver import InheritanceResolver
from ._analyzer.parameter_extractor import extract_parameter_info_from_assignment
from ._analyzer.validation import ParameterValidator
from ._types import AnalysisResult
from .models import ParameterInfo, ParameterizedInfo

if TYPE_CHECKING:
    from parso.tree import NodeOrLeaf

    from ._types import (
        ImportDict,
        ParamClassDict,
        ParsoNode,
        TypeErrorDict,
    )

# Type aliases for better type safety
NumericValue = int | float | None  # Numeric values from nodes
BoolValue = bool | None  # Boolean values from nodes


logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class ParamAnalyzer:
    """Analyzes Python code for Param usage patterns."""

    def __init__(self, workspace_root: str | None = None):
        self.param_classes: ParamClassDict = {}
        self.imports: ImportDict = {}
        # Store file content for source line lookup
        self._current_file_content: str | None = None
        self.type_errors: list[TypeErrorDict] = []

        # Workspace-wide analysis
        self.workspace_root = Path(workspace_root) if workspace_root else None
        self.module_cache: dict[str, AnalysisResult] = {}  # module_name -> analysis_result
        self.file_cache: dict[str, AnalysisResult] = {}  # file_path -> analysis_result

        # Use modular external class inspector
        self.external_inspector = ExternalClassInspector()
        self.external_param_classes = self.external_inspector.external_param_classes

        # Populate external library cache on initialization using modular component
        self.external_inspector.populate_external_library_cache()

        # Use modular AST navigation components (must be created before validator)
        self.parameter_detector = ParameterDetector(self.imports)
        self.import_handler = ImportHandler(self.imports)

        # Use modular parameter validator
        self.validator = ParameterValidator(
            param_classes=self.param_classes,
            external_param_classes=self.external_param_classes,
            imports=self.imports,
            is_parameter_assignment_func=self._is_parameter_assignment,
            external_inspector=self.external_inspector,
            workspace_root=str(self.workspace_root) if self.workspace_root else None,
        )

        # Use modular import resolver
        self.import_resolver = ImportResolver(
            workspace_root=str(self.workspace_root) if self.workspace_root else None,
            imports=self.imports,
            module_cache=self.module_cache,
            file_cache=self.file_cache,
            analyze_file_func=self._analyze_file_for_import_resolver,
        )

        # Use modular inheritance resolver
        # Filter out None values from external_param_classes
        filtered_external_classes = {
            k: v for k, v in self.external_param_classes.items() if v is not None
        }
        self.inheritance_resolver = InheritanceResolver(
            param_classes=self.param_classes,
            external_param_classes=filtered_external_classes,
            imports=self.imports,
            get_imported_param_class_info_func=self.import_resolver.get_imported_param_class_info,
            analyze_external_class_ast_func=self._analyze_external_class_ast,
            resolve_full_class_path_func=self.import_resolver.resolve_full_class_path,
        )

    def _analyze_file_for_import_resolver(
        self, content: str, file_path: str | None = None
    ) -> AnalysisResult:
        """Analyze a file for the import resolver (avoiding circular dependencies)."""
        # Create a new analyzer instance for the imported module to avoid conflicts
        module_analyzer = ParamAnalyzer(str(self.workspace_root) if self.workspace_root else None)
        return module_analyzer.analyze_file(content, file_path)

    def analyze_file(self, content: str, file_path: str | None = None) -> AnalysisResult:
        """Analyze a Python file for Param usage."""
        try:
            # Use parso with error recovery enabled for better handling of incomplete syntax
            tree = parso.parse(content, error_recovery=True)
            self._reset_analysis()
            self._current_file_path = file_path
            self._current_file_content = content

            # Note: parso handles syntax errors internally with error_recovery=True

            # Cache the tree walk to avoid multiple expensive traversals
            all_nodes = list(parso_utils.walk_tree(tree))
        except Exception as e:
            # If parso completely fails, log and return empty result
            logger.error(f"Failed to parse file: {e}")
            return AnalysisResult(param_classes={}, imports={}, type_errors=[])

        # First pass: collect imports using cached nodes
        for node in all_nodes:
            if node.type == "import_name":
                self.import_handler.handle_import(node)
            elif node.type == "import_from":
                self.import_handler.handle_import_from(node)

        # Second pass: collect class definitions in order, respecting inheritance
        class_nodes: list[ParsoNode] = [node for node in all_nodes if node.type == "classdef"]

        # Process classes in dependency order (parents before children)
        processed_classes = set()
        while len(processed_classes) < len(class_nodes):
            progress_made = False
            for node in class_nodes:
                class_name = parso_utils.get_class_name(node)
                if not class_name or class_name in processed_classes:
                    continue

                # Check if all parent classes are processed or are external param classes
                can_process = True
                bases = parso_utils.get_class_bases(node)
                for base in bases:
                    if base.type == "name":
                        parent_name = parso_utils.get_value(base)
                        # If it's a class defined in this file and not processed yet, wait
                        if (
                            any(
                                parso_utils.get_class_name(cn) == parent_name for cn in class_nodes
                            )
                            and parent_name not in processed_classes
                        ):
                            can_process = False
                            break

                if can_process:
                    self._handle_class_def(node)
                    processed_classes.add(class_name)
                    progress_made = True

            # Prevent infinite loop if there are circular dependencies
            if not progress_made:
                # Process remaining classes anyway
                for node in class_nodes:
                    class_name = parso_utils.get_class_name(node)
                    if class_name and class_name not in processed_classes:
                        self._handle_class_def(node)
                        processed_classes.add(class_name)
                break

        # Pre-pass: discover all external Parameterized classes using cached nodes
        self._discover_external_param_classes(tree, all_nodes)

        # Perform parameter validation after parsing using modular validator with cached nodes
        self.type_errors = self.validator.check_parameter_types(
            tree, content.split("\n"), all_nodes
        )

        return {
            "param_classes": self.param_classes,
            "imports": self.imports,
            "type_errors": self.type_errors,
        }

    def _reset_analysis(self) -> None:
        """Reset analysis state."""
        self.param_classes.clear()
        self.imports.clear()
        self.type_errors.clear()

    def _is_parameter_assignment(self, node: ParsoNode) -> bool:
        """Check if a parso assignment statement looks like a parameter definition."""
        return self.parameter_detector.is_parameter_assignment(node)

    def _handle_class_def(self, node: ParsoNode) -> None:
        """Handle class definitions that might inherit from param.Parameterized (parso node)."""
        # Check if class inherits from param.Parameterized (directly or indirectly)
        is_param_class = False
        bases = parso_utils.get_class_bases(node)
        for base in bases:
            if self.inheritance_resolver.is_param_base(
                base, getattr(self, "_current_file_path", None)
            ):
                is_param_class = True
                break

        if is_param_class:
            class_name = parso_utils.get_class_name(node)
            if class_name is None:
                return  # Skip if we can't get the class name
            class_info = ParameterizedInfo(name=class_name)

            # Get inherited parameters from parent classes first
            inherited_parameters = self.inheritance_resolver.collect_inherited_parameters(
                node, getattr(self, "_current_file_path", None)
            )
            # Add inherited parameters first
            class_info.merge_parameters(inherited_parameters)

            # Extract parameters from this class and add them (overriding inherited ones)
            current_parameters = self._extract_parameters(node)
            for param_info in current_parameters:
                class_info.add_parameter(param_info)

            self.param_classes[class_name] = class_info

    def _extract_parameters(self, node) -> list[ParameterInfo]:
        """Extract parameter definitions from a Param class (parso node)."""
        parameters = []

        for assignment_node, target_name in parso_utils.find_all_parameter_assignments(
            node, self._is_parameter_assignment
        ):
            param_info = extract_parameter_info_from_assignment(
                assignment_node, target_name, self.imports, self._current_file_content
            )
            if param_info:
                parameters.append(param_info)

        return parameters

    def _analyze_external_class_ast(self, full_class_path: str) -> ParameterizedInfo | None:
        """Analyze external classes using the modular external inspector."""
        return self.external_inspector.analyze_external_class_ast(full_class_path)

    def _get_parameter_source_location(
        self, param_obj: Any, cls: type, param_name: str
    ) -> dict[str, str] | None:
        """Get source location information for an external parameter."""
        try:
            # Try to find the class where this parameter is actually defined
            defining_class = self._find_parameter_defining_class(cls, param_name)
            if not defining_class:
                return None

            # Try to get the complete parameter definition
            source_definition = None
            try:
                # Try to get the source lines and find parameter definition
                source_lines, _start_line = inspect.getsourcelines(defining_class)
                source_definition = SourceAnalyzer.extract_complete_parameter_definition(
                    source_lines, param_name
                )
            except (OSError, TypeError):
                # Can't get source lines
                pass

            # Return the complete parameter definition
            if source_definition:
                return {
                    "source": source_definition,
                }
            else:
                # No source available
                return None

        except Exception:
            # If anything goes wrong, return None
            return None

    def _find_parameter_defining_class(self, cls: type, param_name: str) -> type | None:
        """Find the class in the MRO where a parameter is actually defined."""
        # Walk up the MRO to find where this parameter was first defined
        for base_cls in cls.__mro__:
            if hasattr(base_cls, "param") and hasattr(base_cls.param, param_name):
                # Check if this class actually defines the parameter (not just inherits it)
                if param_name in getattr(base_cls, "_param_names", []):
                    return base_cls
                # Fallback: check if the parameter object is defined in this class's dict
                if hasattr(base_cls, "_param_watchers") or param_name in base_cls.__dict__:
                    return base_cls

        # If we can't find the defining class, return the original class
        return cls

    def _get_relative_library_path(self, source_file: str, module_name: str) -> str:
        """Convert absolute source file path to a relative library path."""
        path = Path(source_file)

        # Try to find the library root by looking for the top-level package
        module_parts = module_name.split(".")
        library_name = module_parts[0]  # e.g., 'panel', 'holoviews', etc.

        # Find the library root in the path
        path_parts = path.parts
        for i, part in enumerate(reversed(path_parts)):
            if part == library_name:
                # Found the library root, create relative path from there
                lib_root_index = len(path_parts) - i - 1
                relative_parts = path_parts[lib_root_index:]
                return "/".join(relative_parts)

        # Fallback: just use the filename with module info
        return f"{library_name}/{path.name}"

    def _discover_external_param_classes(
        self, tree: NodeOrLeaf, cached_nodes: list[NodeOrLeaf] | None = None
    ) -> None:
        """Pre-pass to discover all external Parameterized classes using parso analysis."""
        nodes_to_check = cached_nodes if cached_nodes is not None else parso_utils.walk_tree(tree)
        for node in nodes_to_check:
            if node.type in ("power", "atom_expr") and parso_utils.is_function_call(node):
                full_class_path = self.import_resolver.resolve_full_class_path(node)
                if full_class_path:
                    self._analyze_external_class_ast(full_class_path)

    def resolve_class_name_from_context(
        self, class_name: str, param_classes: dict[str, ParameterizedInfo], document_content: str
    ) -> str | None:
        """Resolve a class name from context, handling both direct class names and variable names."""
        # If it's already a known param class, return it
        if class_name in param_classes:
            return class_name

        # If it's a variable name, try to find its assignment in the document
        if document_content:
            # Look for assignments like: variable_name = ClassName(...)
            assignment_pattern = re.compile(
                rf"^([^#]*?){re.escape(class_name)}\s*=\s*(\w+(?:\.\w+)*)\s*\(", re.MULTILINE
            )

            for match in assignment_pattern.finditer(document_content):
                assigned_class = match.group(2)

                # Check if the assigned class is a known param class
                if assigned_class in param_classes:
                    return assigned_class

                # Check if it's an external class
                if "." in assigned_class:
                    # Handle dotted names like hv.Curve
                    parts = assigned_class.split(".")
                    if len(parts) >= 2:
                        alias = parts[0]
                        class_part = ".".join(parts[1:])
                        if alias in self.imports:
                            full_module = self.imports[alias]
                            full_class_path = f"{full_module}.{class_part}"
                            class_info = self.external_param_classes.get(full_class_path)
                            if class_info is None:
                                class_info = self._analyze_external_class_ast(full_class_path)
                            if class_info:
                                # Return the original dotted name for external class handling
                                return assigned_class

        return None
