"""Code ingestion — AST/regex-based splitting for Python, Bash, Go."""

import ast
import re


def chunk_code(text: str, language: str | None, cfg: dict) -> list[dict]:
    """Split code at function/class boundaries."""
    chunking_cfg = cfg.get("chunking", {}).get("code", {})
    strategy = chunking_cfg.get("strategy", "ast")
    include_context = chunking_cfg.get("include_context", True)

    if strategy == "fixed":
        return _fixed_chunk(text, chunking_cfg)

    if language == "python":
        chunks = _chunk_python(text, include_context)
    elif language in ("bash", "sh"):
        chunks = _chunk_bash(text, include_context)
    elif language == "go":
        chunks = _chunk_go(text, include_context)
    else:
        chunks = []

    if not chunks:
        return _fixed_chunk(text, chunking_cfg)

    for i, c in enumerate(chunks):
        c["chunk_index"] = i

    return chunks


def _chunk_python(text: str, include_context: bool) -> list[dict]:
    """Split Python using stdlib ast module."""
    try:
        tree = ast.parse(text)
    except SyntaxError:
        return []

    lines = text.splitlines(keepends=True)
    chunks = []

    for node in ast.iter_child_nodes(tree):
        if isinstance(node, ast.ClassDef):
            class_lines = _get_node_source(lines, node)
            class_docstring = ast.get_docstring(node) or ""

            # Each method becomes a chunk
            methods = [n for n in ast.iter_child_nodes(node) if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]

            if methods:
                for method in methods:
                    method_src = _get_node_source(lines, method)
                    if include_context and class_docstring:
                        context = f"class {node.name}:\n    \"\"\"{class_docstring}\"\"\"\n\n"
                        chunk_text = context + method_src
                    elif include_context:
                        chunk_text = f"class {node.name}:\n\n" + method_src
                    else:
                        chunk_text = method_src

                    chunks.append({
                        "text": chunk_text,
                        "metadata": {
                            "symbol_name": f"{node.name}.{method.name}",
                            "line_start": method.lineno,
                            "line_end": method.end_lineno,
                        },
                    })
            else:
                # Class with no methods — single chunk
                chunks.append({
                    "text": class_lines,
                    "metadata": {
                        "symbol_name": node.name,
                        "line_start": node.lineno,
                        "line_end": node.end_lineno,
                    },
                })

        elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            func_src = _get_node_source(lines, node)
            chunks.append({
                "text": func_src,
                "metadata": {
                    "symbol_name": node.name,
                    "line_start": node.lineno,
                    "line_end": node.end_lineno,
                },
            })

    return chunks


def _get_node_source(lines: list[str], node) -> str:
    """Extract source code for an AST node, including decorators."""
    start = node.lineno - 1
    # Include decorators
    if hasattr(node, "decorator_list") and node.decorator_list:
        start = node.decorator_list[0].lineno - 1
    end = node.end_lineno
    return "".join(lines[start:end]).rstrip()


def _chunk_bash(text: str, include_context: bool) -> list[dict]:
    """Split Bash at function boundaries using regex."""
    # Match: function name() {  or  name() {
    func_pattern = re.compile(
        r"^((?:#[^\n]*\n)*)?"  # Optional preceding comment block
        r"(?:function\s+(\w+)\s*\(\s*\)\s*\{|(\w+)\s*\(\s*\)\s*\{)",
        re.MULTILINE,
    )

    chunks = []
    matches = list(func_pattern.finditer(text))

    if not matches:
        return []

    for i, match in enumerate(matches):
        start = match.start()
        # Find end: next function or end of file
        if i + 1 < len(matches):
            end = matches[i + 1].start()
        else:
            end = len(text)

        func_name = match.group(2) or match.group(3)
        chunk_text = text[start:end].rstrip()

        chunks.append({
            "text": chunk_text,
            "metadata": {
                "symbol_name": func_name,
            },
        })

    return chunks


def _chunk_go(text: str, include_context: bool) -> list[dict]:
    """Split Go at func declarations using regex."""
    func_pattern = re.compile(
        r"^func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(",
        re.MULTILINE,
    )

    chunks = []
    matches = list(func_pattern.finditer(text))

    if not matches:
        return []

    for i, match in enumerate(matches):
        start = match.start()
        # Include preceding comment block
        before = text[:start]
        comment_lines = []
        for line in reversed(before.splitlines()):
            stripped = line.strip()
            if stripped.startswith("//") or not stripped:
                comment_lines.insert(0, line)
            else:
                break
        if comment_lines:
            comment_text = "\n".join(comment_lines).strip()
            if comment_text:
                start = text.rfind(comment_lines[0], 0, start)

        # Find end: next func or end of file
        if i + 1 < len(matches):
            end = matches[i + 1].start()
            # Backtrack to exclude preceding comments of next func
            before_next = text[:end]
            for line in reversed(before_next.splitlines()):
                stripped = line.strip()
                if stripped.startswith("//") or not stripped:
                    end = text.rfind(line, 0, end)
                else:
                    break
        else:
            end = len(text)

        func_name = match.group(1)
        chunk_text = text[start:end].rstrip()

        chunks.append({
            "text": chunk_text,
            "metadata": {
                "symbol_name": func_name,
            },
        })

    return chunks


def _fixed_chunk(text: str, chunking_cfg: dict) -> list[dict]:
    """Fixed-size fallback for code without recognisable boundaries."""
    max_tokens = chunking_cfg.get("max_tokens", 1024)
    overlap_tokens = chunking_cfg.get("overlap_tokens", 50)

    lines = text.splitlines()
    if not lines:
        return []

    # Approximate tokens as words
    chunks = []
    current_lines = []
    current_tokens = 0
    idx = 0

    for line in lines:
        line_tokens = len(line.split())
        if current_tokens + line_tokens > max_tokens and current_lines:
            chunks.append({
                "text": "\n".join(current_lines),
                "chunk_index": idx,
                "metadata": {},
            })
            idx += 1
            # Keep some overlap
            overlap_lines = []
            overlap_count = 0
            for l in reversed(current_lines):
                l_tokens = len(l.split())
                if overlap_count + l_tokens > overlap_tokens:
                    break
                overlap_lines.insert(0, l)
                overlap_count += l_tokens
            current_lines = overlap_lines
            current_tokens = overlap_count

        current_lines.append(line)
        current_tokens += line_tokens

    if current_lines:
        chunks.append({
            "text": "\n".join(current_lines),
            "chunk_index": idx,
            "metadata": {},
        })

    return chunks
