"""
GitMem Coding - Semantic Chunking Engine

Splits source code into semantic units (functions, classes, etc.) to enable
granular context retrieval and deduplication.
"""

import ast
import hashlib
import re
import os
from typing import List, Optional, Tuple, Any, Dict
from .models import CodeChunk, FileLanguage, TOKENS_PER_CHAR_RATIO

def detect_language(file_path: str) -> str:
    """Detect language from file extension."""
    ext = os.path.splitext(file_path)[1].lower()
    
    mapping = {
        ".py": "python",
        ".js": "javascript",
        ".ts": "typescript",
        ".java": "java",
        ".cpp": "cpp",
        ".c": "c",
        ".cs": "csharp",
        ".go": "go",
        ".rs": "rust",
        ".rb": "ruby",
        ".php": "php",
        ".swift": "swift",
        ".kt": "kotlin",
        ".html": "html",
        ".css": "css",
        ".sql": "sql",
        ".sh": "shell",
        ".yaml": "yaml",
        ".yml": "yaml",
        ".json": "json",
        ".toml": "toml",
        ".md": "markdown",
        ".xml": "xml"
    }
    
    return mapping.get(ext, "other")

class ChunkingEngine:
    """Base class for language-specific chunkers."""
    
    @staticmethod
    def get_chunker(language: str) -> 'ChunkingEngine':
        """Factory method to get appropriate chunker."""
        if language == FileLanguage.PYTHON:
            return PythonChunker()
        # Fallback for others
        return TextChunker()

    def chunk_file(self, content: str, file_path: str = "") -> List[CodeChunk]:
        """Split file content into CodeChunks."""
        raise NotImplementedError

    def _create_chunk(self, content: str, chunk_type: str, name: str, 
                      start_line: int, end_line: int, language: str, **kwargs) -> CodeChunk:
        """Helper to create a normalized CodeChunk."""
        
        # Add line numbers to the content for consistent output
        lines = content.splitlines()
        numbered_content = " ".join(f"line:{start_line + i} {line}" for i, line in enumerate(lines))
        
        # Normalize for consistent hashing (strip heavy whitespace)
        # We hash the original content to ensure stability
        normalized = re.sub(r'\s+', ' ', content).strip()
        hash_id = hashlib.sha256(normalized.encode('utf-8')).hexdigest()
        
        return CodeChunk(
            hash_id=hash_id,
            content=numbered_content,
            type=chunk_type,
            name=name,
            start_line=start_line,
            end_line=end_line,
            language=language,
            token_count=int(len(numbered_content) * TOKENS_PER_CHAR_RATIO),
            **kwargs
        )


class TextChunker(ChunkingEngine):
    """Fallback chunker treating the whole file as one chunk or splitting by paragraphs."""
    
    def chunk_file(self, content: str, file_path: str = "") -> List[CodeChunk]:
        # For now, just return the whole file as a single 'file' chunk
        lines = content.splitlines()
        return [self._create_chunk(
            content=content,
            chunk_type="file",
            name=file_path.split("/")[-1] if file_path else "unknown",
            start_line=1,
            end_line=len(lines),
            language="text"
        )]


