# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# 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.

"""Manage the user's Streamlit credentials."""

from __future__ import annotations

import json
import os
import sys
import textwrap
from typing import Final, NamedTuple, NoReturn, cast
from uuid import uuid4

from streamlit import cli_util, config, env_util, file_util, util
from streamlit.logger import get_logger

_LOGGER: Final = get_logger(__name__)


_CONFIG_FILE_PATH: Final = (
    r"%userprofile%/.streamlit/config.toml"
    if env_util.IS_WINDOWS
    else "~/.streamlit/config.toml"
)


class _Activation(NamedTuple):
    email: str | None  # the user's email.
    is_valid: bool  # whether the email is valid.


def email_prompt() -> str:
    # Emoji can cause encoding errors on non-UTF-8 terminals
    # (See https://github.com/streamlit/streamlit/issues/2284.)
    # WT_SESSION is a Windows Terminal specific environment variable. If it exists,
    # we are on the latest Windows Terminal that supports emojis
    show_emoji = sys.stdout.encoding == "utf-8" and (
        not env_util.IS_WINDOWS or os.environ.get("WT_SESSION")
    )

    # IMPORTANT: Break the text below at 80 chars.
    return f"""
      {"👋 " if show_emoji else ""}{cli_util.style_for_cli("Welcome to Streamlit!", bold=True)}

      If you'd like to receive helpful onboarding emails, news, offers, promotions,
      and the occasional swag, please enter your email address below. Otherwise,
      leave this field blank.

      {cli_util.style_for_cli("Email: ", fg="blue")}"""


_TELEMETRY_HEADLESS_TEXT = """
Collecting usage statistics. To deactivate, set browser.gatherUsageStats to false.
"""


def _send_email(email: str | None) -> None:
    """Send the user's email for metrics, if submitted."""
    import requests

    if email is None or "@" not in email:
        return

    metrics_url = ""
    try:
        response_json = requests.get(
            "https://data.streamlit.io/metrics.json", timeout=2
        ).json()
        metrics_url = response_json.get("url", "")
    except Exception:
        _LOGGER.exception("Failed to fetch metrics URL")
        return

    headers = {
        "accept": "*/*",
        "accept-language": "en-US,en;q=0.9",
        "content-type": "application/json",
        "origin": "localhost:8501",
        "referer": "localhost:8501/",
    }

    data = {
        "anonymous_id": None,
        "messageId": str(uuid4()),
        "event": "submittedEmail",
        "author_email": email,
        "source": "provided_email",
        "type": "track",
        "userId": email,
    }

    response = requests.post(
        metrics_url,
        headers=headers,
        data=json.dumps(data).encode(),
        timeout=10,
    )

    response.raise_for_status()


