# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
# Licensed under AGPL-3.0-or-later - see LICENSE file
# Part of SuperLocalMemory v3.4.22 — LLD-05 §4

"""Cursor adapter — writes ``.cursor/rules/*.mdc`` per-project AND global.

LLD-05 §4. Verified frontmatter (verification-2026-04-17.md claim 3):
    description, alwaysApply, globs — NOTHING else.

Hard rules covered here:
  - A1: ``safe_resolve`` on every write target.
  - A5: frontmatter limited to ``{description, alwaysApply, globs}``.
  - A3/A4/A7: via ``adapter_base.atomic_write``.
"""

from __future__ import annotations

import logging
import os
from pathlib import Path
from typing import Callable

from superlocalmemory.core.security_primitives import (
    PathTraversalError,
    safe_resolve,
)
from superlocalmemory.hooks.adapter_base import (
    HARD_BYTES_CAP,
    IS_POSIX,
    WriteResult,
    atomic_write,
    record_disable,
    truncate_to_cap,
)
from superlocalmemory.hooks.context_payload import (
    ContextPayload,
    RecallFn,
    VERSION,
    build_payload,
    format_decisions,
    format_entities,
    format_memories,
    format_topics,
    truncate_payload_for_cap,
)

logger = logging.getLogger(__name__)

NAME_PROJECT = "cursor_project"
NAME_GLOBAL = "cursor_global"

PROJECT_REL = ".cursor/rules/slm-active-brain.mdc"
GLOBAL_REL = ".cursor/rules/slm-global.mdc"

_FRONTMATTER_TEMPLATE = (
    "---\n"
    "description: \"SuperLocalMemory active-brain context — "
    "regenerated every 15 min\"\n"
    "alwaysApply: true\n"
    "globs: \"**/*\"\n"
    "---\n"
)

_BODY_TEMPLATE = (
    "# SLM — Active Brain Context\n\n"
    "_Auto-generated by SuperLocalMemory v{version}. Last sync: {generated_at}._\n\n"
    "## What I know about you\n{topics}\n\n"
    "## Entities you work with\n{entities}\n\n"
    "## Recent decisions\n{decisions}\n\n"
    "## Project memories\n{memories}\n\n"
    "---\n"
    "Regenerates every 15 min while the daemon is running.\n"
    "Disable: `slm connect cursor --disable`\n"
)


def render_cursor(payload: ContextPayload) -> bytes:
    """Render a Cursor ``.mdc`` file body (bytes)."""
    body = _BODY_TEMPLATE.format(
        version=payload.version,
        generated_at=payload.generated_at,
        topics=format_topics(payload),
        entities=format_entities(payload),
        decisions=format_decisions(payload),
        memories=format_memories(payload),
    )
    return (_FRONTMATTER_TEMPLATE + body).encode("utf-8")


class CursorAdapter:
    """Per-project OR global Cursor adapter (same class, different scope).

    A single class keeps the frontmatter + rendering + truncation logic
    identical between the two scopes — the only difference is the base
    directory and the rel-path.
    """

    def __init__(
        self,
        *,
        scope: str,
        base_dir: Path,
        sync_log_db: Path,
        recall_fn: RecallFn,
        profile_id: str = "default",
    ) -> None:
        if scope not in ("project", "global"):
            raise ValueError(f"scope must be 'project'|'global', got {scope!r}")
        self._scope = scope
        self._base_dir = Path(base_dir)
        self._sync_log_db = Path(sync_log_db)
        self._recall_fn = recall_fn
        self._profile_id = profile_id
        self.name = NAME_PROJECT if scope == "project" else NAME_GLOBAL
        self._inactive_until_retry = False

    # ------------------------------------------------------------------
    # Adapter protocol
    # ------------------------------------------------------------------

    @property
    def target_path(self) -> Path:
        rel = PROJECT_REL if self._scope == "project" else GLOBAL_REL
        return safe_resolve(self._base_dir, rel)

    def is_active(self) -> bool:
        if os.environ.get("SLM_CURSOR_DISABLED") == "1":
            return False
        if self._inactive_until_retry:
            return False
        if os.environ.get("SLM_CURSOR_FORCE") == "1":
            return True
        if os.environ.get("SLM_ADAPTER_FORCE_CURSOR") == "1":
            return True
        # Generous detection — any platform-specific Cursor directory counts.
        home = Path.home()
        candidates = (
            home / ".cursor",
            home / "Library" / "Application Support" / "Cursor",
            home / "AppData" / "Roaming" / "Cursor",
        )
        return any(p.is_dir() for p in candidates)

    def sync(self) -> bool:
        try:
            resolved = self.target_path
        except PathTraversalError:
            logger.warning("cursor: path-traversal refused; marking inactive")
            self._inactive_until_retry = True
            return False

        payload = build_payload(
            self._profile_id, self._scope, self._base_dir,
            recall_fn=self._recall_fn,
        )
        rendered = truncate_payload_for_cap(
            payload, hard_cap=HARD_BYTES_CAP, render=render_cursor,
        )
        rendered = truncate_to_cap(rendered, cap=HARD_BYTES_CAP)

        result: WriteResult = atomic_write(
            resolved, rendered,
            adapter_name=self.name,
            profile_id=self._profile_id,
            sync_log_db=self._sync_log_db,
        )
        return result.wrote

    def disable(self) -> None:
        try:
            resolved = self.target_path
        except PathTraversalError:
            return
        if resolved.exists():
            try:
                resolved.unlink()
            except OSError:  # pragma: no cover
                pass
        record_disable(
            resolved,
            adapter_name=self.name,
            profile_id=self._profile_id,
            sync_log_db=self._sync_log_db,
        )
        self._inactive_until_retry = True


__all__ = (
    "CursorAdapter",
    "GLOBAL_REL",
    "NAME_GLOBAL",
    "NAME_PROJECT",
    "PROJECT_REL",
    "render_cursor",
)