class PythonChunker(ChunkingEngine):
    """AST-based chunker for Python code."""
    
    def chunk_file(self, content: str, file_path: str = "") -> List[CodeChunk]:
        chunks = []
        try:
            tree = ast.parse(content)
            lines = content.splitlines()
            
            def get_segment(start_line, end_line) -> str:
                return "\n".join(lines[start_line-1:end_line])

            def get_signature(node):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    try:
                        # Extract signature part only
                        import inspect
                        # ast.unparse is Python 3.9+
                        full_code = ast.unparse(node)
                        first_line = full_code.split('\n')[0]
                        return first_line.rstrip(':')
                    except (AttributeError, Exception):
                        # Fallback for older python or errors
                        return node.name
                return ""

            def get_parameters(node):
                params = []
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                    for arg in node.args.args:
                        arg_info = {"name": arg.arg}
                        if arg.annotation:
                            try:
                                arg_info["type"] = ast.unparse(arg.annotation)
                            except: pass
                        params.append(arg_info)
                return params

            def get_return_type(node):
                if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.returns:
                    try:
                        return ast.unparse(node.returns)
                    except: pass
                return ""

            def get_calls(node):
                calls = []
                for sub_node in ast.walk(node):
                    if isinstance(sub_node, ast.Call):
                        call_info = {}
                        try:
                            call_info["target"] = ast.unparse(sub_node.func)
                            call_info["line"] = getattr(sub_node, 'lineno', node.lineno)
                            # Simple extraction of args
                            args_passed = [ast.unparse(a) for a in sub_node.args]
                            kwargs_passed = [f"{k.arg}={ast.unparse(k.value)}" for k in sub_node.keywords if k.arg]
                            call_info["parameters_passed"] = ", ".join(args_passed + kwargs_passed)
                        except: continue
                        calls.append(call_info)
                return calls

            def get_external_imports(tree):
                externals = []
                for node in ast.walk(tree):
                    if isinstance(node, ast.Import):
                        for name in node.names:
                            externals.append(name.name.split('.')[0])
                    elif isinstance(node, ast.ImportFrom):
                        if node.level == 0 and node.module:
                            externals.append(node.module.split('.')[0])
                return sorted(list(set(externals)))

            external_imports = get_external_imports(tree)

            def process_nodes(nodes, prefix=""):
                buffer = []
                
                def flush():
                    if not buffer:
                        return
                    start = buffer[0].lineno
                    end = max(n.end_lineno for n in buffer)
                    
                    types = set()
                    names = []
                    for n in buffer:
                        if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)):
                            types.add("function" if not prefix else "method")
                            names.append(n.name)
                        elif isinstance(n, ast.ClassDef):
                            types.add("class")
                            names.append(n.name)
                        elif isinstance(n, (ast.Import, ast.ImportFrom)):
                            types.add("import")
                        elif isinstance(n, ast.Assign):
                            types.add("assignment")
                    
                    if not types:
                        types.add("block")
                    
                    t_list = list(types)
                    chunk_type = t_list[0] if len(t_list) == 1 else "mixed"
                    if not names:
                        names = ["block"]
                        
                    name = ", ".join(names)
                    if len(name) > 60:
                        name = name[:57] + "..."
                    
                    if prefix:
                        name = f"{prefix}.[{name}]" if len(names) > 1 else f"{prefix}.{name}"
                    
                    # For mixed/block chunks, we can still collect calls
                    chunk_calls = []
                    for n in buffer:
                        chunk_calls.extend(get_calls(n))
                        
                    chunks.append(self._create_chunk(
                        content=get_segment(start, end),
                        chunk_type=chunk_type,
                        name=name,
                        start_line=start,
                        end_line=end,
                        language="python",
                        calls=chunk_calls,
                        external_imports=external_imports
                    ))
                    buffer.clear()

                for node in nodes:
                    if not hasattr(node, 'lineno') or not hasattr(node, 'end_lineno'):
                        continue
                        
                    if isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
                        flush()
                        
                        if isinstance(node, ast.ClassDef):
                            chunk_type = "class"
                        else:
                            is_in_class = True if prefix else False
                            chunk_type = "method" if is_in_class else "function"
                            
                        name = f"{prefix}.{node.name}" if prefix else node.name
                        
                        extra_metadata = {
                            "signature": get_signature(node),
                            "parameters": get_parameters(node),
                            "return_type": get_return_type(node),
                            "calls": get_calls(node),
                            "external_imports": external_imports
                        }
                        
                        chunks.append(self._create_chunk(
                            content=get_segment(node.lineno, node.end_lineno),
                            chunk_type=chunk_type,
                            name=name,
                            start_line=node.lineno,
                            end_line=node.end_lineno,
                            language="python",
                            **extra_metadata
                        ))
                        
                        if len(node.body) > 1 or (len(node.body) == 1 and not isinstance(node.body[0], (ast.Pass, ast.Expr))):
                            process_nodes(node.body, prefix=name)
                    else:
                        buffer.append(node)
                        current_lines = buffer[-1].end_lineno - buffer[0].lineno + 1
                        if current_lines >= 100:
                            flush()
                            
                flush()

            process_nodes(tree.body)
            
            if not chunks:
                return TextChunker().chunk_file(content, file_path)
                
            return chunks

        except SyntaxError:
            return TextChunker().chunk_file(content, file_path)
