# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import io
from pathlib import Path
from typing import TYPE_CHECKING, Any, TypeAlias, cast

from streamlit import url_util
from streamlit.elements.lib.layout_utils import validate_height
from streamlit.elements.lib.utils import normalize_alt
from streamlit.errors import (
    StreamlitAPIException,
    StreamlitInvalidParameterTypeError,
    StreamlitMissingRequiredParameterError,
)
from streamlit.runtime.metrics_util import gather_metrics

if TYPE_CHECKING:
    from streamlit.delta_generator import DeltaGenerator
    from streamlit.elements.lib.layout_utils import HeightWithoutContent

PdfData: TypeAlias = str | Path | bytes | io.BytesIO


def _get_pdf_component() -> Any | None:
    """Get the PDF custom component if available.

    Returns
    -------
    Any | None
        The pdf_viewer function if the streamlit-pdf component is available,
        None otherwise.
    """
    try:
        import streamlit_pdf  # type: ignore

        return streamlit_pdf.pdf_viewer
    except ImportError:  # pragma: no cover - optional dep
        return None


class PdfMixin:
    @gather_metrics("pdf")
    def pdf(
        self,
        data: PdfData,
        *,
        height: HeightWithoutContent = 500,
        key: str | None = None,
        alt: str | None = None,
    ) -> DeltaGenerator:
        """Display a PDF viewer.

        .. Important::

            You must install |streamlit-pdf|_ to use this command. You can
            install it as an extra with Streamlit:

            .. code-block:: shell

                pip install streamlit[pdf]

        .. |streamlit-pdf| replace:: ``streamlit-pdf``
        .. _streamlit-pdf: https://github.com/streamlit/streamlit-pdf

        Parameters
        ----------
        data : str, Path, BytesIO, or bytes
            The PDF file to show. This can be one of the following:

            - A URL (string) for a hosted PDF file.
            - A path to a local PDF file. If you use a relative path, it must
              be relative to the current working directory.
            - A file-like object. For example, this can be an ``UploadedFile``
              from ``st.file_uploader``, or this can be a local file opened
              with ``open()``.
            - Raw bytes data.

        height : int or "stretch"
            The height of the PDF viewer. This can be one of the following:

            - An integer specifying the height in pixels: The viewer has a
              fixed height. If the content is larger than the specified
              height, scrolling is enabled. This is ``500`` by default.
            - ``"stretch"``: The height of the viewer matches the height of
              its content or the height of the parent container, whichever is
              larger. If the viewer is not in a parent container, the height
              of the viewer matches the height of its content.

        key : str or None
            An optional string to use for giving this element a stable
            identity. If this is ``None`` (default), the element's identity
            will be determined based on the values of the other parameters.

        alt : str or None
            A short, plain-text accessible name for the PDF viewer. If this is
            ``None`` (default), the viewer has no accessible name.

            An empty or whitespace-only string is treated the same as ``None``
            and is logged so authors notice the dual meaning of ``alt=""``
            across commands (decorative only on ``st.image`` / ``st.pyplot``).

            Describe what the document is rather than repeating text that is
            already visible on the page. This names the viewer for assistive
            technologies; it is not a full text alternative for the PDF's
            contents.

        Examples
        --------
        >>> st.pdf("https://example.com/sample.pdf")
        >>> st.pdf("https://example.com/sample.pdf", height=600)
        >>> st.pdf("report.pdf", alt="Q3 2026 financial report")
        """
        # Validate data parameter early
        if data is None:
            raise StreamlitMissingRequiredParameterError(
                "data",
                detail=(
                    "Please provide a valid PDF file path, URL, bytes data, "
                    "or file-like object."
                ),
            )

        # Check if custom PDF component is available first
        pdf_component = _get_pdf_component()
        if pdf_component is None:
            return self._show_pdf_warning()

        return self._call_pdf_component(
            pdf_component, data, height, key, normalize_alt(alt)
        )

    def _call_pdf_component(
        self,
        pdf_component: Any,
        data: PdfData,
        height: HeightWithoutContent,
        key: str | None,
        normalized_alt: str | None,
    ) -> DeltaGenerator:
        """Call the custom PDF component with the provided data."""
        # Validate height parameter after confirming component is available
        validate_height(height, allow_content=False)

        # Convert data to the format expected by pdf_viewer component
        file_param: str | bytes

        if isinstance(data, (str, Path)):
            data_str = str(data).strip()  # Strip whitespace from URLs
            if url_util.is_url(data_str, allowed_schemas=("http", "https")):
                # It's a URL - pass directly
                file_param = data_str
            else:
                # It's a local file path - read the content as bytes for security
                try:
                    with open(data_str, "rb") as file:
                        file_param = file.read()
                except (FileNotFoundError, PermissionError) as e:
                    raise StreamlitAPIException(
                        f"Unable to read file '{data_str}': {e}",
                        error_id="pdf-unable-to-read-file",
                    )

        elif isinstance(data, bytes):
            # Pass bytes directly - the component will handle uploading to media storage
            file_param = data
        elif hasattr(data, "read") and hasattr(data, "getvalue"):
            # Handle BytesIO and similar
            file_param = data.getvalue()
        elif hasattr(data, "read"):
            # Handle other file-like objects
            file_param = data.read()
        else:
            raise StreamlitInvalidParameterTypeError(
                "data",
                type(data).__name__,
                ["str", "Path", "bytes", "file-like object"],
                detail="A str can be a file path or a URL.",
            )

        # Convert to component-compatible format
        if height == "stretch":
            # For stretch, we need to pass a special value the component understands
            # This maintains compatibility with the component while using standard layout
            component_height = "stretch"
        else:
            component_height = str(height)

        # Only forward alt when set so unlabeled st.pdf still works on older
        # streamlit-pdf builds that reject unexpected kwargs.
        component_kwargs: dict[str, Any] = {
            "file": file_param,
            "height": component_height,
            "key": key,
        }
        if normalized_alt is not None:
            component_kwargs["alt"] = normalized_alt

        result = pdf_component(**component_kwargs)
        return cast("DeltaGenerator", result)

    def _show_pdf_warning(self) -> DeltaGenerator:
        """Raise an exception that the PDF component is not available."""
        raise StreamlitAPIException(
            "The PDF viewer requires the `streamlit-pdf` component to be installed.\n\n"
            "Please run `pip install streamlit[pdf]` to install it.\n\n"
            "For more information, see the Streamlit PDF documentation at "
            # TODO: Update this URL when docs are updated
            "https://docs.streamlit.io/develop/api-reference/media/st.pdf.",
            error_id="pdf-component-not-installed",
        )

    @property
    def dg(self) -> DeltaGenerator:
        """The associated DeltaGenerator."""
        return cast("DeltaGenerator", self)
