"""Module that provides various utility methods on the portal level."""

from Acquisition import aq_inner
from email.utils import formataddr
from email.utils import parseaddr
from logging import getLogger
from plone.api.exc import CannotGetPortalError
from plone.api.exc import InvalidParameterError
from plone.api.validation import required_parameters
from plone.base.navigationroot import get_navigation_root_object
from plone.registry.interfaces import IRegistry
from Products.CMFCore.interfaces import ISiteRoot
from Products.CMFCore.utils import getToolByName
from Products.statusmessages.interfaces import IStatusMessage
from zope.component import ComponentLookupError
from zope.component import getUtilitiesFor
from zope.component import getUtility
from zope.component import providedBy
from zope.component.hooks import getSite
from zope.globalrequest import getRequest
from zope.interface.interfaces import IInterface
from zope.schema.interfaces import IVocabularyFactory

import datetime as dtime
import re

logger = getLogger("plone.api.portal")

try:
    from Products import PrintingMailHost
except ImportError:
    PrintingMailHost = None

if not PrintingMailHost:
    PRINTINGMAILHOST_ENABLED = False
elif (
    PrintingMailHost.ENABLED is not None
    and PrintingMailHost.ENABLED.lower() in PrintingMailHost.TRUISMS
):
    PRINTINGMAILHOST_ENABLED = True
elif PrintingMailHost.ENABLED is None and PrintingMailHost.DevelopmentMode is True:
    PRINTINGMAILHOST_ENABLED = True
else:
    # PrintingMailHost only patches in debug mode.
    # plone.api.env.debug_mode cannot be used here, because .env imports this
    # file
    from App.config import getConfiguration

    PRINTINGMAILHOST_ENABLED = getConfiguration().debug_mode

MISSING = object()


def get():
    """Get the Plone portal object out of thin air.

    Without the need to import fancy Interfaces and doing multi adapter
    lookups.

    :returns: Plone portal object
    :rtype: Portal object
    :Example: :ref:`portal-get-example`
    """
    closest_site = getSite()
    if closest_site is not None:
        for potential_portal in closest_site.aq_chain:
            if ISiteRoot in providedBy(potential_portal):
                return potential_portal

    raise CannotGetPortalError(
        "Unable to get the portal object. More info on "
        "https://docs.plone.org/develop/plone.api/docs/api/exceptions.html"
        "#plone.api.exc.CannotGetPortalError",
    )


@required_parameters("context")
def get_navigation_root(context=None):
    """Get the navigation root object for the context.

    This traverses the path up and returns the nearest navigation root.
    Useful for multi-lingual installations and sites with subsites.

    :param context: [required] Context on which to get the navigation root.
    :type context: context object
    :returns: Navigation Root
    :rtype: Portal object
    :Example: :ref:`portal-get-navigation-root-example`
    """
    context = aq_inner(context)
    return get_navigation_root_object(context, get())


@required_parameters("name")
def get_tool(name=None):
    """Get a portal tool in a simple way.

    :param name: [required] Name of the tool you want.
    :type name: string
    :returns: The tool that was found by name
    :raises:
        :class:`~plone.api.exc.MissingParameterError`,
        :class:`~plone.api.exc.InvalidParameterError`
    :Example: :ref:`portal-get-tool-example`
    """
    try:
        return getToolByName(get(), name)
    except AttributeError:
        # get a list of all tools to display their names in the error msg
        portal = get()
        tools = []
        for id in portal.objectIds():
            if id.startswith("portal_"):
                tools.append(id)

        raise InvalidParameterError(
            "Cannot find a tool with name '{name}'.\n"
            "Available tools are:\n"
            "{tools}".format(name=name, tools="\n".join(tools)),
        )


