"""Shared constants for the CoPilot module."""

# Special message prefixes for text-based markers (parsed by frontend).
# The hex suffix makes accidental LLM generation of these strings virtually
# impossible, avoiding false-positive marker detection in normal conversation.
COPILOT_ERROR_PREFIX = "[__COPILOT_ERROR_f7a1__]"  # Renders as ErrorCard
COPILOT_RETRYABLE_ERROR_PREFIX = (
    "[__COPILOT_RETRYABLE_ERROR_a9c2__]"  # ErrorCard + retry
)
COPILOT_SYSTEM_PREFIX = "[__COPILOT_SYSTEM_e3b0__]"  # Renders as system info message

# Canonical marker appended as an assistant ChatMessage when the SDK stream
# ends without a ResultMessage (user hit Stop).  Checked by exact equality
# at turn start so the next turn's --resume transcript doesn't carry it.
STOPPED_BY_USER_MARKER = f"{COPILOT_SYSTEM_PREFIX} Execution stopped by user"

# User-facing notice surfaced when the SDK CLI ends without a ResultMessage —
# almost always a CLI-side kill (per-query budget exhausted, max_turns hit,
# OOM, or crash). Not a user cancel: the cancel path raises
# ``asyncio.CancelledError`` and never reaches this branch.
STREAM_INCOMPLETE_MARKER = f"{COPILOT_SYSTEM_PREFIX} Reached the per-query spend limit or the assistant ended without a final response. Send a follow-up message to continue from here."

# SECRT-2333: surfaced when the SDK turn ends with ``ended_with_stream_error``
# (subtype=error ResultMessage, idle timeout, transient retries exhausted,
# circuit breaker) and no marker row has been persisted yet.  Without this,
# the on-wire StreamError dies with the SSE connection and a chat reload sees
# the history just stopping mid-task.
STREAM_ERROR_MARKER = f"{COPILOT_SYSTEM_PREFIX} The assistant ran into an error and stopped. Send a follow-up message to retry."

# Prefix for all synthetic IDs generated by CoPilot block execution.
# Used to distinguish CoPilot-generated records from real graph execution records
# in PendingHumanReview and other tables.
COPILOT_SYNTHETIC_ID_PREFIX = "copilot-"

# Sub-prefixes for session-scoped and node-scoped synthetic IDs.
COPILOT_SESSION_PREFIX = f"{COPILOT_SYNTHETIC_ID_PREFIX}session-"
COPILOT_NODE_PREFIX = f"{COPILOT_SYNTHETIC_ID_PREFIX}node-"

# Separator used in synthetic node_exec_id to encode node_id.
# Format: "{node_id}:{random_hex}" — extract node_id via rsplit(":", 1)[0]
COPILOT_NODE_EXEC_ID_SEPARATOR = ":"

# Compaction notice messages shown to users.
COMPACTION_DONE_MSG = "Earlier messages were summarized to fit within context limits."
COMPACTION_TOOL_NAME = "context_compaction"


# ---------------------------------------------------------------------------
# Tool / stream timing budget
# ---------------------------------------------------------------------------
# Max seconds any single MCP tool call may block the stream before returning
# a "still running" handle. Shared by run_agent (wait_for_result),
# view_agent_output (wait_if_running), run_sub_session (wait_for_result),
# get_sub_session_result (wait_if_running), and run_block (hard cap).
#
# Chosen so the stream idle timeout (2× this) always has headroom — a tool
# that returns right at the cap can't race the idle watchdog.
MAX_TOOL_WAIT_SECONDS = 5 * 60  # 5 minutes

# Idle-stream watchdog: abort the SDK stream if no meaningful event arrives
# for this long. Derived from MAX_TOOL_WAIT_SECONDS so the invariant
# "no tool blocks >= idle_timeout" holds by construction.
STREAM_IDLE_TIMEOUT_SECONDS = MAX_TOOL_WAIT_SECONDS * 2  # 10 minutes

# Redis key prefix for the SDK-stream-level lock that ensures only one
# active SDK stream per session. Released by the SDK turn's finally block
# and force-released by mark_session_completed on cancel/error.
STREAM_LOCK_PREFIX = "copilot:stream:lock:"


def is_copilot_synthetic_id(id_value: str) -> bool:
    """Check if an ID is a CoPilot synthetic ID (not from a real graph execution)."""
    return id_value.startswith(COPILOT_SYNTHETIC_ID_PREFIX)


def parse_node_id_from_exec_id(node_exec_id: str) -> str:
    """Extract node_id from a synthetic node_exec_id.

    Format: "{node_id}:{random_hex}" → returns "{node_id}".
    """
    return node_exec_id.rsplit(COPILOT_NODE_EXEC_ID_SEPARATOR, 1)[0]


# ---------------------------------------------------------------------------
# Transient Anthropic API error detection
# ---------------------------------------------------------------------------
# Patterns in error text that indicate a transient Anthropic API error
# which is retryable.  Covers:
#   - Connection-level: ECONNRESET, dropped TCP connections
#   - HTTP 429: rate-limit / too-many-requests
#   - HTTP 5xx: server errors
#
# Prefer specific status-code patterns over natural-language phrases
# (e.g. "overloaded", "bad gateway") — those phrases can appear in
# application-level SDK messages and would trigger spurious retries.
_TRANSIENT_ERROR_PATTERNS = (
    # Connection-level
    "socket connection was closed unexpectedly",
    "ECONNRESET",
    "connection was forcibly closed",
    "network socket disconnected",
    # 429 rate-limit patterns
    "rate limit",
    "rate_limit",
    "too many requests",
    "status code 429",
    # 5xx server error patterns (status-code-specific to avoid false positives)
    "status code 529",
    "status code 500",
    "status code 502",
    "status code 503",
    "status code 504",
)

FRIENDLY_TRANSIENT_MSG = (
    "Anthropic connection interrupted after repeated attempts — please try again later"
)


def is_transient_api_error(error_text: str) -> bool:
    """Return True if *error_text* matches a known transient Anthropic API error."""
    lower = error_text.lower()
    return any(pat.lower() in lower for pat in _TRANSIENT_ERROR_PATTERNS)
