# ---------------------------------------------------------------------------
#  Copyright (c) DevSecCode. All rights reserved.
# ---------------------------------------------------------------------------
"""Python lexical extractor -- tree-sitter-based symbol and relationship extraction.

Extracts:
  - imports: import x, from x import y, relative imports (from . / from ..)
  - symbols: functions, classes, methods, constructors, properties (@property),
    module-level variables, decorated functions
  - calls: function calls, method calls (obj.method())
  - heritage: class Foo(Bar, Baz) -> extends edges
"""

from __future__ import annotations

import logging
import os
from typing import Optional

from tree_sitter import Language, Parser, Node

from dsc_api.services.code_intel.dtos import (
	RawCall,
	RawCallArgument,
	RawHeritage,
	RawImport,
	RawReturnExpression,
	RawVariableDef,
	SymbolKind,
	SymbolRecord,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Lazy-init parser
# ---------------------------------------------------------------------------

_py_parser: Optional[Parser] = None


def _get_parser() -> Parser:
	global _py_parser
	if _py_parser is None:
		import tree_sitter_python as tspy
		_py_parser = Parser(Language(tspy.language()))
	return _py_parser


def _text(node: Node) -> str:
	"""Return the UTF-8 text of a node."""
	return node.text.decode("utf-8") if node.text else ""


def _child_by_field(node: Node, field: str) -> Optional[Node]:
	return node.child_by_field_name(field)


def _children_by_type(node: Node, *types: str) -> list[Node]:
	return [c for c in node.children if c.type in types]


def _first_child_by_type(node: Node, *types: str) -> Optional[Node]:
	for c in node.children:
		if c.type in types:
			return c
	return None


def _qual(parent: Optional[str], name: str) -> str:
	"""Build a qualified name with ``::`` separator."""
	if parent:
		return f"{parent}::{name}"
	return name


def _walk(node: Node):
	"""Depth-first walk yielding all descendant nodes."""
	yield node
	for child in node.children:
		yield from _walk(child)


def _has_decorator(node: Node, decorator_name: str) -> bool:
	"""Check whether *node* has a decorator matching *decorator_name*."""
	for child in node.children:
		if child.type == "decorator":
			# decorator children: "@" + expression (identifier or attribute or call)
			for dc in child.children:
				if dc.type == "identifier" and _text(dc) == decorator_name:
					return True
				if dc.type == "call":
					fn = _child_by_field(dc, "function")
					if fn and _text(fn) == decorator_name:
						return True
				if dc.type == "attribute":
					# e.g. @abc.abstractmethod -- check last identifier
					parts = _text(dc).split(".")
					if parts and parts[-1] == decorator_name:
						return True
	return False


# ---------------------------------------------------------------------------
# Extraction result container (imported from ts_extractor)
# ---------------------------------------------------------------------------

from dsc_api.services.code_intel.ts_extractor import ExtractionResult  # noqa: E402


# ---------------------------------------------------------------------------
# Main extraction entry point
# ---------------------------------------------------------------------------

def extract_file(file_path: str, source: bytes) -> ExtractionResult:
	"""Parse *source* as Python and extract all symbols, imports, calls, and heritage.

	Args:
		file_path: Workspace-relative file path.
		source: Raw UTF-8 source bytes.
	"""
	parser = _get_parser()
	tree = parser.parse(source)
	root = tree.root_node

	result = ExtractionResult(file_path)

	_extract_node(root, result, parent_name=None)

	return result


# ---------------------------------------------------------------------------
# Recursive node extractor
# ---------------------------------------------------------------------------

def _extract_node(
	node: Node,
	result: ExtractionResult,
	parent_name: Optional[str],
) -> None:
	"""Recursively extract symbols, imports, calls, and heritage from *node*."""

	for child in node.children:
		ctype = child.type

		# ----- Imports -----
		if ctype == "import_statement":
			_extract_import_statement(child, result)
			continue

		if ctype == "import_from_statement":
			_extract_import_from_statement(child, result)
			continue

		# ----- Function definitions -----
		if ctype == "function_definition":
			_extract_function(child, result, parent_name)
			continue

		# ----- Class definitions -----
		if ctype == "class_definition":
			_extract_class(child, result, parent_name)
			continue

		# ----- Decorated definitions -----
		if ctype == "decorated_definition":
			# The actual definition is a child of the decorated_definition node
			inner = _first_child_by_type(child, "function_definition", "class_definition")
			if inner:
				if inner.type == "function_definition":
					_extract_function(inner, result, parent_name, decorator_node=child)
				elif inner.type == "class_definition":
					_extract_class(inner, result, parent_name)
			continue

		# ----- Expression statements (may contain assignments or calls) -----
		if ctype == "expression_statement":
			expr = child.children[0] if child.children else None
			if expr:
				if expr.type == "assignment":
					_extract_assignment(expr, result, parent_name)
				elif expr.type == "call":
					_extract_call(expr, result, parent_name)
				else:
					# Walk for nested calls
					_extract_calls_in_body(expr, result, parent_name)
			continue

		# ----- Top-level call expressions -----
		if ctype == "call":
			_extract_call(child, result, parent_name)
			continue

		# ----- If / for / while / with / try at module level -----
		if ctype in ("if_statement", "for_statement", "while_statement",
					  "with_statement", "try_statement"):
			_extract_calls_in_body(child, result, parent_name)
			continue


# ---------------------------------------------------------------------------
# Import extraction
# ---------------------------------------------------------------------------

def _extract_import_statement(node: Node, result: ExtractionResult) -> None:
	"""Handle ``import x``, ``import x.y``, ``import x as alias``."""
	for child in node.children:
		if child.type == "dotted_name":
			module_path = _text(child)
			result.add_import(
				imported_path=module_path,
				imported_names=[],
				is_default=False,
				is_namespace=True,
				line=node.start_point[0] + 1,
			)
		elif child.type == "aliased_import":
			name_node = _first_child_by_type(child, "dotted_name")
			alias_node = _child_by_field(child, "alias")
			if name_node:
				alias_text = _text(alias_node) if alias_node else None
				result.add_import(
					imported_path=_text(name_node),
					imported_names=[],
					is_default=False,
					is_namespace=True,
					alias=alias_text,
					line=node.start_point[0] + 1,
				)


def _extract_import_from_statement(node: Node, result: ExtractionResult) -> None:
	"""Handle ``from x import y``, ``from . import z``, ``from ..pkg import z``."""
	module_name_node = _child_by_field(node, "module_name")
	module_path: str = ""

	# Count leading dots for relative imports
	dots = ""
	for child in node.children:
		if child.type == "relative_import":
			# relative_import contains dots and optionally a dotted_name
			for rc in child.children:
				if rc.type == "import_prefix":
					dots = _text(rc)
				elif rc.type == "dotted_name":
					module_path = dots + _text(rc)
			if not module_path:
				module_path = dots
			break
		elif child.type == "dotted_name" and module_name_node and child.id == module_name_node.id:
			module_path = _text(child)
			break

	# If we didn't find it via relative_import, look for the module name directly
	if not module_path and module_name_node:
		module_path = _text(module_name_node)

	# If still empty, try to reconstruct from dots + dotted_name children
	if not module_path:
		parts: list[str] = []
		for child in node.children:
			if child.type == ".":
				parts.append(".")
			elif child.type == "dotted_name" and not any(
				c.type == "import" for c in node.children[:node.children.index(child)]
				if c.type == "import"
			):
				# This is the module part, before "import" keyword
				# but only if we haven't passed "import" yet
				pass
		# Fallback: just gather dot tokens before the "import" keyword
		dot_count = 0
		found_import = False
		for child in node.children:
			if child.type == "import":
				found_import = True
				continue
			if not found_import:
				if child.type == ".":
					dot_count += 1
				elif child.type == "dotted_name":
					module_path = ("." * dot_count) + _text(child)
					dot_count = 0
		if not module_path and dot_count > 0:
			module_path = "." * dot_count

	imported_names: list[str] = []
	is_namespace = False

	for child in node.children:
		if child.type == "dotted_name":
			# Skip the module name (we already captured it)
			# Names after "import" keyword
			pass
		elif child.type == "wildcard_import":
			is_namespace = True
		elif child.type == "import_prefix":
			# Already handled above
			pass

	# Gather imported names from the import list
	# tree-sitter-python uses different structures for:
	#   from x import y       -> aliased_import or dotted_name children
	#   from x import (y, z)  -> import_from_statement with multiple children
	found_import_kw = False
	for child in node.children:
		if child.type == "import":
			found_import_kw = True
			continue
		if not found_import_kw:
			continue

		if child.type == "dotted_name":
			imported_names.append(_text(child))
		elif child.type == "aliased_import":
			name_node = _first_child_by_type(child, "dotted_name", "identifier")
			if name_node:
				imported_names.append(_text(name_node))
		elif child.type == "identifier":
			imported_names.append(_text(child))
		elif child.type == "wildcard_import":
			is_namespace = True

	if not module_path:
		return

	result.add_import(
		imported_path=module_path,
		imported_names=imported_names,
		is_default=False,
		is_namespace=is_namespace,
		line=node.start_point[0] + 1,
	)


# ---------------------------------------------------------------------------
# Function extraction
# ---------------------------------------------------------------------------

def _extract_function(
	node: Node,
	result: ExtractionResult,
	parent_name: Optional[str],
	decorator_node: Optional[Node] = None,
) -> None:
	"""Extract a function or method definition."""
	name_node = _child_by_field(node, "name")
	name = _text(name_node) if name_node else "(anonymous)"

	params_node = _child_by_field(node, "parameters")
	sig = _text(params_node) if params_node else "()"

	# Determine kind
	kind = SymbolKind.FUNCTION

	# Check for @property decorator
	check_node = decorator_node or node
	if _has_decorator(check_node, "property"):
		kind = SymbolKind.PROPERTY
	elif name == "__init__":
		kind = SymbolKind.CONSTRUCTOR
	elif parent_name is not None:
		# Nested inside a class body -> method
		kind = SymbolKind.METHOD

	# Use decorator_node for line range if present
	start_node = decorator_node or node

	# Top-level functions are implicitly exported in Python (anything not
	# prefixed with `_` is part of the module's public surface). Methods
	# inside a class don't need to be marked exported themselves -- their
	# enclosing class is.
	exported = parent_name is None and not name.startswith("_")

	result.add_symbol(
		name=name,
		qualified_name=_qual(parent_name, name),
		kind=kind,
		start_line=start_node.start_point[0] + 1,
		end_line=node.end_point[0] + 1,
		start_column=start_node.start_point[1],
		end_column=node.end_point[1],
		signature=sig,
		exported=exported,
	)

	# Extract parameters as variable_defs
	if params_node:
		_extract_parameter_defs(params_node, result, _qual(parent_name, name))

	# Extract calls, variable defs, and return expressions in function body
	body = _child_by_field(node, "body")
	if body:
		fn_qn = _qual(parent_name, name)
		_extract_calls_in_body(body, result, fn_qn)
		_extract_variable_defs_in_body(body, result, fn_qn)
		_extract_return_expressions_in_body(body, result, fn_qn)
		# Also extract nested functions and classes
		_extract_nested_definitions(body, result, fn_qn)


def _extract_nested_definitions(
	body: Node,
	result: ExtractionResult,
	parent_name: Optional[str],
) -> None:
	"""Extract nested function and class definitions from a body block."""
	for child in body.children:
		if child.type == "function_definition":
			_extract_function(child, result, parent_name)
		elif child.type == "class_definition":
			_extract_class(child, result, parent_name)
		elif child.type == "decorated_definition":
			inner = _first_child_by_type(child, "function_definition", "class_definition")
			if inner:
				if inner.type == "function_definition":
					_extract_function(inner, result, parent_name, decorator_node=child)
				elif inner.type == "class_definition":
					_extract_class(inner, result, parent_name)


# ---------------------------------------------------------------------------
# Class extraction
# ---------------------------------------------------------------------------

def _extract_class(
	node: Node,
	result: ExtractionResult,
	parent_name: Optional[str],
) -> None:
	name_node = _child_by_field(node, "name")
	name = _text(name_node) if name_node else "(anonymous)"
	qual = _qual(parent_name, name)

	# Top-level classes are implicitly part of the module's public surface
	# in Python (anything not prefixed with `_`).
	exported = parent_name is None and not name.startswith("_")

	result.add_symbol(
		name=name,
		qualified_name=qual,
		kind=SymbolKind.CLASS,
		start_line=node.start_point[0] + 1,
		end_line=node.end_point[0] + 1,
		start_column=node.start_point[1],
		end_column=node.end_point[1],
		exported=exported,
	)

	# Heritage: class Foo(Bar, Baz, metaclass=Meta)
	_extract_class_heritage(node, result, name)

	# Class body members
	body = _child_by_field(node, "body")
	if body:
		_extract_class_body(body, result, qual)


def _extract_class_heritage(node: Node, result: ExtractionResult, class_name: str) -> None:
	"""Extract base classes from the argument_list of a class definition."""
	arg_list = _first_child_by_type(node, "argument_list")
	if not arg_list:
		return

	for child in arg_list.children:
		if child.type == "identifier":
			parent = _text(child)
			if parent:
				result.add_heritage(
					symbol_name=class_name,
					parent_name=parent,
					kind="extends",
					line=child.start_point[0] + 1,
				)
		elif child.type == "attribute":
			# e.g. module.ClassName
			parent = _text(child)
			if parent:
				result.add_heritage(
					symbol_name=class_name,
					parent_name=parent,
					kind="extends",
					line=child.start_point[0] + 1,
				)
		elif child.type == "call":
			# e.g. Generic[T] parsed as a call-like subscript -- skip
			# But also handles metaclass=... which we skip
			pass
		elif child.type == "keyword_argument":
			# e.g. metaclass=ABCMeta -- skip keyword args
			pass
		elif child.type == "subscript":
			# e.g. Generic[T], Protocol[T]
			value_node = _child_by_field(child, "value")
			if value_node:
				parent = _text(value_node)
				if parent:
					result.add_heritage(
						symbol_name=class_name,
						parent_name=parent,
						kind="extends",
						line=child.start_point[0] + 1,
					)


def _extract_class_body(body: Node, result: ExtractionResult, class_qual: str) -> None:
	"""Extract members from a class body."""
	for child in body.children:
		ctype = child.type

		if ctype == "function_definition":
			_extract_function(child, result, class_qual)
		elif ctype == "decorated_definition":
			inner = _first_child_by_type(child, "function_definition", "class_definition")
			if inner:
				if inner.type == "function_definition":
					_extract_function(inner, result, class_qual, decorator_node=child)
				elif inner.type == "class_definition":
					_extract_class(inner, result, class_qual)
		elif ctype == "class_definition":
			_extract_class(child, result, class_qual)
		elif ctype == "expression_statement":
			# Class-level variable assignments: x: int = 5, or x = 5
			expr = child.children[0] if child.children else None
			if expr:
				if expr.type == "assignment":
					_extract_class_assignment(expr, result, class_qual)
				elif expr.type == "type":
					# Type annotation without assignment: x: int
					_extract_type_annotation(expr, result, class_qual, child)
				elif expr.type == "call":
					_extract_call(expr, result, class_qual)


def _extract_class_assignment(node: Node, result: ExtractionResult, class_qual: str) -> None:
	"""Extract a class-level variable assignment as a PROPERTY."""
	left = _child_by_field(node, "left")
	if not left:
		return

	if left.type == "identifier":
		name = _text(left)
		result.add_symbol(
			name=name,
			qualified_name=_qual(class_qual, name),
			kind=SymbolKind.PROPERTY,
			start_line=node.start_point[0] + 1,
			end_line=node.end_point[0] + 1,
			start_column=node.start_point[1],
			end_column=node.end_point[1],
		)
	elif left.type == "type":
		# Annotated assignment: x: int = 5
		ident = _first_child_by_type(left, "identifier")
		if ident:
			name = _text(ident)
			result.add_symbol(
				name=name,
				qualified_name=_qual(class_qual, name),
				kind=SymbolKind.PROPERTY,
				start_line=node.start_point[0] + 1,
				end_line=node.end_point[0] + 1,
				start_column=node.start_point[1],
				end_column=node.end_point[1],
			)

	# Extract calls on the right side
	right = _child_by_field(node, "right")
	if right:
		_extract_calls_in_body(right, result, class_qual)


def _extract_type_annotation(
	node: Node, result: ExtractionResult, class_qual: str, stmt: Node,
) -> None:
	"""Extract a bare type annotation (``x: int``) as a PROPERTY."""
	ident = _first_child_by_type(node, "identifier")
	if ident:
		name = _text(ident)
		result.add_symbol(
			name=name,
			qualified_name=_qual(class_qual, name),
			kind=SymbolKind.PROPERTY,
			start_line=stmt.start_point[0] + 1,
			end_line=stmt.end_point[0] + 1,
			start_column=stmt.start_point[1],
			end_column=stmt.end_point[1],
		)


# ---------------------------------------------------------------------------
# Assignment extraction (module level)
# ---------------------------------------------------------------------------

def _extract_assignment(node: Node, result: ExtractionResult, parent_name: Optional[str]) -> None:
	"""Extract module-level variable assignments."""
	left = _child_by_field(node, "left")
	if not left:
		return

	name: Optional[str] = None
	if left.type == "identifier":
		name = _text(left)
	elif left.type == "type":
		# Annotated assignment: x: int = 5
		ident = _first_child_by_type(left, "identifier")
		if ident:
			name = _text(ident)

	if name and not name.startswith("_") or (name and parent_name is None):
		# Module-level non-underscore variables are part of the public surface.
		exported = parent_name is None and not name.startswith("_")
		result.add_symbol(
			name=name,
			qualified_name=_qual(parent_name, name),
			kind=SymbolKind.VARIABLE,
			start_line=node.start_point[0] + 1,
			end_line=node.end_point[0] + 1,
			start_column=node.start_point[1],
			end_column=node.end_point[1],
			exported=exported,
		)

	# Extract calls on the right side
	right = _child_by_field(node, "right")
	if right:
		_extract_calls_in_body(right, result, parent_name)


# ---------------------------------------------------------------------------
# Call expression extraction
# ---------------------------------------------------------------------------

def _extract_calls_in_body(node: Node, result: ExtractionResult, caller_name: Optional[str]) -> None:
	"""Walk a body node and extract all call expressions."""
	for child in _walk(node):
		if child.type == "call":
			_extract_call(child, result, caller_name)


def _extract_call(node: Node, result: ExtractionResult, caller_name: Optional[str]) -> None:
	"""Extract a single call expression and its arguments."""
	func_node = _child_by_field(node, "function")
	if not func_node:
		return

	callee_name: str
	receiver: Optional[str] = None

	if func_node.type == "attribute":
		# obj.method() or module.func()
		obj = _child_by_field(func_node, "object")
		attr = _child_by_field(func_node, "attribute")
		if obj and attr:
			receiver = _text(obj)
			callee_name = _text(attr)
		else:
			callee_name = _text(func_node)
	elif func_node.type == "identifier":
		callee_name = _text(func_node)
	else:
		# Subscript calls like Generic[T]() or complex expressions
		callee_name = _text(func_node)

	if not callee_name:
		return

	call_index = len(result.calls)
	result.add_call(
		caller_symbol=caller_name,
		callee_name=callee_name,
		receiver=receiver,
		line=node.start_point[0] + 1,
		column=node.start_point[1],
	)

	# Extract arguments
	args_node = _child_by_field(node, "arguments")
	if args_node:
		_extract_call_arguments(args_node, result, call_index)


# ---------------------------------------------------------------------------
# Variable definition extraction (data-flow support)
# ---------------------------------------------------------------------------

def _extract_parameter_defs(params_node: Node, result: ExtractionResult, scope_qn: str) -> None:
	"""Extract function parameters as variable_defs with kind='parameter'."""
	arg_index = 0
	for child in params_node.children:
		if child.type in ("identifier", "typed_parameter", "default_parameter",
						  "typed_default_parameter", "list_splat_pattern",
						  "dictionary_splat_pattern"):
			param_name: Optional[str] = None
			if child.type == "identifier":
				param_name = _text(child)
			elif child.type in ("typed_parameter", "default_parameter", "typed_default_parameter"):
				name_node = _first_child_by_type(child, "identifier")
				if name_node:
					param_name = _text(name_node)
			elif child.type in ("list_splat_pattern", "dictionary_splat_pattern"):
				ident = _first_child_by_type(child, "identifier")
				if ident:
					param_name = _text(ident)

			if param_name and param_name != "self" and param_name != "cls":
				result.variable_defs.append(RawVariableDef(
					file_path=result.file_path,
					scope_symbol=scope_qn,
					variable_name=param_name,
					kind="parameter",
					source_type="parameter",
					line=child.start_point[0] + 1,
					col=child.start_point[1],
				))
				arg_index += 1


def _extract_variable_defs_in_body(body: Node, result: ExtractionResult, scope_qn: str) -> None:
	"""Walk a function body and extract variable definitions with source info."""
	for child in body.children:
		if child.type == "expression_statement":
			expr = child.children[0] if child.children else None
			if expr and expr.type == "assignment":
				_extract_vardef_assignment(expr, result, scope_qn)
		elif child.type in ("if_statement", "for_statement", "while_statement",
							"with_statement", "try_statement"):
			# Recurse into compound statement bodies
			for sub in child.children:
				if sub.type in ("block", "else_clause", "elif_clause",
								"except_clause", "finally_clause"):
					_extract_variable_defs_in_body(sub, result, scope_qn)


def _extract_vardef_assignment(node: Node, result: ExtractionResult, scope_qn: str) -> None:
	"""Extract a variable assignment as a RawVariableDef."""
	left = _child_by_field(node, "left")
	right = _child_by_field(node, "right")
	if not left or not right:
		return

	if left.type == "identifier":
		var_name = _text(left)
		_record_vardef(var_name, right, result, scope_qn, node)
	elif left.type == "type":
		# Annotated: x: int = ...
		ident = _first_child_by_type(left, "identifier")
		if ident:
			var_name = _text(ident)
			_record_vardef(var_name, right, result, scope_qn, node)
	elif left.type == "pattern_list" or left.type == "tuple_pattern":
		# Destructuring: a, b = ...
		for item in left.children:
			if item.type == "identifier":
				var_name = _text(item)
				result.variable_defs.append(RawVariableDef(
					file_path=result.file_path,
					scope_symbol=scope_qn,
					variable_name=var_name,
					kind="destructuring",
					source_type=_classify_rhs_type(right),
					source_property=_text(right) if right.type == "attribute" else None,
					line=node.start_point[0] + 1,
					col=node.start_point[1],
				))


def _record_vardef(var_name: str, rhs: Node, result: ExtractionResult, scope_qn: str, node: Node) -> None:
	"""Record a single variable definition with classified RHS."""
	source_type = _classify_rhs_type(rhs)
	source_call: Optional[RawCall] = None
	source_variable: Optional[str] = None
	source_property: Optional[str] = None

	if source_type == "call_result":
		# Build a RawCall for the source
		func_node = _child_by_field(rhs, "function")
		if func_node:
			if func_node.type == "attribute":
				obj = _child_by_field(func_node, "object")
				attr = _child_by_field(func_node, "attribute")
				if obj and attr:
					source_call = RawCall(
						file_path=result.file_path,
						caller_symbol=scope_qn,
						callee_name=_text(attr),
						receiver=_text(obj),
						line=rhs.start_point[0] + 1,
						column=rhs.start_point[1],
					)
				else:
					source_call = RawCall(
						file_path=result.file_path,
						caller_symbol=scope_qn,
						callee_name=_text(func_node),
						line=rhs.start_point[0] + 1,
						column=rhs.start_point[1],
					)
			else:
				source_call = RawCall(
					file_path=result.file_path,
					caller_symbol=scope_qn,
					callee_name=_text(func_node),
					line=rhs.start_point[0] + 1,
					column=rhs.start_point[1],
				)
	elif source_type == "identifier":
		source_variable = _text(rhs)
	elif source_type == "property_access":
		source_property = _text(rhs)
		# For subscripts like body['name'], also capture the base as source_variable
		if rhs.type == "subscript":
			value_node = _child_by_field(rhs, "value")
			if value_node:
				source_variable = _text(value_node)

	result.variable_defs.append(RawVariableDef(
		file_path=result.file_path,
		scope_symbol=scope_qn,
		variable_name=var_name,
		kind="assignment",
		source_type=source_type,
		source_call=source_call,
		source_variable=source_variable,
		source_property=source_property,
		line=node.start_point[0] + 1,
		col=node.start_point[1],
	))


def _classify_rhs_type(node: Node) -> str:
	"""Classify the RHS expression type for data-flow tracking."""
	if node.type == "call":
		return "call_result"
	elif node.type == "identifier":
		return "identifier"
	elif node.type == "attribute":
		return "property_access"
	elif node.type == "subscript":
		# e.g., body['name'], request.args['key'] -- treat as property access
		return "property_access"
	elif node.type in ("string", "integer", "float", "true", "false", "none",
						"concatenated_string", "formatted_string"):
		return "literal"
	elif node.type == "await":
		# await expr -- classify the inner expression
		inner = node.children[-1] if node.children else None
		if inner:
			return _classify_rhs_type(inner)
	return "expression"


# ---------------------------------------------------------------------------
# Call argument extraction
# ---------------------------------------------------------------------------

def _extract_call_arguments(args_node: Node, result: ExtractionResult, call_index: int) -> None:
	"""Extract arguments from a call's argument_list node."""
	arg_idx = 0
	for child in args_node.children:
		if child.type in ("(", ")", ","):
			continue
		if child.type == "keyword_argument":
			# keyword arg: key=value -- extract the value part
			value = _child_by_field(child, "value")
			if value:
				_record_call_argument(value, result, call_index, arg_idx)
			arg_idx += 1
		elif child.type in ("list_splat", "dictionary_splat"):
			# *args or **kwargs -- extract the inner expression
			inner = child.children[-1] if child.children else None
			if inner:
				_record_call_argument(inner, result, call_index, arg_idx)
			arg_idx += 1
		else:
			_record_call_argument(child, result, call_index, arg_idx)
			arg_idx += 1


def _record_call_argument(node: Node, result: ExtractionResult, call_index: int, arg_idx: int) -> None:
	"""Classify and record a single call argument."""
	kind = _classify_rhs_type(node)
	variable_name: Optional[str] = None
	literal_value: Optional[str] = None
	property_chain: Optional[str] = None
	nested_call: Optional[RawCall] = None

	if kind == "identifier":
		variable_name = _text(node)
	elif kind == "literal":
		literal_value = _text(node)
	elif kind == "property_access":
		property_chain = _text(node)
		# Also record the base as variable_name
		obj = _child_by_field(node, "object")
		if obj:
			variable_name = _text(obj)
	elif kind == "call_result":
		func_node = _child_by_field(node, "function")
		if func_node:
			if func_node.type == "attribute":
				obj = _child_by_field(func_node, "object")
				attr = _child_by_field(func_node, "attribute")
				nested_call = RawCall(
					file_path=result.file_path,
					callee_name=_text(attr) if attr else _text(func_node),
					receiver=_text(obj) if obj else None,
					line=node.start_point[0] + 1,
					column=node.start_point[1],
				)
			else:
				nested_call = RawCall(
					file_path=result.file_path,
					callee_name=_text(func_node),
					line=node.start_point[0] + 1,
					column=node.start_point[1],
				)

	result.call_arguments.append(RawCallArgument(
		file_path=result.file_path,
		call_index=call_index,
		argument_index=arg_idx,
		kind=kind,
		variable_name=variable_name,
		literal_value=literal_value,
		property_chain=property_chain,
		nested_call=nested_call,
		line=node.start_point[0] + 1,
	))


# ---------------------------------------------------------------------------
# Return expression extraction
# ---------------------------------------------------------------------------

def _extract_return_expressions_in_body(body: Node, result: ExtractionResult, scope_qn: str) -> None:
	"""Walk a function body and extract all return expressions."""
	for child in _walk(body):
		if child.type == "return_statement":
			# Children: "return" [expression]
			expr = None
			for sub in child.children:
				if sub.type != "return":
					expr = sub
					break
			if expr:
				kind = _classify_rhs_type(expr)
				variable_name: Optional[str] = None
				property_chain: Optional[str] = None
				call: Optional[RawCall] = None

				if kind == "identifier":
					variable_name = _text(expr)
				elif kind == "property_access":
					property_chain = _text(expr)
				elif kind == "call_result":
					func_node = _child_by_field(expr, "function")
					if func_node:
						if func_node.type == "attribute":
							obj = _child_by_field(func_node, "object")
							attr = _child_by_field(func_node, "attribute")
							call = RawCall(
								file_path=result.file_path,
								caller_symbol=scope_qn,
								callee_name=_text(attr) if attr else _text(func_node),
								receiver=_text(obj) if obj else None,
								line=expr.start_point[0] + 1,
								column=expr.start_point[1],
							)
						else:
							call = RawCall(
								file_path=result.file_path,
								caller_symbol=scope_qn,
								callee_name=_text(func_node),
								line=expr.start_point[0] + 1,
								column=expr.start_point[1],
							)

				result.return_expressions.append(RawReturnExpression(
					file_path=result.file_path,
					scope_symbol=scope_qn,
					kind=kind,
					variable_name=variable_name,
					property_chain=property_chain,
					call=call,
					line=child.start_point[0] + 1,
				))


# ---------------------------------------------------------------------------
# LanguageProfile implementation for Python
# ---------------------------------------------------------------------------

class PythonProfile:
	"""LanguageProfile implementation for Python."""

	@property
	def language_id(self) -> str:
		return "python"

	@property
	def extensions(self) -> frozenset[str]:
		return frozenset({".py", ".pyi"})

	def get_parser(self):
		return _get_parser()

	def extract_file(self, file_path: str, source: bytes) -> ExtractionResult:
		return extract_file(file_path, source)

	def language_for_file(self, file_path: str) -> str:
		return "python"

	def resolve_import_path(
		self,
		from_file: str,
		imported_path: str,
		store: object,
		workspace_root: str,
	) -> Optional[str]:
		"""Resolve a Python import path to a workspace-relative file path.

		Handles:
		  - Relative imports (leading dots)
		  - Absolute imports (dotted module paths)
		  - ``pyproject.toml`` source roots (optional)
		"""
		# Load source roots (cached on store)
		src_roots = self._get_source_roots(store, workspace_root)

		# Relative import
		if imported_path.startswith("."):
			return self._resolve_relative(from_file, imported_path, store)

		# Absolute import -- try each source root
		module_parts = imported_path.split(".")
		for root in src_roots:
			base = os.path.join(root, *module_parts) if root else os.path.join(*module_parts)
			found = self.find_file_candidates(base, store)
			if found:
				return found

		# Fallback: try without source root prefix
		base = os.path.join(*module_parts)
		return self.find_file_candidates(base, store)

	def find_file_candidates(self, base_path: str, store: object) -> Optional[str]:
		"""Try to find a file with Python extensions and __init__.py patterns."""
		# Direct match
		if hasattr(store, "get_file") and store.get_file(base_path):  # type: ignore[union-attr]
			return base_path

		for ext in (".py", ".pyi"):
			candidate = base_path + ext
			if hasattr(store, "get_file") and store.get_file(candidate):  # type: ignore[union-attr]
				return candidate

		# Package: base_path/__init__.py
		for ext in (".py", ".pyi"):
			candidate = os.path.join(base_path, f"__init__{ext}")
			if hasattr(store, "get_file") and store.get_file(candidate):  # type: ignore[union-attr]
				return candidate

		# __init__.py (without extension duplication)
		init_candidate = os.path.join(base_path, "__init__.py")
		if hasattr(store, "get_file") and store.get_file(init_candidate):  # type: ignore[union-attr]
			return init_candidate

		return None

	def load_config(self, workspace_root: str) -> None:
		pass  # Source roots loaded lazily during resolution

	# -- private helpers ---

	def _resolve_relative(
		self, from_file: str, imported_path: str, store: object,
	) -> Optional[str]:
		"""Resolve a relative import like ``from . import x`` or ``from ..pkg import y``."""
		# Count leading dots
		dot_count = 0
		for ch in imported_path:
			if ch == ".":
				dot_count += 1
			else:
				break
		remainder = imported_path[dot_count:]

		# Start from the directory of the importing file
		dir_of_importer = os.path.dirname(from_file)

		# Go up (dot_count - 1) levels: one dot = current package, two dots = parent, etc.
		for _ in range(dot_count - 1):
			dir_of_importer = os.path.dirname(dir_of_importer)

		if remainder:
			parts = remainder.split(".")
			base = os.path.join(dir_of_importer, *parts)
		else:
			base = dir_of_importer

		return self.find_file_candidates(base, store)

	def _get_source_roots(self, store: object, workspace_root: str) -> list[str]:
		"""Read source roots from pyproject.toml (cached on *store*)."""
		cached = getattr(store, "_py_src_roots", None)
		if cached is not None:
			return cached

		src_roots: list[str] = [""]  # empty string = workspace root

		pyproject_path = os.path.join(workspace_root, "pyproject.toml")
		if os.path.exists(pyproject_path):
			try:
				import tomllib
			except ModuleNotFoundError:
				try:
					import tomli as tomllib  # type: ignore[no-redef]
				except ModuleNotFoundError:
					tomllib = None  # type: ignore[assignment]

			if tomllib is not None:
				try:
					with open(pyproject_path, "rb") as f:
						data = tomllib.load(f)
					# setuptools style
					packages_dir = (
						data.get("tool", {})
						.get("setuptools", {})
						.get("package-dir", {})
					)
					if isinstance(packages_dir, dict):
						for _key, val in packages_dir.items():
							if val and val not in src_roots:
								src_roots.append(os.path.normpath(val))

					# hatchling / src layout
					sources = (
						data.get("tool", {})
						.get("hatch", {})
						.get("build", {})
						.get("sources", [])
					)
					if isinstance(sources, list):
						for s in sources:
							if s and s not in src_roots:
								src_roots.append(os.path.normpath(s))

					# Common src/ convention
					if "src" not in src_roots and os.path.isdir(
						os.path.join(workspace_root, "src")
					):
						src_roots.append("src")

				except (OSError, ValueError):
					pass

		try:
			store._py_src_roots = src_roots  # type: ignore[attr-defined]
		except AttributeError:
			pass  # store might be frozen

		return src_roots


# ---------------------------------------------------------------------------
# Auto-register on import
# ---------------------------------------------------------------------------

def _register() -> None:
	from dsc_api.services.code_intel.language_profile import LanguageRegistry
	LanguageRegistry.register(PythonProfile())


_register()
