"""
Google Slides API client for g-gremlin.

Provides presentation read, export, and create-from-markdown helpers.
"""

from __future__ import annotations

import re
import uuid
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from google.auth.transport.requests import AuthorizedSession
from googleapiclient.discovery import Resource, build
from googleapiclient.errors import HttpError

from g_gremlin.google_client import get_drive_service, load_credentials

PDF_MIME = "application/pdf"
PPTX_MIME = "application/vnd.openxmlformats-officedocument.presentationml.presentation"

TITLE_PLACEHOLDERS = {"TITLE", "CENTERED_TITLE"}
BODY_PLACEHOLDERS = {"BODY", "SUBTITLE"}


class GoogleSlidesError(RuntimeError):
    """Raised when Google Slides operations fail."""


@dataclass
class SlideTextBlock:
    """Text content with bullet metadata."""

    text: str
    is_bullet: bool = False


@dataclass
class SlideElement:
    """Low-level slide element metadata."""

    object_id: str
    element_type: str
    text: str = ""
    placeholder_type: Optional[str] = None
    shape_type: Optional[str] = None
    image_url: Optional[str] = None


@dataclass
class Slide:
    """A single slide with parsed content."""

    slide_id: str
    title: str = ""
    body: List[SlideTextBlock] = field(default_factory=list)
    speaker_notes: str = ""
    elements: List[SlideElement] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.slide_id,
            "title": self.title,
            "body": [
                {"text": block.text, "is_bullet": block.is_bullet}
                for block in self.body
            ],
            "speaker_notes": self.speaker_notes,
            "elements": [
                {
                    "object_id": element.object_id,
                    "element_type": element.element_type,
                    "text": element.text,
                    "placeholder_type": element.placeholder_type,
                    "shape_type": element.shape_type,
                    "image_url": element.image_url,
                }
                for element in self.elements
            ],
        }

    def to_markdown(self) -> str:
        lines: List[str] = []
        if self.title:
            lines.append(f"# {self.title}")
            lines.append("")
        for block in self.body:
            if not block.text:
                continue
            if block.is_bullet:
                lines.append(f"- {block.text}")
            else:
                lines.append(block.text)
        if self.speaker_notes:
            lines.append("")
            lines.append(":::notes")
            lines.append(self.speaker_notes)
        if not lines and self.elements:
            lines.append("[Non-text elements omitted]")
        return "\n".join(lines).rstrip()


@dataclass
class SlidePresentation:
    """Presentation with a list of slides."""

    id: str
    title: str
    url: str
    slides: List[Slide] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "title": self.title,
            "url": self.url,
            "slides": [slide.to_dict() for slide in self.slides],
        }

    def to_markdown(self) -> str:
        parts = [slide.to_markdown() for slide in self.slides]
        return "\n\n---\n\n".join([part for part in parts if part]).rstrip()


@dataclass
class MarkdownParagraph:
    """Markdown paragraph parsed for slide creation."""

    text: str
    is_bullet: bool = False


@dataclass
class MarkdownSlide:
    """Markdown slide parsed into title/body/notes."""

    title: str = ""
    body: List[MarkdownParagraph] = field(default_factory=list)
    notes: str = ""


