"""
InsAIts SDK - Task Anchor Injection (Phase 3B)
===============================================
Auto-injects user anchor + memory check messages at every new task boundary.

The TaskAnchorManager:
- Loads anchor text from markdown files (hot-reload on file change via mtime)
- Detects task boundaries via heuristic (tool name change or time gap)
- Builds combined injection text for the hook to inject

Author: Cristi Bogdan -- Steddy Nova SRL / YuyAI
"""

import logging
import os
import time
from pathlib import Path
from typing import Optional

logger = logging.getLogger("insaits.task_anchor")

# Default paths relative to package directory
# Prefer user_anchor.md (user-created), fall back to user_anchor.example.md
_PACKAGE_DIR = Path(__file__).parent
_ANCHOR_PATH = _PACKAGE_DIR / "user_anchor.md"
_ANCHOR_EXAMPLE = _PACKAGE_DIR / "user_anchor.example.md"
DEFAULT_ANCHOR_FILE = str(
    _ANCHOR_PATH if (_ANCHOR_PATH.exists() and _ANCHOR_PATH.stat().st_size > 0) else _ANCHOR_EXAMPLE
)

_MEMORY_PATH = _PACKAGE_DIR / "memory_check.md"
_MEMORY_EXAMPLE = _PACKAGE_DIR / "memory_check.example.md"
DEFAULT_MEMORY_CHECK_FILE = str(
    _MEMORY_PATH if (_MEMORY_PATH.exists() and _MEMORY_PATH.stat().st_size > 0) else _MEMORY_EXAMPLE
)

# Time gap (seconds) that signals a new task boundary
NEW_TASK_GAP_SECONDS = 120


class TaskAnchorManager:
    """Manages task anchor injection at task boundaries.

    Loads user anchor and memory check text from markdown files,
    with hot-reload support (re-reads file if mtime changes).
    Detects new task boundaries via heuristic.
    """

    def __init__(
        self,
        anchor_file: str = DEFAULT_ANCHOR_FILE,
        memory_check_file: str = DEFAULT_MEMORY_CHECK_FILE,
        enabled: bool = True,
        memory_check_enabled: bool = True,
    ) -> None:
        self._anchor_file = anchor_file
        self._memory_check_file = memory_check_file
        self._enabled = enabled
        self._memory_check_enabled = memory_check_enabled

        # Cache for hot-reload: (mtime, content)
        self._anchor_cache: Optional[tuple] = None
        self._memory_check_cache: Optional[tuple] = None

        # Task boundary tracking
        self._last_tool_name: Optional[str] = None
        self._last_tool_time: float = 0.0

    @property
    def enabled(self) -> bool:
        return self._enabled

    @enabled.setter
    def enabled(self, value: bool) -> None:
        self._enabled = value

    @property
    def memory_check_enabled(self) -> bool:
        return self._memory_check_enabled

    @memory_check_enabled.setter
    def memory_check_enabled(self, value: bool) -> None:
        self._memory_check_enabled = value

    def _read_file_with_cache(
        self, file_path: str, cache: Optional[tuple]
    ) -> tuple:
        """Read file content with mtime-based hot-reload.

        Returns (mtime, content) tuple. Re-reads only if mtime changed.
        """
        try:
            current_mtime = os.path.getmtime(file_path)
        except OSError:
            logger.warning("Anchor file not found: %s", file_path)
            return (0.0, "")

        if cache is not None and cache[0] == current_mtime:
            return cache

        try:
            content = Path(file_path).read_text(encoding="utf-8").strip()
            return (current_mtime, content)
        except OSError as exc:
            logger.warning("Failed to read anchor file %s: %s", file_path, exc)
            return (0.0, "")

    def get_anchor_text(self) -> str:
        """Load user anchor text from file (hot-reload on mtime change)."""
        self._anchor_cache = self._read_file_with_cache(
            self._anchor_file, self._anchor_cache
        )
        return self._anchor_cache[1]

    def get_memory_check_text(self) -> str:
        """Load memory check text from file (hot-reload on mtime change)."""
        self._memory_check_cache = self._read_file_with_cache(
            self._memory_check_file, self._memory_check_cache
        )
        return self._memory_check_cache[1]

    def build_injection(self) -> str:
        """Build combined injection text from enabled sources.

        Returns empty string if nothing is enabled.
        """
        if not self._enabled:
            return ""

        parts = []

        anchor_text = self.get_anchor_text()
        if anchor_text:
            parts.append(f"[USER ANCHOR]\n{anchor_text}")

        if self._memory_check_enabled:
            memory_text = self.get_memory_check_text()
            if memory_text:
                parts.append(f"[MEMORY CHECK]\n{memory_text}")

        return "\n\n".join(parts)

    def is_new_task(
        self,
        tool_name: str,
        tool_input: dict,
        previous_tool_name: Optional[str] = None,
        is_subagent: bool = False,
    ) -> bool:
        """Detect if this tool call represents a new task boundary.

        Heuristic:
        1. Time gap > NEW_TASK_GAP_SECONDS since last tool call -> new task
        2. First tool call ever -> new task
        3. previous_tool_name provided and differs from tool_name,
           AND the previous was None (non-tool) -> new task
        4. Agent tool call (subagent spawn) -> new task (subagent gets anchor)
        5. is_subagent=True and tool is the first call from that subagent

        Uses internal state (_last_tool_name, _last_tool_time) for tracking.
        The previous_tool_name parameter overrides internal state if provided.
        """
        now = time.time()

        # First call ever is always a new task
        if self._last_tool_time == 0.0:
            self._last_tool_name = tool_name
            self._last_tool_time = now
            return True

        # Agent tool spawning a subagent = new task boundary
        # The subagent gets the anchor so it stays on track
        if tool_name == "Agent":
            self._last_tool_name = tool_name
            self._last_tool_time = now
            return True

        # First call from a subagent context = new task boundary
        if is_subagent and self._last_tool_name != "__subagent__":
            self._last_tool_name = "__subagent__"
            self._last_tool_time = now
            return True

        # Time gap heuristic
        elapsed = now - self._last_tool_time
        if elapsed > NEW_TASK_GAP_SECONDS:
            self._last_tool_name = tool_name
            self._last_tool_time = now
            return True

        # Tool transition heuristic: previous was None (non-tool) -> tool
        prev = previous_tool_name if previous_tool_name is not None else self._last_tool_name
        if prev is None and tool_name is not None:
            self._last_tool_name = tool_name
            self._last_tool_time = now
            return True

        # Continuation — not a new task
        self._last_tool_name = tool_name
        self._last_tool_time = now
        return False
