"""Photo management — search, tag, slideshow, and lifecycle operations.

Photo search shares the same backend as the search_photos core tool.

Usage:
    from boxbot_sdk import photos

    results = photos.search(query="beach sunset", tags=["vacation"])
    for p in results:
        print(f"{p.id}: {p.description}")

    photos.set_tags("photo_123", tags=["family", "beach"])
    photos.add_to_slideshow("photo_123")
    photos.delete("photo_456")
"""

from __future__ import annotations

from typing import Any

from . import _transport, _validators as v


class PhotosError(Exception):
    """Raised when a photo operation fails on the main process side."""


def _check(resp: dict[str, Any]) -> dict[str, Any]:
    if isinstance(resp, dict) and resp.get("status") == "error":
        raise PhotosError(resp.get("error", "photo operation failed"))
    return resp


class PhotoRecord:
    """A photo record returned from search or get."""

    def __init__(self, data: dict[str, Any]) -> None:
        self._data = data

    @property
    def id(self) -> str:
        return self._data.get("id", "")

    @property
    def description(self) -> str:
        return self._data.get("description", "")

    @property
    def tags(self) -> list[str]:
        return self._data.get("tags", [])

    @property
    def people(self) -> list[str]:
        return self._data.get("people", [])

    @property
    def file_path(self) -> str:
        return self._data.get("file_path", "")

    @property
    def created_at(self) -> str:
        return self._data.get("created_at", "")

    @property
    def in_slideshow(self) -> bool:
        return self._data.get("in_slideshow", False)

    def __repr__(self) -> str:
        return f"PhotoRecord(id={self.id!r}, description={self.description!r})"


class StorageInfo:
    """Photo storage quota information."""

    def __init__(self, data: dict[str, Any]) -> None:
        self._data = data

    @property
    def used_gb(self) -> float:
        return self._data.get("used_gb", 0.0)

    @property
    def quota_gb(self) -> float:
        return self._data.get("quota_gb", 0.0)

    @property
    def used_percent(self) -> float:
        return self._data.get("used_percent", 0.0)

    @property
    def photo_count(self) -> int:
        return self._data.get("photo_count", 0)

    def __repr__(self) -> str:
        return (f"StorageInfo({self.used_gb:.1f}GB / {self.quota_gb:.1f}GB, "
                f"{self.used_percent:.0f}%)")


# --- Search and retrieval ---

def search(query: str | None = None, *,
           tags: list[str] | None = None,
           people: list[str] | None = None,
           limit: int = 10) -> list[PhotoRecord]:
    """Search photos.

    Args:
        query: Text search query.
        tags: Filter by tags.
        people: Filter by people in photo.
        limit: Maximum results.

    Returns:
        List of PhotoRecord objects sorted by relevance.
    """
    payload: dict[str, Any] = {"limit": limit}
    if query is not None:
        v.require_str(query, "query")
        payload["query"] = query
    if tags is not None:
        v.require_list(tags, "tags")
        payload["tags"] = tags
    if people is not None:
        v.require_list(people, "people")
        payload["people"] = people
    v.require_int(limit, "limit", min_val=1, max_val=100)

    resp = _check(_transport.request("photos.search", payload, timeout=30))
    return [PhotoRecord(r) for r in resp.get("results", [])]


def get(photo_id: str) -> PhotoRecord:
    """Get full details for a photo.

    Args:
        photo_id: Photo ID.

    Returns:
        PhotoRecord with full details.
    """
    v.require_str(photo_id, "photo_id")
    resp = _check(_transport.request("photos.get", {"id": photo_id}, timeout=30))
    return PhotoRecord(resp)


def view(photo_id: str) -> dict[str, Any]:
    """Attach a photo's pixels to the tool result so the agent sees it.

    Returns ``{id, filename, kind: "image", attached: True}``. The
    actual image bytes are delivered as an image content block on the
    tool result, not as a field in this dict.
    """
    v.require_str(photo_id, "photo_id")
    return _check(_transport.request("photos.view", {"id": photo_id}, timeout=30))


def view_path(path: str) -> dict[str, Any]:
    """Attach a local image file's pixels to the tool result.

    Use this for files that haven't been ingested into the photo
    library yet — most often an inbound WhatsApp image staged at
    ``[image attached at <path>]`` in the user's message. Path must
    live under one of the allowlisted roots (sandbox tmp, workspace,
    photos, perception crops); anything else is refused.

    Returns ``{path, filename, kind: "image", attached: True}``. The
    image bytes ride back as an image content block on the tool
    result, not as a field in this dict.
    """
    v.require_str(path, "path")
    return _check(_transport.request("photos.view_path", {"path": path}, timeout=30))


