"""
cortex/extraction/coding_noise.py — CodingNoiseFilter for ExtractionPipeline (M2).

A zero-cost, deterministic pre-filter that rejects messages containing primarily
coding artifacts (file diffs, build output, stack traces, import statements,
etc.) before they reach the LLM extraction step.

Design rationale:
    - LLM extraction is the most expensive step in the remember() pipeline.
    - Many coding assistant conversations contain large blocks of code,
      build output, and stack traces that have zero memory value.
    - This filter uses regex patterns to detect noise BEFORE the LLM call,
      saving tokens and reducing extraction cost significantly.

Algorithm:
    1. Check _KEEP_PATTERNS first — if any match, the message is kept
       regardless of noise score (these indicate decision/preference signal).
    2. For each line, check against _NOISE_PATTERNS.
    3. If ≥60% of non-empty lines match noise patterns → reject.
    4. Very short messages (<5 words) are only rejected if a noise pattern hits.

Dependencies: None (pure Python, no external imports beyond stdlib).
Used by: ExtractionPipeline.extract() — called before LLM completion.
"""

from __future__ import annotations

import re
from typing import Any, Dict, List, TypedDict


class Message(TypedDict, total=False):
    role: str
    content: str


# --- Noise patterns: if a line matches any of these, it's likely noise ---
# These cover common coding artifacts: file paths, import statements,
# git commands, build output, stack traces, diffs, JSON/YAML fragments,
# Docker commands, and console debug output. Order doesn't matter —
# we check all patterns on every line and count the hit ratio.
_NOISE_PATTERNS: list[re.Pattern] = [
    # File paths (unix and windows)
    re.compile(r'(/[\w./\-]+\.\w{1,6}|[A-Za-z]:\\[\w\\./\-]+\.\w{1,6})'),
    # Line number references: "line 42", "at line 42", ":42:"
    re.compile(r'\bline[s]?\s+\d+|\bat\s+line\s+\d+|:\d{1,5}:\d{1,5}'),
    # Import / require statements
    re.compile(r'^\s*(import|from|require|#include)\s+[\w.\'"]+', re.MULTILINE),
    # Variable assignments (bare assignments)
    re.compile(r'^\s*[\w_]+\s*=\s*.+$', re.MULTILINE),
    # Git hashes (7-40 hex chars as standalone token)
    re.compile(r'\b[0-9a-f]{7,40}\b'),
    # Git commands
    re.compile(r'\bgit\s+(commit|push|pull|merge|rebase|checkout|clone|add|diff|log|status)\b'),
    # Build / package manager output
    re.compile(r'\b(npm|pip|yarn|cargo|pnpm|poetry|brew)\s+(install|uninstall|run|build|test|update|add|remove)\b'),
    re.compile(r'\b(Downloading|Installing|Building|Collecting|Resolving|Fetching)\s+\w'),
    re.compile(r'\bSuccessfully installed\b|\bRequirement already satisfied\b'),
    # Stack traces
    re.compile(r'^\s*at\s+[\w.<>$]+\(', re.MULTILINE),
    re.compile(r'^\s*File "[^"]+", line \d+', re.MULTILINE),
    re.compile(r'Traceback \(most recent call last\)'),
    # Docker commands / output
    re.compile(r'\b(docker|kubectl|podman)\s+(run|build|push|pull|exec|ps|logs|stop|start|rm|rmi)\b'),
    re.compile(r'^Step \d+/\d+\s*:', re.MULTILINE),
    re.compile(r'---> [0-9a-f]{12}'),
    # JSON / YAML fragments (bare { } or key: value with no prose)
    re.compile(r'^\s*"[\w_]+"\s*:\s*["{[0-9ntf]', re.MULTILINE),
    re.compile(r'^\s*\{[^}]*"[\w_]+"\s*:\s*[^}]+\}', re.MULTILINE),  # inline JSON obj
    re.compile(r'^\s*[\w_]+:\s+[|>-]?\s*\S', re.MULTILINE),
    # Diff output
    re.compile(r'^[+-]{3}\s+\S', re.MULTILINE),
    re.compile(r'^@@\s+-\d+,\d+\s+\+\d+,\d+\s+@@', re.MULTILINE),
    # Console/debug output
    re.compile(r'\b(console\.log|print\(|debugger|printf|fprintf|System\.out)\b'),
    # Numeric-heavy lines (mostly numbers/symbols, e.g., build IDs, checksums)
    re.compile(r'^[\s\d.,|+\-*/:=#>]{10,}$', re.MULTILINE),
]

