import base64
import logging
import typing
import warnings
from pathlib import Path
from typing import Any, Dict, List, Literal, Optional, Tuple, Union

import requests

from .types import APIConfig, APIVersion, Country, Environment, Facet, Flavor, JSONType
from .utils import URLBuilder, http_session

logger = logging.getLogger(__name__)


def get_http_auth(environment: Environment) -> Optional[Tuple[str, str]]:
    return ("off", "off") if environment is Environment.net else None


def _send_request(
    url: str,
    api_config: APIConfig,
    method: Literal["GET", "POST", "DELETE", "HEAD", "PUT"] = "GET",
    return_none_on_404: bool = False,
    **request_args,
) -> requests.Response | None:
    """Send an HTTP request to the given URL.

    :param url: the URL to send the request to.
    :param api_config: the API configuration.
    :param method: type of HTTP request. Defaults to "GET".
    :param return_none_on_404: if True, None is returned if the response
        status code is 404. Defaults to False.
    :param request_args: Optional arguments that requests.Session.request takes
    :return: the API response.
    """
    # Raise early error on unknown methods
    if method.upper() not in ("GET", "POST", "DELETE", "HEAD", "PUT"):
        raise NotImplementedError(f'The HTTP method "{method}" is not implemented.')

    # Build up the request
    request_args |= {
        "url": url,
        "method": method,
        "headers": request_args.get("headers", dict())
        | {"User-Agent": api_config.user_agent},
        "timeout": api_config.timeout,
    }

    # Handle authentication
    if api_config.username and api_config.password:
        key = "json" if request_args.get("json") else "data"
        request_args[key] = request_args.get(key, dict()) | {
            "user_id": api_config.username,
            "password": api_config.password,
        }
    elif api_config.session_cookie:
        request_args["cookies"] = request_args.get("cookies", dict()) | {
            "session": api_config.session_cookie,
        }
    elif api_config.access_token:
        request_args["headers"] = request_args.get("headers", dict()) | {
            "Authorization": f"Bearer {api_config.access_token}",
        }
    if "auth" not in request_args:
        request_args["auth"] = get_http_auth(api_config.environment)

    # Handle request and return data
    r = http_session.request(**request_args)
    if r.status_code == 404 and return_none_on_404:
        return None
    r.raise_for_status()
    return r


def send_get_request(
    url: str,
    api_config: APIConfig,
    params: Optional[Dict[str, Any]] = None,
    return_none_on_404: bool = False,
    auth: Optional[Tuple[str, str]] = None,
) -> Optional[JSONType]:
    """Send a GET request to the given URL.

    :param url: the URL to send the request to
    :param api_config: the API configuration
    :param params: the query parameters, defaults to None
    :param return_none_on_404: if True, None is returned if the response
        status code is 404, defaults to False
    :return: the API response
    """
    r = _send_request(
        url,
        api_config=api_config,
        params=params,
        return_none_on_404=return_none_on_404,
        auth=auth,
        method="GET",
    )
    if r is None:
        return None
    else:
        return r.json()


def send_form_urlencoded_post_request(
    url: str, body: Dict[str, Any], api_config: APIConfig
) -> requests.Response | None:
    return _send_request(
        url,
        data=body,
        api_config=api_config,
        method="POST",
    )


def send_json_post_request(
    url: str, body: JSONType, api_config: APIConfig
) -> requests.Response | None:
    return _send_request(
        url,
        json=body,
        api_config=api_config,
        method="POST",
    )


class RobotoffResource:
    def __init__(self, api_config: APIConfig):
        self.api_config = api_config
        self.base_url = URLBuilder.robotoff(environment=api_config.environment)

    def predict_lang(self, text: str, k: int = 10, threshold: float = 0.01) -> JSONType:
        """Predict the language of a text.

        :param text: the text to predict the language of
        :param k: the number of predictions to return, defaults to 10
        :param threshold: the minimum probability for a prediction to be
            returned, defaults to 0.01
        :return: the API response
        """
        return http_session.post(
            url=f"{self.base_url}/api/v1/predict/lang",
            data={"text": text, "k": k, "threshold": threshold},
        ).json()


