"""
Semantic compression for chunk content.

Compresses chunk content to summaries while preserving key information.
Supports both LLM-based compression and extractive fallback.

Requirements: MEM-07 (compression with 56x target ratio)
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, List, Optional

from gsd_rlm.memory.retrieval.chunker import Chunk

if TYPE_CHECKING:
    from gsd_rlm.agents.definition import LLMProvider


# Default prompt template for LLM compression
COMPRESSION_PROMPT = """Summarize this content in approximately {target_tokens} tokens.
Preserve all key information, decisions, and code patterns.

Content:
{content}

Summary:"""


class SemanticCompressor:
    """Compresses chunks while preserving semantic meaning.

    Supports two compression modes:
    1. LLM-based: Uses LLM to generate intelligent summaries
    2. Extractive: Falls back to extracting key sentences when no LLM

    Target compression ratio is 1/56 (56x compression) to enable
    processing of 10M+ token documents.

    Example:
        ```python
        compressor = SemanticCompressor(target_compression=56.0)
        compressed = await compressor.compress_chunks(chunks, llm_provider=ollama)
        ```
    """

    def __init__(self, target_compression: float = 56.0):
        """Initialize the compressor.

        Args:
            target_compression: Target compression ratio (default: 56x)
        """
        self.target_compression = target_compression
        # Target ratio is 1/compression (e.g., 1/56 = 0.018)
        self.target_ratio = 1.0 / target_compression

    async def compress_chunks(
        self,
        chunks: List[Chunk],
        llm_provider: Optional["LLMProvider"] = None,
    ) -> List[Chunk]:
        """Compress a list of chunks.

        Compresses each chunk's content to a summary while preserving
        key information. Chunks are modified in place.

        Args:
            chunks: List of chunks to compress
            llm_provider: Optional LLM provider for intelligent compression

        Returns:
            Same list of chunks with summaries populated
        """
        for chunk in chunks:
            if not chunk.content:
                chunk.summary = ""
                continue

            compressed = await self._compress_content(
                chunk.content, llm_provider, self.target_ratio
            )
            chunk.summary = compressed

        return chunks

    async def _compress_content(
        self,
        content: str,
        llm_provider: Optional["LLMProvider"],
        target_ratio: float = 0.018,
    ) -> str:
        """Compress a single content string.

        Uses LLM if available, otherwise falls back to extractive compression.

        Args:
            content: Content to compress
            llm_provider: Optional LLM provider
            target_ratio: Target size ratio (compressed/original)

        Returns:
            Compressed summary
        """
        if not content:
            return ""

        # Calculate target token count
        original_tokens = self._estimate_tokens(content)
        target_tokens = max(10, int(original_tokens * target_ratio))

        # Try LLM-based compression if provider available
        if llm_provider is not None:
            try:
                return await self._llm_compress(content, llm_provider, target_tokens)
            except Exception:
                # Fall back to extractive on LLM failure
                pass

        # Extractive compression fallback
        return self._extractive_compress(content, target_tokens)

    async def _llm_compress(
        self,
        content: str,
        llm_provider: "LLMProvider",
        target_tokens: int,
    ) -> str:
        """Use LLM to generate compressed summary.

        Args:
            content: Content to compress
            llm_provider: LLM provider to use
            target_tokens: Target token count for summary

        Returns:
            LLM-generated summary

        Note:
            This is a placeholder for actual LLM integration.
            In production, this would call the LLM API.
        """
        # Build prompt
        prompt = COMPRESSION_PROMPT.format(target_tokens=target_tokens, content=content)

        # In production, this would call the LLM
        # For now, we return a placeholder that tests can mock
        # The actual implementation would be:
        # response = await llm_provider.generate(prompt)
        # return response.text

        # Placeholder: return extractive result
        return self._extractive_compress(content, target_tokens)

    def _extractive_compress(self, content: str, target_tokens: int) -> str:
        """Extractive compression without LLM.

        Extracts first sentence and key phrases to create summary.

        Args:
            content: Content to compress
            target_tokens: Target token count

        Returns:
            Extractive summary
        """
        # Split into sentences
        sentences = self._split_sentences(content)
        if not sentences:
            return content[: target_tokens * 4]  # Char approximation

        # Always include first sentence
        summary_parts = [sentences[0]]

        # Calculate remaining budget
        first_tokens = self._estimate_tokens(sentences[0])
        remaining_budget = target_tokens - first_tokens

        if remaining_budget > 10 and len(sentences) > 1:
            # Extract key phrases from remaining sentences
            key_phrases = self._extract_key_phrases(
                " ".join(sentences[1:]), remaining_budget
            )
            if key_phrases:
                summary_parts.append(key_phrases)

        summary = " ".join(summary_parts)

        # Truncate if still too long
        if self._estimate_tokens(summary) > target_tokens * 1.5:
            summary = summary[: int(target_tokens * 4 * 1.5)]

        return summary

    def _split_sentences(self, text: str) -> List[str]:
        """Split text into sentences.

        Args:
            text: Text to split

        Returns:
            List of sentences
        """
        # Split at sentence boundaries
        pattern = re.compile(r"(?<=[.!?])\s+")
        sentences = [s.strip() for s in pattern.split(text) if s.strip()]
        return sentences

    def _extract_key_phrases(self, text: str, max_tokens: int) -> str:
        """Extract key phrases from text.

        Uses simple heuristics to identify important phrases:
        - Capitalized phrases
        - Phrases with numbers
        - Technical terms (camelCase, snake_case)

        Args:
            text: Text to extract from
            max_tokens: Maximum tokens for extracted phrases

        Returns:
            String of key phrases
        """
        # Find phrases with capital letters, numbers, or code patterns
        patterns = [
            r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b",  # Title Case Phrases
            r"\b\d+(?:\.\d+)?%?\b",  # Numbers and percentages
            r"\b[a-z]+[A-Z][a-zA-Z]*\b",  # camelCase
            r"\b[a-z]+(?:_[a-z]+)+\b",  # snake_case
            r"\b[A-Z]{2,}\b",  # Acronyms
        ]

        phrases = []
        for pattern in patterns:
            matches = re.findall(pattern, text)
            phrases.extend(matches)

        # Remove duplicates while preserving order
        seen = set()
        unique_phrases = []
        for phrase in phrases:
            if phrase.lower() not in seen:
                seen.add(phrase.lower())
                unique_phrases.append(phrase)

        # Join with commas, respecting token budget
        result_parts = []
        current_tokens = 0

        for phrase in unique_phrases:
            phrase_tokens = self._estimate_tokens(phrase) + 1  # +1 for comma/space
            if current_tokens + phrase_tokens <= max_tokens:
                result_parts.append(phrase)
                current_tokens += phrase_tokens

        if result_parts:
            return "Key: " + ", ".join(result_parts)

        return ""

    def _estimate_tokens(self, text: str) -> int:
        """Estimate token count for text.

        Uses ~4 chars per token approximation.

        Args:
            text: Text to estimate

        Returns:
            Estimated token count
        """
        return max(1, len(text) // 4)

    def _estimate_compression_ratio(self, original: str, compressed: str) -> float:
        """Calculate compression ratio achieved.

        Args:
            original: Original content
            compressed: Compressed content

        Returns:
            Compression ratio (e.g., 56.0 means 56x compression)
        """
        if not compressed:
            return 0.0

        original_tokens = self._estimate_tokens(original)
        compressed_tokens = self._estimate_tokens(compressed)

        if compressed_tokens == 0:
            return 0.0

        return original_tokens / compressed_tokens
