"""
Splitting Methods Module

This module provides all splitting methods as simple, reusable functions for
text chunking and splitting. It supports multiple splitting approaches ranging
from simple character-based splitting to advanced KG/ontology-aware chunking.

Supported Methods:

Standard Text Splitting:
    - "recursive": Recursive splitting with separator hierarchy
    - "token": Token-based splitting using tiktoken/transformers
    - "sentence": Sentence boundary splitting (regex, NLTK, spaCy)
    - "paragraph": Paragraph boundary splitting
    - "character": Character count splitting
    - "word": Word count splitting
    - "semantic_transformer": Sentence transformer-based splitting
    - "llm": LLM-based optimal split point detection
    - "huggingface": HuggingFace model-based splitting
    - "nltk": NLTK-based splitting

KG/Ontology/Graph Analytics Methods:
    - "entity_aware": Entity boundary-preserving splitting
    - "relation_aware": Triplet-preserving splitting
    - "graph_based": Graph structure-based splitting
    - "ontology_aware": Ontology concept/hierarchy-based splitting
    - "embedding_semantic": Embedding similarity-based boundaries
    - "hierarchical": Multi-level hierarchical chunking
    - "community_detection": Community detection-based chunking
    - "centrality_based": Centrality-based chunking
    - "subgraph": Subgraph extraction-based chunking
    - "topic_based": Topic modeling-based chunking

Algorithms Used:

Standard Splitting:
    - Recursive Splitting: Separator hierarchy and greedy splitting
    - Token Counting: BPE tokenization (tiktoken, transformers)
    - Sentence Segmentation: NLTK, spaCy, regex-based
    - Semantic Boundary Detection: Sentence transformer embeddings and similarity
    - LLM-based Splitting: Prompt engineering for optimal split point detection

KG/Ontology/Graph Analytics:
    - Entity Boundary Detection: NER-based entity extraction and boundary preservation
    - Triplet Preservation: Graph-based triplet integrity checking
    - Graph Centrality Analysis: Degree, betweenness, closeness, eigenvector centrality
    - Community Detection: Louvain algorithm, Leiden algorithm, modularity optimization
    - Graph Connectivity: Connected components, shortest paths, bridge detection
    - Ontology Hierarchy Traversal: Taxonomic structure traversal and concept grouping
    - Embedding Similarity: Cosine similarity, Euclidean distance for semantic boundaries
    - Subgraph Extraction: k-hop neighborhood extraction, connected component detection
    - Topic Modeling: LDA (Latent Dirichlet Allocation), BERTopic for theme detection
    - Hierarchical Segmentation: Multi-level document structure analysis

Key Features:
    - Multiple standard splitting methods
    - KG/ontology/graph analytics-specific chunking methods
    - Method dispatchers with registry support
    - Custom method registration capability
    - Consistent interface across all methods
    - Integration with existing chunkers

Main Functions:
    - split_recursive: Recursive splitting with separator hierarchy
    - split_by_tokens: Token-based splitting
    - split_by_sentences: Sentence boundary splitting
    - split_by_paragraphs: Paragraph boundary splitting
    - split_by_characters: Character count splitting
    - split_by_words: Word count splitting
    - split_semantic_transformer: Sentence transformer-based splitting
    - split_llm: LLM-based optimal split point detection
    - split_entity_aware: Entity boundary-preserving splitting
    - split_relation_aware: Triplet-preserving splitting
    - split_graph_based: Graph structure-based splitting
    - split_ontology_aware: Ontology concept-based splitting
    - split_hierarchical: Multi-level hierarchical chunking
    - get_split_method: Get splitting method by name

Example Usage:
    >>> from semantica.split.methods import get_split_method
    >>> split_fn = get_split_method("recursive")
    >>> chunks = split_fn(text, chunk_size=1000, chunk_overlap=200)

Author: Semantica Contributors
License: MIT
"""

import re
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Union

from ..utils.exceptions import ProcessingError
from ..utils.helpers import safe_import
from ..utils.logging import get_logger
from .semantic_chunker import Chunk

logger = get_logger("split_methods")

# Try to import optional dependencies
spacy, SPACY_AVAILABLE = safe_import("spacy")

nltk, NLTK_AVAILABLE = safe_import("nltk")
tiktoken, TIKTOKEN_AVAILABLE = safe_import("tiktoken")
_sentence_transformers, SENTENCE_TRANSFORMER_AVAILABLE = safe_import("sentence_transformers")
if SENTENCE_TRANSFORMER_AVAILABLE:
    from sentence_transformers import SentenceTransformer
else:
    SentenceTransformer = None
_transformers, TRANSFORMERS_AVAILABLE = safe_import("transformers")
if TRANSFORMERS_AVAILABLE:
    from transformers import AutoTokenizer
else:
    AutoTokenizer = None

networkx, NETWORKX_AVAILABLE = safe_import("networkx")
if NETWORKX_AVAILABLE:
    nx = networkx
else:
    nx = None

# Try community package with fallback
_community1, _avail1 = safe_import("community.community_louvain")
if _avail1:
    import community.community_louvain as community_louvain
    COMMUNITY_AVAILABLE = True
else:
    _community2, _avail2 = safe_import("community")
    if _avail2:
        try:
            from community import community_louvain
            COMMUNITY_AVAILABLE = True
        except (ImportError, OSError):
            COMMUNITY_AVAILABLE = False
    else:
        COMMUNITY_AVAILABLE = False

# Import from semantic_extract for entity/relation extraction
try:
    from ..semantic_extract.ner_extractor import NERExtractor
    from ..semantic_extract.providers import create_provider
    from ..semantic_extract.relation_extractor import RelationExtractor

    SEMANTIC_EXTRACT_AVAILABLE = True
