# This file is part of Gajim.
#
# SPDX-License-Identifier: GPL-3.0-only

# Message Util module

from __future__ import annotations

from typing import Literal

import base64
from datetime import datetime
from datetime import UTC

import nbxmpp.structs
from nbxmpp.modules.security_labels import SecurityLabel
from nbxmpp.namespaces import Namespace
from nbxmpp.protocol import JID
from nbxmpp.structs import MessageProperties

from gajim.common import app
from gajim.common.const import EME_MESSAGES
from gajim.common.const import EME_PROTOCOLS
from gajim.common.const import IMAGE_MIME_TYPES
from gajim.common.const import VALUE_MISSING
from gajim.common.i18n import _
from gajim.common.modules.contacts import GroupchatParticipant
from gajim.common.open_graph_parser import OpenGraphData
from gajim.common.storage.archive import models as mod
from gajim.common.storage.archive.const import ChatDirection
from gajim.common.storage.archive.const import MessageType
from gajim.common.structs import MUCData
from gajim.common.structs import ReplyData
from gajim.common.util.uri import DataUri

UNKNOWN_MESSAGE = _("Message content unknown")


def get_eme_message(eme_data: nbxmpp.structs.EMEData) -> str:
    try:
        return EME_MESSAGES[eme_data.namespace]
    except KeyError:
        encryption_name = EME_PROTOCOLS.get(eme_data.namespace)
        if not encryption_name:
            encryption_name = eme_data.name or eme_data.namespace
        return EME_MESSAGES["fallback"] % encryption_name


def get_eme_protocol(eme_data: nbxmpp.structs.EMEData) -> str:
    return EME_PROTOCOLS.get(eme_data.namespace, "Unknown")


def get_chat_type_and_direction(
    muc_data: MUCData | None, own_jid: JID, properties: MessageProperties
) -> tuple[MessageType, ChatDirection]:

    assert properties.jid is not None
    if properties.type.is_groupchat:
        assert muc_data is not None
        direction = ChatDirection.INCOMING
        if muc_data.occupant_id is not None:
            if muc_data.occupant_id == properties.occupant_id:
                direction = ChatDirection.OUTGOING

        elif muc_data.nick == properties.jid.resource:
            direction = ChatDirection.OUTGOING

        return MessageType.GROUPCHAT, direction

    assert properties.from_ is not None
    if properties.from_.bare_match(own_jid):
        direction = ChatDirection.OUTGOING
    else:
        direction = ChatDirection.INCOMING

    if properties.is_muc_pm:
        return MessageType.PM, direction

    return MessageType.CHAT, direction


def get_real_jid(
    properties: MessageProperties,
    contact: GroupchatParticipant,
) -> JID | None:

    if properties.is_mam_message:
        if properties.muc_user is None:
            return None
        return properties.muc_user.jid

    real_jid = contact.real_jid
    if real_jid is None:
        return None
    return real_jid.new_as_bare()


def get_occupant_info(
    account: str,
    remote_jid: JID,
    own_bare_jid: JID,
    direction: ChatDirection,
    m_type: MessageType,
    timestamp: datetime,
    contact: GroupchatParticipant,
    properties: MessageProperties,
) -> mod.Occupant | None:

    assert properties.jid is not None
    if properties.jid.is_bare:
        return None

    if direction == ChatDirection.OUTGOING:
        real_jid = own_bare_jid
    else:
        real_jid = get_real_jid(properties, contact)

    occupant_id = get_occupant_id(contact, properties) or real_jid
    if occupant_id is None:
        return None

    if m_type == MessageType.PM and direction == ChatDirection.OUTGOING:
        # Outgoing PMs which we receive as carbons or via MAM
        # have our bound jid in the from attribute, not the joined MUC
        # resource. This means for these kind of messages the nickname
        # cannot be determined
        resource = None
    else:
        resource = properties.jid.resource
        assert resource is not None

    occupant = mod.Occupant(
        account_=account,
        remote_jid_=remote_jid.new_as_bare(),
        id=str(occupant_id),
        real_remote_jid_=real_jid or VALUE_MISSING,
        nickname=resource,
        updated_at=timestamp,
    )

    if contact.avatar_sha is not None:
        # avatar_sha is only available if we have presence from this contact
        # We don’t want to overwrite the previous avatar
        occupant.avatar_sha = contact.avatar_sha

    return occupant


