"""Memory toolset for searching and reading memory files."""

from __future__ import annotations

import re
from pathlib import Path
from typing import TYPE_CHECKING

from agno.tools import Toolkit

if TYPE_CHECKING:
    from asedclaw.agent.memory_index import MemoryIndexManager


class MemoryToolkit(Toolkit):
    """Memory tools: hybrid search (vector+BM25) with term-matching fallback, and bounded file reads."""

    _DEFAULT_GET_LINES = 200
    _MAX_GET_LINES = 1000
    _MAX_SNIPPET_CHARS = 240

    def __init__(self, workspace: Path, index: MemoryIndexManager | None = None):
        self._workspace = workspace
        self._memory_dir = workspace / "memory"
        self._memory_file = self._memory_dir / "MEMORY.md"
        self._index = index
        super().__init__(name="memory_tools", tools=[self.memory_search, self.memory_get])

    def _resolve_allowed_path(self, path: str) -> Path:
        p = Path(path).expanduser()
        if not p.is_absolute():
            p = self._workspace / p
        resolved = p.resolve()

        if resolved == self._memory_file.resolve():
            return resolved

        try:
            rel = resolved.relative_to(self._memory_dir.resolve())
        except ValueError as exc:
            raise PermissionError(f"Path {path} is outside memory directory") from exc

        if rel.suffix.lower() != ".md":
            raise PermissionError(f"Path {path} is not an allowed markdown memory file")
        return resolved

    def _iter_memory_files(self) -> list[Path]:
        files: list[Path] = []
        if self._memory_file.exists():
            files.append(self._memory_file)
        if self._memory_dir.exists():
            for fp in sorted(self._memory_dir.glob("*.md")):
                if fp.name == "MEMORY.md":
                    continue
                files.append(fp)
        return files

    async def memory_get(self, path: str, from_line: int = 1, lines: int = _DEFAULT_GET_LINES) -> str:
        """Read a memory markdown file safely.

        Args:
            path: Workspace-relative or absolute path. Allowed: MEMORY.md or memory/*.md.
            from_line: 1-based start line.
            lines: Number of lines to read.
        """
        try:
            target = self._resolve_allowed_path(path)
            if not target.exists() or not target.is_file():
                return f"Error: Memory file not found: {path}"
            start = max(1, int(from_line))
            take = min(self._MAX_GET_LINES, max(1, int(lines)))
            all_lines = target.read_text(encoding="utf-8").splitlines()
            segment = all_lines[start - 1 : start - 1 + take]
            if not segment:
                return f"{target}\n(no content at requested range)"
            return f"{target}\n" + "\n".join(segment)
        except PermissionError as exc:
            return f"Error: {exc}"
        except Exception as exc:
            return f"Error reading memory file: {exc}"

    # Evidence quality thresholds
    _HIGH_CONFIDENCE_SCORE = 0.6
    _LOW_CONFIDENCE_SCORE = 0.3

    async def memory_search(self, query: str, max_results: int = 10, min_score: float = 0.0) -> str:
        """Search MEMORY.md and daily logs with hybrid vector+BM25 scoring (falls back to term matching)."""
        q = (query or "").strip()
        if not q:
            return "Error: query cannot be empty"

        limit = max(1, min(50, int(max_results)))

        # Try hybrid search if index is available
        if self._index is not None:
            try:
                results = await self._index.search_hybrid(q, limit)
                if results:
                    out = []
                    for r in results:
                        if r.score < min_score:
                            continue
                        out.append(
                            f"- score={r.score:.2f} path={r.path} lines={r.start_line}-{r.end_line} [{r.source}]\n"
                            f"  {r.snippet[:self._MAX_SNIPPET_CHARS]}"
                        )
                    if out:
                        return self._add_evidence_advisory(out, [r.score for r in results if r.score >= min_score])
                    return self._NO_EVIDENCE_MSG
            except Exception:
                pass  # fall through to term matching

        return self._term_search(q, limit, min_score)

    def _term_search(self, query: str, limit: int, min_score: float) -> str:
        """Lightweight term-matching fallback when no index is available."""
        terms = [t for t in re.split(r"\s+", query.lower()) if t]
        if not terms:
            return "Error: query has no valid terms"

        results: list[tuple[float, Path, int, str]] = []
        for fp in self._iter_memory_files():
            try:
                lines = fp.read_text(encoding="utf-8").splitlines()
            except Exception:
                continue
            for idx, line in enumerate(lines, start=1):
                text = line.strip()
                if not text:
                    continue
                lower = text.lower()
                hits = sum(1 for t in terms if t in lower)
                if hits == 0 and query.lower() not in lower:
                    continue
                score = hits / max(1.0, float(len(terms)))
                if query.lower() in lower:
                    score += 0.35
                if score < min_score:
                    continue
                snippet = text[: self._MAX_SNIPPET_CHARS]
                results.append((score, fp, idx, snippet))

        if not results:
            return self._NO_EVIDENCE_MSG

        results.sort(key=lambda x: x[0], reverse=True)
        out = []
        for score, fp, line_no, snippet in results[:limit]:
            out.append(f"- score={score:.2f} path={fp} line={line_no}\n  {snippet}")
        return self._add_evidence_advisory(out, [s for s, *_ in results[:limit]])

    # -- Evidence quality advisory --

    _NO_EVIDENCE_MSG = (
        "No matching memory entries found.\n"
        "[Evidence policy] No supporting evidence in memory. "
        "Do NOT fabricate facts — state clearly that no relevant memory was found."
    )

    def _add_evidence_advisory(self, lines: list[str], scores: list[float]) -> str:
        """Append an evidence-quality advisory based on top scores."""
        body = "\n".join(lines)
        if not scores:
            return body

        top = max(scores)
        if top >= self._HIGH_CONFIDENCE_SCORE:
            return body  # high confidence — no extra advisory needed
        if top >= self._LOW_CONFIDENCE_SCORE:
            return (
                body + "\n\n[Evidence policy] Results are low-confidence (best score {:.2f}). "
                "Cross-check with other sources before drawing conclusions.".format(top)
            )
        return (
            body + "\n\n[Evidence policy] Results are very weak (best score {:.2f}). "
            "Treat as insufficient evidence — do NOT base conclusions solely on these results. "
            "State the uncertainty to the user.".format(top)
        )