except (ImportError, OSError):
    SEMANTIC_EXTRACT_AVAILABLE = False

# Import specialized chunkers
try:
    from .structural_chunker import StructuralChunker
    from .sliding_window_chunker import SlidingWindowChunker
    
    SPECIALIZED_CHUNKERS_AVAILABLE = True
except (ImportError, OSError):
    SPECIALIZED_CHUNKERS_AVAILABLE = False


# ============================================================================
# Standard Splitting Methods
# ============================================================================


def split_recursive(
    text: str,
    chunk_size: int = 1000,
    chunk_overlap: int = 200,
    separators: Optional[List[str]] = None,
    **kwargs,
) -> List[Chunk]:
    """
    Recursive splitting with separator hierarchy.

    Args:
        text: Input text
        chunk_size: Target chunk size
        chunk_overlap: Overlap between chunks
        separators: List of separators in priority order
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if separators is None:
        separators = ["\n\n", "\n", ". ", " ", ""]

    chunks = []
    text_length = len(text)
    start = 0

    while start < text_length:
        # Find the best split point
        end = start + chunk_size
        if end >= text_length:
            chunk_text = text[start:]
            chunks.append(
                Chunk(
                    text=chunk_text,
                    start_index=start,
                    end_index=text_length,
                    metadata={"method": "recursive", "chunk_size": len(chunk_text)},
                )
            )
            break

        # Try each separator in priority order
        split_pos = -1
        for separator in separators:
            if separator:
                pos = text.rfind(separator, start, end)
                if pos > start + chunk_size * 0.5:  # At least 50% of target size
                    split_pos = pos + len(separator)
                    break
            else:
                # Last resort: split at character boundary
                split_pos = end

        if split_pos == -1:
            split_pos = end

        chunk_text = text[start:split_pos].strip()
        if chunk_text:
            chunks.append(
                Chunk(
                    text=chunk_text,
                    start_index=start,
                    end_index=split_pos,
                    metadata={"method": "recursive", "chunk_size": len(chunk_text)},
                )
            )

        # Move to next chunk with overlap
        start = max(start + 1, split_pos - chunk_overlap)

    return chunks


def split_by_tokens(
    text: str,
    chunk_size: int = 512,
    chunk_overlap: int = 50,
    tokenizer: str = "gpt-4",
    **kwargs,
) -> List[Chunk]:
    """
    Token-based splitting using tiktoken or transformers.

    Args:
        text: Input text
        chunk_size: Target chunk size in tokens
        chunk_overlap: Overlap in tokens
        tokenizer: Tokenizer name (tiktoken model or HuggingFace model)
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    # Try tiktoken first
    if TIKTOKEN_AVAILABLE:
        try:
            enc = tiktoken.encoding_for_model(tokenizer)
            tokens = enc.encode(text)
        except Exception:
            # Fallback to cl100k_base
            enc = tiktoken.get_encoding("cl100k_base")
            tokens = enc.encode(text)
    elif TRANSFORMERS_AVAILABLE:
        try:
            tokenizer_obj = AutoTokenizer.from_pretrained(tokenizer)
            tokens = tokenizer_obj.encode(text, add_special_tokens=False)
        except Exception:
            # Fallback to simple word splitting
            tokens = text.split()
    else:
        # Fallback to word-based approximation
        words = text.split()
        tokens = words
        chunk_size = chunk_size * 4  # Approximate: 1 token ≈ 4 chars

    chunks = []
    start_idx = 0
    text_start = 0

    while start_idx < len(tokens):
        end_idx = min(start_idx + chunk_size, len(tokens))
        chunk_tokens = tokens[start_idx:end_idx]

        # Convert tokens back to text
        if TIKTOKEN_AVAILABLE:
            chunk_text = enc.decode(chunk_tokens)
        elif TRANSFORMERS_AVAILABLE:
            chunk_text = tokenizer_obj.decode(chunk_tokens, skip_special_tokens=True)
        else:
            chunk_text = " ".join(chunk_tokens)

        # Find position in original text
        text_pos = text.find(chunk_text[:50], text_start)
        if text_pos == -1:
            text_pos = text_start

        text_end = text_pos + len(chunk_text)

        chunks.append(
            Chunk(
                text=chunk_text,
                start_index=text_pos,
                end_index=text_end,
                metadata={
                    "method": "token",
                    "token_count": len(chunk_tokens),
                    "chunk_size": len(chunk_text),
                },
            )
        )

        # Move to next chunk with overlap
        start_idx = max(start_idx + 1, end_idx - chunk_overlap)
        text_start = text_end - chunk_overlap * 4  # Approximate

    return chunks