class SlideMarkdownParser:
    """Parse markdown into slide structures."""

    SLIDE_SEPARATOR = re.compile(r"^\s*---\s*$")
    NOTES_MARKER = re.compile(r"^\s*:::notes\s*$", re.IGNORECASE)
    HEADER_PATTERN = re.compile(r"^\s*(#{1,6})\s+(.+)$")
    BULLET_PATTERN = re.compile(r"^\s*[-*]\s+(.+)$")

    def parse(self, markdown: str) -> List[MarkdownSlide]:
        slides: List[MarkdownSlide] = []
        current = MarkdownSlide()
        notes_lines: List[str] = []
        in_notes = False

        for raw_line in markdown.splitlines():
            line = raw_line.rstrip()
            if self.SLIDE_SEPARATOR.match(line):
                self._finalize_slide(current, notes_lines, slides)
                current = MarkdownSlide()
                notes_lines = []
                in_notes = False
                continue

            if self.NOTES_MARKER.match(line):
                in_notes = True
                continue

            if in_notes:
                notes_lines.append(line)
                continue

            header_match = self.HEADER_PATTERN.match(line)
            if header_match:
                header_text = header_match.group(2).strip()
                if not current.title:
                    current.title = header_text
                else:
                    current.body.append(MarkdownParagraph(text=header_text))
                continue

            bullet_match = self.BULLET_PATTERN.match(line)
            if bullet_match:
                current.body.append(MarkdownParagraph(text=bullet_match.group(1).strip(), is_bullet=True))
                continue

            if not line.strip():
                continue

            current.body.append(MarkdownParagraph(text=line.strip()))

        self._finalize_slide(current, notes_lines, slides)
        return slides

    @staticmethod
    def _finalize_slide(
        slide: MarkdownSlide,
        notes_lines: List[str],
        slides: List[MarkdownSlide],
    ) -> None:
        slide.notes = "\n".join(notes_lines).strip()
        if slide.title or slide.body or slide.notes:
            slides.append(slide)


def extract_presentation_id(presentation_id_or_url: str) -> str:
    """
    Extract presentation ID from URL or return the ID as-is.
    """
    if "docs.google.com" in presentation_id_or_url:
        match = re.search(r"/presentation/d/([a-zA-Z0-9_-]+)", presentation_id_or_url)
        if match:
            return match.group(1)
        raise GoogleSlidesError(
            f"Could not extract presentation ID from URL: {presentation_id_or_url}"
        )
    return presentation_id_or_url


