"""Generic AST symbol extractor using tree-sitter.

Walks the AST for any supported language and extracts Symbol objects
with byte offsets for O(1) source retrieval. Per-file error handling
ensures a single broken file never kills the entire indexing run.
"""

import logging
from collections import Counter
from typing import Optional

from tree_sitter_language_pack import get_parser

from .symbols import Symbol, make_symbol_id, compute_content_hash
from .languages import LanguageSpec, LANGUAGE_REGISTRY

log = logging.getLogger(__name__)


def parse_file(
    content: str,
    filename: str,
    language: str,
) -> list[Symbol]:
    """Parse source code and extract symbols.

    Args:
        content: Raw source code string.
        filename: Relative file path (for symbol IDs).
        language: Language key (must be in LANGUAGE_REGISTRY).

    Returns:
        List of extracted Symbol objects. Empty list on error.
    """
    if language not in LANGUAGE_REGISTRY:
        return []

    spec = LANGUAGE_REGISTRY[language]
    source_bytes = content.encode('utf-8')

    try:
        parser = get_parser(spec.ts_language)
        tree = parser.parse(source_bytes)
    except Exception:
        log.warning('tree-sitter parse failed for %s', filename, exc_info=True)
        return []

    symbols: list[Symbol] = []
    _walk_tree(tree.root_node, spec, source_bytes, filename, language, symbols, None)
    symbols = _disambiguate_overloads(symbols)
    return symbols


def _walk_tree(
    node,
    spec: LanguageSpec,
    source_bytes: bytes,
    filename: str,
    language: str,
    symbols: list[Symbol],
    parent_symbol: Optional[Symbol] = None,
) -> None:
    """Recursively walk the AST and extract symbols."""
    if node.type in spec.symbol_node_types:
        symbol = _extract_symbol(
            node, spec, source_bytes, filename, language, parent_symbol,
        )
        if symbol:
            symbols.append(symbol)
            parent_symbol = symbol

    if node.type in spec.constant_patterns and parent_symbol is None:
        const = _extract_constant(node, spec, source_bytes, filename, language)
        if const:
            symbols.append(const)

    for child in node.children:
        _walk_tree(
            child, spec, source_bytes, filename, language, symbols, parent_symbol,
        )


def _extract_symbol(
    node,
    spec: LanguageSpec,
    source_bytes: bytes,
    filename: str,
    language: str,
    parent_symbol: Optional[Symbol] = None,
) -> Optional[Symbol]:
    """Extract a single Symbol from an AST node."""
    kind = spec.symbol_node_types[node.type]

    if node.has_error:
        return None

    name = _extract_name(node, spec, source_bytes)
    if not name:
        return None

    if parent_symbol:
        qualified_name = f'{parent_symbol.name}.{name}'
        if kind == 'function':
            kind = 'method'
    else:
        qualified_name = name

    signature = _build_signature(node, source_bytes)
    docstring = _extract_docstring(node, spec, source_bytes)
    decorators = _extract_decorators(node, spec, source_bytes)

    symbol_bytes = source_bytes[node.start_byte:node.end_byte]

    return Symbol(
        id=make_symbol_id(filename, qualified_name, kind),
        file=filename,
        name=name,
        qualified_name=qualified_name,
        kind=kind,
        language=language,
        signature=signature,
        docstring=docstring,
        decorators=decorators,
        parent=parent_symbol.id if parent_symbol else None,
        line=node.start_point[0] + 1,
        end_line=node.end_point[0] + 1,
        byte_offset=node.start_byte,
        byte_length=node.end_byte - node.start_byte,
        content_hash=compute_content_hash(symbol_bytes),
    )


def _extract_name(
    node,
    spec: LanguageSpec,
    source_bytes: bytes,
) -> Optional[str]:
    """Extract the symbol name from an AST node."""
    # Arrow functions don't carry their own name
    if node.type == 'arrow_function':
        return None

    # Go type_declaration: name lives inside type_spec child
    if node.type == 'type_declaration':
        for child in node.children:
            if child.type == 'type_spec':
                name_node = child.child_by_field_name('name')
                if name_node:
                    return _node_text(name_node, source_bytes)
        return None

    # C/C++ function_definition: declarator may be nested
    if node.type == 'function_definition' and spec.ts_language in ('c', 'cpp'):
        return _extract_c_function_name(node, source_bytes)

    if node.type not in spec.name_fields:
        return None

    field_name = spec.name_fields[node.type]
    name_node = node.child_by_field_name(field_name)
    if name_node:
        return _node_text(name_node, source_bytes)
    return None


def _extract_c_function_name(node, source_bytes: bytes) -> Optional[str]:
    """Extract function name from C/C++ function_definition.

    The declarator field may be a function_declarator wrapping an identifier,
    or a pointer_declarator wrapping a function_declarator, etc.
    """
    declarator = node.child_by_field_name('declarator')
    if not declarator:
        return None
    return _unwrap_declarator_name(declarator, source_bytes)