def ingest(
    path: str,
    *,
    source: str,
    sender: str | None = None,
    caption: str | None = None,
) -> str:
    """Hand a local image file to the photo intake pipeline.

    Use this once you've decided an inbound or scratch image belongs in
    the photo library. The pipeline copies the bytes into
    ``data/photos/``, runs detection + tagging, and indexes for search.
    The original ``path`` is deleted on success (it was only staging).

    Args:
        path: Local file path (must be under an allowlisted root).
        source: Where the image came from — ``"whatsapp"``, ``"camera"``,
            or ``"upload"``. Stored on the photo for filtering later.
        sender: Person who sent it, if known.
        caption: Optional human-supplied description; used as the
            initial photo description until the small-model tagger
            replaces it.

    Returns the new photo's ID.
    """
    v.require_str(path, "path")
    v.require_str(source, "source")
    payload: dict[str, Any] = {"path": path, "source": source}
    if sender is not None:
        v.require_str(sender, "sender")
        payload["sender"] = sender
    if caption is not None:
        v.require_str(caption, "caption")
        payload["caption"] = caption
    resp = _check(_transport.request("photos.ingest", payload, timeout=120))
    return resp["photo_id"]


def show_on_screen(photo_ids: list[str]) -> dict[str, Any]:
    """Display one or more photos on the 7\" screen.

    Dispatches through the display manager to the ``picture`` display.
    The call returns immediately; the display manager handles rendering.
    """
    v.require_list(photo_ids, "photo_ids")
    for pid in photo_ids:
        v.require_str(pid, "photo_id")
    return _check(_transport.request(
        "photos.show_on_screen",
        {"photo_ids": photo_ids},
        timeout=10,
    ))


# --- Metadata updates ---

def update(photo_id: str, *, description: str) -> None:
    """Update a photo's description.

    Args:
        photo_id: Photo ID.
        description: New description text.
    """
    v.require_str(photo_id, "photo_id")
    v.require_str(description, "description")
    _transport.emit_action("photos.update", {
        "id": photo_id,
        "description": description,
    })


def set_tags(photo_id: str, *, tags: list[str]) -> None:
    """Set tags on a photo (replaces existing tags).

    Args:
        photo_id: Photo ID.
        tags: List of tag strings.
    """
    v.require_str(photo_id, "photo_id")
    v.require_list(tags, "tags")
    _transport.emit_action("photos.set_tags", {
        "id": photo_id,
        "tags": tags,
    })


def set_person(photo_id: str, *, person_index: int, name: str) -> None:
    """Tag a person in a photo.

    Args:
        photo_id: Photo ID.
        person_index: Index in the photo's people list (0-based).
        name: Person name to assign.
    """
    v.require_str(photo_id, "photo_id")
    v.require_int(person_index, "person_index", min_val=0)
    v.require_str(name, "name")
    _transport.emit_action("photos.set_person", {
        "id": photo_id,
        "person_index": person_index,
        "name": name,
    })


# --- Slideshow management ---

def add_to_slideshow(photo_id: str) -> None:
    """Add a photo to the slideshow rotation.

    Args:
        photo_id: Photo ID.
    """
    v.require_str(photo_id, "photo_id")
    _transport.emit_action("photos.add_to_slideshow", {"id": photo_id})


def remove_from_slideshow(photo_id: str) -> None:
    """Remove a photo from the slideshow rotation.

    Args:
        photo_id: Photo ID.
    """
    v.require_str(photo_id, "photo_id")
    _transport.emit_action("photos.remove_from_slideshow", {"id": photo_id})


# --- Tag library management ---

def merge_tags(source: str, *, into: str) -> None:
    """Merge one tag into another (consolidate synonyms).

    Args:
        source: Tag to merge from (will be removed).
        into: Tag to merge into (will be kept).
    """
    v.require_str(source, "source tag")
    v.require_str(into, "target tag")
    _transport.emit_action("photos.merge_tags", {
        "source": source,
        "into": into,
    })


def rename_tag(old: str, *, to: str) -> None:
    """Rename a tag (fix typos).

    Args:
        old: Current tag name.
        to: New tag name.
    """
    v.require_str(old, "old tag name")
    v.require_str(to, "new tag name")
    _transport.emit_action("photos.rename_tag", {
        "old": old,
        "new": to,
    })


def delete_tag(tag: str) -> None:
    """Delete a tag from the library.

    Args:
        tag: Tag name to delete.
    """
    v.require_str(tag, "tag")
    _transport.emit_action("photos.delete_tag", {"tag": tag})


# --- Soft delete / restore ---

def delete(photo_id: str) -> None:
    """Soft-delete a photo (30-day retention, restorable).

    Args:
        photo_id: Photo ID.
    """
    v.require_str(photo_id, "photo_id")
    _transport.emit_action("photos.delete", {"id": photo_id})


def restore(photo_id: str) -> None:
    """Restore a soft-deleted photo.

    Args:
        photo_id: Photo ID.
    """
    v.require_str(photo_id, "photo_id")
    _transport.emit_action("photos.restore", {"id": photo_id})


def list_deleted() -> list[PhotoRecord]:
    """List soft-deleted photos.

    Returns:
        List of PhotoRecord objects that are soft-deleted.
    """
    response = _transport.request("photos.list_deleted", {}, timeout=30)
    results = response.get("results", [])
    return [PhotoRecord(r) for r in results]


# --- Storage info ---

def storage_info() -> StorageInfo:
    """Get photo storage quota information.

    Returns:
        StorageInfo with usage and quota details.
    """
    response = _transport.request("photos.storage_info", {}, timeout=30)
    return StorageInfo(response)
