from typing import Optional
import os
import ssl
import warnings

import requests
from urllib.request import urlopen
from urllib.error import URLError, HTTPError
from matplotlib.font_manager import FontProperties, fontManager
from matplotlib import rcParams

from pyfonts.is_valid import _is_url, _is_valid_raw_url
from pyfonts.cache import _create_cache_from_fontfile
from pyfonts.decompress import _decompress_woff_to_ttf
from pyfonts.utils import _get_font_provider_metadata, _get_fonturl


def load_font(
    font_url: Optional[str] = None,
    use_cache: bool = True,
    danger_not_verify_ssl: bool = False,
    font_path: Optional[str] = None,
) -> FontProperties:
    """
    Loads a matplotlib `FontProperties` object from a remote url or a local file,
    that you can then use in your matplotlib charts.

    This function is most useful when the font you are looking for is stored locally
    or is not available in Google Fonts. Otherwise, it's easier to use the
    [`load_google_font()`](load_google_font.md) function instead.

    If the url points to a font file on Github, add `?raw=true` at the end of the
    url (see examples below).

    Args:
        font_url: It may be one of the following:
            - A URL pointing to a binary font file.
            - The local file path of the font.
        use_cache: Whether or not to cache fonts (to make pyfonts faster). Default to `True`.
        danger_not_verify_ssl: Whether or not to to skip SSL certificate on
            `ssl.SSLCertVerificationError`. If `True`, it's a **security risk** (such as data breaches or
            man-in-the-middle attacks), but can be convenient in some cases, like local
            development when behind a firewall.
        font_path: (deprecated) The local file path of the font. Use `font_url` instead.

    Returns:
        matplotlib.font_manager.FontProperties: A `FontProperties` object containing the loaded font.

    Examples:

        ```python
        from pyfonts import load_font

        font = load_font(
            "https://github.com/y-sunflower/pyfonts/blob/main/tests/Ultra-Regular.ttf?raw=true"
        )
        ```
    """
    if font_path is not None:
        warnings.warn(
            "`font_path` argument is deprecated and will be removed in a future version."
            f" Please replace `load_font(font_path='{font_path}')` by `load_font('{font_path}')`."
        )
        font_prop: FontProperties = FontProperties(fname=font_path)
        try:
            font_prop.get_name()
        except FileNotFoundError:
            raise FileNotFoundError(f"Font file not found: '{font_path}'.")
        return font_prop

    if font_url is not None:
        if not _is_url(font_url):
            # if it's not an url, it should be a path
            font_prop: FontProperties = FontProperties(fname=font_url)
            try:
                font_prop.get_name()
            except FileNotFoundError:
                raise FileNotFoundError(f"Font file not found: '{font_url}'.")
            return font_prop
        if not _is_valid_raw_url(font_url):
            raise ValueError(
                f"""The URL provided ({font_url}) does not appear to be valid.
                It must point to a binary font file from Github.
                Have you forgotten to append `?raw=true` to the end of the URL?
                """
            )

        cached_fontfile, cache_dir = _create_cache_from_fontfile(font_url)

        if use_cache:
            # check if file is in cache
            if os.path.exists(cached_fontfile):
                try:
                    # woff/woff2 are not supported by matplotlib, so we convert them
                    # to ttf. This is mostly useful to work with Bunny fonts API.
                    if cached_fontfile.endswith(("woff", "woff2")):
                        cached_fontfile: str = _decompress_woff_to_ttf(cached_fontfile)

                    font_prop: FontProperties = FontProperties(fname=cached_fontfile)
                    font_prop.get_name()  # triggers an error if invalid
                    return font_prop
                except Exception:
                    # cached file is invalid, remove and proceed to download
                    os.remove(cached_fontfile)

        try:
            response = urlopen(font_url)
        except HTTPError as e:
            if e.code == 404:
                raise Exception(
                    "404 error. The url passed does not exist: font file not found."
                )
            else:
                raise ValueError(f"An HTTPError has occurred. Code: {e.code}")
        except URLError as e:
            if isinstance(e.reason, ssl.SSLCertVerificationError):
                if danger_not_verify_ssl:
                    warnings.warn(
                        "SSL certificate verification disabled. This is insecure and vulnerable "
                        "to man-in-the-middle attacks. Use only in trusted environments.",
                        UserWarning,
                    )
                    response = urlopen(
                        font_url, context=ssl._create_unverified_context()
                    )
                else:
                    raise Exception(
                        "SSL certificate verification failed. "
                        "If you are behind a firewall or using a proxy, "
                        "try setting `danger_not_verify_ssl=True` to bypass verification."
                        "verification."
                    )
            else:
                raise Exception(
                    "Failed to load font. This may be due to a lack of internet connection "
                    "or an environment where local files are not accessible (Pyodide, etc)."
                )

        content = response.read()
        with open(cached_fontfile, "wb") as f:
            f.write(content)

        if cached_fontfile.endswith(("woff", "woff2")):
            cached_fontfile: str = _decompress_woff_to_ttf(cached_fontfile)

        return FontProperties(fname=cached_fontfile)
    else:
        raise ValueError("You must provide a `font_url`.")