class NutriPatrolResource:
    def __init__(self, api_config: APIConfig):
        self.api_config = api_config
        self.base_url = URLBuilder.nutripatrol(environment=api_config.environment)

    def check_auth_method(self) -> None:
        """Check that the only authentication method is through session cookie,
        as other methods are currently not supported."""
        if not self.api_config.session_cookie:
            raise ValueError("session_cookie must be set to call Nutripatrol API")

        if self.api_config.username or self.api_config.password:
            raise ValueError(
                "Only authentication based on session cookie is supported with "
                "Nutripatrol API. Don't provide username and password, only "
                "session_cookie."
            )

        if self.api_config.access_token:
            raise ValueError(
                "Only authentication based on session cookie is supported with "
                "Nutripatrol API. Don't provide access_token, only "
                "session_cookie."
            )

    def get_flag(self, flag_id: int) -> JSONType:
        self.check_auth_method()
        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/flags/{flag_id}",
                api_config=self.api_config,
                method="GET",
            ),
        )
        return r.json()

    def get_flags(self) -> JSONType:
        self.check_auth_method()
        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/flags",
                api_config=self.api_config,
                method="GET",
            ),
        )
        return r.json()

    def create_flag(self, flag_data: JSONType) -> JSONType:
        self.check_auth_method()
        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/flags",
                api_config=self.api_config,
                method="POST",
                json=flag_data,
            ),
        )
        return r.json()

    def get_flags_by_ticket_batch(self, ticket_ids: list[int]) -> JSONType:
        self.check_auth_method()
        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/flags/batch",
                api_config=self.api_config,
                method="POST",
                json={"ticket_ids": ticket_ids},
            ),
        )
        return r.json()

    def get_ticket(self, ticket_id: int) -> JSONType:
        self.check_auth_method()
        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/tickets/{ticket_id}",
                api_config=self.api_config,
                method="GET",
            ),
        )
        return r.json()

    def get_tickets(
        self,
        barcode: str | None = None,
        status: str | None = None,
        type_: str | None = None,
        reason: list[str] | None = None,
        page: int = 1,
        page_size: int = 10,
    ) -> JSONType:
        self.check_auth_method()
        params: dict = {"page": page, "page_size": page_size}
        if barcode is not None:
            params["barcode"] = barcode
        if status is not None:
            params["status"] = status
        if type_ is not None:
            params["type_"] = type_
        if reason is not None:
            params["reason"] = reason

        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/tickets",
                api_config=self.api_config,
                method="GET",
                params=params,
            ),
        )
        return r.json()

    def update_ticket_status(self, ticket_id: int, status: str) -> JSONType:
        self.check_auth_method()
        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/tickets/{ticket_id}/status",
                api_config=self.api_config,
                method="PUT",
                params={"status": status},
            ),
        )
        return r.json()

    def get_stats(self, n_days: int = 31) -> JSONType:
        self.check_auth_method()
        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/stats",
                api_config=self.api_config,
                method="GET",
                params={"n_days": n_days},
            ),
        )
        return r.json()

    def status(self) -> JSONType:
        r = typing.cast(
            requests.Response,
            _send_request(
                f"{self.base_url}/api/v1/status",
                api_config=self.api_config,
                method="GET",
            ),
        )
        return r.json()


