"""Field projection and response size guard for MCGhidra.

Provides jq-style field projection, grep filtering, and token budget
enforcement to prevent oversized MCP tool results.
"""

import json
import re
import time
from typing import Any, Dict, List, Optional

from ..config import get_config

# Token estimation (same ratio as pagination.py)
TOKEN_ESTIMATION_RATIO = 4.0


def project_fields(items: List[Any], fields: List[str]) -> List[Any]:
    """Select only specified keys from each item (jq-style projection).

    Works on dicts and strings. For dicts, returns only the requested
    keys. For non-dict items (e.g. lines of decompiled code), returns
    them unchanged.

    Args:
        items: List of items to project
        fields: List of field names to keep

    Returns:
        List of projected items
    """
    if not fields or not items:
        return items

    field_set = set(fields)
    projected = []
    for item in items:
        if isinstance(item, dict):
            projected.append({k: v for k, v in item.items() if k in field_set})
        else:
            projected.append(item)
    return projected


def apply_grep(items: List[Any], pattern: str, ignorecase: bool = True) -> List[Any]:
    """Filter items by regex pattern across all string values.

    Searches all string-coercible values in each item. For dicts,
    searches all values recursively. For strings, searches directly.

    Args:
        items: List of items to filter
        pattern: Regex pattern string
        ignorecase: Case-insensitive matching (default True)

    Returns:
        Filtered list of matching items
    """
    if not pattern or not items:
        return items

    flags = re.IGNORECASE if ignorecase else 0
    compiled = re.compile(pattern, flags)

    return [item for item in items if _matches(item, compiled)]


def _matches(item: Any, pattern: re.Pattern, depth: int = 0) -> bool:
    """Check if item matches pattern (recursive for nested structures)."""
    if depth > 10:
        return False

    if isinstance(item, dict):
        for value in item.values():
            if isinstance(value, str) and pattern.search(value):
                return True
            elif isinstance(value, (int, float)):
                if pattern.search(str(value)):
                    return True
            elif isinstance(value, (dict, list, tuple)):
                if _matches(value, pattern, depth + 1):
                    return True
        return False
    elif isinstance(item, (list, tuple)):
        return any(_matches(i, pattern, depth + 1) for i in item)
    elif isinstance(item, str):
        return bool(pattern.search(item))
    else:
        return bool(pattern.search(str(item)))


def _estimate_tokens(data: Any) -> int:
    """Estimate token count from serialized JSON size.

    Uses a simple heuristic: ~4 characters per token on average.
    This matches the TOKEN_ESTIMATION_RATIO constant.

    Args:
        data: Any JSON-serializable data structure

    Returns:
        Estimated token count
    """
    text = json.dumps(data, default=str)
    return int(len(text) / TOKEN_ESTIMATION_RATIO)


def _extract_available_fields(items: List[Any]) -> List[str]:
    """Extract the set of field names from the first few dict items.

    Samples up to 5 items to discover available keys, useful for
    suggesting field projections to reduce response size.

    Args:
        items: List of items (only dicts are examined)

    Returns:
        Sorted list of unique field names (excludes internal _links)
    """
    fields = set()
    for item in items[:5]:
        if isinstance(item, dict):
            fields.update(item.keys())
    # Remove internal/HATEOAS fields
    fields.discard("_links")
    return sorted(fields)


def estimate_and_guard(
    data: List[Any],
    tool_name: str,
    budget: Optional[int] = None,
    query_hints: Optional[Dict[str, Any]] = None,
) -> Optional[Dict[str, Any]]:
    """Check if data exceeds token budget; return guard response if so.

    If data fits within budget, returns None (caller should proceed
    normally). If data exceeds budget, returns a structured summary
    with instructions for narrowing the query.

    Args:
        data: The full data list to check
        tool_name: Name of the tool (for hint messages)
        budget: Token budget override (defaults to config.max_response_tokens)
        query_hints: Original query params (for building hint commands)

    Returns:
        None if data fits within budget, or a guard response dict
    """
    config = get_config()
    if budget is None:
        budget = config.max_response_tokens

    estimated = _estimate_tokens(data)
    if estimated <= budget:
        return None

    # Build sample from first 3 items
    sample = data[:3]
    available_fields = _extract_available_fields(data)

    # Build actionable hints based on the tool name
    hints = _build_hints(tool_name, available_fields, query_hints)

    return {
        "success": True,
        "guarded": True,
        "total_count": len(data),
        "estimated_tokens": estimated,
        "budget": budget,
        "sample": sample,
        "available_fields": available_fields,
        "message": (
            "Response too large (%d items, ~%s tokens, budget: %s). "
            "To read this data:\n%s"
        ) % (
            len(data),
            _format_tokens(estimated),
            _format_tokens(budget),
            hints,
        ),
        "timestamp": int(time.time() * 1000),
    }


def _format_tokens(n: int) -> str:
    """Format token count for human-readable display.

    Large numbers are abbreviated with 'k' suffix for readability
    in error messages and hints.

    Args:
        n: Token count

    Returns:
        Formatted string (e.g., 45000 -> '45k', 500 -> '500')
    """
    if n >= 1000:
        return "%dk" % (n // 1000)
    return str(n)


def _build_hints(
    tool_name: str,
    available_fields: List[str],
    query_hints: Optional[Dict[str, Any]] = None,
) -> str:
    """Build actionable hint text for the guard message."""
    lines = []

    # Pagination hint
    lines.append(
        "  - Paginate: %s(page_size=50) then cursor_next(cursor_id='...')"
        % tool_name
    )

    # Grep hint
    grep_example = "main" if "functions" in tool_name else ".*pattern.*"
    lines.append(
        "  - Filter: %s(grep='%s')" % (tool_name, grep_example)
    )

    # Fields hint (only if we have dict items with fields)
    if available_fields:
        short_fields = available_fields[:2]
        lines.append(
            "  - Project: %s(fields=%s)" % (tool_name, short_fields)
        )

    # Combined hint
    if available_fields:
        lines.append(
            "  - Combine: %s(grep='...', fields=%s, return_all=True)"
            % (tool_name, available_fields[:2])
        )

    return "\n".join(lines)
