"""
cortex/validation.py — Reusable input validators for all public API methods.

All validators:
  - Strip leading/trailing whitespace from string inputs
  - Reject null bytes (\\x00) with a clear error
  - Enforce maximum length limits with descriptive messages
  - Raise ValueError for all constraint violations

Usage::

    from cortex.validation import validate_str, validate_conversation

    subject = validate_str(subject, "subject", max_len=10_000)
    validate_conversation(messages)
"""
from __future__ import annotations

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


# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

MAX_CLAIM_FIELD_LEN = 10_000       # subject / predicate / object_value
MAX_QUERY_LEN = 8_000              # retrieve() query
MAX_CORNERSTONE_CONTENT_LEN = 5_000
MAX_CORNERSTONE_LABEL_LEN = 200
MAX_CONVERSATION_LEN = 100_000     # total chars across all messages
MAX_MESSAGE_LEN = 10_000           # individual message content

# Control characters to strip (keep printable ASCII + unicode; strip \x00-\x1f
# except common whitespace like \t \n \r, and \x7f DEL)
_CONTROL_CHARS_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")


# ---------------------------------------------------------------------------
# Core primitive validators
# ---------------------------------------------------------------------------

def validate_str(
    value: Any,
    field_name: str,
    *,
    max_len: int,
    allow_empty: bool = False,
) -> str:
    """
    Validate and sanitize a single string value.

    Steps:
      1. Assert it is a str (raises TypeError if not).
      2. Strip leading/trailing whitespace.
      3. Strip null bytes and control characters (\\x00-\\x08, \\x0b, \\x0c,
         \\x0e-\\x1f, \\x7f). Newlines (\\n), tabs (\\t), and carriage returns
         (\\r) are preserved.
      4. Reject empty strings (unless allow_empty=True).
      5. Enforce max_len — raises ValueError with field name + limit.

    Returns the sanitized string.
    """
    if not isinstance(value, str):
        raise TypeError(
            f"'{field_name}' must be a str, got {type(value).__name__!r}"
        )

    # Strip whitespace
    value = value.strip()

    # Strip null bytes / dangerous control chars
    original_len = len(value)
    value = _CONTROL_CHARS_RE.sub("", value)
    if len(value) != original_len:
        # Null byte specifically is worth calling out
        pass  # silently stripped; caller will see sanitized output

    if not allow_empty and not value:
        raise ValueError(
            f"'{field_name}' must not be empty or contain only whitespace"
        )

    if len(value) > max_len:
        raise ValueError(
            f"'{field_name}' exceeds maximum length: "
            f"{len(value):,} chars (limit: {max_len:,})"
        )

    return value


def validate_optional_str(
    value: Any,
    field_name: str,
    *,
    max_len: int,
) -> str | None:
    """Like validate_str but allows None, returning None unchanged."""
    if value is None:
        return None
    return validate_str(value, field_name, max_len=max_len, allow_empty=True)


# ---------------------------------------------------------------------------
# Domain-level validators
# ---------------------------------------------------------------------------

def validate_claim_fields(
    subject: str,
    predicate: str,
    object_value: str,
) -> tuple[str, str, str]:
    """
    Validate and sanitize the three core claim fields.

    Limits: subject / predicate / object_value — each max 10,000 chars.

    Returns: (subject, predicate, object_value) sanitized.
    Raises: ValueError on any violation.
    """
    subject = validate_str(subject, "subject", max_len=MAX_CLAIM_FIELD_LEN)
    predicate = validate_str(predicate, "predicate", max_len=MAX_CLAIM_FIELD_LEN)
    object_value = validate_str(
        object_value, "object_value", max_len=MAX_CLAIM_FIELD_LEN
    )
    return subject, predicate, object_value


def validate_retrieve_query(query: str) -> str:
    """
    Validate a retrieval query string.

    Limit: 2,000 chars.
    Returns sanitized query.
    """
    return validate_str(query, "query", max_len=MAX_QUERY_LEN)


def validate_cornerstone_fields(
    label: str,
    content: str,
) -> tuple[str, str]:
    """
    Validate cornerstone label and content.

    Limits: label 200 chars, content 5,000 chars.
    Returns (label, content) sanitized.
    """
    label = validate_str(label, "label", max_len=MAX_CORNERSTONE_LABEL_LEN)
    content = validate_str(content, "content", max_len=MAX_CORNERSTONE_CONTENT_LEN)
    return label, content


def validate_conversation(
    conversation: Any,
) -> List[Dict[str, Any]]:
    """
    Validate a conversation (list of message dicts).

    Rules:
      - Must be a non-empty list.
      - Total character length across all message content ≤ 100,000.
      - Individual message 'content' field ≤ 10,000 chars.
      - Strips null bytes / control chars from all string values.

    Accepts both:
      - List of dicts with 'content' key (standard chat format)
      - Plain text string (wrapped in a synthetic message list)

    Returns the (possibly sanitized) list of message dicts.
    Raises: ValueError or TypeError on violations.
    """
    # Accept plain text as a convenience
    if isinstance(conversation, str):
        conversation = validate_str(
            conversation, "conversation", max_len=MAX_CONVERSATION_LEN
        )
        return [{"role": "user", "content": conversation}]

    if not isinstance(conversation, list):
        raise TypeError(
            f"'conversation' must be a list of message dicts or a str, "
            f"got {type(conversation).__name__!r}"
        )

    if not conversation:
        raise ValueError("'conversation' must not be empty")

    total_len = 0
    sanitized: List[Dict[str, Any]] = []

    for idx, msg in enumerate(conversation):
        # Accept both dict messages and plain strings (e.g. ["user: hello"])
        if isinstance(msg, str):
            content = validate_str(
                msg,
                f"conversation[{idx}]",
                max_len=MAX_MESSAGE_LEN,
                allow_empty=True,
            )
            total_len += len(content)
            if total_len > MAX_CONVERSATION_LEN:
                raise ValueError(
                    f"Conversation total length exceeds maximum: "
                    f"{total_len:,} chars across {idx + 1} messages "
                    f"(limit: {MAX_CONVERSATION_LEN:,})"
                )
            sanitized.append({"role": "user", "content": content})
            continue

        if not isinstance(msg, dict):
            raise TypeError(
                f"'conversation[{idx}]' must be a dict or str, "
                f"got {type(msg).__name__!r}"
            )

        msg = dict(msg)  # shallow copy

        content = msg.get("content", "")
        if isinstance(content, str):
            content = validate_str(
                content,
                f"conversation[{idx}].content",
                max_len=MAX_MESSAGE_LEN,
                allow_empty=True,
            )
            msg["content"] = content
            total_len += len(content)
        elif content is None:
            total_len += 0
        else:
            # Non-string content (e.g. list of blocks) — just count chars
            total_len += len(str(content))

        if total_len > MAX_CONVERSATION_LEN:
            raise ValueError(
                f"Conversation total length exceeds maximum: "
                f"{total_len:,} chars across {idx + 1} messages "
                f"(limit: {MAX_CONVERSATION_LEN:,})"
            )

        sanitized.append(msg)

    return sanitized
