#!/usr/bin/env python3

# Libervia ActivityPub Gateway
# Copyright (C) 2009-2025 Jérôme Poisson (goffi@goffi.org)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


from typing import Any, Literal

from pydantic import BaseModel, Field, field_validator

from libervia.backend.tools.config import ComponentSettings, TLSSettings

from .constants import PLUGIN_INFO

HTMLRedirectDict = dict[
    Literal["actor", "item", "followers", "following"], list["HTMLRedictData"]
]


class HTMLRedictData(BaseModel):

    url: str = Field(description="Target URL for the redirect")
    filters: dict[Literal["jid", "jid_user", "node", "item"], str] = Field(
        default_factory=dict,
        description=("Filters to apply when matching redirection rules."),
    )


class APGatewaySettings(ComponentSettings, plugin_info=PLUGIN_INFO):
    # HTTP server configuration
    http_port: int = Field(
        default=8123,
        description="""
    port where the HTTP server should listen. Port ``80`` is not used directly as it would
    require root privileges, and it is strongly recommended against launching Libervia
    under a privileged account. An HTTP Proxy or a port redirection should be set to
    redirect the ``80`` port to the port specified here.
    """,
    )
    http_connection_type: Literal["http", "https"] = Field(
        default="https",
        description="""
    either ``http`` or ``https``. If you have a HTTP proxy such as Apache or NGINX which
    already handles HTTPS, you may want to use ``http``.

    Note that the HTTP server should always use ``https`` with end-user, the ``http``
    option is only to be used with an HTTPS proxy.
    """,
    )
    http_sign_get: bool = Field(
        default=True,
        description="""
    A boolean value indicating if HTTP ``GET`` request made to AP implementations must be
    signed. Some AP implementations require those requests to be signed, notably Mastodon
    when "secure mode" is activated.
    """,
    )

    # TLS
    tls: TLSSettings | None = None

    # ActivityPub configuration
    public_url: str | None = Field(
        default=None,
        description=""""
    Main user-facing domain of the HTTP server, this will be used to construct all AP URLs

    **default**: if not set, ``xmpp_domain`` is used. If ``xmpp_domain`` is not set
    either, an error is raised.
        """,
    )
    xmpp_domain: str | None = Field(
        default=None, description="XMPP domain. Default to ``common.xmpp_domain``."
    )
    ap_path: str = Field(
        default="_ap",
        description="""
    Path prefix to use for ActivityPub request. It's usually not necessary to change the
    default value.
        """,
    )
    local_only: bool = Field(
        default=True,
        description="""
    A boolean value indicating if the gateway is allowed to convert pubsub node from
    external XMPP service or not. A JID is considered external if its domain part doesn't
    end with the gateway's server. For instance, if a gateway ``ap.example.org`` is set on
    the server ``example.org``, the JIDs ``pierre@example.org`` or
    ``some-node@pubsub.example.org`` will be considered local, but
    ``buenaventura@example.net`` won't (note the different domain).

    Most of time, ``local_only`` should be used.
    """,
    )
    auto_mentions: bool = Field(
        default=True,
        description="""
    A boolean value indicating if received XMPP pubsub blog items bodies must be scanned
    to find ``@user@server.tld`` type mentions. If mentions are found, they'll be added to
    the resulting AP items.
    """,
    )

    # PubSub configuration
    new_node_max_items: int = Field(
        default=50, description="Maximum items for new pubsub nodes"
    )
    comments_max_depth: int = Field(
        default=0,
        description="""
    An integer value indicating the maximum number of comment nodes that can be created.
    See :ref:`ap-xmpp-threads-conversion`
    """,
    )

    # HTML redirection configuration
    html_redirect: HTMLRedirectDict = Field(
        default_factory=HTMLRedirectDict,
        description="""
    A dictionary used to redirect HTTP requests when it's not an ActivityPub request (i.e.
    when the ``Accept`` header is not set to ``application/json``). Several ActivityPub
    implementations link to original server when clicking on some links such as post
    author or item ID, and this result in a request to the corresponding ActivityPub
    endpoint, but made to retrieve HTML instead of ActivityPub JSON data.

    By default, this request is showing the corresponding AP JSON data, but if you set
    HTML redirection, you can redirect the page to your XMPP client web frontend (or
    anything making sense), which results in better user experience.

    The expected dictionary is a mapping URL request types to destination URL.

    The URL request types is the first path element after ``ap_path``, it can be:

    ``actor``
      Actor information. It should link to the profile page of the corresponding XMPP
      entity

    ``item``
      A specific publication. It should link to the rendered item

    ``followers``
      Entities publicly subscribed to the actor. It is usually not used for redirection.

    ``following``
      Entities that the actor is publicly subscribed to. It is usually not used for
      redirection.

    In the target URL you can uses template values in curly braces (something like
    ``{item}``). The values that you can use are:

    ``jid``
      Full JID corresponding to the AP actor.

    ``jid_user``
      Only the user part of the JID (what is before the ``@``)

    ``node``
      Name of the pubsub node corresponding to the AP actor.

    ``item``
      ID of the XMPP pubsub item.

    You can use a slash ``/`` followed by a part of a node name to filter only on specific
    nodes. For instance, if you want to redirect microblog items to a different URL that
    event items, you can use ``item/urn:xmpp:microblog:0`` and ``item/urn:xmpp:events:0``,
    the first redirection to match will be used. Note that anything containing the filter
    value will match, thus you can also use ``item/microblog`` (but it then would match an
    event which has ``microblog`` in is node name).

    You can also use a dictionary as value instead of the target URL. This can be useful
    to filter on an other value than the node name, if you want to make specific
    redirection for a particular user for instance. If you use a dictionary, you must use
    a ``url`` key with the target URL (which may have template values as usual), and you
    may use a ``filters`` key mapping to an other dictionary where each key is a filter on
    a specific template value (in other words: key is the template value, like ``jid`` or
    ``node``, and value is the filter to match).

    *examples*

    Redirect actor page to ``/profile/<JID user part>``, blogs items to ``/blog/<full
    JID>/<node name>/<item id>`` and events items to ``/event/<full JID>/<node name>/<item
    id>`` ::

      [components.ap_gateway]
      html_redirect.actor = "/profile/{jid_user}"
      html_redirect.item = [
        {node="microblog", url="/blog/{jid}/{node}/{item}"},
        {node="events", url="/event/{jid}/{node}/{item}"},
      ]

    Redirect items of user ``louise@example.org`` to ``/b/<item>``::

      [components.ap_gateway]
      html_redirect.item = {url = "/b/{item}", filters = { jid = "louise@example.org" }}
      """,
    )

    @field_validator(
        "html_redirect",
        mode="before",
        json_schema_input_type=str | HTMLRedictData | list[HTMLRedictData],
    )
    @classmethod
    def html_redirect_parse_simple_syntax(cls, html_redirect: Any) -> Any:
        if isinstance(html_redirect, dict):
            for key, value in html_redirect.items():
                if isinstance(value, str):
                    html_redirect[key] = [HTMLRedictData(url=value)]
                elif isinstance(value, HTMLRedictData):
                    html_redirect[key] = [value]
                elif isinstance(value, dict):
                    html_redirect[key] = [HTMLRedictData(**value)]
        return html_redirect
