"""Shared constants and utilities for all analyzers."""

import re

# ---------------------------------------------------------------------------
# Tool name sets
# ---------------------------------------------------------------------------

SHELL_TOOLS = {"Bash", "shell_command", "exec_command", "run_shell_command", "run_terminal_cmd"}
EDIT_TOOLS = {"Edit", "Write", "MultiEdit", "MultiEditTool", "replace"}
EXPLORE_TOOLS = {"Read", "Grep", "Glob", "read_file", "grep_search", "codebase_search", "list_dir", "file_search"}
PLAN_TOOLS = {"TodoWrite", "TodoRead", "TaskCreate", "TaskUpdate", "ExitPlanMode"}
DELEGATION_TOOLS = {"Task", "TaskOutput"}
WEB_TOOLS = {"WebSearch", "WebFetch"}

_TOOL_CATEGORY_MAP = {}
for _name in SHELL_TOOLS:
    _TOOL_CATEGORY_MAP[_name] = "shell"
for _name in EDIT_TOOLS:
    _TOOL_CATEGORY_MAP[_name] = "edit"
for _name in EXPLORE_TOOLS:
    _TOOL_CATEGORY_MAP[_name] = "explore"
for _name in PLAN_TOOLS:
    _TOOL_CATEGORY_MAP[_name] = "plan"
for _name in DELEGATION_TOOLS:
    _TOOL_CATEGORY_MAP[_name] = "delegation"
for _name in WEB_TOOLS:
    _TOOL_CATEGORY_MAP[_name] = "web"


def categorize_tool(tool_name: str) -> str:
    """Categorize a tool name into shell/edit/explore/plan/delegation/web/mcp/other."""
    if not tool_name:
        return "other"
    if tool_name in _TOOL_CATEGORY_MAP:
        return _TOOL_CATEGORY_MAP[tool_name]
    if tool_name.startswith("mcp__"):
        return "mcp"
    return "other"


# ---------------------------------------------------------------------------
# Content cleaning
# ---------------------------------------------------------------------------

_CODEX_INSTRUCTIONS_RE = re.compile(r"^<INSTRUCTIONS>.*?</INSTRUCTIONS>\s*", re.DOTALL)
_CODEX_ENV_CONTEXT_RE = re.compile(r"<environment_context>.*?</environment_context>\s*", re.DOTALL)
_CODEX_AGENTS_MD_RE = re.compile(r"^# AGENTS\.md\b.*", re.DOTALL)
_CLAUDE_COMMAND_RE = re.compile(r"<command-name>.*?</command-name>", re.DOTALL)
_GEMINI_FILE_REF_MARKER = "--- Content from referenced files ---"

COMPACTION_PREFIX = "This session is being continued from a previous conversation"


def clean_user_content(content: str, source: str = "") -> str:
    """Strip source-specific noise from user message content."""
    if not content:
        return ""

    if is_compaction_message(content):
        return ""

    source_lower = (source or "").lower()

    if "codex" in source_lower:
        content = _CODEX_INSTRUCTIONS_RE.sub("", content)
        content = _CODEX_ENV_CONTEXT_RE.sub("", content)
        content = _CODEX_AGENTS_MD_RE.sub("", content)

    if "gemini" in source_lower:
        idx = content.find(_GEMINI_FILE_REF_MARKER)
        if idx != -1:
            content = content[:idx]

    if "claude" in source_lower:
        content = _CLAUDE_COMMAND_RE.sub("", content)

    stripped = content.strip()

    # Claude Code warmup/init messages are not real prompts
    if stripped.lower() in _WARMUP_TOKENS:
        return ""

    return stripped


# Tokens that represent warmup/init messages, not real user prompts
_WARMUP_TOKENS = {"warmup", "init", "initialize", "startup"}


def is_compaction_message(content: str) -> bool:
    """Check if a message is a compaction continuation."""
    return bool(content and content.startswith(COMPACTION_PREFIX))


def is_codex_system_message(content: str) -> bool:
    """Check if content is a Codex system-injected message (XML preamble)."""
    if not content:
        return False
    return content.lstrip().startswith(("<INSTRUCTIONS>", "<environment_context>", "# AGENTS.md"))


def get_first_user_prompt(messages: list[dict], source: str = "") -> str:
    """Extract the first real user prompt from a message list, after noise stripping.

    messages: list of dicts with 'msg_type' and 'content' keys.
    """
    for m in messages:
        if m.get("msg_type") != "user":
            continue
        content = m.get("content")
        if not isinstance(content, str):
            continue
        cleaned = clean_user_content(content, source)
        if cleaned:
            return cleaned
    return ""


# ---------------------------------------------------------------------------
# Interrupt pattern detection
# ---------------------------------------------------------------------------

INLINE_GUIDANCE_SIG = "the user said:"
STOP_SIG = "doesn't want to take this action"
REJECTED_SIG = "was rejected"
# Claude Code interrupt signatures
CLAUDE_REJECTED_SIG = "was rejected"
CODEX_ABORT_SIG = "<turn_aborted>"


