"""JavaScript and TypeScript parser — tree-sitter primary (accurate end_line), regex fallback."""

from __future__ import annotations

import re

from codectx.parser.types import DependencyRecord, EntityRecord, ParseResult, RelationRecord

# ── regex patterns (fallback) ────────────────────────────────────────────────
RE_IMPORT   = re.compile(r"^\s*import\s+.*?from\s+['\"]([^'\"]+)['\"]", re.MULTILINE)
RE_REQUIRE  = re.compile(r"require\s*\(\s*['\"]([^'\"]+)['\"]\s*\)")
RE_CLASS    = re.compile(r"\bclass\s+([A-Za-z_][\w$]*)")
RE_FUNC     = re.compile(r"\bfunction\s+([A-Za-z_][\w$]*)\s*\(")
RE_ARROW    = re.compile(r"\b(?:const|let|var)\s+([A-Za-z_][\w$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>")
RE_METHOD   = re.compile(r"^\s*([A-Za-z_][\w$]*)\s*\([^)]*\)\s*\{", re.MULTILINE)
RE_CALL     = re.compile(r"(?<!function\s)(?<!class\s)\b(?:[A-Za-z_][\w$]*\.)?([A-Za-z_][\w$]*)\s*\(")
JS_KEYWORDS = {"if", "for", "while", "switch", "catch", "new", "return", "typeof", "await", "super"}


# ── tree-sitter implementation ───────────────────────────────────────────────

def _ts_lang_name(file_path: str, language: str) -> str:
    if file_path.endswith(".tsx"):
        return "tsx"
    if language == "typescript":
        return "typescript"
    return "javascript"


def _ts_parse_js(file_path: str, content: str, src: bytes, language: str) -> "ParseResult | None":
    try:
        from codectx.parser.ts_util import get_parser, node_text as nt, start_line as sl, end_line as el, walk_type
    except ImportError:
        return None

    ts_lang = _ts_lang_name(file_path, language)
    parser, _ = get_parser(ts_lang)
    if parser is None:
        return None
    try:
        tree = parser.parse(src)
    except Exception:
        return None

    root = tree.root_node
    result = ParseResult(file_path=file_path, language=language)

    # imports
    for node in walk_type(root, "import_statement"):
        source_node = node.child_by_field_name("source")
        if source_node:
            path = nt(source_node, src).strip("\"'`")
            result.dependencies.append(DependencyRecord(path=path, type="import", line_number=sl(node)))

    # require()
    for node in walk_type(root, "call_expression"):
        fn_node = node.child_by_field_name("function")
        if fn_node and fn_node.type == "identifier" and nt(fn_node, src) == "require":
            args = node.child_by_field_name("arguments")
            if args:
                for arg in args.children:
                    if arg.type in ("string", "template_string"):
                        result.dependencies.append(DependencyRecord(
                            path=nt(arg, src).strip("\"'`"), type="require", line_number=sl(node)
                        ))
                        break

    # classes + their methods
    class_entity_map: dict[str, object] = {}
    for node in walk_type(root, "class_declaration", "class"):
        name_node = node.child_by_field_name("name")
        if name_node is None:
            continue
        cname = nt(name_node, src)
        class_entity_map[cname] = node
        result.entities.append(EntityRecord(
            name=cname, qualified_name=cname, type="class", kind="class",
            start_line=sl(node), end_line=el(node), language=language, scope="global",
        ))

    func_scope_nodes: list[tuple[str, object]] = []  # (qualified_name, ts_node)

    for cname, class_node in class_entity_map.items():
        body_node = class_node.child_by_field_name("body")
        if body_node is None:
            continue
        for method_node in walk_type(body_node, "method_definition", "method_signature"):
            name_node = method_node.child_by_field_name("name")
            if name_node is None:
                continue
            mname = nt(name_node, src)
            etype = "constructor" if mname == "constructor" else "method"
            qname = f"{cname}.{mname}"
            entity = EntityRecord(
                name=mname, qualified_name=qname, type=etype, kind=etype,
                start_line=sl(method_node), end_line=el(method_node),
                language=language, parent=cname, scope="class",
            )
            result.entities.append(entity)
            result.relations.append(RelationRecord(
                source_qualified_name=cname, target_qualified_name=qname,
                relation_type="contains", line_number=sl(method_node),
            ))
            func_scope_nodes.append((qname, method_node))

    # top-level function declarations
    for node in walk_type(root, "function_declaration"):
        name_node = node.child_by_field_name("name")
        if name_node is None:
            continue
        fname = nt(name_node, src)
        result.entities.append(EntityRecord(
            name=fname, qualified_name=fname, type="function", kind="function",
            start_line=sl(node), end_line=el(node), language=language, scope="global",
        ))
        func_scope_nodes.append((fname, node))

    # arrow functions and function expressions: const fn = () => {}
    for node in walk_type(root, "lexical_declaration", "variable_declaration"):
        for decl in walk_type(node, "variable_declarator"):
            name_node = decl.child_by_field_name("name")
            val_node  = decl.child_by_field_name("value")
            if name_node is None or val_node is None:
                continue
            if val_node.type in ("arrow_function", "function"):
                fname = nt(name_node, src)
                kind = "arrow_function" if val_node.type == "arrow_function" else "function"
                result.entities.append(EntityRecord(
                    name=fname, qualified_name=fname, type="function", kind=kind,
                    start_line=sl(decl), end_line=el(val_node),
                    language=language, scope="global",
                ))
                func_scope_nodes.append((fname, val_node))

    # TypeScript-specific entity types
    if language == "typescript":
        for node in walk_type(root, "interface_declaration"):
            name_node = node.child_by_field_name("name")
            if name_node:
                name = nt(name_node, src)
                result.entities.append(EntityRecord(
                    name=name, qualified_name=name, type="interface", kind="interface",
                    start_line=sl(node), end_line=el(node), language=language, scope="global",
                ))
        for node in walk_type(root, "type_alias_declaration"):
            name_node = node.child_by_field_name("name")
            if name_node:
                name = nt(name_node, src)
                result.entities.append(EntityRecord(
                    name=name, qualified_name=name, type="type_alias", kind="type_alias",
                    start_line=sl(node), end_line=el(node), language=language, scope="global",
                ))
        for node in walk_type(root, "enum_declaration"):
            name_node = node.child_by_field_name("name")
            if name_node:
                name = nt(name_node, src)
                result.entities.append(EntityRecord(
                    name=name, qualified_name=name, type="enum", kind="enum",
                    start_line=sl(node), end_line=el(node), language=language, scope="global",
                ))

    # call relations
    for qname, fn_node in func_scope_nodes:
        body = fn_node.child_by_field_name("body") or fn_node
        for call in walk_type(body, "call_expression"):
            fn_field = call.child_by_field_name("function")
            if fn_field is None:
                continue
            if fn_field.type == "identifier":
                callee = nt(fn_field, src)
            elif fn_field.type == "member_expression":
                prop = fn_field.child_by_field_name("property")
                callee = nt(prop, src) if prop else ""
            else:
                continue
            if callee and callee not in JS_KEYWORDS:
                result.relations.append(RelationRecord(
                    source_qualified_name=qname, target_qualified_name=callee,
                    relation_type="calls",
                    line_number=src[:call.start_byte].count(b"\n") + 1,
                    context=callee,
                ))

    return result