def split_by_sentences(
    text: str, chunk_size: int = 1000, max_sentences: Optional[int] = None, **kwargs
) -> List[Chunk]:
    """
    Sentence boundary splitting using regex, NLTK, or spaCy.

    Args:
        text: Input text
        chunk_size: Target chunk size
        max_sentences: Maximum sentences per chunk
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    # Try spaCy first
    if SPACY_AVAILABLE and kwargs.get("use_spacy", True):
        try:
            nlp = spacy.load("en_core_web_sm")
            doc = nlp(text)
            sentences = [sent.text for sent in doc.sents]
        except Exception:
            sentences = _split_sentences_regex(text)
    elif NLTK_AVAILABLE and kwargs.get("use_nltk", False):
        try:
            nltk.download("punkt", quiet=True)
            sentences = nltk.sent_tokenize(text)
        except Exception:
            sentences = _split_sentences_regex(text)
    else:
        sentences = _split_sentences_regex(text)

    chunks = []
    current_chunk = []
    current_size = 0
    text_start = 0

    for sentence in sentences:
        sentence = sentence.strip()
        if not sentence:
            continue

        sentence_size = len(sentence)

        # Check if adding this sentence would exceed limits
        if (max_sentences and len(current_chunk) >= max_sentences) or (
            current_size + sentence_size > chunk_size and current_chunk
        ):
            # Create chunk
            chunk_text = " ".join(current_chunk)
            text_pos = text.find(chunk_text[:50], text_start)
            if text_pos == -1:
                text_pos = text_start

            text_end = text_pos + len(chunk_text)
            chunks.append(
                Chunk(
                    text=chunk_text,
                    start_index=text_pos,
                    end_index=text_end,
                    metadata={
                        "method": "sentence",
                        "sentence_count": len(current_chunk),
                        "chunk_size": len(chunk_text),
                    },
                )
            )

            text_start = text_end
            current_chunk = []
            current_size = 0

        current_chunk.append(sentence)
        current_size += sentence_size + 1  # +1 for space

    # Add final chunk
    if current_chunk:
        chunk_text = " ".join(current_chunk)
        text_pos = text.find(chunk_text[:50], text_start)
        if text_pos == -1:
            text_pos = text_start

        text_end = text_pos + len(chunk_text)
        chunks.append(
            Chunk(
                text=chunk_text,
                start_index=text_pos,
                end_index=text_end,
                metadata={
                    "method": "sentence",
                    "sentence_count": len(current_chunk),
                    "chunk_size": len(chunk_text),
                },
            )
        )

    return chunks


def _split_sentences_regex(text: str) -> List[str]:
    """Fallback regex-based sentence splitting."""
    # Simple sentence splitting
    sentences = re.split(r"(?<=[.!?])\s+", text)
    return [s.strip() for s in sentences if s.strip()]


def split_by_paragraphs(text: str, chunk_size: int = 2000, **kwargs) -> List[Chunk]:
    """
    Paragraph boundary splitting.

    Args:
        text: Input text
        chunk_size: Target chunk size
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]

    chunks = []
    current_chunk = []
    current_size = 0
    text_start = 0

    for para in paragraphs:
        para_size = len(para)

        if current_size + para_size > chunk_size and current_chunk:
            chunk_text = "\n\n".join(current_chunk)
            text_pos = text.find(chunk_text[:50], text_start)
            if text_pos == -1:
                text_pos = text_start

            text_end = text_pos + len(chunk_text)
            chunks.append(
                Chunk(
                    text=chunk_text,
                    start_index=text_pos,
                    end_index=text_end,
                    metadata={
                        "method": "paragraph",
                        "paragraph_count": len(current_chunk),
                        "chunk_size": len(chunk_text),
                    },
                )
            )

            text_start = text_end
            current_chunk = []
            current_size = 0

        current_chunk.append(para)
        current_size += para_size + 2  # +2 for \n\n

    # Add final chunk
    if current_chunk:
        chunk_text = "\n\n".join(current_chunk)
        text_pos = text.find(chunk_text[:50], text_start)
        if text_pos == -1:
            text_pos = text_start

        text_end = text_pos + len(chunk_text)
        chunks.append(
            Chunk(
                text=chunk_text,
                start_index=text_pos,
                end_index=text_end,
                metadata={
                    "method": "paragraph",
                    "paragraph_count": len(current_chunk),
                    "chunk_size": len(chunk_text),
                },
            )
        )

    return chunks


def split_by_characters(
    text: str, chunk_size: int = 1000, chunk_overlap: int = 200, **kwargs
) -> List[Chunk]:
    """
    Character count splitting.

    Args:
        text: Input text
        chunk_size: Chunk size in characters
        chunk_overlap: Overlap in characters
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    chunks = []
    text_length = len(text)
    start = 0

    while start < text_length:
        end = min(start + chunk_size, text_length)
        chunk_text = text[start:end]

        chunks.append(
            Chunk(
                text=chunk_text,
                start_index=start,
                end_index=end,
                metadata={"method": "character", "chunk_size": len(chunk_text)},
            )
        )

        start = max(start + 1, end - chunk_overlap)

    return chunks


def split_by_words(
    text: str, chunk_size: int = 200, chunk_overlap: int = 40, **kwargs
) -> List[Chunk]:
    """
    Word count splitting.

    Args:
        text: Input text
        chunk_size: Chunk size in words
        chunk_overlap: Overlap in words
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    words = text.split()
    chunks = []

    start_idx = 0
    while start_idx < len(words):
        end_idx = min(start_idx + chunk_size, len(words))
        chunk_words = words[start_idx:end_idx]
        chunk_text = " ".join(chunk_words)

        # Find position in original text
        text_pos = text.find(chunk_text[:50])
        if text_pos == -1:
            # Approximate position
            text_pos = sum(len(w) + 1 for w in words[:start_idx])

        text_end = text_pos + len(chunk_text)

        chunks.append(
            Chunk(
                text=chunk_text,
                start_index=text_pos,
                end_index=text_end,
                metadata={
                    "method": "word",
                    "word_count": len(chunk_words),
                    "chunk_size": len(chunk_text),
                },
            )
        )

        start_idx = max(start_idx + 1, end_idx - chunk_overlap)

    return chunks


