#!/usr/bin/env python3

# Libervia Email 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 TYPE_CHECKING, NamedTuple, cast

from twisted.mail.imap4 import IMAP4Exception

from libervia.backend.core.constants import Const as C
from libervia.backend.core.core_types import SatXMPPEntity
from libervia.backend.core.i18n import D_, _
from libervia.backend.core.log import getLogger
from libervia.backend.plugins.plugin_xep_0004 import (
    Boolean,
    DataForm,
    ListMulti,
    Option,
    Text,
)
from libervia.backend.plugins.plugin_xep_0050 import (
    AdHocCallbackData,
    AdHocCommand,
    AdHocError,
    Error,
    Note,
    NoteType,
    PageData,
    Status,
    XEP_0050,
)

from .imap import IMAPClient

log = getLogger(__name__)
if TYPE_CHECKING:
    from . import EmailGatewayComponent
    from .imap import IMAPClient


class ImapInfo(NamedTuple):
    client: "IMAPClient"
    uid: int


class EmailAdHocService:
    """Ad-Hoc commands for AP Gateway"""

    def __init__(self, email_gw: "EmailGatewayComponent"):
        """Initialize the EmailAdHocService.

        @param email_gw: The EmailGatewayComponent instance.
        """
        self.host = email_gw.host
        self.email_gw = email_gw
        self._c = cast(XEP_0050, self.host.plugins["XEP-0050"])

    def init(self, client: SatXMPPEntity) -> None:
        """Initialize ad-hoc commands for email gateway.

        @param client: Libervia client instance.
        """
        self._c.register_ad_hoc_command(
            client,
            AdHocCommand(
                callback=self.delete_email,
                label=D_("Delete Email"),
                form=DataForm(
                    fields=[
                        Text(var="uid", label=D_("Message ID"), required=True),
                        Boolean(
                            label=D_(
                                "This email will be permanently deleted, please check "
                                "this box to confirm this operation."
                            ),
                            var="confirm",
                            required=True,
                        ),
                    ]
                ),
                allowed_magics=[C.ENTITY_ALL],
            ),
        )
        self._c.register_ad_hoc_command(
            client,
            AdHocCommand(
                callback=self.move_email,
                label=D_("Move Email"),
                form=DataForm(
                    fields=[
                        Text(var="uid", label=D_("Message ID"), required=True),
                        Text(
                            var="dest",
                            label=D_("Name of Destination Mailbox"),
                            required=True,
                        ),
                    ]
                ),
                allowed_magics=[C.ENTITY_ALL],
            ),
        )
        self._c.register_ad_hoc_command(
            client,
            AdHocCommand(
                callback=self.change_flags,
                label=D_("Change Flags"),
                form=DataForm(
                    fields=[
                        Text(var="uid", label=D_("Message ID"), required=True),
                    ]
                ),
                allowed_magics=[C.ENTITY_ALL],
            ),
        )

    async def get_uid(self, page_data: PageData) -> ImapInfo:
        """Get IMAP UID for a given message ID.

        @param page_data: Ad-Hoc page data containing the message ID.
        @return: ImapInfo instance, with UID and client.
        """
        try:
            message_id = page_data.form.get_field("uid", Text).value
            if message_id is None:
                raise ValueError('"uid" must be set.')
        except Exception as e:
            raise AdHocError(Error.BAD_PAYLOAD, text=f'"uid" field is mandatory. {e}')
        requestor = page_data.requestor
        try:
            user_data = self.email_gw.users_data[requestor]
        except KeyError:
            log.warning(f"No user data for {requestor}, is the entity registered?")
            raise AdHocError(Error.FORBIDDEN)
        if user_data.imap_client is None:
            log.warning(f"IMAP client is not connected for {requestor}.")
            raise AdHocError(Error.FORBIDDEN)

        uid = await user_data.imap_client.get_imap_uid_from_message_id(message_id)
        if uid is None:
            raise AdHocError(
                Error.ITEM_NOT_FOUND, f"The message {message_id!r} is not found."
            )
        return ImapInfo(user_data.imap_client, uid)

    async def delete_email(
        self,
        client: SatXMPPEntity,
        page_data: PageData,
    ) -> AdHocCallbackData:
        """Delete an email from IMAP server.

        @param client: Libervia client instance.
        @param page_data: Ad-Hoc page data.
        @return: Command results.
        """
        imap_info = await self.get_uid(page_data)
        confirm = page_data.form.get_field("confirm", Boolean).value
        if confirm is None:
            raise AdHocError(Error.BAD_PAYLOAD, text='"confirm" must be set.')
        if not confirm:
            return AdHocCallbackData(
                status=Status.CANCELED,
                notes=[Note(text=f"User didn't confirm deletion.")],
            )

        await imap_info.client.addFlags(imap_info.uid, [r"\Deleted"], uid=True)
        await imap_info.client.expunge()

        return AdHocCallbackData(
            status=Status.COMPLETED,
            notes=[Note(text=f"Email {imap_info.uid} successfully deleted.")],
        )

    async def move_email(
        self,
        client: SatXMPPEntity,
        page_data: PageData,
    ) -> AdHocCallbackData:
        """Move an email to another IMAP mailbox.

        @param client: Libervia client instance.
        @param page_data: Ad-Hoc page data.
        @return: Command results.
        """
        imap_info = await self.get_uid(page_data)
        dest_mailbox = page_data.form.get_field("dest", Text).value
        if dest_mailbox is None or not (dest_mailbox := dest_mailbox.strip()):
            raise AdHocError(
                Error.BAD_PAYLOAD, D_("You must indicate a destination mailbox.")
            )

        try:
            await imap_info.client.copy(imap_info.uid, dest_mailbox, uid=True)
        except IMAP4Exception as e:
            return AdHocCallbackData(
                status=Status.CANCELED,
                notes=[Note(type=NoteType.ERROR, text=f"Can't move email: {e}")],
            )

        await imap_info.client.addFlags(imap_info.uid, [r"\Deleted"], uid=True)

        await imap_info.client.expunge()

        return AdHocCallbackData(
            status=Status.COMPLETED,
            notes=[
                Note(
                    text=(
                        f"Email {imap_info.uid} successfully moved to mailbox "
                        f"{dest_mailbox!r}."
                    )
                )
            ],
        )

    async def change_flags(
        self,
        client: SatXMPPEntity,
        page_data: PageData,
    ) -> AdHocCallbackData:
        """Change flags of an email.

        @param client: Libervia client instance.
        @param page_data: Ad-Hoc page data.
        @return: Command results.
        """
        if page_data.idx == 1:
            # We are on the second page of the commands, were user select flags.
            imap_info = await self.get_uid(page_data)
            page_data.session_data["imap_info"] = imap_info
            # FIXME: We only support INBOX for now.
            mailbox = "INBOX"
            mailbox_data = imap_info.client.mailboxes_data.get(mailbox, {})
            acceptable_flags = mailbox_data.get("FLAGS")
            if not acceptable_flags:
                return AdHocCallbackData(
                    status=Status.CANCELED,
                    notes=[
                        Note(
                            type=NoteType.ERROR,
                            text=f"Can't find data on mailbox {mailbox}]",
                        )
                    ],
                )

            flags_data = await imap_info.client.fetchFlags(imap_info.uid, uid=True)
            str_uid = str(imap_info.uid)
            for flags_value in flags_data.values():
                if flags_value["UID"] == str_uid:
                    current_flags = flags_value["FLAGS"]
                    break
            else:
                return AdHocCallbackData(
                    status=Status.CANCELED,
                    notes=[
                        Note(
                            type=NoteType.ERROR,
                            text=f"Can't find current flags ({flags_data=}).",
                        )
                    ],
                )

            form = DataForm(
                instructions=D_("Select the flags to set on the email."),
                fields=[
                    ListMulti(
                        var="flags",
                        options=[Option(value=flag) for flag in acceptable_flags],
                        values=current_flags,
                    )
                ],
            )
            return AdHocCallbackData(form=form, status=Status.EXECUTING)
        else:
            # This is the third and last page of the command, where flags are actually
            # set.
            imap_info = page_data.session_data["imap_info"]
            new_flags = page_data.form.get_field("flags", ListMulti).values
            await imap_info.client.setFlags(imap_info.uid, flags=new_flags, uid=True)
            return AdHocCallbackData(
                status=Status.COMPLETED, notes=[Note(text=D_("Flags set successfully."))]
            )