# ── regex fallback (original implementation) ─────────────────────────────────

def _regex_parse_js(file_path: str, content: str) -> ParseResult:
    language = "typescript" if file_path.endswith((".ts", ".tsx")) else "javascript"
    entities: list[EntityRecord] = []
    dependencies: list[DependencyRecord] = []
    relations: list[RelationRecord] = []

    for match in RE_IMPORT.finditer(content):
        dependencies.append(DependencyRecord(path=match.group(1), type="import", line_number=_line(content, match.start())))
    for match in RE_REQUIRE.finditer(content):
        dependencies.append(DependencyRecord(path=match.group(1), type="require", line_number=_line(content, match.start())))

    class_names: list[str] = []
    for match in RE_CLASS.finditer(content):
        name = match.group(1)
        class_names.append(name)
        line_number = _line(content, match.start())
        entities.append(EntityRecord(name=name, qualified_name=name, type="class", kind="class", start_line=line_number, end_line=line_number, language=language, scope="global"))

    for match in RE_FUNC.finditer(content):
        name = match.group(1)
        line_number = _line(content, match.start())
        entities.append(EntityRecord(name=name, qualified_name=name, type="function", kind="function", start_line=line_number, end_line=line_number, language=language, scope="global"))

    for match in RE_ARROW.finditer(content):
        name = match.group(1)
        line_number = _line(content, match.start())
        entities.append(EntityRecord(name=name, qualified_name=name, type="function", kind="arrow_function", start_line=line_number, end_line=line_number, language=language, scope="global"))

    if class_names:
        primary_class = class_names[0]
        for match in RE_METHOD.finditer(content):
            name = match.group(1)
            if name in {"constructor"}:
                continue
            line_number = _line(content, match.start())
            qualified = f"{primary_class}.{name}"
            entities.append(EntityRecord(name=name, qualified_name=qualified, type="method", kind="method", start_line=line_number, end_line=line_number, language=language, parent=primary_class, scope="class"))
            relations.append(RelationRecord(source_qualified_name=primary_class, target_qualified_name=qualified, relation_type="contains", line_number=line_number))

    callers = [entity for entity in entities if entity.type in {"function", "method"}]
    for caller in callers:
        body = _entity_body(content, caller.start_line)
        for call in RE_CALL.finditer(body):
            target = call.group(1)
            if target in JS_KEYWORDS or target == caller.name:
                continue
            relations.append(RelationRecord(source_qualified_name=caller.qualified_name or caller.name, target_qualified_name=target, relation_type="calls", line_number=caller.start_line + body.count("\n", 0, call.start()), context=target))

    return ParseResult(file_path=file_path, language=language, entities=entities, dependencies=dependencies, relations=relations)


# ── public entry point ────────────────────────────────────────────────────────

def parse_javascript(file_path: str, content: str) -> ParseResult:
    language = "typescript" if file_path.endswith((".ts", ".tsx")) else "javascript"
    src = content.encode("utf-8")
    result = _ts_parse_js(file_path, content, src, language)
    if result is not None:
        return result
    return _regex_parse_js(file_path, content)


def _line(content: str, offset: int) -> int:
    return content.count("\n", 0, offset) + 1


def _entity_body(content: str, start_line: int, window: int = 80) -> str:
    lines = content.splitlines()
    start = max(0, start_line - 1)
    return "\n".join(lines[start:min(len(lines), start + window)])



def _line(content: str, offset: int) -> int:
    return content.count("\n", 0, offset) + 1


def _entity_body(content: str, start_line: int, window: int = 80) -> str:
    lines = content.splitlines()
    start = max(0, start_line - 1)
    end = min(len(lines), start + window)
    return "\n".join(lines[start:end])
