# Copyright 2010-2011 Florent Le Coz <louiz@louiz.org>
#
# This file is part of Poezio.
#
# Poezio is free software: you can redistribute it and/or modify
# it under the terms of the GPL-3.0+ license. See the COPYING file.
"""
Defines the Connection class
"""

import logging
import getpass
import subprocess
import sys
import base64
import random
import ssl
from pathlib import Path
from typing import Any, TYPE_CHECKING

import slixmpp
from slixmpp import JID, InvalidJID
from slixmpp.xmlstream import ET
from slixmpp.plugins import PluginNotFound
from slixmpp.plugins.xep_0184 import XEP_0184
from slixmpp.plugins.xep_0030 import DiscoInfo
from slixmpp.util import FileSystemCache

from poezio import common
from poezio import fixes
from poezio import xdg
from poezio.config import config

if TYPE_CHECKING:
    from poezio.core.core import Core


log = logging.getLogger(__name__)


class Connection(slixmpp.ClientXMPP):
    """
    Receives everything from Jabber and emits the
    appropriate signals
    """
    core: 'Core | None'

    def __init__(self, custom_version: str = '') -> None:
        keyfile = config.getstr('keyfile')
        certfile = config.getstr('certfile')

        device_id = config.getstr('device_id')
        if not device_id:
            rng = random.SystemRandom()
            device_id = base64.urlsafe_b64encode(
                rng.getrandbits(24).to_bytes(3, 'little')).decode('ascii')
            config.set_and_save('device_id', device_id)

        if config.getstr('jid'):
            # Field used to know if we are anonymous or not.
            # many features will be handled differently
            # depending on this setting
            self.anon = False
            jid_str = config.getstr('jid')
            password = config.getstr('password')
            eval_password = config.getstr('eval_password')
            if not password and not eval_password and not (keyfile
                                                           and certfile):
                password = getpass.getpass()
            elif not password and not (keyfile and certfile):
                sys.stderr.write(
                    "No password or certificates provided, using the eval_password command.\n"
                )
                process = subprocess.Popen(
                    ['sh', '-c', eval_password],
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    close_fds=True)
                code = process.wait()
                if code != 0:
                    sys.stderr.write(
                        'The eval_password command (%s) returned a '
                        'nonzero status code: %s.\n' % (eval_password, code))
                    sys.stderr.write('Poezio will now exit\n')
                    sys.exit(code)
                if process.stdout:
                    password = process.stdout.readline().decode('utf-8').strip('\n')
                else:
                    password = ''
        else:  # anonymous auth
            self.anon = True
            jid_str = config.getstr('server')
            password = ''
        try:
            jid = JID(jid_str)
        except InvalidJID:
            sys.stderr.write('Invalid jid option: "%s" is not a valid JID\n' % jid_str)
            sys.exit(1)
        jid.resource = '%s-%s' % (
            jid.resource,
            device_id) if jid.resource else 'poezio-%s' % device_id
        # TODO: use the system language
        slixmpp.ClientXMPP.__init__(
            self, jid, password or '', lang=config.getstr('lang'))

        force_encryption = config.getbool('force_encryption')
        if force_encryption:
            self['feature_mechanisms'].unencrypted_plain = False
            self['feature_mechanisms'].unencrypted_digest = False
            self['feature_mechanisms'].unencrypted_cram = False
            self['feature_mechanisms'].unencrypted_scram = False

        self.keyfile = keyfile
        self.certfile = certfile
        if keyfile and not certfile:
            log.error(
                'keyfile is present in configuration file without certfile')
        elif certfile and not keyfile:
            log.error(
                'certfile is present in configuration file without keyfile')

        self.core = None
        self.auto_reconnect = config.getbool('auto_reconnect')
        self.auto_authorize = None
        # prosody defaults, lowest is AES128-SHA, it should be a minimum
        # for anything that came out after 2002
        self.ciphers = config.getstr(
            'ciphers', 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK'
            ':!SRP:!3DES:!aNULL')
        self.ca_certs = None
        ca_certs = config.getlist('ca_cert_path')
        if ca_certs and ca_certs != ['']:
            self.ca_certs = list(map(Path, config.getlist('ca_cert_path')))
        interval = config.getint('whitespace_interval')
        if int(interval) > 0:
            self.whitespace_keepalive_interval = int(interval)
        else:
            self.whitespace_keepalive = False
        self.__register_slix_plugins(custom_version=custom_version)
        self.init_plugins()

    def __register_slix_plugins(self, custom_version: str = '') -> None:
        """Register all slix plugins"""
        self.register_plugin('xep_0004')
        self.register_plugin('xep_0012')
        # Must be loaded before 0030.
        self.register_plugin(
            'xep_0115', {
                'caps_node':
                'https://poez.io',
                'cache':
                FileSystemCache(
                    str(xdg.CACHE_HOME),
                    'caps',
                    encode=str,
                    decode=lambda x: DiscoInfo(ET.fromstring(x))),
            })
        self.register_plugin('xep_0030')
        self.register_plugin('xep_0045')
        self.register_plugin('xep_0048')
        self.register_plugin('xep_0050')
        self.register_plugin('xep_0054')
        self.register_plugin('xep_0060')
        self.register_plugin('xep_0066')
        self.register_plugin('xep_0070')
        self.register_plugin('xep_0071')
        self.register_plugin('xep_0077')
        self.plugin['xep_0077'].create_account = False
        self.register_plugin('xep_0084')
        self.register_plugin('xep_0085')
        self.register_plugin('xep_0153')

        # monkey-patch xep_0184 to avoid requesting receipts for messages
        # without a body
        setattr(  # noqa: B010
            XEP_0184,
            "_filter_add_receipt_request",
            fixes._filter_add_receipt_request,
        )
        self.register_plugin('xep_0184')
        self.plugin['xep_0184'].auto_ack = config.getbool('ack_message_receipts')
        self.plugin['xep_0184'].auto_request = config.getbool(
            'request_message_receipts')

        self.register_plugin('xep_0191')
        if config.getbool('enable_smacks'):
            self.register_plugin('xep_0198')
        self.register_plugin('xep_0199')

        if config.getbool('enable_user_nick'):
            self.register_plugin('xep_0172')

        if config.getbool('send_poezio_info'):
            info = {'name': 'poezio', 'version': custom_version}
            if config.getbool('send_os_info'):
                info['os'] = common.get_os_info()
            self.plugin['xep_0030'].set_identities(identities={('client',
                                                                'console',
                                                                None,
                                                                'Poezio')})
        else:
            info = {'name': '', 'version': ''}
            self.plugin['xep_0030'].set_identities(identities={('client',
                                                                'console',
                                                                None, '')})
        self.register_plugin('xep_0092', pconfig=info)
        if config.getbool('send_time'):
            self.register_plugin('xep_0202')
        self.register_plugin('xep_0203')
        self.register_plugin('xep_0224')
        self.register_plugin('xep_0231')
        self.register_plugin('xep_0249')
        self.register_plugin('xep_0257')
        self.register_plugin('xep_0280')
        self.register_plugin('xep_0297')
        self.register_plugin('xep_0308')
        self.register_plugin('xep_0313')
        self.register_plugin('xep_0334')
        self.register_plugin('xep_0352')
        self.register_plugin('xep_0359')
        try:
            self.register_plugin('xep_0363')
            self.register_plugin('xep_0454')
        except PluginNotFound:
            log.exception(
                'Failed to load HTTP File Upload plugin, it can only be '
                'used with aiohttp installed'
            )
        self.register_plugin('xep_0377')
        self.register_plugin('xep_0380')
        self.register_plugin('xep_0410')
        self.register_plugin('xep_0421')
        self.register_plugin('xep_0424')
        self.register_plugin('xep_0425')
        self.register_plugin('xep_0444')

    def set_keepalive_values(self, option: str | None = None, value: str | None = None) -> None:
        """
        Called after the XMPP session has been started, or triggered when one of
        "connection_timeout_delay" and "connection_check_interval" options
        is changed.  Unload and reload the ping plugin, with the new values.
        """
        if not self.is_connected():
            # Happens when we change the value with /set while we are not
            # connected. Do nothing in that case
            return
        ping_interval = config.getint('connection_check_interval')
        timeout_delay = config.getint('connection_timeout_delay')
        if timeout_delay <= 0:
            # We help the stupid user (with a delay of 0, poezio will try to
            # reconnect immediately because the timeout is immediately
            # passed)
            # 1 second is short, but, well
            timeout_delay = 1
        self.plugin['xep_0199'].disable_keepalive()
        # If the ping_interval is 0 or less, we just disable the keepalive
        if ping_interval > 0:
            self.plugin['xep_0199'].enable_keepalive(ping_interval,
                                                     timeout_delay)

    def get_ssl_context(self) -> ssl.SSLContext:
        """
        Override the slixmpp-provided sslcontext if the user configured
        poezio to not check certificates at all
        (e.g. when testing on localhost)
        """
        ctx = super().get_ssl_context()
        verify = config.getbool('tls_verify_cert')
        if not verify:
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE
        return ctx

    def start(self) -> None:
        """
        Connect and process events.
        """
        custom_host = config.getstr('custom_host')
        custom_port = config.get('custom_port', 5222)
        if custom_port == -1:
            custom_port = 5222
        if custom_host:
            self.connect(host=custom_host, port=custom_port)
        elif custom_port != 5222 and custom_port != -1:
            self.connect(host=self.boundjid.host, port=custom_port)
        else:
            self.connect()

    def send_raw(self, data: str | bytes) -> None:
        """
        Overrides XMLStream.send_raw, with an event added
        """
        if self.core:
            self.core.handler.outgoing_stanza(data)  # pylint: disable=E1101
        slixmpp.ClientXMPP.send_raw(self, data)

    def _handle_disconnected(self, event: Any) -> None:
        """
        Override BaseXMPP disconnected to avoid resetting the roster whenever
        we get disconnected.
        """
        # Keep the clearing of the session bind event, though
        self.session_bind_event.clear()


class MatchAll(slixmpp.xmlstream.matcher.base.MatcherBase):
    """
    Callback to retrieve all the stanzas for the XML tab
    """

    def match(self, xml: Any) -> bool:
        "match everything"
        return True