def split_semantic_transformer(
    text: str,
    chunk_size: int = 1000,
    model: str = "all-MiniLM-L6-v2",
    similarity_threshold: float = 0.7,
    **kwargs,
) -> List[Chunk]:
    """
    Sentence transformer-based semantic splitting.

    Args:
        text: Input text
        chunk_size: Target chunk size
        model: Sentence transformer model name
        similarity_threshold: Similarity threshold for boundaries
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not SENTENCE_TRANSFORMER_AVAILABLE:
        logger.warning(
            "sentence-transformers not available, falling back to sentence splitting"
        )
        return split_by_sentences(text, chunk_size=chunk_size, **kwargs)

    try:
        # Load model
        model_obj = SentenceTransformer(model)

        # Split into sentences first
        sentences = _split_sentences_regex(text)
        if not sentences:
            return []

        # Get embeddings
        embeddings = model_obj.encode(sentences)

        # Find split points based on similarity
        chunks = []
        current_chunk = []
        current_size = 0
        text_start = 0

        for i, sentence in enumerate(sentences):
            sentence_size = len(sentence)

            # Check similarity with previous sentence
            if i > 0 and len(current_chunk) > 0:
                similarity = _cosine_similarity(embeddings[i - 1], embeddings[i])
                if similarity < similarity_threshold and current_chunk:
                    # Create chunk at boundary
                    chunk_text = " ".join(current_chunk)
                    text_pos = text.find(chunk_text[:50], text_start)
                    if text_pos == -1:
                        text_pos = text_start

                    text_end = text_pos + len(chunk_text)
                    chunks.append(
                        Chunk(
                            text=chunk_text,
                            start_index=text_pos,
                            end_index=text_end,
                            metadata={
                                "method": "semantic_transformer",
                                "sentence_count": len(current_chunk),
                                "chunk_size": len(chunk_text),
                            },
                        )
                    )

                    text_start = text_end
                    current_chunk = []
                    current_size = 0

            # Check size limit
            if current_size + sentence_size > chunk_size and current_chunk:
                chunk_text = " ".join(current_chunk)
                text_pos = text.find(chunk_text[:50], text_start)
                if text_pos == -1:
                    text_pos = text_start

                text_end = text_pos + len(chunk_text)
                chunks.append(
                    Chunk(
                        text=chunk_text,
                        start_index=text_pos,
                        end_index=text_end,
                        metadata={
                            "method": "semantic_transformer",
                            "sentence_count": len(current_chunk),
                            "chunk_size": len(chunk_text),
                        },
                    )
                )

                text_start = text_end
                current_chunk = []
                current_size = 0

            current_chunk.append(sentence)
            current_size += sentence_size + 1

        # Add final chunk
        if current_chunk:
            chunk_text = " ".join(current_chunk)
            text_pos = text.find(chunk_text[:50], text_start)
            if text_pos == -1:
                text_pos = text_start

            text_end = text_pos + len(chunk_text)
            chunks.append(
                Chunk(
                    text=chunk_text,
                    start_index=text_pos,
                    end_index=text_end,
                    metadata={
                        "method": "semantic_transformer",
                        "sentence_count": len(current_chunk),
                        "chunk_size": len(chunk_text),
                    },
                )
            )

        return chunks

    except Exception as e:
        logger.warning(
            f"Error in semantic transformer splitting: {e}, falling back to sentence splitting"
        )
        return split_by_sentences(text, chunk_size=chunk_size, **kwargs)


def _cosine_similarity(vec1, vec2):
    """Calculate cosine similarity between two vectors."""
    import numpy as np

    dot_product = np.dot(vec1, vec2)
    norm1 = np.linalg.norm(vec1)
    norm2 = np.linalg.norm(vec2)
    if norm1 == 0 or norm2 == 0:
        return 0.0
    return dot_product / (norm1 * norm2)


def split_llm(
    text: str,
    chunk_size: int = 1000,
    provider: str = "openai",
    model: Optional[str] = None,
    **kwargs,
) -> List[Chunk]:
    """
    LLM-based optimal split point detection.

    Args:
        text: Input text
        chunk_size: Target chunk size
        provider: LLM provider name
        model: Model name
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not SEMANTIC_EXTRACT_AVAILABLE:
        logger.warning(
            "semantic_extract not available, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=chunk_size, **kwargs)

    try:
        llm_provider = create_provider(provider, model=model or "gpt-3.5-turbo")

        # First, split roughly by size
        rough_chunks = split_recursive(text, chunk_size=chunk_size, chunk_overlap=0)

        # Use LLM to refine boundaries
        refined_chunks = []
        for rough_chunk in rough_chunks:
            prompt = f"""Analyze the following text and identify the best split points 
            (sentence boundaries) that would create semantically coherent chunks of approximately 
            {chunk_size} characters. Return only the indices where splits should occur, 
            separated by commas:

            {rough_chunk.text[:2000]}

            Split indices:"""

            response = llm_provider.generate(prompt)

            # Parse response to get split indices
            try:
                split_indices = [
                    int(x.strip()) for x in response.split(",") if x.strip().isdigit()
                ]
                # Use split indices to create refined chunks
                # For simplicity, use the rough chunk if parsing fails
                refined_chunks.append(rough_chunk)
            except Exception:
                refined_chunks.append(rough_chunk)

        return refined_chunks if refined_chunks else rough_chunks

    except Exception as e:
        logger.warning(
            f"Error in LLM splitting: {e}, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=chunk_size, **kwargs)


def split_huggingface(
    text: str, chunk_size: int = 1000, model: str = "bert-base-uncased", **kwargs
) -> List[Chunk]:
    """
    HuggingFace model-based splitting.

    Args:
        text: Input text
        chunk_size: Target chunk size
        model: HuggingFace model name
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not TRANSFORMERS_AVAILABLE:
        logger.warning("transformers not available, falling back to token splitting")
        return split_by_tokens(text, chunk_size=chunk_size, **kwargs)

    try:
        tokenizer = AutoTokenizer.from_pretrained(model)
        return split_by_tokens(text, chunk_size=chunk_size, tokenizer=model, **kwargs)
    except Exception as e:
        logger.warning(
            f"Error in HuggingFace splitting: {e}, falling back to token splitting"
        )
        return split_by_tokens(text, chunk_size=chunk_size, **kwargs)


def split_nltk(text: str, chunk_size: int = 1000, **kwargs) -> List[Chunk]:
    """
    NLTK-based splitting.

    Args:
        text: Input text
        chunk_size: Target chunk size
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not NLTK_AVAILABLE:
        logger.warning("NLTK not available, falling back to sentence splitting")
        return split_by_sentences(text, chunk_size=chunk_size, use_nltk=False, **kwargs)

    return split_by_sentences(text, chunk_size=chunk_size, use_nltk=True, **kwargs)


# ============================================================================
# KG/Ontology/Graph Analytics Methods
# ============================================================================


def split_entity_aware(
    text: str,
    chunk_size: int = 1000,
    ner_method: str = "ml",
    preserve_entities: bool = True,
    **kwargs,
) -> List[Chunk]:
    """
    Entity boundary-preserving splitting.

    Args:
        text: Input text
        chunk_size: Target chunk size
        ner_method: NER method to use
        preserve_entities: Whether to preserve entity boundaries
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not SEMANTIC_EXTRACT_AVAILABLE:
        logger.warning(
            "semantic_extract not available, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=chunk_size, **kwargs)

    try:
        # Extract entities
        ner_extractor = NERExtractor(method=ner_method, **kwargs)
        entities = ner_extractor.extract(text)

        # Create entity boundaries map
        entity_boundaries = set()
        for entity in entities:
            entity_boundaries.add(entity.start_char)
            entity_boundaries.add(entity.end_char)

        # Split text respecting entity boundaries
        chunks = []
        current_chunk = ""
        current_size = 0
        text_start = 0

        sentences = _split_sentences_regex(text)
        char_pos = 0

        for sentence in sentences:
            sentence_size = len(sentence)
            sentence_start = char_pos
            sentence_end = char_pos + sentence_size

            # Check if sentence contains entity boundaries
            has_entity_boundary = any(
                sentence_start <= boundary <= sentence_end
                for boundary in entity_boundaries
            )

            # Check size limit
            if current_size + sentence_size > chunk_size and current_chunk:
                # Try to split at entity boundary if possible
                if preserve_entities and has_entity_boundary:
                    # Don't split here, add to current chunk
                    pass
                else:
                    # Create chunk
                    chunk_text = current_chunk.strip()
                    text_pos = text.find(chunk_text[:50], text_start)
                    if text_pos == -1:
                        text_pos = text_start

                    text_end = text_pos + len(chunk_text)
                    chunks.append(
                        Chunk(
                            text=chunk_text,
                            start_index=text_pos,
                            end_index=text_end,
                            metadata={
                                "method": "entity_aware",
                                "chunk_size": len(chunk_text),
                                "entity_count": len(
                                    [
                                        e
                                        for e in entities
                                        if text_pos <= e.start_char < text_end
                                    ]
                                ),
                                "entities": [
                                    e
                                    for e in entities
                                    if text_pos <= e.start_char < text_end
                                ],
                            },
                        )
                    )

                    text_start = text_end
                    current_chunk = ""
                    current_size = 0

            current_chunk += sentence + " "
            current_size += sentence_size + 1
            char_pos = sentence_end + 1

        # Add final chunk
        if current_chunk.strip():
            chunk_text = current_chunk.strip()
            text_pos = text.find(chunk_text[:50], text_start)
            if text_pos == -1:
                text_pos = text_start

            text_end = text_pos + len(chunk_text)
            chunks.append(
                Chunk(
                    text=chunk_text,
                    start_index=text_pos,
                    end_index=text_end,
                    metadata={
                        "method": "entity_aware",
                        "chunk_size": len(chunk_text),
                        "entity_count": len(
                            [e for e in entities if text_pos <= e.start_char < text_end]
                        ),
                        "entities": [
                            e for e in entities if text_pos <= e.start_char < text_end
                        ],
                    },
                )
            )

        return chunks

    except Exception as e:
        logger.warning(
            f"Error in entity-aware splitting: {e}, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=chunk_size, **kwargs)


def split_relation_aware(
    text: str,
    chunk_size: int = 1000,
    relation_method: str = "ml",
    preserve_triplets: bool = True,
    **kwargs,
) -> List[Chunk]:
    """
    Triplet-preserving splitting.

    Args:
        text: Input text
        chunk_size: Target chunk size
        relation_method: Relation extraction method
        preserve_triplets: Whether to preserve triplet integrity
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not SEMANTIC_EXTRACT_AVAILABLE:
        logger.warning(
            "semantic_extract not available, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=chunk_size, **kwargs)

    try:
        # Extract entities first (required for relation extraction)
        ner_method = kwargs.get("ner_method", "ml")
        ner_extractor = NERExtractor(method=ner_method, **kwargs)
        entities = ner_extractor.extract(text)

        # Extract relations/triplets
        relation_extractor = RelationExtractor(method=relation_method, **kwargs)
        relations = relation_extractor.extract(text, entities)

        # Create triplet boundaries (subject, relation, object must be in same chunk)
        triplet_boundaries = []
        for relation in relations:
            start = min(relation.subject.start_char, relation.object.start_char)
            end = max(relation.subject.end_char, relation.object.end_char)
            triplet_boundaries.append((start, end))

        # Split text ensuring triplets are not split
        chunks = []
        current_chunk = ""
        current_size = 0
        text_start = 0

        sentences = _split_sentences_regex(text)
        char_pos = 0

        for sentence in sentences:
            sentence_size = len(sentence)
            sentence_start = char_pos
            sentence_end = char_pos + sentence_size

            # Check if sentence is part of a triplet
            is_in_triplet = any(
                start <= sentence_start <= end or start <= sentence_end <= end
                for start, end in triplet_boundaries
            )

            # Check size limit
            if current_size + sentence_size > chunk_size and current_chunk:
                # Don't split if it would break a triplet
                if preserve_triplets and is_in_triplet:
                    # Add to current chunk even if it exceeds size
                    pass
                else:
                    # Create chunk
                    chunk_text = current_chunk.strip()
                    text_pos = text.find(chunk_text[:50], text_start)
                    if text_pos == -1:
                        text_pos = text_start

                    text_end = text_pos + len(chunk_text)
                    chunks.append(
                        Chunk(
                            text=chunk_text,
                            start_index=text_pos,
                            end_index=text_end,
                            metadata={
                                "method": "relation_aware",
                                "chunk_size": len(chunk_text),
                                "relation_count": len(
                                    [
                                        r
                                        for r in relations
                                        if text_pos <= r.subject.start_char < text_end
                                    ]
                                ),
                                "relationships": [
                                    r
                                    for r in relations
                                    if text_pos <= r.subject.start_char < text_end
                                ],
                                "triplets": [
                                    r
                                    for r in relations
                                    if text_pos <= r.subject.start_char < text_end
                                ],
                            },
                        )
                    )

                    text_start = text_end
                    current_chunk = ""
                    current_size = 0

            current_chunk += sentence + " "
            current_size += sentence_size + 1
            char_pos = sentence_end + 1

        # Add final chunk
        if current_chunk.strip():
            chunk_text = current_chunk.strip()
            text_pos = text.find(chunk_text[:50], text_start)
            if text_pos == -1:
                text_pos = text_start

            text_end = text_pos + len(chunk_text)
            chunks.append(
                Chunk(
                    text=chunk_text,
                    start_index=text_pos,
                    end_index=text_end,
                    metadata={
                        "method": "relation_aware",
                        "chunk_size": len(chunk_text),
                        "relation_count": len(
                            [
                                r
                                for r in relations
                                if text_pos <= r.subject.start_char < text_end
                            ]
                        ),
                        "relationships": [
                            r
                            for r in relations
                            if text_pos <= r.subject.start_char < text_end
                        ],
                        "triplets": [
                            r
                            for r in relations
                            if text_pos <= r.subject.start_char < text_end
                        ],
                    },
                )
            )

        return chunks

    except Exception as e:
        logger.warning(
            f"Error in relation-aware splitting: {e}, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=chunk_size, **kwargs)


def split_graph_based(
    text: str,
    chunk_size: int = 1000,
    strategy: str = "community",
    algorithm: str = "louvain",
    **kwargs,
) -> List[Chunk]:
    """
    Graph structure-based splitting using centrality or communities.

    Args:
        text: Input text
        chunk_size: Target chunk size
        strategy: Strategy ("community", "centrality")
        algorithm: Algorithm name ("louvain", "leiden", "betweenness", etc.)
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not SEMANTIC_EXTRACT_AVAILABLE or not NETWORKX_AVAILABLE:
        logger.warning(
            "Required dependencies not available, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=chunk_size, **kwargs)

    try:
        # Extract entities and relations to build graph
        ner_extractor = NERExtractor(method=kwargs.get("ner_method", "ml"), **kwargs)
        relation_extractor = RelationExtractor(
            method=kwargs.get("relation_method", "ml"), **kwargs
        )

        entities = ner_extractor.extract(text)
        relations = relation_extractor.extract(text)

        # Build graph
        G = nx.Graph()
        entity_map = {}

        for entity in entities:
            G.add_node(entity.text, type="entity", label=entity.label)
            entity_map[entity.text] = entity

        for relation in relations:
            subject_text = relation.subject.text
            object_text = relation.object.text
            if subject_text in entity_map and object_text in entity_map:
                G.add_edge(subject_text, object_text, label=relation.predicate)

        if len(G.nodes()) == 0:
            return split_recursive(text, chunk_size=chunk_size, **kwargs)

        # Apply graph-based strategy
        if strategy == "community":
            if algorithm == "louvain" and COMMUNITY_AVAILABLE:
                communities = community_louvain.best_partition(G)
            else:
                # Fallback to simple connected components
                communities = {}
                for i, component in enumerate(nx.connected_components(G)):
                    for node in component:
                        communities[node] = i

            # Group nodes by community
            community_groups = {}
            for node, comm_id in communities.items():
                if comm_id not in community_groups:
                    community_groups[comm_id] = []
                community_groups[comm_id].append(node)

            # Create chunks based on communities
            chunks = []
            for comm_id, nodes in community_groups.items():
                # Get text segments for these entities
                entity_texts = []
                for node in nodes:
                    if node in entity_map:
                        entity = entity_map[node]
                        # Find surrounding context
                        context_start = max(0, entity.start_char - 100)
                        context_end = min(len(text), entity.end_char + 100)
                        entity_texts.append(text[context_start:context_end])

                if entity_texts:
                    chunk_text = " ".join(entity_texts)
                    # Find position in original text
                    text_pos = text.find(chunk_text[:50])
                    if text_pos == -1:
                        text_pos = 0

                    chunks.append(
                        Chunk(
                            text=chunk_text[:chunk_size],
                            start_index=text_pos,
                            end_index=text_pos + min(len(chunk_text), chunk_size),
                            metadata={
                                "method": "graph_based",
                                "strategy": strategy,
                                "algorithm": algorithm,
                                "community_id": comm_id,
                                "node_count": len(nodes),
                            },
                        )
                    )

            return (
                chunks
                if chunks
                else split_recursive(text, chunk_size=chunk_size, **kwargs)
            )

        elif strategy == "centrality":
            # Use centrality to find important nodes and chunk around them
            if algorithm == "betweenness":
                centrality = nx.betweenness_centrality(G)
            elif algorithm == "degree":
                centrality = dict(G.degree())
            else:
                centrality = nx.degree_centrality(G)

            # Sort nodes by centrality
            sorted_nodes = sorted(centrality.items(), key=lambda x: x[1], reverse=True)

            # Create chunks around high-centrality nodes
            chunks = []
            used_nodes = set()

            for node, cent_score in sorted_nodes:
                if node in used_nodes:
                    continue

                # Get neighbors (k-hop)
                k = kwargs.get("k_hop", 2)
                neighbors = set([node])
                current_level = [node]

                for _ in range(k):
                    next_level = []
                    for n in current_level:
                        neighbors.update(G.neighbors(n))
                        next_level.extend(G.neighbors(n))
                    current_level = next_level
                    if not current_level:
                        break

                # Get text for these nodes
                entity_texts = []
                for n in neighbors:
                    if n in entity_map:
                        entity = entity_map[n]
                        context_start = max(0, entity.start_char - 100)
                        context_end = min(len(text), entity.end_char + 100)
                        entity_texts.append(text[context_start:context_end])
                        used_nodes.add(n)

                if entity_texts:
                    chunk_text = " ".join(entity_texts)
                    text_pos = text.find(chunk_text[:50])
                    if text_pos == -1:
                        text_pos = 0

                    chunks.append(
                        Chunk(
                            text=chunk_text[:chunk_size],
                            start_index=text_pos,
                            end_index=text_pos + min(len(chunk_text), chunk_size),
                            metadata={
                                "method": "graph_based",
                                "strategy": strategy,
                                "algorithm": algorithm,
                                "centrality_score": cent_score,
                                "node_count": len(neighbors),
                            },
                        )
                    )

            return (
                chunks
                if chunks
                else split_recursive(text, chunk_size=chunk_size, **kwargs)
            )

        else:
            return split_recursive(text, chunk_size=chunk_size, **kwargs)

    except Exception as e:
        logger.warning(
            f"Error in graph-based splitting: {e}, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=chunk_size, **kwargs)


def split_ontology_aware(
    text: str,
    chunk_size: int = 1000,
    ontology_uri: Optional[str] = None,
    preserve_concepts: bool = True,
    **kwargs,
) -> List[Chunk]:
    """
    Ontology concept and hierarchy-based splitting.

    Args:
        text: Input text
        chunk_size: Target chunk size
        ontology_uri: Ontology URI (optional)
        preserve_concepts: Whether to preserve concept boundaries
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    # For now, use entity-aware splitting as ontology concepts are similar to entities
    # In a full implementation, this would use ontology hierarchies
    logger.info("Ontology-aware splitting using entity-aware method as base")
    return split_entity_aware(
        text, chunk_size=chunk_size, preserve_entities=preserve_concepts, **kwargs
    )


def split_embedding_semantic(
    text: str,
    chunk_size: int = 1000,
    model: str = "all-MiniLM-L6-v2",
    similarity_threshold: float = 0.7,
    **kwargs,
) -> List[Chunk]:
    """
    Embedding similarity-based semantic boundary detection.

    Args:
        text: Input text
        chunk_size: Target chunk size
        model: Embedding model name
        similarity_threshold: Similarity threshold for boundaries
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    # This is essentially the same as semantic_transformer
    return split_semantic_transformer(
        text,
        chunk_size=chunk_size,
        model=model,
        similarity_threshold=similarity_threshold,
        **kwargs,
    )


def split_hierarchical(
    text: str,
    levels: List[str] = ["section", "paragraph", "sentence"],
    chunk_sizes: Optional[List[int]] = None,
    **kwargs,
) -> List[Chunk]:
    """
    Multi-level hierarchical chunking.

    Args:
        text: Input text
        levels: Hierarchy levels (e.g., ["section", "paragraph", "sentence"])
        chunk_sizes: Chunk sizes for each level
        **kwargs: Additional options

    Returns:
        List of chunks with hierarchical metadata
    """
    if chunk_sizes is None:
        chunk_sizes = [2000, 1000, 500]

    # Start with largest level
    if "section" in levels:
        # Try to detect sections (headings)
        sections = re.split(r"\n#{1,6}\s+", text)
        if len(sections) > 1:
            chunks = []
            for section in sections:
                if section.strip():
                    # Recursively chunk section
                    sub_chunks = split_hierarchical(
                        section,
                        levels=levels[1:] if len(levels) > 1 else ["paragraph"],
                        chunk_sizes=chunk_sizes[1:] if len(chunk_sizes) > 1 else [1000],
                        **kwargs,
                    )
                    chunks.extend(sub_chunks)
            return chunks

    # Fall back to paragraph level
    if "paragraph" in levels:
        # Remove chunk_size from kwargs to avoid multiple values error
        para_kwargs = kwargs.copy()
        if "chunk_size" in para_kwargs:
            del para_kwargs["chunk_size"]
            
        return split_by_paragraphs(
            text, chunk_size=chunk_sizes[0] if chunk_sizes else 1000, **para_kwargs
        )

    # Fall back to sentence level
    # Remove chunk_size from kwargs to avoid multiple values error
    sent_kwargs = kwargs.copy()
    if "chunk_size" in sent_kwargs:
        del sent_kwargs["chunk_size"]
        
    return split_by_sentences(
        text, chunk_size=chunk_sizes[0] if chunk_sizes else 1000, **sent_kwargs
    )


def split_community_detection(
    text: str, chunk_size: int = 1000, algorithm: str = "louvain", **kwargs
) -> List[Chunk]:
    """
    Community detection-based chunking.

    Args:
        text: Input text
        chunk_size: Target chunk size
        algorithm: Community detection algorithm ("louvain", "leiden")
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    return split_graph_based(
        text, chunk_size=chunk_size, strategy="community", algorithm=algorithm, **kwargs
    )


def split_centrality_based(
    text: str, chunk_size: int = 1000, algorithm: str = "betweenness", **kwargs
) -> List[Chunk]:
    """
    Centrality-based chunking around important nodes.

    Args:
        text: Input text
        chunk_size: Target chunk size
        algorithm: Centrality algorithm ("betweenness", "degree", "eigenvector")
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    return split_graph_based(
        text,
        chunk_size=chunk_size,
        strategy="centrality",
        algorithm=algorithm,
        **kwargs,
    )


def split_subgraph(
    text: str, chunk_size: int = 1000, k_hop: int = 2, **kwargs
) -> List[Chunk]:
    """
    Subgraph extraction-based chunking (k-hop neighborhoods).

    Args:
        text: Input text
        chunk_size: Target chunk size
        k_hop: k-hop neighborhood size
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    return split_graph_based(
        text,
        chunk_size=chunk_size,
        strategy="centrality",
        algorithm="degree",
        k_hop=k_hop,
        **kwargs,
    )


def split_topic_based(
    text: str, chunk_size: int = 1000, model: str = "lda", num_topics: int = 5, **kwargs
) -> List[Chunk]:
    """
    Topic modeling-based chunking.

    Args:
        text: Input text
        chunk_size: Target chunk size
        model: Topic model ("lda", "bertopic")
        num_topics: Number of topics
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    # For now, use semantic transformer as approximation
    # Full implementation would use LDA or BERTopic
    logger.info(
        f"Topic-based splitting using semantic transformer (full {model} implementation pending)"
    )
    return split_semantic_transformer(text, chunk_size=chunk_size, **kwargs)


def split_structural(
    text: str,
    max_chunk_size: int = 2000,
    respect_headers: bool = True,
    respect_sections: bool = True,
    **kwargs,
) -> List[Chunk]:
    """
    Structure-aware chunking respecting document hierarchy.

    Args:
        text: Input text
        max_chunk_size: Maximum chunk size
        respect_headers: Whether to respect heading hierarchy
        respect_sections: Whether to respect section boundaries
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not SPECIALIZED_CHUNKERS_AVAILABLE:
        logger.warning(
            "StructuralChunker not available, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=max_chunk_size, **kwargs)

    try:
        chunker = StructuralChunker(
            max_chunk_size=max_chunk_size,
            respect_headers=respect_headers,
            respect_sections=respect_sections,
            **kwargs,
        )
        return chunker.chunk(text, **kwargs)
    except Exception as e:
        logger.warning(
            f"Error in structural splitting: {e}, falling back to recursive splitting"
        )
        return split_recursive(text, chunk_size=max_chunk_size, **kwargs)


def split_sliding_window(
    text: str,
    chunk_size: int = 1000,
    overlap: int = 200,
    stride: Optional[int] = None,
    preserve_boundaries: bool = True,
    **kwargs,
) -> List[Chunk]:
    """
    Sliding window chunking with optional boundary preservation.

    Args:
        text: Input text
        chunk_size: Chunk size in characters
        overlap: Overlap size in characters
        stride: Stride size (default: chunk_size - overlap)
        preserve_boundaries: Whether to preserve word/sentence boundaries
        **kwargs: Additional options

    Returns:
        List of chunks
    """
    if not SPECIALIZED_CHUNKERS_AVAILABLE:
        logger.warning(
            "SlidingWindowChunker not available, falling back to recursive splitting"
        )
        return split_recursive(
            text, chunk_size=chunk_size, chunk_overlap=overlap, **kwargs
        )

    try:
        chunker = SlidingWindowChunker(
            chunk_size=chunk_size, overlap=overlap, stride=stride, **kwargs
        )
        return chunker.chunk(text, preserve_boundaries=preserve_boundaries, **kwargs)
    except Exception as e:
        logger.warning(
            f"Error in sliding window splitting: {e}, falling back to recursive splitting"
        )
        return split_recursive(
            text, chunk_size=chunk_size, chunk_overlap=overlap, **kwargs
        )


# ============================================================================
# Method Dispatcher
# ============================================================================

_SPLIT_METHODS = {
    # Standard methods
    "recursive": split_recursive,
    "token": split_by_tokens,
    "sentence": split_by_sentences,
    "paragraph": split_by_paragraphs,
    "character": split_by_characters,
    "word": split_by_words,
    "semantic_transformer": split_semantic_transformer,
    "llm": split_llm,
    "huggingface": split_huggingface,
    "nltk": split_nltk,
    # KG/Ontology methods
    "entity_aware": split_entity_aware,
    "relation_aware": split_relation_aware,
    "graph_based": split_graph_based,
    "ontology_aware": split_ontology_aware,
    "embedding_semantic": split_embedding_semantic,
    "hierarchical": split_hierarchical,
    "community_detection": split_community_detection,
    "centrality_based": split_centrality_based,
    "subgraph": split_subgraph,
    "topic_based": split_topic_based,
    # Specialized methods
    "structural": split_structural,
    "sliding_window": split_sliding_window,
}


def get_split_method(method: str) -> Optional[Callable]:
    """
    Get splitting method by name.

    Args:
        method: Method name

    Returns:
        Method function or None
    """
    # Check registry first
    try:
        from .registry import method_registry

        registered = method_registry.get("split", method)
        if registered:
            return registered
    except (ImportError, OSError):
        pass

    # Check built-in methods
    return _SPLIT_METHODS.get(method)


def list_available_methods() -> List[str]:
    """
    List all available splitting methods.

    Returns:
        List of method names
    """
    methods = list(_SPLIT_METHODS.keys())

    # Add registered methods
    try:
        from .registry import method_registry

        registered = method_registry.list_all("split")
        if registered and "split" in registered:
            methods.extend(registered["split"])
    except (ImportError, OSError):
        pass

    return sorted(set(methods))  # Remove duplicates and sort