class FacetResource:
    def __init__(self, api_config: APIConfig):
        self.api_config = api_config
        self.base_url = URLBuilder.country(
            self.api_config.flavor,
            environment=api_config.environment,
            country_code=self.api_config.country.name,
        )

    def get(
        self,
        facet_name: Union[Facet, str],
        page: int = 1,
        page_size: int = 20,
        **kwargs: JSONType,
    ) -> JSONType:
        """Return all possible values for a given facet.

        :param facet_name: the facet name, e.g. "label"
        :param page: the page number, defaults to 1
        :param page_size: the number of items per page, defaults to 20
        :param kwargs: additional query parameters to pass to the API
        :return: the API response containing all possible values for the facet
        """
        facet = Facet.from_str_or_enum(facet_name)
        facet_plural = facet.value.replace("_", "-")
        r = _send_request(
            url=f"{self.base_url}/facets/{facet_plural}",
            params={"json": "1", "page": page, "page_size": page_size, **kwargs},
            api_config=self.api_config,
            method="GET",
        )
        return typing.cast(requests.Response, r).json()

    def get_products(
        self,
        facet_name: Union[Facet, str],
        facet_value: str,
        page: int = 1,
        page_size: int = 25,
        fields: Optional[List[str]] = None,
        sort_by: Optional[str] = None,
    ) -> JSONType:
        """Return products for a given facet value.

        :param facet_name: the facet name, e.g. "labels"
        :param facet_value: the facet value, e.g. "en:organic"
        :param page: the page number, defaults to 1
        :param page_size: the number of items per page, defaults to 25
        :param fields: a list of fields to return. If None, all fields are
            returned.
        :param sort_by: the sorting key, defaults to None (no sorting)
            possible values (not exhaustive) are: "popularity",
            "last_modified_t", "created_t".
        :return: the API response
        """
        facet = Facet.from_str_or_enum(facet_name)
        facet_plural = facet.value.replace("_", "-")
        params: JSONType = {"page": page, "page_size": page_size, "json": "1"}
        if fields is not None:
            params["fields"] = ",".join(fields)
        if sort_by is not None:
            params["sort_by"] = sort_by

        r = _send_request(
            url=f"{self.base_url}/facets/{facet_plural}/{facet_value}",
            params=params,
            api_config=self.api_config,
            method="GET",
        )
        return typing.cast(requests.Response, r).json()