@required_parameters("recipient", "subject", "body")
def send_email(
    sender=None,
    recipient=None,
    subject=None,
    body=None,
    immediate=False,
):
    """Send an email.

    :param sender: Email sender, 'from' field. If not set, the portal default
        will be used.
    :type sender: string
    :param recipient: [required] Email recipient, 'to' field.
    :type recipient: string
    :param subject: [required] Subject of the email.
    :type subject: string
    :param body: [required] Body text of the email
    :type body: string or python's email object
    :param immediate: Send immediate or queued at transaction commit time. When
        sending immediate the mail might get sent out multiple time in case of
        transaction aborts and retries.
    :type body: boolean
    :raises:
        ValueError
    :Example: :ref:`portal-send-email-example`
    """
    portal = get()

    if not PRINTINGMAILHOST_ENABLED:
        from plone.api import content

        ctrlOverview = content.get_view(
            context=portal,
            request=portal.REQUEST,
            name="overview-controlpanel",
        )
        if ctrlOverview.mailhost_warning():
            raise ValueError("MailHost is not configured.")

    encoding = get_registry_record("plone.email_charset")

    if not sender:
        from_address = get_registry_record("plone.email_from_address")
        from_name = get_registry_record("plone.email_from_name")
        sender = formataddr((from_name, from_address))
        if parseaddr(sender)[1] != from_address:
            # formataddr probably got confused by special characters.
            sender = from_address

    # If the mail headers are not properly encoded we need to extract
    # them and let MailHost manage the encoding.
    if isinstance(body, str):
        body = body.encode(encoding)

    host = get_tool("MailHost")
    host.send(
        body,
        recipient,
        sender,
        subject=subject,
        charset=encoding,
        immediate=immediate,
    )


@required_parameters("datetime")
def get_localized_time(datetime=None, long_format=False, time_only=False):
    """Display a date/time in a user-friendly way.

    It should be localized to the user's preferred language.

    Note that you can specify both long_format and time_only as True
    (or any other value that can be converted to a boolean True
    value), but time_only then wins: the long_format value is ignored.

    You can also use datetime.datetime or datetime.date instead of Plone's
    DateTime. In case of datetime.datetime everything works the same, in
    case of datetime.date the long_format parameter is ignored and on time_only
    an empty string is returned.

    :param datetime: [required] Message to show.
    :type datetime: DateTime, datetime or date
    :param long_format: When true, show long date format. When false
        (default), show the short date format.
    :type long_format: boolean
    :param time_only: When true, show only the time, when false
        (default), show the date.
    :type time_only: boolean
    :returns: Localized time
    :rtype: string
    :raises:
        ValueError
    :Example: :ref:`portal-get-localized-time-example`
    """
    tool = get_tool(name="translation_service")
    request = getRequest()

    # isinstance won't work because of date -> datetime inheritance
    if type(datetime) is dtime.date:
        if time_only:
            return ""
        datetime = dtime.datetime(datetime.year, datetime.month, datetime.day)
        long_format = False

    return tool.ulocalized_time(
        datetime,
        long_format,
        time_only,
        domain="plonelocales",
        request=request,
    )


@required_parameters("message")
def show_message(message=None, request=None, type="info"):
    """Display a status message.

    :param message: [required] Message to show.
    :type message: string
    :param request: [required] Request.
    :type request: HTTPRequest
    :param type: Message type. Possible values: 'info', 'warn', 'error'
    :type type: string
    :raises:
        ValueError
    :Example: :ref:`portal-show-message-example`
    """
    if request is None:
        request = getRequest()
    IStatusMessage(request).add(message, type=type)


@required_parameters("name")
def get_registry_record(name=None, interface=None, default=MISSING):
    """Get a record value from ``plone.app.registry``.

    :param name: [required] Name
    :type name: string
    :param interface: interface whose attributes are plone.app.registry
        settings
    :type interface: zope.interface.Interface
    :param default: The value returned if the record is not found
    :type default: anything
    :returns: Registry record value
    :rtype: plone.app.registry registry record
    :Example: :ref:`portal-get-registry-record-example`
    """
    if not isinstance(name, str):
        raise InvalidParameterError("The 'name' parameter has to be a string")

    if interface is not None and not IInterface.providedBy(interface):
        raise InvalidParameterError(
            "The interface parameter has to derive from " "zope.interface.Interface",
        )

    registry = getUtility(IRegistry)

    if interface is not None:
        records = registry.forInterface(interface, check=False)
        _marker = object()
        if getattr(records, name, _marker) != _marker:
            return registry[f"{interface.__identifier__}.{name}"]

        if default is not MISSING:
            return default

        # Show all records on the interface.
        records = [key for key in interface.names()]
        msg = (
            'Cannot find a record with name "{name}"'
            " on interface {identifier}.\n"
            "Did you mean?\n"
            "{records}".format(
                name=name,
                identifier=interface.__identifier__,
                records="\n".join(records),
            )
        )
        raise InvalidParameterError(msg)

    if name in registry:
        return registry[name]

    if default is not MISSING:
        return default

    # Show all records that 'look like' name.
    # We don't dump the whole list, because it 1500+ items.
    msg = f"Cannot find a record with name '{name}'"
    records = [key for key in registry.records.keys() if name in key]
    if records:
        msg = (
            "{message}\n"
            "Did you mean?:\n"
            "{records}".format(message=msg, records="\n".join(records))
        )
    raise InvalidParameterError(msg)