class GoogleSlidesClient:
    """Google Slides API client."""

    def __init__(self) -> None:
        self._service: Optional[Resource] = None
        self._drive_service: Optional[Resource] = None
        self._session: Optional[AuthorizedSession] = None

    def _get_service(self) -> Resource:
        if self._service is None:
            credentials = load_credentials()
            self._service = build("slides", "v1", credentials=credentials, cache_discovery=False)
        return self._service

    def _get_drive_service(self) -> Resource:
        if self._drive_service is None:
            self._drive_service = get_drive_service()
        return self._drive_service

    def _get_session(self) -> AuthorizedSession:
        if self._session is None:
            self._session = AuthorizedSession(load_credentials())
        return self._session

    def get_presentation(self, presentation_id_or_url: str) -> SlidePresentation:
        presentation_id = extract_presentation_id(presentation_id_or_url)
        service = self._get_service()
        try:
            presentation = service.presentations().get(
                presentationId=presentation_id
            ).execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to read presentation: {exc}") from exc
        return self._parse_presentation(presentation)

    def export_pdf(self, presentation_id_or_url: str, output_path: Path) -> Path:
        return self._export_file(presentation_id_or_url, output_path, PDF_MIME)

    def export_pptx(self, presentation_id_or_url: str, output_path: Path) -> Path:
        return self._export_file(presentation_id_or_url, output_path, PPTX_MIME)

    def export_slide_png(
        self,
        presentation_id_or_url: str,
        slide_id: str,
        output_path: Path,
    ) -> Path:
        presentation_id = extract_presentation_id(presentation_id_or_url)
        service = self._get_service()
        try:
            response = service.presentations().pages().getThumbnail(
                presentationId=presentation_id,
                pageObjectId=slide_id,
                thumbnailProperties={"thumbnailSize": "LARGE"},
            ).execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to get thumbnail for slide {slide_id}: {exc}") from exc

        content_url = response.get("contentUrl")
        if not content_url:
            raise GoogleSlidesError(f"No thumbnail URL returned for slide {slide_id}.")

        session = self._get_session()
        resp = session.get(content_url, timeout=30)
        if resp.status_code >= 400:
            raise GoogleSlidesError(
                f"Failed to download thumbnail for slide {slide_id}: {resp.status_code}"
            )

        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_bytes(resp.content)
        return output_path

    def export_all_slides_png(
        self,
        presentation_id_or_url: str,
        output_dir: Path,
    ) -> List[Path]:
        presentation = self.get_presentation(presentation_id_or_url)
        output_dir.mkdir(parents=True, exist_ok=True)
        outputs: List[Path] = []
        for idx, slide in enumerate(presentation.slides, start=1):
            filename = f"slide-{idx:03d}.png"
            output_path = output_dir / filename
            outputs.append(
                self.export_slide_png(presentation.id, slide.slide_id, output_path)
            )
        return outputs

    def create_presentation(self, title: str, markdown: Optional[str] = None) -> SlidePresentation:
        service = self._get_service()
        try:
            presentation = service.presentations().create(body={"title": title}).execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to create presentation: {exc}") from exc

        presentation_id = presentation.get("presentationId")
        if not presentation_id:
            raise GoogleSlidesError("No presentation ID returned from create call.")

        if not markdown:
            return self.get_presentation(presentation_id)

        slides = SlideMarkdownParser().parse(markdown)
        if not slides:
            return self.get_presentation(presentation_id)

        requests: List[Dict[str, Any]] = []
        slide_id_map: Dict[str, MarkdownSlide] = {}
        for idx, slide in enumerate(slides, start=1):
            slide_id = _new_object_id(f"slide_{idx}")
            title_id = _new_object_id(f"title_{idx}")
            body_id = _new_object_id(f"body_{idx}")
            slide_id_map[slide_id] = slide

            requests.append({
                "createSlide": {
                    "objectId": slide_id,
                    "slideLayoutReference": {"predefinedLayout": "TITLE_AND_BODY"},
                    "placeholderIdMappings": [
                        {
                            "layoutPlaceholder": {"type": "TITLE", "index": 0},
                            "objectId": title_id,
                        },
                        {
                            "layoutPlaceholder": {"type": "BODY", "index": 0},
                            "objectId": body_id,
                        },
                    ],
                }
            })

            requests.append({
                "deleteText": {"objectId": title_id, "textRange": {"type": "ALL"}}
            })
            if slide.title:
                requests.append({
                    "insertText": {
                        "objectId": title_id,
                        "text": slide.title,
                        "insertionIndex": 0,
                    }
                })

            requests.append({
                "deleteText": {"objectId": body_id, "textRange": {"type": "ALL"}}
            })
            body_text, bullet_ranges = _build_body_text(slide.body)
            if body_text:
                requests.append({
                    "insertText": {
                        "objectId": body_id,
                        "text": body_text,
                        "insertionIndex": 0,
                    }
                })
                for start, end in bullet_ranges:
                    if start >= end:
                        continue
                    requests.append({
                        "createParagraphBullets": {
                            "objectId": body_id,
                            "textRange": {
                                "type": "FIXED_RANGE",
                                "startIndex": start,
                                "endIndex": end,
                            },
                            "bulletPreset": "BULLET_DISC_CIRCLE_SQUARE",
                        }
                    })

        default_slide_id = _get_default_slide_id(presentation)
        if default_slide_id:
            requests.append({"deleteObject": {"objectId": default_slide_id}})

        if requests:
            try:
                service.presentations().batchUpdate(
                    presentationId=presentation_id,
                    body={"requests": requests},
                ).execute()
            except HttpError as exc:
                raise GoogleSlidesError(f"Failed to populate presentation: {exc}") from exc

        notes_slides = {
            slide_id: slide.notes
            for slide_id, slide in slide_id_map.items()
            if slide.notes
        }
        if notes_slides:
            self._apply_speaker_notes(presentation_id, notes_slides)

        return self.get_presentation(presentation_id)

    def insert_linked_chart(
        self,
        presentation_id_or_url: str,
        spreadsheet_id: str,
        chart_id: int,
        *,
        slide_index: int = 0,
        layout: str = "full",
        replace: bool = False,
        tag: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Insert a linked Google Sheets chart into a slide.
        """
        presentation_id = extract_presentation_id(presentation_id_or_url)
        service = self._get_service()
        presentation = self._get_presentation_raw(presentation_id)
        slide_id = _resolve_slide_id(presentation, slide_index)
        page_width, page_height = _get_page_size(presentation)
        element_props = _layout_element_properties(page_width, page_height, slide_id, layout)
        element_id = _new_object_id("chart")
        tag_value = tag or f"g-gremlin:chart:{chart_id}"

        requests: List[Dict[str, Any]] = []
        deleted = []
        if replace:
            deleted = _find_elements_by_tag(presentation, tag_value)
            for object_id in deleted:
                requests.append({"deleteObject": {"objectId": object_id}})

        requests.append(
            {
                "createSheetsChart": {
                    "objectId": element_id,
                    "spreadsheetId": spreadsheet_id,
                    "chartId": chart_id,
                    "linkingMode": "LINKED",
                    "elementProperties": element_props,
                }
            }
        )
        requests.append(
            {
                "updatePageElementAltText": {
                    "objectId": element_id,
                    "description": tag_value,
                }
            }
        )

        try:
            service.presentations().batchUpdate(
                presentationId=presentation_id,
                body={"requests": requests},
            ).execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to insert chart: {exc}") from exc

        return {
            "presentation_id": presentation_id,
            "slide_id": slide_id,
            "chart_id": chart_id,
            "element_id": element_id,
            "tag": tag_value,
            "replaced": len(deleted),
        }

    def insert_image(
        self,
        presentation_id_or_url: str,
        image_url: str,
        *,
        slide_index: int = 0,
        layout: str = "full",
        replace: bool = False,
        tag: Optional[str] = None,
    ) -> Dict[str, Any]:
        """
        Insert an image into a slide using a URL.
        """
        presentation_id = extract_presentation_id(presentation_id_or_url)
        service = self._get_service()
        presentation = self._get_presentation_raw(presentation_id)
        slide_id = _resolve_slide_id(presentation, slide_index)
        page_width, page_height = _get_page_size(presentation)
        element_props = _layout_element_properties(page_width, page_height, slide_id, layout)
        element_id = _new_object_id("image")
        tag_value = tag or f"g-gremlin:image:{element_id}"

        requests: List[Dict[str, Any]] = []
        deleted = []
        if replace:
            deleted = _find_elements_by_tag(presentation, tag_value)
            for object_id in deleted:
                requests.append({"deleteObject": {"objectId": object_id}})

        requests.append(
            {
                "createImage": {
                    "objectId": element_id,
                    "url": image_url,
                    "elementProperties": element_props,
                }
            }
        )
        requests.append(
            {
                "updatePageElementAltText": {
                    "objectId": element_id,
                    "description": tag_value,
                }
            }
        )

        try:
            service.presentations().batchUpdate(
                presentationId=presentation_id,
                body={"requests": requests},
            ).execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to insert image: {exc}") from exc

        return {
            "presentation_id": presentation_id,
            "slide_id": slide_id,
            "element_id": element_id,
            "tag": tag_value,
            "replaced": len(deleted),
        }

    def _get_presentation_raw(self, presentation_id: str) -> Dict[str, Any]:
        service = self._get_service()
        try:
            return service.presentations().get(presentationId=presentation_id).execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to fetch presentation: {exc}") from exc

    def _apply_speaker_notes(self, presentation_id: str, notes_map: Dict[str, str]) -> None:
        service = self._get_service()
        try:
            presentation = service.presentations().get(
                presentationId=presentation_id
            ).execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to fetch presentation for notes: {exc}") from exc

        requests: List[Dict[str, Any]] = []
        for slide in presentation.get("slides", []):
            slide_id = slide.get("objectId")
            notes_text = notes_map.get(slide_id)
            if not notes_text:
                continue
            notes_body_id = _find_notes_body_id(slide)
            if not notes_body_id:
                continue
            requests.append({
                "deleteText": {"objectId": notes_body_id, "textRange": {"type": "ALL"}}
            })
            requests.append({
                "insertText": {
                    "objectId": notes_body_id,
                    "text": notes_text,
                    "insertionIndex": 0,
                }
            })

        if not requests:
            return

        try:
            service.presentations().batchUpdate(
                presentationId=presentation_id,
                body={"requests": requests},
            ).execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to update speaker notes: {exc}") from exc

    def _export_file(
        self,
        presentation_id_or_url: str,
        output_path: Path,
        mime_type: str,
    ) -> Path:
        presentation_id = extract_presentation_id(presentation_id_or_url)
        service = self._get_drive_service()
        try:
            request = service.files().export(
                fileId=presentation_id,
                mimeType=mime_type,
            )
            data = request.execute()
        except HttpError as exc:
            raise GoogleSlidesError(f"Failed to export presentation: {exc}") from exc

        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_path.write_bytes(data)
        return output_path

    def _parse_presentation(self, presentation: Dict[str, Any]) -> SlidePresentation:
        presentation_id = presentation.get("presentationId", "")
        slides = [
            self._parse_slide(slide) for slide in presentation.get("slides", [])
        ]
        url = f"https://docs.google.com/presentation/d/{presentation_id}/edit"
        return SlidePresentation(
            id=presentation_id,
            title=presentation.get("title", ""),
            url=url,
            slides=slides,
        )

    def _parse_slide(self, slide: Dict[str, Any]) -> Slide:
        slide_id = slide.get("objectId", "")
        title = ""
        body: List[SlideTextBlock] = []
        elements: List[SlideElement] = []

        for element in slide.get("pageElements", []):
            element_id = element.get("objectId", "")
            if "shape" in element:
                shape = element["shape"]
                placeholder_type = shape.get("placeholder", {}).get("type")
                paragraphs = _extract_paragraphs(shape.get("text", {}).get("textElements", []))
                if paragraphs:
                    if placeholder_type in TITLE_PLACEHOLDERS and not title:
                        title = " ".join(p.text for p in paragraphs if p.text).strip()
                    elif placeholder_type in BODY_PLACEHOLDERS or placeholder_type is None:
                        for para in paragraphs:
                            if para.text:
                                body.append(para)
                text = "\n".join(p.text for p in paragraphs if p.text)
                elements.append(SlideElement(
                    object_id=element_id,
                    element_type="shape",
                    text=text,
                    placeholder_type=placeholder_type,
                    shape_type=shape.get("shapeType"),
                ))
            elif "image" in element:
                image = element["image"]
                elements.append(SlideElement(
                    object_id=element_id,
                    element_type="image",
                    image_url=image.get("contentUrl") or image.get("sourceUrl"),
                ))
            elif "table" in element:
                elements.append(SlideElement(
                    object_id=element_id,
                    element_type="table",
                ))
            else:
                elements.append(SlideElement(
                    object_id=element_id,
                    element_type="other",
                ))

        notes = _extract_notes(slide)
        return Slide(
            slide_id=slide_id,
            title=title,
            body=body,
            speaker_notes=notes,
            elements=elements,
        )


def _extract_paragraphs(text_elements: List[Dict[str, Any]]) -> List[SlideTextBlock]:
    paragraphs: List[SlideTextBlock] = []
    current: Optional[SlideTextBlock] = None

    for element in text_elements:
        if "paragraphMarker" in element:
            bullet = bool(element["paragraphMarker"].get("bullet"))
            current = SlideTextBlock(text="", is_bullet=bullet)
            paragraphs.append(current)
            continue
        text_run = element.get("textRun")
        if not text_run:
            continue
        content = text_run.get("content", "")
        if current is None:
            current = SlideTextBlock(text="")
            paragraphs.append(current)
        current.text += content

    cleaned: List[SlideTextBlock] = []
    for para in paragraphs:
        text = para.text.strip()
        if text:
            cleaned.append(SlideTextBlock(text=text, is_bullet=para.is_bullet))
    return cleaned


def _extract_notes(slide: Dict[str, Any]) -> str:
    notes_page = slide.get("slideProperties", {}).get("notesPage", {})
    if not notes_page:
        return ""
    texts: List[str] = []
    for element in notes_page.get("pageElements", []):
        shape = element.get("shape")
        if not shape:
            continue
        placeholder_type = shape.get("placeholder", {}).get("type")
        if placeholder_type != "BODY":
            continue
        paragraphs = _extract_paragraphs(shape.get("text", {}).get("textElements", []))
        for para in paragraphs:
            if para.text:
                texts.append(para.text)
    return "\n".join(texts).strip()


def _resolve_slide_id(presentation: Dict[str, Any], slide_index: int) -> str:
    slides = presentation.get("slides", []) or []
    if not slides:
        raise GoogleSlidesError("Presentation has no slides.")
    if slide_index < 0 or slide_index >= len(slides):
        raise GoogleSlidesError(
            f"slide_index {slide_index} out of range (0..{len(slides) - 1})."
        )
    slide_id = slides[slide_index].get("objectId")
    if not slide_id:
        raise GoogleSlidesError("Slide object ID not found.")
    return slide_id


def _dimension_to_emu(dim: Dict[str, Any]) -> float:
    magnitude = float(dim.get("magnitude", 0))
    unit = str(dim.get("unit", "EMU")).upper()
    if unit == "EMU":
        return magnitude
    if unit == "PT":
        return magnitude * 12700
    if unit == "IN":
        return magnitude * 914400
    if unit == "CM":
        return magnitude * 360000
    return magnitude


def _get_page_size(presentation: Dict[str, Any]) -> Tuple[float, float]:
    page_size = presentation.get("pageSize", {}) or {}
    width = _dimension_to_emu(page_size.get("width", {}) or {})
    height = _dimension_to_emu(page_size.get("height", {}) or {})
    if not width or not height:
        # Default 16:9 slide size in EMU (10 in x 5.625 in)
        return 9144000, 5143500
    return width, height


def _layout_element_properties(
    page_width: float,
    page_height: float,
    slide_id: str,
    layout: str,
) -> Dict[str, Any]:
    normalized = (layout or "full").strip().lower()
    margin_x = page_width * 0.05
    margin_y = page_height * 0.15

    if normalized == "left":
        width = page_width * 0.42
        height = page_height * 0.6
        x = margin_x
        y = page_height * 0.2
    elif normalized == "right":
        width = page_width * 0.42
        height = page_height * 0.6
        x = page_width * 0.53
        y = page_height * 0.2
    else:
        width = page_width * 0.9
        height = page_height * 0.7
        x = margin_x
        y = margin_y

    return {
        "pageObjectId": slide_id,
        "size": {
            "height": {"magnitude": height, "unit": "EMU"},
            "width": {"magnitude": width, "unit": "EMU"},
        },
        "transform": {
            "scaleX": 1,
            "scaleY": 1,
            "translateX": x,
            "translateY": y,
            "unit": "EMU",
        },
    }


def _find_elements_by_tag(presentation: Dict[str, Any], tag: str) -> List[str]:
    matches: List[str] = []
    for slide in presentation.get("slides", []) or []:
        for element in slide.get("pageElements", []) or []:
            if element.get("description") == tag or element.get("title") == tag:
                object_id = element.get("objectId")
                if object_id:
                    matches.append(object_id)
    return matches


def _find_notes_body_id(slide: Dict[str, Any]) -> Optional[str]:
    notes_page = slide.get("slideProperties", {}).get("notesPage", {})
    for element in notes_page.get("pageElements", []):
        shape = element.get("shape")
        if not shape:
            continue
        placeholder_type = shape.get("placeholder", {}).get("type")
        if placeholder_type == "BODY":
            return element.get("objectId")
    return None


def _build_body_text(paragraphs: List[MarkdownParagraph]) -> Tuple[str, List[Tuple[int, int]]]:
    lines: List[str] = []
    bullet_ranges: List[Tuple[int, int]] = []
    cursor = 0

    for idx, para in enumerate(paragraphs):
        text = para.text.strip()
        if not text:
            continue
        line = text
        if idx < len(paragraphs) - 1:
            line += "\n"
        lines.append(line)

        if para.is_bullet:
            start = cursor
            end = cursor + len(text)
            if idx < len(paragraphs) - 1:
                end += 1
            bullet_ranges.append((start, end))
        cursor += len(line)

    return "".join(lines), bullet_ranges


def _get_default_slide_id(presentation: Dict[str, Any]) -> Optional[str]:
    slides = presentation.get("slides") or []
    if not slides:
        return None
    return slides[0].get("objectId")


def _new_object_id(prefix: str) -> str:
    return f"{prefix}_{uuid.uuid4().hex}"


# Convenience functions
def get_slides_service() -> Resource:
    """
    Build and return the Google Slides API service client.
    """
    credentials = load_credentials()
    return build("slides", "v1", credentials=credentials, cache_discovery=False)


def read_presentation(presentation_id_or_url: str) -> SlidePresentation:
    """Read a Google Slides presentation by ID or URL."""
    client = GoogleSlidesClient()
    return client.get_presentation(presentation_id_or_url)


def create_presentation(title: str, markdown: Optional[str] = None) -> SlidePresentation:
    """Create a new Google Slides presentation."""
    client = GoogleSlidesClient()
    return client.create_presentation(title, markdown)


def export_presentation(
    presentation_id_or_url: str,
    output_path: Path,
    export_format: str = "pdf",
) -> Path:
    """Export a presentation to PDF or PPTX."""
    client = GoogleSlidesClient()
    export_format = export_format.lower()
    if export_format == "pdf":
        return client.export_pdf(presentation_id_or_url, output_path)
    if export_format == "pptx":
        return client.export_pptx(presentation_id_or_url, output_path)
    raise GoogleSlidesError(f"Unsupported export format: {export_format}")


def insert_linked_chart(
    presentation_id_or_url: str,
    spreadsheet_id: str,
    chart_id: int,
    *,
    slide_index: int = 0,
    layout: str = "full",
    replace: bool = False,
    tag: Optional[str] = None,
) -> Dict[str, Any]:
    """Insert a linked Sheets chart into a slide."""
    client = GoogleSlidesClient()
    return client.insert_linked_chart(
        presentation_id_or_url,
        spreadsheet_id,
        chart_id,
        slide_index=slide_index,
        layout=layout,
        replace=replace,
        tag=tag,
    )


def insert_image(
    presentation_id_or_url: str,
    image_url: str,
    *,
    slide_index: int = 0,
    layout: str = "full",
    replace: bool = False,
    tag: Optional[str] = None,
) -> Dict[str, Any]:
    """Insert an image into a slide from a URL."""
    client = GoogleSlidesClient()
    return client.insert_image(
        presentation_id_or_url,
        image_url,
        slide_index=slide_index,
        layout=layout,
        replace=replace,
        tag=tag,
    )


__all__ = [
    "GoogleSlidesError",
    "GoogleSlidesClient",
    "SlidePresentation",
    "Slide",
    "SlideElement",
    "SlideTextBlock",
    "MarkdownSlide",
    "MarkdownParagraph",
    "SlideMarkdownParser",
    "extract_presentation_id",
    "get_slides_service",
    "read_presentation",
    "create_presentation",
    "export_presentation",
    "insert_linked_chart",
    "insert_image",
]
