"""Helper for OpenCode plugin: append a pre-built block to today's daily log.

Reads JSON from stdin: {"slug": "...", "sessionId": "...", "block": "..."}
Appends `block` to $LLM_WIKI_ROOT/knowledge/daily/<date>.md.

Why this exists: the OpenCode plugin does LLM work in JS (via OpenCode SDK),
then needs to write the result to a markdown file. Calling Python for the
file I/O keeps path handling cross-platform and reuses the canonical
daily-log location without re-implementing it in JS.

Never fails — always exits 0. Errors go to stderr.
"""
from __future__ import annotations

import contextlib
import hashlib
import io
import json
import os
import sys
import time
from collections.abc import Callable
from datetime import datetime
from pathlib import Path

if hasattr(sys.stdout, "reconfigure"):
    try:
        sys.stdout.reconfigure(encoding="utf-8")
    except (AttributeError, io.UnsupportedOperation):
        pass

from markdown_transaction import append_knowledge, stable_operation_id  # noqa: E402
from memory_state import STATE_ROOT, _is_pid_alive  # noqa: E402
from secret_redact import redact_secrets  # noqa: E402


@contextlib.contextmanager
def _daily_lock(timeout: float = 10.0, poll: float = 0.05):
    """Cross-platform advisory lock via O_CREAT|O_EXCL on a sidecar file.

    Same atomic pattern as memory_state._state_lock. Works on Windows AND
    POSIX. Fail-closed: raises TimeoutError if lock can't be acquired.

    Includes stale-lock recovery: if the lock file is older than 30s,
    checks if the owner PID is alive. If dead, steals the lock. If alive,
    waits. This prevents perpetual lockout if a holder crashes.
    """
    lock_file = STATE_ROOT / "run" / "daily-append.lock"
    lock_file.parent.mkdir(parents=True, exist_ok=True)
    deadline = time.time() + timeout
    owner_pid = str(os.getpid())
    fd: int | None = None
    while True:
        try:
            fd = os.open(str(lock_file), os.O_CREAT | os.O_EXCL | os.O_RDWR)
            os.write(fd, owner_pid.encode("utf-8"))
            break
        except FileExistsError:
            try:
                age = time.time() - lock_file.stat().st_mtime
            except OSError:
                age = 0.0
            if age > 30.0:
                # Check if owner is alive before stealing
                try:
                    prev_pid = int(lock_file.read_text(encoding="utf-8").strip())
                    alive = _is_pid_alive(prev_pid)
                    if alive:
                        if time.time() > deadline:
                            raise TimeoutError(
                                f"Could not acquire daily-log lock: {lock_file}"
                            )
                        time.sleep(poll)
                        continue
                except TimeoutError:
                    raise
                except (ValueError, OSError):
                    pass
                try:
                    lock_file.unlink()
                except OSError:
                    pass
                if time.time() > deadline:
                    raise TimeoutError(
                        f"Could not acquire daily-log lock: {lock_file}"
                    )
                continue
            if time.time() > deadline:
                raise TimeoutError(f"Could not acquire daily-log lock: {lock_file}")
            time.sleep(poll)
    try:
        yield
    finally:
        if fd is not None:
            try:
                os.close(fd)
            except OSError:
                pass
        # Owner-aware deletion
        try:
            current = lock_file.read_text(encoding="utf-8").strip()
            if current == owner_pid:
                lock_file.unlink()
        except OSError:
            pass