def set_default_font(font: FontProperties) -> None:
    """
    Set the default font for all text elements generated by matplotlib,
    including axis labels, tick labels, legend entries, titles, etc.

    Under the hood it updates all the relevant matplotlib rcParams and
    registers the loaded font files with matplotlib. When the font comes
    from Google Fonts or Bunny Fonts, pyfonts also attempts to register
    the other family variants so later `weight=` or `style=` overrides
    can be resolved by matplotlib.

    Args:
        font: A `FontProperties` object containing the font to set as default.

    Examples:

        ```python
        from pyfonts import set_default_font, load_google_font

        set_default_font(load_google_font("Fascinate Inline"))
        plt.title("Title") # will be in Fascinate Inline
        plt.plot([1, 2, 3], label="Plot")
        # ^ axis labels, ticks, legend entries all also in Fascinate Inline
        ```
    """
    _register_font_variants(font)
    rcParams.update(
        {
            "font.family": _get_default_font_families(font),
            "font.style": font.get_style(),
            "font.weight": font.get_weight(),
            "font.size": font.get_size(),
            "font.stretch": font.get_stretch(),
            "font.variant": font.get_variant(),
        }
    )


def _get_default_font_families(font: FontProperties) -> list[str]:
    families: list[str] = [font.get_name()]
    provider_metadata = _get_font_provider_metadata(font)

    if provider_metadata is None:
        return families

    requested_family: str = provider_metadata["family"]
    if requested_family not in families:
        families.append(requested_family)

    return families


def _register_font_variants(font: FontProperties) -> None:
    for font_file in _get_font_variant_files(font):
        fontManager.addfont(font_file)


def _get_font_variant_files(font: FontProperties) -> set[str]:
    font_files: set[str] = set()
    current_font_file = font.get_file()

    if current_font_file is not None:
        font_files.add(str(current_font_file))

    provider_metadata = _get_font_provider_metadata(font)
    if provider_metadata is None:
        return font_files

    # Best-effort preload of provider variants so matplotlib can honor
    # later weight/style overrides against the same family.
    for italic in (False, True):
        for weight in range(100, 1000, 100):
            try:
                font_url = _get_fonturl(
                    endpoint=provider_metadata["endpoint"],
                    family=provider_metadata["family"],
                    weight=weight,
                    italic=italic,
                    allowed_formats=provider_metadata["allowed_formats"],
                    use_cache=provider_metadata["use_cache"],
                )
                variant_font = load_font(
                    font_url=font_url,
                    use_cache=provider_metadata["use_cache"],
                    danger_not_verify_ssl=provider_metadata["danger_not_verify_ssl"],
                )
            except (
                requests.exceptions.HTTPError,
                ValueError,
                RuntimeError,
            ):
                continue
            except requests.exceptions.RequestException:
                return font_files

            variant_file = variant_font.get_file()
            if variant_file is not None:
                font_files.add(str(variant_file))

    return font_files