def detect_interrupt_pattern(tool_result_output: str) -> dict | None:
    """Detect interrupt pattern from tool_result output text.

    Returns {"pattern": "inline"|"rejected"|"stop"|"codex_abort", "guidance_text": str|None}
    or None if no interrupt pattern found.
    """
    if not tool_result_output:
        return None

    # Codex abort
    if CODEX_ABORT_SIG in tool_result_output:
        return {"pattern": "codex_abort", "guidance_text": None}

    # Check inline guidance first (most specific)
    lower = tool_result_output.lower()
    sig_lower = INLINE_GUIDANCE_SIG.lower()
    if sig_lower in lower:
        idx = lower.index(sig_lower)
        guidance = tool_result_output[idx + len(INLINE_GUIDANCE_SIG):].strip()
        return {"pattern": "inline", "guidance_text": guidance}

    # Stop signal
    if STOP_SIG in tool_result_output:
        return {"pattern": "stop", "guidance_text": None}

    # Rejected only
    if REJECTED_SIG in tool_result_output:
        return {"pattern": "rejected", "guidance_text": None}

    return None


def extract_inline_guidance(tool_result_output: str) -> str | None:
    """Extract inline guidance text from a tool_result output.

    Looks for 'the user said:' marker and returns everything after it.
    """
    if not tool_result_output:
        return None
    lower = tool_result_output.lower()
    sig_lower = INLINE_GUIDANCE_SIG.lower()
    if sig_lower in lower:
        idx = lower.index(sig_lower)
        return tool_result_output[idx + len(INLINE_GUIDANCE_SIG):].strip()
    return None


# ---------------------------------------------------------------------------
# Message stream helpers (work with build_message_stream output)
# ---------------------------------------------------------------------------

def get_tool_name_from_msg(msg: dict) -> str:
    """Get tool_name from a unified message stream entry."""
    return msg.get("tool_name") or ""


def get_tool_input_from_msg(msg: dict) -> str:
    """Get tool_input as string from a unified message stream entry."""
    ti = msg.get("tool_input")
    if ti is None:
        return ""
    if isinstance(ti, dict):
        # For shell tools, return command; for edit tools, return as string
        return ti.get("command") or ti.get("file_path") or str(ti)
    return str(ti)


def get_tool_input_dict(msg: dict) -> dict:
    """Get tool_input as dict from a unified message stream entry."""
    ti = msg.get("tool_input")
    if isinstance(ti, dict):
        return ti
    return {}


# ---------------------------------------------------------------------------
# Session-level detection helpers (work with message stream lists)
# ---------------------------------------------------------------------------

def has_interrupts(messages: list[dict]) -> int:
    """Count interrupts in a message list. Works with unified stream (result_output key)."""
    count = 0
    for m in messages:
        if m.get("msg_type") == "tool_result":
            output = m.get("result_output") or m.get("content") or ""
            if detect_interrupt_pattern(output) is not None:
                count += 1
    return count


def ai_asked_clarification(messages: list[dict]) -> bool:
    """Check if AI asked real clarification questions in the message list."""
    clarification_pats = [
        "would you like me to", "do you want me to", "should i ",
        "could you clarify", "which one", "before i proceed", "would you prefer",
    ]
    signoff_pats = ["let me know", "feel free to", "if you have any"]

    for m in messages:
        if m.get("msg_type") != "assistant":
            continue
        content = (m.get("content") or "").lower()
        if not content:
            continue
        is_real = any(pat in content for pat in clarification_pats)
        is_signoff_only = all(pat in content for pat in signoff_pats if pat in content) and not is_real
        if is_real and not is_signoff_only:
            return True
    return False


def has_error_cascade(messages: list[dict]) -> bool:
    """Check if message list has an error cascade (3+ consecutive tool errors)."""
    streak = 0
    for m in messages:
        if m.get("msg_type") == "tool_result":
            output = m.get("result_output") or m.get("content") or ""
            if is_shell_error(output):
                streak += 1
                if streak >= 3:
                    return True
            else:
                streak = 0
        elif m.get("msg_type") == "user":
            streak = 0
    return False


def has_undo_request(messages: list[dict]) -> bool:
    """Check if any user message contains an undo request."""
    undo_pats = ["undo", "revert this", "revert that", "revert the",
                 "roll back", "go back to", "put it back"]
    for m in messages:
        if m.get("msg_type") != "user":
            continue
        content = (m.get("content") or "").lower()
        if any(pat in content for pat in undo_pats):
            return True
    return False


# ---------------------------------------------------------------------------
# Shell error detection
# ---------------------------------------------------------------------------

_ERROR_PATTERNS = [
    re.compile(r"exit code[:\s]+[1-9]\d*", re.IGNORECASE),
    re.compile(r"\bError\b"),
    re.compile(r"\bFAILED\b"),
    re.compile(r"\berror:", re.IGNORECASE),
    re.compile(r"command not found"),
    re.compile(r"No such file"),
]


def is_shell_error(output: str) -> bool:
    """Check tool_result output for shell error indicators."""
    if not output:
        return False
    return any(p.search(output) for p in _ERROR_PATTERNS)