class ProductResource:
    def __init__(self, api_config: APIConfig):
        self.api_config = api_config
        self.base_url = URLBuilder.country(
            self.api_config.flavor,
            environment=api_config.environment,
            country_code=self.api_config.country.name,
        )
        self.base_search_url = URLBuilder.search(self.api_config.environment)

    def get(
        self,
        code: str,
        fields: Optional[List[str]] = None,
        raise_if_invalid: bool = False,
    ) -> Optional[JSONType]:
        """Return a product.

        If the product does not exist, None is returned.

        :param code: barcode of the product
        :param fields: a list of fields to return. If None, all fields are
            returned.
        :param raise_if_invalid: if True, a ValueError is raised if the
            barcode is invalid, defaults to False.
        :return: the API response
        """
        if len(code) == 0:
            raise ValueError("code must be a non-empty string")

        fields = fields or []
        url = f"{self.base_url}/api/{self.api_config.version}/product/{code}"

        if fields:
            # requests escape comma in URLs, as expected, but openfoodfacts
            # server does not recognize escaped commas.
            # See
            # https://github.com/openfoodfacts/openfoodfacts-server/issues/1607
            url += "?fields={}".format(",".join(fields))

        r = _send_request(
            url=url,
            api_config=self.api_config,
            return_none_on_404=True,
            method="GET",
        )

        if r is None:
            # product not found
            return None
        else:
            resp = r.json()

        if resp["status"] == 0:
            # invalid barcode
            if raise_if_invalid:
                raise ValueError(f"invalid barcode: {code}")
            return None

        return resp["product"] if resp is not None else None

    def text_search(
        self,
        query: str,
        page: int = 1,
        page_size: int = 20,
        sort_by: Optional[str] = None,
    ):
        """Search products using a textual query.

        :param query: the search query
        :param page: requested page (starts at 1), defaults to 1
        :param page_size: number of items per page, defaults to 20
        :param sort_by: result sorting key, defaults to None (no sorting)
        :return: the search results
        """
        # We force usage of v2 of API
        params = {
            "search_terms": query,
            "page": page,
            "page_size": page_size,
            "sort_by": sort_by,
            "json": "1",
        }

        if sort_by is not None:
            params["sort_by"] = sort_by

        r = _send_request(
            url=f"{self.base_url}/cgi/search.pl",
            api_config=self.api_config,
            params=params,
            method="GET",
        )
        return typing.cast(requests.Response, r).json()

    def search(
        self,
        query: str,
        page: int = 1,
        page_size: int = 24,
        sort_by: Optional[str] = None,
    ) -> JSONType:
        """Search products using the search-a-licious endpoint.

        .. warning::
            This feature is in alpha. The search-a-licious API may
            change without notice.

        :param query: the search query
        :param page: page number (starts at 1), defaults to 1
        :param page_size: number of results per page, defaults to 24
        :param sort_by: field to sort by, optional
        :return: search results dict with a 'hits' key containing products
        """
        params: dict = {"q": query, "page": page, "page_size": page_size}
        if sort_by is not None:
            params["sort_by"] = sort_by
        resp = http_session.get(
            f"{self.base_search_url}/search",
            params=params,
        )
        resp.raise_for_status()
        return typing.cast(JSONType, resp.json())

    def update(self, body: Dict[str, Any]):
        """Create a new product or update an existing one."""
        if not body.get("code"):
            raise ValueError("missing code from body")

        url = f"{self.base_url}/cgi/product_jqm2.pl"
        return _send_request(
            url,
            api_config=self.api_config,
            method="POST",
            data=body,
        )

    def select_image(
        self,
        code: str,
        image_id: str,
        image_key: str,
        rotate: Optional[int] = None,
        crop_bounding_box: Optional[tuple[float, float, float, float]] = None,
    ):
        """Select an image (front/ingredients/nutrition/packaging) for a
        product.

        It's possible to rotate and crop the selection.

        :param code: the product barcode
        :param image_id: the raw image ID, it must be a digit (ex: 1, 2, 3, 4)
        :param image_key: the image field name (one of `front`, `ingredients`,
            `nutrition`, `packaging`) and the language code, separated by a
            `_`. Example: `front_fr`, `ingredients_en`, `nutrition_es`, etc.
        :param rotate: rotation angle in degrees (90, 180, 270), defaults to
            None (no rotation)
        :param crop_bounding_box: a tuple of 4 floats
            (y_min, x_min, y_max, x_max) that defines the bounding box of the
            crop, defaults to None (no crop)
        :raises ValueError: if the rotation angle is invalid or if no password
            or session cookie is provided
        :return: the API response
        """
        url = f"{self.base_url}/cgi/product_image_crop.pl"
        params: JSONType = {
            "code": code,
            "imgid": image_id,
            # We need to tell Product Opener that the bounding box coordinates
            # are related to the full image
            "coordinates_image_size": "full",
        }

        if rotate is not None and rotate != 0:
            if rotate not in (90, 180, 270):
                raise ValueError(f"invalid value for rotation angle: {rotate}")
            params["angle"] = str(rotate)

        if crop_bounding_box is not None:
            y_min, x_min, y_max, x_max = crop_bounding_box
            params["x1"] = x_min
            params["y1"] = y_min
            params["x2"] = x_max
            params["y2"] = y_max

        if image_key is not None:
            params["id"] = image_key

        if not (self.api_config.session_cookie or self.api_config.password):
            raise ValueError(
                "a password or a session cookie is required to select an image"
            )

        return _send_request(
            url,
            data=params,
            api_config=self.api_config,
            method="POST",
        )

    def parse_ingredients(
        self, text: str, lang: str, timeout: int = 10
    ) -> list[JSONType]:
        """Parse ingredients text using Product Opener API.

        It is only available for `off` flavor (food).

        The result is a list of ingredients, each ingredient is a dict with the
        following keys:

        - id: the ingredient ID. Having an ID does not means that the
            ingredient is recognized, you must check if it exists in the
            taxonomy.
        - text: the ingredient text (as it appears in the input ingredients
            list)
        - percent_min: the minimum percentage of the ingredient in the product
        - percent_max: the maximum percentage of the ingredient in the product
        - percent_estimate: the estimated percentage of the ingredient in the
            product
        - vegan (bool): optional key indicating if the ingredient is vegan
        - vegetarian (bool): optional key indicating if the ingredient is
            vegetarian

        :param server_type: the server type (project) to use
        :param text: the ingredients text to parse
        :param lang: the language of the text (used for parsing) as a 2-letter
            code
        :param timeout: the request timeout in seconds, defaults to 10s
        :raises RuntimeError: a RuntimeError is raised if the parsing fails
        :return: the list of parsed ingredients
        """
        if self.api_config.flavor != Flavor.off:
            raise ValueError("ingredient parsing is only available for food")

        api_version = self.api_config.version

        if not api_version.startswith("v3"):
            logger.warning(
                "ingredient parsing is only available in v3 of the API (here: %s), "
                "using v3",
                self.api_config.version,
            )
            api_version = "v3"

        # by using "test" as code, we don't save any information to database
        # This endpoint is specifically designed for testing purposes
        url = f"{self.base_url}/api/{api_version}/product/test"

        if len(text) == 0:
            raise ValueError("text must be a non-empty string")

        try:
            r = http_session.patch(
                url,
                auth=get_http_auth(self.api_config.environment),
                json={
                    "fields": "ingredients",
                    "lc": lang,
                    "tags_lc": lang,
                    "product": {
                        "lang": lang,
                        f"ingredients_text_{lang}": text,
                    },
                },
                timeout=timeout,
            )
        except (
            requests.exceptions.ConnectionError,
            requests.exceptions.SSLError,
            requests.exceptions.Timeout,
        ) as e:
            raise RuntimeError(
                f"Unable to parse ingredients: error during HTTP request: {e}"
            ) from e

        if not r.ok:
            raise RuntimeError(
                "Unable to parse ingredients (non-200 status code): "
                f"{r.status_code}, {r.text}"
            )

        response_data = r.json()

        if response_data.get("status") != "success":
            raise RuntimeError(f"Unable to parse ingredients: {response_data}")

        return response_data["product"].get("ingredients", [])

    def upload_image(
        self,
        code: str,
        image_path: Path | str | None = None,
        image_data_base64: str | None = None,
        selected: JSONType | None = None,
    ):
        """Upload an image for a product, and optionally select this image for
        some specific field and language (example: `nutrition` image for `es`
        language).

        This method requires a password to be set in the API configuration.
        Authentication through a session cookie does not seem to work
        currently.

        We force here the usage of v3 of the API.

        The image can be provided either as a file path or as a base64-encoded
        string.

        :param code: the product barcode
        :param image_path: the path to the image file, defaults to None. One of
            image_path or image_data_base64 must be provided.
        :param image_data_base64: the base64-encoded image, defaults to None.
            One of image_path or image_data_base64 must be provided.
        :param selected: a dict that describes how to select the image after
            upload, defaults to None (no selection). Example:
            {
                "front": {"fr": {}},
                "ingredients": {"en": {}},
            }
            will select the image as the front image for french language and
            as the ingredients image for english language.

        :return: the API response (a requests.Response object)
        :raises ValueError: if no password or session cookie is provided, or if
            both image_path and image_data_base64 are None.
        """
        api_version = self.api_config.version
        if not self.api_config.version.startswith("v3"):
            warnings.warn(
                "image upload is only available in v3 of the API (here: %s), "
                "forcing use of v3" % self.api_config.version,
                stacklevel=2,
            )
            api_version = "v3"

        if (self.api_config.username is None or self.api_config.password is None) and (
            self.api_config.session_cookie is None
        ):
            raise ValueError(
                "a password or a session cookie is required to upload an image"
            )

        if not code.isdigit():
            raise ValueError("code must be a numeric string")

        if image_path is None and image_data_base64 is None:
            raise ValueError("one of image_path or image_data_base64 must be provided")

        if image_path is not None and image_data_base64 is not None:
            raise ValueError(
                "only one of image_path or image_data_base64 must be provided"
            )

        if image_data_base64 is None and image_path is not None:
            with Path(image_path).open("rb") as f:
                image_data_base64 = base64.b64encode(f.read()).decode("utf-8")

        if selected:
            for key in selected:
                if key not in ("front", "ingredients", "nutrition", "packaging"):
                    raise ValueError(
                        f"invalid image field name in selected: {key} "
                        "(must be one of front, ingredients, nutrition, packaging)"
                    )
                lang_info = selected[key]
                if not isinstance(lang_info, dict):
                    raise ValueError(
                        f"selected[{key}] must be a dict with a language code, "
                        f"got {type(lang_info)}"
                    )

                for lang_code, value in lang_info.items():
                    if not isinstance(lang_code, str) or len(lang_code) != 2:
                        raise ValueError(
                            f"invalid language code in selected[{key}]: {lang_code}"
                        )
                    if not isinstance(value, dict):
                        raise ValueError(
                            f"selected[{key}][{lang_code}] must be a dict, got "
                            f"{type(value)}"
                        )

        # copy the config but force v3 of the API
        api_config = self.api_config.model_copy(update={"version": APIVersion.v3})
        url = f"{self.base_url}/api/{api_version}/product/{code}/images"
        data: JSONType = {"image_data_base64": image_data_base64}

        if selected:
            data["selected"] = selected

        return _send_request(
            url,
            json=data,
            api_config=api_config,
            method="POST",
        )