def _unwrap_declarator_name(node, source_bytes: bytes) -> Optional[str]:
    """Recursively unwrap C/C++ declarator to find the identifier."""
    if node.type == 'identifier':
        return _node_text(node, source_bytes)
    if node.type == 'qualified_identifier':
        return _node_text(node, source_bytes)
    if node.type == 'field_identifier':
        return _node_text(node, source_bytes)
    # Try the 'declarator' field (function_declarator, pointer_declarator)
    inner = node.child_by_field_name('declarator')
    if inner:
        return _unwrap_declarator_name(inner, source_bytes)
    # Try 'name' field
    name_node = node.child_by_field_name('name')
    if name_node:
        return _node_text(name_node, source_bytes)
    return None


def _build_signature(node, source_bytes: bytes) -> str:
    """Build a clean signature from node start to body start."""
    body = node.child_by_field_name('body')
    end_byte = body.start_byte if body else node.end_byte

    sig = source_bytes[node.start_byte:end_byte].decode('utf-8', errors='replace').strip()
    return sig.rstrip('{: \n\t')


def _extract_docstring(
    node,
    spec: LanguageSpec,
    source_bytes: bytes,
) -> str:
    """Extract docstring using language-specific strategy."""
    if spec.docstring_strategy == 'next_sibling_string':
        return _extract_python_docstring(node, source_bytes)
    if spec.docstring_strategy == 'preceding_comment':
        return _extract_preceding_comments(node, source_bytes)
    return ''


def _extract_python_docstring(node, source_bytes: bytes) -> str:
    """Extract Python docstring from the first expression in the body."""
    body = node.child_by_field_name('body')
    if not body or body.child_count == 0:
        return ''

    for child in body.children:
        if child.type == 'expression_statement':
            expr = child.child_by_field_name('expression')
            if expr and expr.type == 'string':
                return _strip_quotes(_node_text(expr, source_bytes))
            if child.child_count > 0:
                first = child.children[0]
                if first.type in ('string', 'concatenated_string'):
                    return _strip_quotes(_node_text(first, source_bytes))
        elif child.type == 'string':
            return _strip_quotes(_node_text(child, source_bytes))

    return ''


def _extract_preceding_comments(node, source_bytes: bytes) -> str:
    """Extract comments immediately preceding a node."""
    comments = []
    prev = node.prev_named_sibling
    while prev and prev.type in ('comment', 'line_comment', 'block_comment'):
        comments.insert(0, _node_text(prev, source_bytes))
        prev = prev.prev_named_sibling

    if not comments:
        return ''

    return _clean_comment_markers('\n'.join(comments))


def _clean_comment_markers(text: str) -> str:
    """Strip comment markers from docstrings."""
    lines = text.split('\n')
    cleaned = []
    for line in lines:
        line = line.strip()
        for prefix in ('/**', '/*', '///', '//!', '//', '*'):
            if line.startswith(prefix):
                line = line[len(prefix):]
                break
        if line.endswith('*/'):
            line = line[:-2]
        cleaned.append(line.strip())
    return '\n'.join(cleaned).strip()


def _extract_decorators(
    node,
    spec: LanguageSpec,
    source_bytes: bytes,
) -> list[str]:
    """Extract decorators/attributes preceding a node."""
    if not spec.decorator_node_type:
        return []

    decorators = []
    prev = node.prev_named_sibling
    while prev and prev.type == spec.decorator_node_type:
        decorators.insert(0, _node_text(prev, source_bytes).strip())
        prev = prev.prev_named_sibling
    return decorators


def _extract_constant(
    node,
    spec: LanguageSpec,
    source_bytes: bytes,
    filename: str,
    language: str,
) -> Optional[Symbol]:
    """Extract UPPER_CASE constants from top-level assignments."""
    if node.type == 'assignment':
        left = node.child_by_field_name('left')
        if left and left.type == 'identifier':
            name = _node_text(left, source_bytes)
            if name.isupper() or (len(name) > 1 and name[0].isupper() and '_' in name):
                sig = _node_text(node, source_bytes).strip()
                const_bytes = source_bytes[node.start_byte:node.end_byte]
                return Symbol(
                    id=make_symbol_id(filename, name, 'constant'),
                    file=filename,
                    name=name,
                    qualified_name=name,
                    kind='constant',
                    language=language,
                    signature=sig[:200],
                    line=node.start_point[0] + 1,
                    end_line=node.end_point[0] + 1,
                    byte_offset=node.start_byte,
                    byte_length=node.end_byte - node.start_byte,
                    content_hash=compute_content_hash(const_bytes),
                )
    return None


def _strip_quotes(text: str) -> str:
    """Strip surrounding quotes from a docstring."""
    text = text.strip()
    for q in ('"""', "'''", '"', "'"):
        if text.startswith(q) and text.endswith(q) and len(text) > len(q):
            return text[len(q):-len(q)].strip()
    return text


def _disambiguate_overloads(symbols: list[Symbol]) -> list[Symbol]:
    """Append ordinal suffixes to symbols with duplicate IDs."""
    counts = Counter(s.id for s in symbols)
    duped = {sid for sid, n in counts.items() if n > 1}
    if not duped:
        return symbols

    ordinals: dict[str, int] = {}
    result = []
    for sym in symbols:
        if sym.id in duped:
            ordinals[sym.id] = ordinals.get(sym.id, 0) + 1
            sym.id = f'{sym.id}~{ordinals[sym.id]}'
        result.append(sym)
    return result


def _node_text(node, source_bytes: bytes) -> str:
    """Get the text content of a tree-sitter node."""
    return source_bytes[node.start_byte:node.end_byte].decode('utf-8', errors='replace')