class Credentials:
    """Credentials class."""

    _singleton: Credentials | None = None

    @classmethod
    def get_current(cls) -> Credentials:
        """Return the singleton instance."""
        if cls._singleton is None:
            Credentials()

        return cast("Credentials", Credentials._singleton)

    def __init__(self) -> None:
        """Initialize class."""
        if Credentials._singleton is not None:
            raise RuntimeError(
                "Credentials already initialized. Use .get_current() instead"
            )

        self.activation: _Activation | None = None
        self._conf_file: str = _get_credential_file_path()

        Credentials._singleton = self

    def __repr__(self) -> str:
        return util.repr_(self)

    def load(self, auto_resolve: bool = False) -> None:
        """Load from toml file."""
        if self.activation is not None:
            _LOGGER.error("Credentials already loaded. Not rereading file.")
            return

        import toml

        try:
            with open(self._conf_file) as f:
                data = toml.load(f).get("general")
            if data is None:
                raise RuntimeError  # noqa: TRY301
            self.activation = _verify_email(data.get("email"))
        except FileNotFoundError:
            if auto_resolve:
                self.activate(show_instructions=not auto_resolve)
                return
            raise RuntimeError(
                'Credentials not found. Please run "streamlit activate".'
            )
        except Exception:
            if auto_resolve:
                self.reset()
                self.activate(show_instructions=not auto_resolve)
                return
            raise RuntimeError(
                textwrap.dedent(
                    """
                Unable to load credentials from %s.
                Run "streamlit reset" and try again.
                """
                )
                % (self._conf_file)
            )

    def _check_activated(self, auto_resolve: bool = True) -> None:
        """Check if streamlit is activated.

        Used by `streamlit run script.py`
        """
        try:
            self.load(auto_resolve)
        except (Exception, RuntimeError) as e:
            _exit(str(e))

        if self.activation is None or not self.activation.is_valid:
            _exit("Activation email not valid.")

    @classmethod
    def reset(cls) -> None:
        """Reset credentials by removing file.

        This is used by `streamlit activate reset` in case a user wants
        to start over.
        """
        c = Credentials.get_current()
        c.activation = None

        try:
            os.remove(c._conf_file)
        except OSError:
            _LOGGER.exception("Error removing credentials file.")

    def save(self) -> None:
        """Save to toml file and send email."""
        from requests.exceptions import RequestException

        if self.activation is None:
            return

        # Create intermediate directories if necessary
        os.makedirs(os.path.dirname(self._conf_file), exist_ok=True)

        # Write the file
        data = {"email": self.activation.email}

        import toml

        with open(self._conf_file, "w") as f:
            toml.dump({"general": data}, f)

        try:
            _send_email(self.activation.email)
        except RequestException:
            _LOGGER.exception("Error saving email:")

    def activate(self, show_instructions: bool = True) -> None:
        """Activate Streamlit.

        Used by `streamlit activate`.
        """
        try:
            self.load()
        except RuntimeError:
            # Runtime Error is raised if credentials file is not found. In that case,
            # `self.activation` is None and we will show the activation prompt below.
            pass

        if self.activation:
            if self.activation.is_valid:
                _exit("Already activated")
            else:
                _exit(
                    "Activation not valid. Please run "
                    "`streamlit activate reset` then `streamlit activate`"
                )
        else:
            activated = False

            while not activated:
                import click

                email = click.prompt(
                    text=email_prompt(),
                    prompt_suffix="",
                    default="",
                    show_default=False,
                )

                self.activation = _verify_email(email)
                if self.activation.is_valid:
                    self.save()
                    # IMPORTANT: Break the text below at 80 chars.
                    telemetry_text = f"""
  You can find our privacy policy at {cli_util.style_for_cli("https://streamlit.io/privacy-policy", underline=True)}

  Summary:
  - This open source library collects usage statistics.
  - We cannot see and do not store information contained inside Streamlit apps,
    such as text, charts, images, etc.
  - Telemetry data is stored in servers in the United States.
  - If you'd like to opt out, add the following to {cli_util.style_for_cli(_CONFIG_FILE_PATH)},
    creating that file if necessary:

    [browser]
    gatherUsageStats = false
"""

                    cli_util.print_to_cli(telemetry_text)
                    if show_instructions:
                        # IMPORTANT: Break the text below at 80 chars.
                        instructions_text = f"""
  {cli_util.style_for_cli("Get started by typing:", fg="blue", bold=True)}
  {cli_util.style_for_cli("$", fg="blue")} {cli_util.style_for_cli("streamlit hello", bold=True)}
"""

                        cli_util.print_to_cli(instructions_text)
                    activated = True
                else:  # pragma: nocover
                    _LOGGER.error("Please try again.")


def _verify_email(email: str) -> _Activation:
    """Verify the user's email address.

    The email can either be an empty string (if the user chooses not to enter
    it), or a string with a single '@' somewhere in it.

    Parameters
    ----------
    email : str

    Returns
    -------
    _Activation
        An _Activation object. Its 'is_valid' property will be True only if
        the email was validated.

    """
    email = email.strip()

    # We deliberately use simple email validation here
    # since we do not use email address anywhere to send emails.
    if len(email) > 0 and email.count("@") != 1:
        _LOGGER.error("That doesn't look like an email :(")
        return _Activation(None, False)

    return _Activation(email, True)


def _exit(message: str) -> NoReturn:
    """Exit program with error."""
    _LOGGER.error(message)
    sys.exit(-1)


def _get_credential_file_path() -> str:
    return file_util.get_streamlit_file_path("credentials.toml")


def _check_credential_file_exists() -> bool:
    return os.path.exists(_get_credential_file_path())


def check_credentials() -> None:
    """Check credentials and potentially activate.

    Note
    ----
    If there is no credential file and we are in headless mode, we should not
    check, since credential would be automatically set to an empty string.

    """

    if not _check_credential_file_exists() and (
        config.get_option("server.headless")
        or not config.get_option("server.showEmailPrompt")
    ):
        if not config.is_manually_set("browser.gatherUsageStats"):
            # If not manually defined, show short message about usage stats gathering.
            cli_util.print_to_cli(_TELEMETRY_HEADLESS_TEXT)
        return
    Credentials.get_current()._check_activated()
