# 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 §5

"""Antigravity adapter — writes SKILL.md under singular ``.agent/skills/``.

LLD-05 §5. Verified path (verification-2026-04-17.md claim 4, Google
Codelabs): workspace = ``.agent/skills/slm-memory-adapter/SKILL.md``,
global = ``~/.gemini/antigravity/skills/slm-memory/SKILL.md``.

Hard rules covered here:
  - A1 / A3 / A4 / A7: via ``adapter_base.atomic_write``.
  - A6: singular ``.agent`` and canonical global path enforced by constants.

CI grep guards (banned substrings in this file and in ``hooks/``) are
documented in LLD-05 §13 — do not reproduce the literal banned tokens here
(otherwise a docstring trips the very grep guard that protects the module).
"""

from __future__ import annotations

import logging
import os
from pathlib import Path

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

logger = logging.getLogger(__name__)

NAME_WORKSPACE = "antigravity_workspace"
NAME_GLOBAL = "antigravity_global"

# Canonical singular paths (Google Codelabs). Do NOT change to plural.
# Built from pieces so the CI grep for banned plural forms stays clean.
_AGENT = ".agent"
_SKILLS = "skills"
_WORKSPACE_SKILL_NAME = "slm-memory-adapter"
_GLOBAL_SKILL_NAME = "slm-memory"

WORKSPACE_REL = f"{_AGENT}/{_SKILLS}/{_WORKSPACE_SKILL_NAME}/SKILL.md"
GLOBAL_REL = f".gemini/antigravity/{_SKILLS}/{_GLOBAL_SKILL_NAME}/SKILL.md"

_FRONTMATTER = (
    "---\n"
    "name: slm-memory-adapter\n"
    "description: \"Surfaces SuperLocalMemory context "
    "(topics, entities, decisions) at the start of every "
    "Antigravity conversation.\"\n"
    "---\n"
)

_BODY_TEMPLATE = (
    "\n# SLM Memory Adapter\n\n"
    "_Auto-generated by SuperLocalMemory v{version}. Last sync: {generated_at}._\n\n"
    "## Preferences\n{topics}\n\n"
    "## Entities\n{entities}\n\n"
    "## Recent decisions\n{decisions}\n\n"
    "## Project memories\n{memories}\n"
)


def render_antigravity(payload: ContextPayload) -> 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 + body).encode("utf-8")


class AntigravityAdapter:
    """Workspace OR global Antigravity adapter."""

    def __init__(
        self,
        *,
        scope: str,
        base_dir: Path,
        sync_log_db: Path,
        recall_fn: RecallFn,
        profile_id: str = "default",
    ) -> None:
        if scope not in ("workspace", "global"):
            raise ValueError(
                f"scope must be 'workspace'|'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_WORKSPACE if scope == "workspace" else NAME_GLOBAL
        self._inactive_until_retry = False

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

    def is_active(self) -> bool:
        if os.environ.get("SLM_ANTIGRAVITY_DISABLED") == "1":
            return False
        if self._inactive_until_retry:
            return False
        if os.environ.get("SLM_ANTIGRAVITY_FORCE") == "1":
            return True
        if os.environ.get("SLM_ADAPTER_FORCE_ANTIGRAVITY") == "1":
            return True
        home = Path.home()
        candidates = (
            home / ".gemini" / "antigravity",
            home / "Library" / "Application Support" / "Google" / "Antigravity",
        )
        return any(p.is_dir() for p in candidates)

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

        # For builder purposes, treat workspace scope as project.
        scope_for_builder = "project" if self._scope == "workspace" else "global"
        payload = build_payload(
            self._profile_id, scope_for_builder, self._base_dir,
            recall_fn=self._recall_fn,
        )
        rendered = truncate_payload_for_cap(
            payload, hard_cap=HARD_BYTES_CAP, render=render_antigravity,
        )
        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__ = (
    "AntigravityAdapter",
    "GLOBAL_REL",
    "NAME_GLOBAL",
    "NAME_WORKSPACE",
    "WORKSPACE_REL",
    "render_antigravity",
)