def locked_append(
    daily_path: Path,
    text: str,
    operation_id: str | None = None,
    *,
    deadline: float = float("inf"),
    cancelled: Callable[[], bool] | None = None,
) -> None:
    """Append text to a daily-log file under the shared cross-process lock.

    This is the lowest-level locked writer. Higher-level functions like
    ``append_daily()`` and external callers (``flush_memory``,
    ``session_end_project_tag``) both delegate here so that all daily-log
    writes share a single serialization point.
    """
    text = redact_secrets(text)
    header = f"# Daily Session Memory — {daily_path.stem}\n".encode()
    if not daily_path.exists():
        append_knowledge(
            stable_operation_id("daily-header", daily_path.name, header),
            daily_path,
            header,
            deadline=deadline,
            cancelled=cancelled,
        )
    block = (text if text.endswith("\n") else text + "\n").encode("utf-8")
    append_knowledge(
        operation_id,
        daily_path,
        block,
        deadline=deadline,
        cancelled=cancelled,
    )


def locked_append_once(
    daily_path: Path,
    text: str,
    operation_id: str,
    *,
    deadline: float = float("inf"),
    cancelled: Callable[[], bool] | None = None,
) -> bool:
    """Append one operation exactly once while holding the daily-log lock."""
    if not operation_id:
        raise ValueError("operation_id must be non-empty")
    marker_id = hashlib.sha256(operation_id.encode("utf-8")).hexdigest()
    marker = f"<!-- llm-wiki-operation:{marker_id} -->"
    if daily_path.exists() and marker in daily_path.read_text(encoding="utf-8"):
        return False
    header = f"# Daily Session Memory — {daily_path.stem}\n".encode()
    if not daily_path.exists():
        append_knowledge(
            stable_operation_id("daily-header", daily_path.name, header),
            daily_path,
            header,
            deadline=deadline,
            cancelled=cancelled,
        )
    block = redact_secrets(
        f"\n{marker}\n{text}{'' if text.endswith(chr(10)) else chr(10)}"
    ).encode("utf-8")
    append_knowledge(
        operation_id,
        daily_path,
        block,
        deadline=deadline,
        cancelled=cancelled,
    )
    return True


def append_daily(
    slug: str,
    session_id: str,
    block: str,
    operation_id: str | None = None,
    *,
    deadline: float = float("inf"),
    cancelled: Callable[[], bool] | None = None,
) -> Path:
    """Append a pre-built block to today's daily log (unified locked writer).

    This is the SINGLE entry point all daily-log writers must use. It
    acquires the cross-process ``_daily_lock()`` so concurrent hooks
    (UserPromptSubmit, PostToolUse, flush_memory) cannot interleave
    their writes and corrupt the daily file.

    Args:
        slug: Project slug (for context — included in the block by caller).
        session_id: Session identifier (for context — included by caller).
        block: The pre-formatted markdown block to append.

    Returns:
        Path to the daily log file that was written.
    """
    root = Path(
        os.environ.get("LLM_WIKI_ROOT", str(Path(__file__).resolve().parent.parent))
    ).resolve()
    daily_dir = root / "knowledge" / "daily"
    day = datetime.now().strftime("%Y-%m-%d")
    path = daily_dir / f"{day}.md"
    text = "\n" + block if not block.startswith("\n") else block
    if operation_id:
        locked_append_once(
            path,
            text,
            operation_id,
            deadline=deadline,
            cancelled=cancelled,
        )
    else:
        locked_append(
            path,
            text,
            deadline=deadline,
            cancelled=cancelled,
        )
    return path


def main() -> int:
    try:
        raw = sys.stdin.read()
        payload = json.loads(raw) if raw.strip() else {}
    except (json.JSONDecodeError, OSError):
        return 0

    if not isinstance(payload, dict):
        return 0

    block = payload.get("block") or ""
    if not block:
        return 0
    block = redact_secrets(block)

    try:
        event_id = payload.get("eventId") or payload.get("operationId")
        operation_id = f"daily-event:{event_id}" if isinstance(event_id, str) and event_id else None
        append_daily(
            payload.get("slug", ""),
            payload.get("sessionId", ""),
            block,
            operation_id=operation_id,
        )
    except OSError as e:
        print(f"daily_log_append: write failed: {type(e).__name__}: {e}", file=sys.stderr)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
