"""Filesystem layout + collision handling for imported OneNote pages.

Layout:
    <vault>/<target>/
    ├── .onenote-state.json                 # {page_id: {filename, last_modified}}
    ├── <Notebook>/
    │   └── <Section Group>/<Section>/      # section_path may be 0..N deep
    │       ├── .images/                    # per-section assets
    │       │   └── img-<hash>.<ext>
    │       ├── <Page Title>.md
    │       └── <Page Title>.xml            # original OneNote XML

Filename safety uses the existing ``agntspace.vault.writer.title_to_filename``
helper for the page title (Windows reserved, illegal chars, length cap).
For path SEGMENTS (notebook, section group, section) we pre-replace path
separators since ``title_to_filename`` doesn't strip them (its regex targets
only ``:*?"<>|``).
"""

from __future__ import annotations

import json
import re
from dataclasses import dataclass, field
from pathlib import Path

from agntspace.vault.writer import title_to_filename


@dataclass
class ImportState:
    """Map of page_id → {filename, last_modified}, persisted as JSON.

    Filename is relative to the notebook / section directory (no extension),
    so the same page keeps its path even after a title-rename happens in the
    state file but OneNote still reports the old title.
    """

    version: int = 1
    pages: dict[str, dict[str, str]] = field(default_factory=dict)

    def get(self, page_id: str) -> dict[str, str] | None:
        return self.pages.get(page_id)

    def put(self, page_id: str, *, filename: str, last_modified: str) -> None:
        self.pages[page_id] = {"filename": filename, "last_modified": last_modified}

    def to_json(self) -> str:
        return json.dumps({"version": self.version, "pages": self.pages}, indent=2, ensure_ascii=False)

    @classmethod
    def from_json(cls, raw: str) -> ImportState:
        try:
            data = json.loads(raw)
        except json.JSONDecodeError:
            return cls()
        if not isinstance(data, dict):
            return cls()
        pages = data.get("pages", {})
        if not isinstance(pages, dict):
            pages = {}
        return cls(version=int(data.get("version", 1) or 1), pages=pages)


def load_state(path: Path) -> ImportState:
    if not path.exists():
        return ImportState()
    try:
        return ImportState.from_json(path.read_text(encoding="utf-8"))
    except OSError:
        return ImportState()


def save_state(path: Path, state: ImportState) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    # Atomic write via tmp + replace, consistent with the existing
    # compile-state pattern in kb/service.py.
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(state.to_json(), encoding="utf-8")
    tmp.replace(path)


def sanitize_segment(name: str) -> str:
    """Sanitize a path segment (notebook / section group / section name).

    Replaces path separators (``/`` ``\\``) with ``-`` — they'd escape the
    directory tree otherwise — then runs through the shared
    ``title_to_filename`` helper for the rest of the illegal-char cleanup.
    """
    if not name or not name.strip():
        return "untitled"
    # Pre-clean path separators title_to_filename doesn't handle.
    safe = re.sub(r"[\\/]+", "-", name)
    return title_to_filename(safe)


def notebook_dir(base: Path, notebook: str) -> Path:
    return base / sanitize_segment(notebook)


def section_dir(base: Path, notebook: str, section_path: list[str]) -> Path:
    d = notebook_dir(base, notebook)
    for segment in section_path:
        d = d / sanitize_segment(segment)
    return d


def images_dir(base: Path, notebook: str, section_path: list[str]) -> Path:
    return section_dir(base, notebook, section_path) / ".images"


def resolve_page_filename(
    *,
    target_root: Path,
    notebook: str,
    section_path: list[str],
    page_title: str,
    page_id: str,
    state: ImportState,
) -> str:
    """Decide the filename (without extension) for this page.

    Priority:
        1. If the page_id is already in state, reuse the stored filename
           (even if the title changed in OneNote — the file on disk may
           have been renamed or linked to from the user's notes).
        2. Otherwise compute a title-based filename; if it collides with
           a DIFFERENT page_id already on disk (via state or filesystem),
           append ``-2``, ``-3``, ... until free.
    """
    existing = state.get(page_id)
    if existing and existing.get("filename"):
        return existing["filename"]

    base = title_to_filename(page_title) or "untitled"
    sec = section_dir(target_root, notebook, section_path)

    # Build a set of already-claimed filenames in this section from state.
    # Only consider entries whose resolved section matches this one.
    claimed: set[str] = set()
    for pid, info in state.pages.items():
        if pid == page_id:
            continue
        # We don't track section path in state; rely on filesystem check.
        existing_name = info.get("filename")
        if existing_name:
            if (sec / f"{existing_name}.md").exists():
                claimed.add(existing_name)

    candidate = base
    n = 2
    while candidate in claimed or (sec / f"{candidate}.md").exists():
        # Only keep incrementing if the existing file isn't OURS — if state
        # already maps us to this filename, we'd have returned early above.
        candidate = f"{base}-{n}"
        n += 1
        if n > 999:
            # Pathological case — give up and use the page_id suffix.
            candidate = f"{base}-{page_id[-6:]}"
            break
    return candidate


def write_page(
    *,
    target_root: Path,
    notebook: str,
    section_path: list[str],
    filename: str,
    markdown: str,
) -> Path:
    """Write ``<filename>.md`` into the section directory.

    Returns the markdown path. Parent dirs are created as needed.
    Uses plain ``Path.write_text`` (not ``VaultWriter``) because contract
    authorization is enforced at the plugin boundary, not per file.

    Previously wrote an ``<filename>.xml`` sidecar holding OneNote's raw
    XML as a re-conversion safety net; dropped 2026-04-24 because (a)
    OneNote itself is the real source of truth — re-importing re-fetches
    fresh XML from the COM bridge — (b) the OneNote schema isn't useful
    to read directly, and (c) the sidecars added 3-10× disk overhead
    per page.
    """
    sec = section_dir(target_root, notebook, section_path)
    sec.mkdir(parents=True, exist_ok=True)
    md_path = sec / f"{filename}.md"
    md_path.write_text(markdown, encoding="utf-8")
    return md_path


def write_image(
    *,
    target_root: Path,
    notebook: str,
    section_path: list[str],
    filename: str,
    binary: bytes,
) -> str:
    """Write an image/attachment binary into the section's ``.images/`` dir.

    Returns the relative path (from the page's .md file) that should appear
    in the markdown — ``.images/<filename>`` — so the caller can pass that
    through converter's ``image_sink``.
    """
    img_dir = images_dir(target_root, notebook, section_path)
    img_dir.mkdir(parents=True, exist_ok=True)
    out = img_dir / filename
    # Don't rewrite if identical bytes already on disk — hash-based filenames
    # make this cheap and safe.
    if not out.exists() or out.stat().st_size != len(binary):
        out.write_bytes(binary)
    return f".images/{filename}"
