"""OneNote Import plugin — Windows COM-based importer.

Uses the OneNote desktop app (via pywin32 COM) to read notebooks / sections /
pages directly from the local cache — no Azure app registration, no OAuth.
Writes markdown + original XML into ``<vault>/<target_folder>/...`` where
the existing inbox watcher picks them up for KB ingest.

Registers:
    - tool:  ``onenote_import``
    - routes: ``GET /api/plugins/onenote-import/status``,
              ``POST /api/plugins/onenote-import/run``

See README.md for requirements, flow, and parameters.
"""

from __future__ import annotations

import asyncio
import sys
from typing import TYPE_CHECKING

from agntspace.plugins.base import PluginBase, PluginContext

from .com_client import OneNoteCOMClient
from .importer import OneNoteImporter
from .jobs import JobRegistry, JobState
from .router import make_router

if TYPE_CHECKING:
    from fastapi import APIRouter


def _format_completion_message(job: JobState) -> str:
    """Templated chat follow-up message posted on terminal state.

    Kept deterministic + short (no second LLM call) — the point is to
    close the conversational loop cheaply. Users who want richer
    post-import narration can ask the agent separately.
    """
    summary = job.summary or {}
    pages = int(summary.get("pages_imported") or job.pages_imported)
    skipped = int(summary.get("pages_skipped") or job.pages_skipped)
    errors = int(summary.get("errors_count") or len(job.errors))
    duration = float(summary.get("duration_s") or 0.0)
    target = job.target_folder or "OneNote"
    mins = int(duration // 60)
    secs = int(duration % 60)
    duration_str = f"{mins}m{secs:02d}s" if mins else f"{secs}s"

    if job.state == "completed":
        lines = [
            f"✅ OneNote import done — **{pages}** pages from **{job.notebooks_total}** notebook(s).",
            f"Skipped {skipped} · errors {errors} · took {duration_str}.",
            f"Files under `<vault>/{target}/` — the inbox watcher will pick them up.",
        ]
    elif job.state == "cancelled":
        lines = [
            f"⚠ OneNote import cancelled after **{pages}** pages (skipped {skipped}, errors {errors}).",
            f"Partial results are saved under `<vault>/{target}/`; re-running will resume from the state file.",
        ]
    else:  # failed or anything else
        err = job.error or "see /plugins for details"
        lines = [
            f"❌ OneNote import failed — {err}",
            f"Imported {pages} pages before failing ({duration_str}). Check /plugins → OneNote Import for the full error.",
        ]
    return "\n\n".join(lines)


class OneNoteImportPlugin(PluginBase):
    def __init__(self) -> None:
        self._ctx: PluginContext | None = None
        self._lock: asyncio.Lock | None = None
        self._router: APIRouter | None = None
        # One registry per plugin instance, shared between HTTP routes and
        # the agent tool handler — so /progress works regardless of which
        # path started the import.
        self._jobs = JobRegistry()

    def init(self, ctx: PluginContext) -> None:
        self._ctx = ctx
        ctx.log.debug("onenote-import plugin initializing")

        if sys.platform != "win32":
            ctx.log.info(
                "onenote-import: non-Windows platform (%s) — the plugin loads "
                "but imports will fail. Use on Windows with OneNote desktop installed.",
                sys.platform,
            )

        ctx.registry.register_tool(
            name="onenote_import",
            description=(
                "Import Microsoft OneNote notebooks into the vault as markdown. "
                "Windows-only — uses the OneNote desktop app via COM; no Azure "
                "or OAuth setup. Writes to <vault>/<target_folder>/<notebook>/"
                "<section>/<page>.md with original .xml preserved alongside and "
                "inline images saved under .images/ (dot-prefixed so the "
                "inbox watcher skips them). The inbox watcher will "
                "auto-ingest the new markdown. Use when the user asks to "
                "'import OneNote', 'sync my OneNote notebooks', or 'pull "
                "notebooks into the vault'."
            ),
            input_schema={
                "type": "object",
                "properties": {
                    "target_folder": {
                        "type": "string",
                        "default": "OneNote",
                        "description": "Vault subfolder for imports.",
                    },
                    "notebooks": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": (
                            "Optional: notebook display names to restrict to. "
                            "Omit to import all accessible notebooks."
                        ),
                    },
                    "incremental": {
                        "type": "boolean",
                        "default": True,
                        "description": (
                            "Skip pages unchanged since last import. Set false "
                            "to force a full re-import."
                        ),
                    },
                    "skip_meaningless_pages": {
                        "type": "boolean",
                        "default": True,
                        "description": (
                            "Filter out pages with empty bodies or only "
                            "boilerplate scaffolding (headings, lone bullets, "
                            "TODO markers). Recommended on — keeps the vault "
                            "free of stub pages from auto-created OneNote "
                            "templates and untouched daily entries. Set false "
                            "to import every page including empty ones."
                        ),
                    },
                },
                "required": [],
            },
            handler=self._sync_handler,
            async_handler=self._async_handler,
            contract_action="write_vault",
            category="onenote",
            namespace="integrations",
        )

        ctx.registry.register_tool(
            name="onenote_list_notebooks",
            description=(
                "List the OneNote notebooks available to the desktop OneNote "
                "app, with their names, IDs, and page counts. Use this to "
                "confirm scope with the user BEFORE calling `onenote_import` — "
                "show the user the list, propose defaults (incremental mode, "
                "all notebooks), and only run the import after they agree. "
                "Windows-only, read-only, no side effects."
            ),
            input_schema={
                "type": "object",
                "properties": {},
                "required": [],
            },
            handler=self._list_notebooks_sync,
            async_handler=self._list_notebooks_async,
            # Read-only probe → no write gate needed. ``read_vault`` is a
            # broadly-allowed domain action used by other inventory-style
            # tools (vault_search, list_tasks). This is consistent.
            contract_action="read_vault",
            category="onenote",
            namespace="integrations",
        )

        self._router = make_router(ctx, self._jobs)
        ctx.log.info("onenote-import plugin ready")

    async def async_init(self, ctx: PluginContext) -> None:
        # One lock per plugin instance — prevents two concurrent imports from
        # racing on the same state file. Created here so it binds to the
        # running event loop (asyncio.Lock binds eagerly on some Python
        # versions).
        self._lock = asyncio.Lock()

    def get_router(self) -> APIRouter | None:
        return self._router

    def shutdown(self) -> None:
        # Nothing to release — COM connections are acquired per-run.
        pass

    # ── tool handlers ────────────────────────────────────────────────

    async def _async_handler(self, inp: dict) -> dict:
        """Agent-invoked path — FIRE-AND-FORGET.

        Returns ``{job_id, state, progress_url}`` within ~100ms; the actual
        import runs as a background task. The agent's chat reply comes back
        fast, the user gets batched progress toasts during the run, and on
        terminal state the plugin posts a templated follow-up message back
        into the originating chat thread (captured via the
        ``current_conversation_id()`` contextvar the Agent sets at turn
        start).

        Previously this awaited ``importer.run()`` directly, blocking the
        LLM's chat turn for 2+ minutes on a 300-page notebook — that's the
        UX gap this rewrite closes.
        """
        if self._ctx is None:
            return {"error": "plugin not initialized"}
        if sys.platform != "win32":
            return {
                "error": (
                    f"OneNote COM is Windows-only (this is {sys.platform}). "
                    "Run on a Windows machine with OneNote desktop installed."
                ),
            }

        target = str(inp.get("target_folder") or "OneNote").strip() or "OneNote"
        notebooks_raw = inp.get("notebooks")
        notebooks = [str(n) for n in notebooks_raw] if isinstance(notebooks_raw, list) else None
        incremental = bool(inp.get("incremental", True))
        skip_meaningless = bool(inp.get("skip_meaningless_pages", True))

        # Capture the active chat conversation so the completion hook can
        # post a follow-up message back into it. Empty string when called
        # from a non-chat path (REST, CLI) — the hook becomes a no-op.
        conv_id = ""
        try:
            from agntspace.agent.agent import current_conversation_id
            conv_id = current_conversation_id()
        except Exception:
            pass

        job = await self._jobs.create(
            target_folder=target,
            notebooks_filter=notebooks,
            incremental=incremental,
        )
        job.origin_conversation_id = conv_id

        importer = OneNoteImporter(
            # root_dir is the user's chosen vault root (sibling of the
            # agntspace-* dirs), NOT root_dir/agntspace/ which is
            # ``settings.vault_dir`` (agent internals). We want OneNote
            # sitting next to agntspace-inbox, agntspace-journal, etc.
            # so the inbox watcher picks it up naturally.
            vault_dir=self._ctx.settings.root_dir,
            target_folder=target,
            notebooks_filter=notebooks,
            incremental=incremental,
            skip_meaningless=skip_meaningless,
            activity_logger=self._ctx.activity_logger,
            decisions=self._ctx.decisions,
            job_state=job,
        )

        lock = self._lock

        async def _run_and_followup() -> None:
            """Background task: run the import, then post follow-up."""
            try:
                if lock is None:
                    await importer.run()
                else:
                    async with lock:
                        await importer.run()
            except Exception as exc:  # pragma: no cover — safety net
                # importer.run() already catches everything; this is only
                # reached on truly unexpected failures inside the task.
                job.mark_failed(f"task crashed: {exc}")
            # Always attempt the follow-up (even on failure) so the user
            # isn't left staring at "Started…" with no closure.
            await self._post_completion_followup(job)

        asyncio.create_task(_run_and_followup())

        return {
            "job_id": job.job_id,
            "state": "running",
            "target_folder": target,
            "progress_url": f"/api/plugins/onenote-import/progress/{job.job_id}",
            "message": (
                "Started. I'll post a follow-up in this chat when it's done "
                "— you can watch progress in the top-right toasts or on /plugins "
                "in the meantime."
            ),
        }

    async def _post_completion_followup(self, job: JobState) -> None:  # type: ignore[name-defined]
        """Templated follow-up posted to the originating chat thread.

        No-op unless the job carries an ``origin_conversation_id`` (set
        from the agent contextvar at tool-call time) AND the plugin has
        ``conversation_memory`` wired in PluginContext. Kept template-based
        (no second LLM call) for cost + determinism.
        """
        if not job.origin_conversation_id:
            return
        if self._ctx is None or self._ctx.conversation_memory is None:
            return
        try:
            text = _format_completion_message(job)
            await self._ctx.conversation_memory.add_message(
                job.origin_conversation_id, "assistant", text,
            )
        except Exception:
            # Never let a follow-up failure crash the background task. The
            # user still has the progress toasts + the status endpoint.
            if self._ctx:
                self._ctx.log.exception(
                    "onenote-import: completion follow-up failed for conv %s",
                    job.origin_conversation_id,
                )

    # ── list_notebooks (read-only probe for confirm UX) ──────────────

    async def _list_notebooks_async(self, inp: dict) -> dict:
        """Return ``{notebooks: [{name, id, page_count}, ...], total_notebooks, total_pages}``.

        Runs entirely off the same COM bridge as ``onenote_import`` —
        reuses ``OneNoteCOMClient.list_pages()`` which already parses the
        full hierarchy — then groups by notebook for page counts. Read-only,
        no side effects, ~2-3 seconds on a vault with 17 notebooks / 5000
        pages. The agent calls this BEFORE ``onenote_import`` to show the
        user what they're about to commit to.
        """
        if self._ctx is None:
            return {"error": "plugin not initialized"}
        if sys.platform != "win32":
            return {
                "error": (
                    f"OneNote COM is Windows-only (this is {sys.platform}). "
                    "Run on a Windows machine with OneNote desktop installed."
                ),
            }
        # Run the COM calls in a thread so we don't block the event loop.
        try:
            notebooks = await asyncio.to_thread(self._list_notebooks_sync_impl)
        except Exception as exc:  # noqa: BLE001
            return {"error": str(exc)}
        total_pages = sum(n["page_count"] for n in notebooks)
        return {
            "notebooks": notebooks,
            "total_notebooks": len(notebooks),
            "total_pages": total_pages,
        }

    def _list_notebooks_sync_impl(self) -> list[dict]:
        """Blocking helper — actually talks to the COM bridge.

        Returns one entry per notebook: ``{name, id, page_count}``.
        Kept separate from ``_list_notebooks_async`` so the sync path
        (``_list_notebooks_sync`` fallback) can reuse it.
        """
        from collections import Counter

        with OneNoteCOMClient() as client:
            pages = client.list_pages()
        # Group by notebook_id so duplicate notebook names don't collide.
        nb_by_id: dict[str, dict] = {}
        counts: Counter[str] = Counter()
        for p in pages:
            key = p.notebook_id or p.notebook
            if key not in nb_by_id:
                nb_by_id[key] = {
                    "id": p.notebook_id,
                    "name": p.notebook,
                    "page_count": 0,
                }
            counts[key] += 1
        for key, count in counts.items():
            nb_by_id[key]["page_count"] = count
        # Sort by name for stable presentation.
        return sorted(nb_by_id.values(), key=lambda d: d["name"].lower())

    def _list_notebooks_sync(self, inp: dict) -> dict:
        """Sync fallback for ``onenote_list_notebooks``. Same policy as
        ``_sync_handler`` — prefer the async path when we're already in a
        running loop, otherwise spin up a temporary loop."""
        try:
            loop = asyncio.get_event_loop()
            if loop.is_running():
                return {
                    "error": (
                        "Sync dispatch of onenote_list_notebooks from a running "
                        "loop is not supported. The dispatcher should prefer "
                        "async_handler for this tool."
                    ),
                }
        except RuntimeError:
            loop = asyncio.new_event_loop()
            try:
                return loop.run_until_complete(self._list_notebooks_async(inp))
            finally:
                loop.close()
        return loop.run_until_complete(self._list_notebooks_async(inp))

    def _sync_handler(self, inp: dict) -> dict:
        """Sync fallback — required by some tool-dispatch paths.

        The agent's normal tool path uses ``async_handler`` directly (see
        ``agent.py``'s ``_dispatch_tool``), but the registry requires a
        ``handler`` too. This wraps the async version for those paths.
        """
        try:
            loop = asyncio.get_event_loop()
            if loop.is_running():
                # In an async context, launching another loop explodes.
                # The async_handler path should be preferred; if we hit this,
                # surface an explicit error rather than hanging.
                return {
                    "error": (
                        "Sync dispatch of onenote_import from a running loop "
                        "is not supported. The dispatcher should prefer "
                        "async_handler for this tool."
                    ),
                }
        except RuntimeError:
            # No event loop on this thread — create one.
            loop = asyncio.new_event_loop()
            try:
                return loop.run_until_complete(self._async_handler(inp))
            finally:
                loop.close()
        return loop.run_until_complete(self._async_handler(inp))


plugin = OneNoteImportPlugin()
