"""AST-aware and fixed-size text chunking for code and documents."""

from __future__ import annotations

import ast
import hashlib
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

# Directories to always skip
SKIP_DIRS = {
    ".git",
    "node_modules",
    "__pycache__",
    ".tox",
    ".venv",
    "venv",
    "vendor",
    "vendored",
    "third_party",
    "dist",
    "build",
    ".eggs",
    "egg-info",
}

DEFAULT_EXTENSIONS = {
    ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".go", ".rs", ".rb",
    ".c", ".cpp", ".h", ".hpp", ".yaml", ".yml", ".json", ".toml",
    ".md", ".rst", ".txt", ".sh", ".bash", ".zsh", ".sql", ".html",
    ".css", ".xml",
}

MAX_FILE_SIZE = 512 * 1024  # 512KB


@dataclass
class Chunk:
    """A single chunk of text with metadata."""

    chunk_id: str
    text: str
    source: str  # e.g. "github", "gitlab", "jira", "slack", "web"
    org: str
    repo: str
    path: str
    start_line: int = 0
    end_line: int = 0
    node_type: str | None = None  # "function", "class", or None
    node_name: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)
    author: str | None = None
    timestamp: str | None = None  # ISO 8601
    url: str | None = None
    jira_issue_id: str | None = None
    content_type: str | None = None  # "code", "pr_comment", "issue_comment", "jira_issue", "jira_comment", "slack_message", "web_page"
    status: str | None = None  # Jira issue status
    priority: str | None = None  # Jira priority
    labels: list[str] | None = None
    pr_number: str | None = None  # PR/MR number


def _generate_chunk_id(source: str, org: str, repo: str, path: str, content: str) -> str:
    """Generate a deterministic chunk ID: {source}/{org}/{repo}/{path}::sha256[:12]."""
    content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()[:12]
    return f"{source}/{org}/{repo}/{path}::{content_hash}"


def _is_vendored(path: Path) -> bool:
    """Check if a path is inside a vendored/skipped directory."""
    return any(part in SKIP_DIRS for part in path.parts)


def chunk_python_ast(
    source_text: str,
    file_path: str,
    source: str,
    org: str,
    repo: str,
) -> list[Chunk] | None:
    """Try to chunk a Python file by AST (one chunk per function/class).

    Returns None if AST parsing fails.
    """
    import warnings

    try:
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", SyntaxWarning)
            tree = ast.parse(source_text)
    except SyntaxError:
        return None

    lines = source_text.splitlines(keepends=True)
    chunks: list[Chunk] = []

    # Collect top-level functions/classes AND methods inside classes
    # but avoid duplicates by tracking line ranges
    nodes_to_chunk: list[ast.stmt] = []
    for node in tree.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            nodes_to_chunk.append(node)
            # For classes, also add individual methods as separate chunks
            if isinstance(node, ast.ClassDef):
                for child in node.body:
                    if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
                        nodes_to_chunk.append(child)

    for node in nodes_to_chunk:
        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            continue

        start = node.lineno - 1  # 0-indexed
        end = node.end_lineno or (start + 1)
        text = "".join(lines[start:end])

        if not text.strip():
            continue

        node_type = "class" if isinstance(node, ast.ClassDef) else "function"
        # Include line range in hash input to avoid collisions for identical content
        chunk_id = _generate_chunk_id(
            source, org, repo, file_path, f"{start}:{end}:{text}"
        )

        chunks.append(Chunk(
            chunk_id=chunk_id,
            text=text,
            source=source,
            org=org,
            repo=repo,
            path=file_path,
            start_line=node.lineno,
            end_line=end,
            node_type=node_type,
            node_name=node.name,
        ))

    return chunks if chunks else None


def chunk_fixed_size(
    source_text: str,
    file_path: str,
    source: str,
    org: str,
    repo: str,
    chunk_size: int = 500,
    chunk_overlap: int = 50,
) -> list[Chunk]:
    """Chunk text by word count with overlap."""
    words = source_text.split()
    if not words:
        return []

    step = max(1, chunk_size - chunk_overlap)
    chunks: list[Chunk] = []
    i = 0

    while i < len(words):
        chunk_words = words[i : i + chunk_size]
        text = " ".join(chunk_words)

        # Approximate line numbers
        char_start = len(" ".join(words[:i]))
        char_end = char_start + len(text)
        start_line = source_text[:char_start].count("\n") + 1
        end_line = source_text[:char_end].count("\n") + 1

        chunk_id = _generate_chunk_id(source, org, repo, file_path, text)
        chunks.append(Chunk(
            chunk_id=chunk_id,
            text=text,
            source=source,
            org=org,
            repo=repo,
            path=file_path,
            start_line=start_line,
            end_line=end_line,
        ))

        i += step

    return chunks


def chunk_file(
    file_path: Path,
    repo_root: Path,
    source: str,
    org: str,
    repo: str,
    chunk_size: int = 500,
    chunk_overlap: int = 50,
    max_file_size: int = MAX_FILE_SIZE,
    index_extensions: set[str] | None = None,
) -> list[Chunk]:
    """Chunk a single file, using AST for Python or fixed-size for everything else.

    Args:
        file_path: Absolute path to the file.
        repo_root: Root of the repo (for computing relative paths).
        source: Source type (e.g. "github").
        org: Organization name.
        repo: Repository name.
        chunk_size: Words per chunk (fixed mode).
        chunk_overlap: Overlap words between chunks.
        max_file_size: Skip files larger than this.
        index_extensions: Set of file extensions to index.
    """
    extensions = index_extensions or DEFAULT_EXTENSIONS

    if file_path.suffix not in extensions:
        return []

    if _is_vendored(file_path.relative_to(repo_root)):
        return []

    try:
        file_size = file_path.stat().st_size
    except OSError:
        return []

    if file_size > max_file_size:
        return []

    try:
        text = file_path.read_text(encoding="utf-8", errors="replace")
    except OSError:
        return []

    rel_path = str(file_path.relative_to(repo_root))

    # Try AST chunking for Python files
    if file_path.suffix == ".py":
        ast_chunks = chunk_python_ast(text, rel_path, source, org, repo)
        if ast_chunks is not None:
            return ast_chunks

    # Fall back to fixed-size chunking
    return chunk_fixed_size(text, rel_path, source, org, repo, chunk_size, chunk_overlap)


def chunk_repo(
    repo_root: Path,
    source: str,
    org: str,
    repo: str,
    chunk_size: int = 500,
    chunk_overlap: int = 50,
    max_file_size: int = MAX_FILE_SIZE,
    index_extensions: set[str] | None = None,
) -> list[Chunk]:
    """Chunk all eligible files in a repository."""
    all_chunks: list[Chunk] = []

    for file_path in repo_root.rglob("*"):
        if not file_path.is_file():
            continue

        chunks = chunk_file(
            file_path,
            repo_root,
            source,
            org,
            repo,
            chunk_size,
            chunk_overlap,
            max_file_size,
            index_extensions,
        )
        all_chunks.extend(chunks)

    return all_chunks