# --- Keep patterns: if matched, override noise (these carry real signal) ---
# These detect decision-making language, preferences, and lessons — the kinds
# of statements we WANT to extract even if they appear alongside code noise.
# Keep patterns take precedence over noise patterns.
_KEEP_PATTERNS: list[re.Pattern] = [
    re.compile(r'\b(decided|choosing|migrating|switching|adopting)\b', re.IGNORECASE),
    re.compile(r'\b(discovered|found|realized|learned|noticed)\b', re.IGNORECASE),
    re.compile(r'\b(pattern|approach|strategy|architecture|principle)\b', re.IGNORECASE),
    re.compile(r'\b(should always|never|best practice|lesson learned)\b', re.IGNORECASE),
    re.compile(r'\b(bug|issue|limitation|constraint|incompatibility)\s+in\b', re.IGNORECASE),
    re.compile(r'\b(prefer|prefers|preference|like|dislike|hate|love)\b', re.IGNORECASE),
]

# Threshold: if >= this fraction of non-empty lines look like noise, reject
_NOISE_LINE_THRESHOLD = 0.6
# Minimum useful words for a message to be worth extracting from
_MIN_USEFUL_WORDS = 5


class CodingNoiseFilter:
    """
    Fast, deterministic pre-filter. Run before LLM extraction to avoid
    wasting tokens on coding artifacts.
    """

    def is_noise(self, text: str) -> bool:
        """Determine if a text block is primarily coding/build noise.

        Algorithm:
            1. Empty text → noise.
            2. Any _KEEP_PATTERN match → NOT noise (decision language detected).
            3. Short text (<5 words) → noise only if a _NOISE_PATTERN matches.
            4. Otherwise: count noisy lines / total non-empty lines.
               If ratio ≥ 0.6 → noise.

        Args:
            text: Raw message content to classify.

        Returns:
            True if the message should be filtered out (it's coding noise).
        """
        if not text or not text.strip():
            return True

        # Quick keep-pattern check — if the text has real signal, keep it
        if any(p.search(text) for p in _KEEP_PATTERNS):
            return False

        # Short texts with no noise patterns pass through
        word_count = len(text.split())
        if word_count < _MIN_USEFUL_WORDS:
            # Very short — only reject if a noise pattern matches
            return any(p.search(text) for p in _NOISE_PATTERNS)

        # Count noisy lines vs total lines
        lines = [ln for ln in text.splitlines() if ln.strip()]
        if not lines:
            return True

        noisy_lines = sum(
            1 for ln in lines if any(p.search(ln) for p in _NOISE_PATTERNS)
        )
        ratio = noisy_lines / len(lines)
        return ratio >= _NOISE_LINE_THRESHOLD

    def filter_conversation(
        self,
        messages: List[Dict[str, Any]],
    ) -> List[Dict[str, Any]]:
        """Filter a conversation, removing messages classified as coding noise.

        Preserves message order and dict structure. Each message is
        independently classified via is_noise() on its 'content' field.

        Args:
            messages: List of message dicts with at least a 'content' key.

        Returns:
            Filtered list (shallow copy — original dicts are reused, not cloned).

        Note:
            The count of filtered messages (len(input) - len(output)) is reported
            as ExtractionResult.noise_filtered in the upstream pipeline.
        """
        filtered = []
        for msg in messages:
            content = msg.get("content", "")
            if not self.is_noise(content):
                filtered.append(msg)
        return filtered