@required_parameters("name", "value")
def set_registry_record(name=None, value=None, interface=None):
    """Set a record value in the ``plone.app.registry``.

    :param name: [required] Name of the record
    :type name: string
    :param value: [required] Value to set
    :type value: python primitive
    :param interface: interface whose attributes are plone.app.registry
        settings
    :type interface: zope.interface.Interface
    :Example: :ref:`portal-set-registry-record-example`
    """
    if not isinstance(name, str):
        raise InvalidParameterError("The parameter 'name' has to be a string")

    if interface is not None and not IInterface.providedBy(interface):
        raise InvalidParameterError(
            "The interface parameter has to derive from " "zope.interface.Interface",
        )

    registry = getUtility(IRegistry)

    if interface is not None:
        # confirm that the name exists on the interface
        get_registry_record(name=name, interface=interface)

        from zope.schema._bootstrapinterfaces import WrongType

        try:
            registry[interface.__identifier__ + "." + name] = value
        except WrongType:
            field_type = None
            for field in interface.namesAndDescriptions():
                if field[0] == name:
                    field_type = field[1]
                    break
            raise InvalidParameterError(
                "The value parameter for the field {name} needs to be "
                "{of_class} instead of {of_type}".format(
                    name=name,
                    of_class=str(field_type.__class__),
                    of_type=type(value),
                ),
            )

    elif isinstance(name, str):
        # confirm that the record exists before setting the value
        get_registry_record(name)

        registry[name] = value


def get_default_language():
    """Return the default language.

    :returns: language identifier
    :rtype: string
    :Example: :ref:`portal-get-default-language-example`
    """
    from plone.i18n.interfaces import ILanguageSchema

    registry = getUtility(IRegistry)
    settings = registry.forInterface(ILanguageSchema, prefix="plone")
    return settings.default_language


def get_current_language(context=None):
    """Return the current negotiated language.

    :param context: context object
    :type context: object
    :returns: language identifier
    :rtype: string
    :Example: :ref:`portal-get-current-language-example`
    """
    request = getRequest()
    return (
        request.get("LANGUAGE", None)
        or (context and aq_inner(context).Language())
        or get_default_language()
    )


def translate(msgid, domain="plone", lang=None):
    """Translate a message into a given language.

    Default to current negotiated language if no target language specified.

    :param msgid: [required] message to translate
    :type msgid: string
    :type msgid: zope.i18nmessageid.Message
    :param domain: i18n domain to use. When ``msgid`` is an instance of ``Message``,
                   then the ``Message``'s domain is used.
    :type domain: string
    :param lang: target language
    :type lang: string
    :returns: translated message
    :rtype: str
    :Example: :ref:`portal-translate-example`
    """
    translation_service = get_tool("translation_service")
    if lang and re.match(r"\D{2}-\D{2}", lang):
        lang = f"{lang[:2]}_{lang[-2:].upper()}"
    query = {
        "msgid": msgid,
        "domain": domain,
        "target_language": lang,
    }
    if lang is None:
        # Pass the request, so zope.i18n.translate can negotiate the language.
        query["context"] = getRequest()
    return translation_service.utranslate(**query)


@required_parameters("name")
def get_vocabulary(name=None, context=None):
    """Return a vocabulary object with the given name.

    :param name: Name of the vocabulary.
    :type name: str
    :param context: Context to be applied to the vocabulary. Default: portal root.
    :type context: object
    :returns: A SimpleVocabulary instance that implements IVocabularyTokenized.
    :rtype: zope.schema.vocabulary.SimpleVocabulary
    :Example: :ref:`portal-get-vocabulary-example`
    """
    if context is None:
        context = get()
    try:
        vocabulary = getUtility(IVocabularyFactory, name)
    except ComponentLookupError:
        raise InvalidParameterError(
            "Cannot find a vocabulary with name '{name}'.\n"
            "Available vocabularies are:\n"
            "{vocabularies}".format(
                name=name,
                vocabularies="\n".join(get_vocabulary_names()),
            ),
        )
    return vocabulary(context)


def get_vocabulary_names():
    """Return a list of vocabulary names.

    :returns: A sorted list of vocabulary names.
    :rtype: list[str]
    :Example: :ref:`portal-get-all-vocabulary-names-example`
    """
    return sorted([name for name, vocabulary in getUtilitiesFor(IVocabularyFactory)])