def get_occupant_id(
    contact: GroupchatParticipant,
    properties: MessageProperties,
) -> str | None:

    if not properties.occupant_id:
        return None

    if contact.room.supports(Namespace.OCCUPANT_ID):
        return properties.occupant_id
    return None


def get_message_timestamp(properties: MessageProperties) -> datetime:
    timestamp = properties.timestamp
    if properties.mam is not None:
        timestamp = properties.mam.timestamp
    return datetime.fromtimestamp(timestamp, tz=UTC)


def convert_message_type(type_: MessageType) -> Literal["chat", "groupchat"]:
    if type_ in (MessageType.CHAT, MessageType.PM):
        return "chat"
    return "groupchat"


def get_nickname_from_message(account: str, message: mod.Message) -> tuple[str, str]:
    # This method is tailored for the ChatListRow
    def _get_nickname() -> str:
        if occupant := message.occupant:
            if nickname := occupant.nickname:
                return nickname

        if message.resource is not None:
            return message.resource

        nickname = message.remote.jid.localpart
        assert nickname is not None
        return nickname

    match message.type:
        case MessageType.CHAT:
            if message.direction == ChatDirection.INCOMING:
                return "", ""

            # FIXME: app.nicks is not correctly filled on start
            return _("Me"), app.nicks[account]

        case MessageType.PM:
            if message.direction == ChatDirection.INCOMING:
                return "", ""
            return _("Me"), _get_nickname()

        case MessageType.GROUPCHAT:
            if message.direction == ChatDirection.INCOMING:
                return _get_nickname(), ""
            return _("Me"), _get_nickname()

        case _:
            raise ValueError(f"Unknown type: {message.type}")


def get_security_label(
    account: str, remote_jid: JID, timestamp: datetime, label: SecurityLabel | None
) -> mod.SecurityLabel | None:
    if label is None:
        return None

    displaymarking = label.displaymarking
    if displaymarking is None:
        return None

    return mod.SecurityLabel(
        account_=account,
        remote_jid_=remote_jid,
        label_hash=label.get_label_hash(),
        displaymarking=displaymarking.name,
        fgcolor=displaymarking.fgcolor,
        bgcolor=displaymarking.bgcolor,
        updated_at=timestamp,
    )


def get_reply(data: nbxmpp.structs.ReplyData | ReplyData | None) -> mod.Reply | None:
    if data is None:
        return None

    return mod.Reply(id=data.id, to=data.to)


def get_open_graph(
    open_graph: dict[str, nbxmpp.structs.OpenGraphData] | dict[str, OpenGraphData],
) -> list[mod.OpenGraph]:

    og_data: list[mod.OpenGraph] = []
    for url, data in open_graph.items():
        if not data.title:
            continue

        if isinstance(data, OpenGraphData):
            image_type, image_bytes = None, None
            if thumbnail := data.thumbnail:
                image_type, image_bytes = thumbnail.type, thumbnail.data
        else:
            image_type, image_bytes = get_thumbnail(data.image)

        og_data.append(
            mod.OpenGraph(
                about=url,
                title=data.title,
                description=data.description or None,
                image_type=image_type,
                image_bytes=image_bytes,
            )
        )

    return og_data


def get_thumbnail(image_uri: str | None) -> tuple[str, bytes] | tuple[None, None]:
    if not image_uri:
        return None, None

    def _parse() -> tuple[str, bytes]:
        datauri = DataUri.from_string(image_uri)
        if not datauri.is_base64:
            raise ValueError

        image_bytes = base64.b64decode(datauri.data)
        if not image_bytes:
            raise ValueError

        if datauri.media_type not in IMAGE_MIME_TYPES:
            raise ValueError

        if len(image_bytes) > 50_000:
            raise ValueError

        return datauri.media_type, image_bytes

    try:
        return _parse()
    except Exception:
        return None, None


def successfully_decrypted(properties: MessageProperties) -> bool:
    return properties.eme is None or properties.encrypted is not None