class API:
    def __init__(
        self,
        user_agent: str,
        username: Optional[str] = None,
        password: Optional[str] = None,
        country: Union[Country, str] = Country.world,
        flavor: Union[Flavor, str] = Flavor.off,
        version: Union[APIVersion, str] = APIVersion.v2,
        environment: Union[Environment, str] = Environment.org,
        session_cookie: Optional[str] = None,
        timeout: int = 10,
        access_token: Optional[str] = None,
    ) -> None:
        """Initialize the API instance.

        :param user_agent: the user agent to use for HTTP requests, this is
            mandatory. Give a meaningful user agent that describes your
            app/script.
        :param username: user username, only used for write requests, defaults
            to None
        :param password: user password, only used for write requests, defaults
            to None
        :param country: the country, has an effect on the default display
            language and when uploading products, defaults to Country.world
        :param flavor: which O*F project to use, defaults to Flavor.off (Open
            Food Facts)
        :param version: the API version to use, defaults to APIVersion.v2
        :param environment: what environment (prod/staging) to use, defaults
            to Environment.org
        :param session_cookie: a session cookie, only used for write requests,
            defaults to None
        :param timeout: the timeout for HTTP requests, defaults to 10 seconds
        :param access_token: a keycloak access token, generated with single sign on,
            only used for write requests, defaults to None
        """
        if not isinstance(country, Country):
            country = Country[country]

        if isinstance(version, APIVersion):
            version = version.value

        self.api_config = APIConfig(
            user_agent=user_agent,
            country=country,
            flavor=Flavor[flavor],
            version=version,
            environment=Environment[environment],
            username=username,
            password=password,
            session_cookie=session_cookie,
            timeout=timeout,
            access_token=access_token,
        )
        self.password = password
        self.country = country
        self.product = ProductResource(self.api_config)
        self.facet = FacetResource(self.api_config)
        self.robotoff = RobotoffResource(self.api_config)
        self.nutripatrol = NutriPatrolResource(self.api_config)


def parse_ingredients(text: str, lang: str, api_config: APIConfig) -> list[JSONType]:
    """Parse ingredients text using Product Opener API.

    It is only available for `off` flavor (food).

    The result is a list of ingredients, each ingredient is a dict with the
    following keys:

    - id: the ingredient ID. Having an ID does not means that the ingredient
        is recognized, you must check if it exists in the taxonomy.
    - text: the ingredient text (as it appears in the input ingredients list)
    - percent_min: the minimum percentage of the ingredient in the product
    - percent_max: the maximum percentage of the ingredient in the product
    - percent_estimate: the estimated percentage of the ingredient in the
        product
    - vegan (bool): optional key indicating if the ingredient is vegan
    - vegetarian (bool): optional key indicating if the ingredient is
        vegetarian


    :param text: the ingredients text to parse
    :param lang: the language of the text (used for parsing) as a 2-letter code
    :param api_config: the API configuration
    :raises RuntimeError: a RuntimeError is raised if the parsing fails
    :return: the list of parsed ingredients
    """
    base_url = URLBuilder.country(
        Flavor.off,
        environment=api_config.environment,
        country_code=api_config.country.name,
    )
    # by using "test" as code, we don't save any information to database
    # This endpoint is specifically designed for testing purposes
    url = f"{base_url}/api/v3/product/test"

    if len(text) == 0:
        raise ValueError("text must be a non-empty string")

    try:
        r = http_session.patch(
            url,
            auth=get_http_auth(api_config.environment),
            json={
                "fields": "ingredients",
                "lc": lang,
                "tags_lc": lang,
                "product": {
                    "lang": lang,
                    f"ingredients_text_{lang}": text,
                },
            },
            timeout=api_config.timeout,
        )
    except (
        requests.exceptions.ConnectionError,
        requests.exceptions.SSLError,
        requests.exceptions.Timeout,
    ) as e:
        raise RuntimeError(
            f"Unable to parse ingredients: error during HTTP request: {e}"
        ) from e

    if not r.ok:
        raise RuntimeError(
            "Unable to parse ingredients (non-200 status code): "
            f"{r.status_code}, {r.text}"
        )

    response_data = r.json()

    if response_data.get("status") != "success":
        raise RuntimeError(f"Unable to parse ingredients: {response_data}")

    return response_data["product"]["ingredients"]
