from datetime import datetime, timezone
from enum import Enum
import os
import sys
import argparse
import netrc
import getpass
import logging
import time

import paho.mqtt.client

from weconnect import weconnect, addressable

from .__version import __version__

LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
DEFAULT_LOG_LEVEL = "ERROR"

LOG = logging.getLogger("weconnect-mqtt")


class NumberRangeArgument:

    def __init__(self, imin=None, imax=None):
        self.imin = imin
        self.imax = imax

    def __call__(self, arg):
        try:
            value = int(arg)
        except ValueError as e:
            raise self.exception() from e
        if (self.imin is not None and value < self.imin) or (self.imax is not None and value > self.imax):
            raise self.exception()
        return value

    def exception(self):
        if self.imin is not None and self.imax is not None:
            return argparse.ArgumentTypeError(f'Must be a number from {self.imin} to {self.imax}')
        if self.imin is not None:
            return argparse.ArgumentTypeError(f'Must be a number not smaller than {self.imin}')
        if self.imax is not None:
            return argparse.ArgumentTypeError(f'Must be number not larger than {self.imax}')

        return argparse.ArgumentTypeError('Must be a number')


def main():  # noqa: C901
    parser = argparse.ArgumentParser(
        prog='weconnect-mqtt',
        description='Commandline Interface to interact with the Volkswagen WeConnect Services')
    parser.add_argument('--version', action='version',
                        version='%(prog)s {version}'.format(version=__version__))
    parser.add_argument('--mqttbroker', type=str, help='Address of MQTT Broker to connect to', required=True)
    parser.add_argument('--mqttport', type=str, help='Port of MQTT Broker to connect to', required=False, default=1883)
    parser.add_argument('-mu', '--mqtt-username', type=str, dest='mqttusername',
                        help='Username for MQTT broker', required=False)
    parser.add_argument('-mp', '--mqtt-password', type=str, dest='mqttpassword',
                        help='Password for MQTT broker', required=False)
    parser.add_argument('-u', '--username', type=str, help='Username of Volkswagen id', required=False)
    parser.add_argument('-p', '--password', type=str, help='Password of Volkswagen id', required=False)
    defaultNetRc = os.path.join(os.path.expanduser("~"), ".netrc")
    parser.add_argument('--netrc', help=f'File in netrc syntax providing login (default: {defaultNetRc}).'
                        ' Netrc is only used when username and password are not provided  as arguments',
                        default=None, required=False)
    parser.add_argument('-i', '--interval', help='Query interval in seconds',
                              type=NumberRangeArgument(300, 3500), required=False, default=300)
    parser.add_argument('--prefix', help='MQTT topic prefix',
                        type=str, required=False, default='weconnect/0')
    parser.add_argument('-v', '--verbose', action="append_const", const=-1,)

    args = parser.parse_args()

    username = None
    password = None

    if args.username is not None and args.password is not None:
        username = args.username
        password = args.password
    else:
        if args.netrc is not None:
            netRcFilename = args.netrc
        else:
            netRcFilename = defaultNetRc
        try:
            secrets = netrc.netrc(file=args.netrc)
            secret = secrets.authenticators("volkswagen.de")
            username, _, password = secret
        except TypeError:
            if not args.username:
                LOG.error('volkswagen.de entry was not found in %s netrc-file. Create it or provide at least a username'
                          ' with --username', netRcFilename)
                sys.exit(1)
            username = args.username
            password = getpass.getpass()
        except FileNotFoundError:
            if not args.username:
                LOG.error('%s netrc-file was not found. Create it or provide at least a username with --username',
                          netRcFilename)
                sys.exit(1)
            username = args.username
            password = getpass.getpass()

    mqttusername = None
    mqttpassword = None
    if args.mqttusername is not None:
        mqttusername = args.mqttusername
    if args.mqttpassword is not None:
        mqttpassword = args.mqttpassword

    if mqttusername is None and mqttpassword is None:
        if args.netrc is not None:
            netRcFilename = args.netrc
        else:
            netRcFilename = defaultNetRc
        try:
            secrets = netrc.netrc(file=args.netrc)
            authenticator = secrets.authenticators(args.mqttbroker)
            if authenticator is not None:
                mqttusername, _, mqttpassword = authenticator
        except FileNotFoundError:
            if args.netrc is not None:
                LOG.error('%s netrc-file was not found. Create it or provide at least a username with --username',
                          netRcFilename)
                sys.exit(1)

    logging.basicConfig(level=logging.INFO)

    mqttCLient = WeConnectMQTTClient(interval=args.interval, prefix=args.prefix)

    if mqttusername is not None:
        mqttCLient.username_pw_set(username=mqttusername, password=mqttpassword)

    try:
        mqttCLient.connectWeConnect(username=username, password=password)
        mqttCLient.connect(args.mqttbroker, args.mqttport, 60)
        # blocking run
        mqttCLient.run()
        mqttCLient.disconnect()
        mqttCLient.weConnect.persistTokens()
    except weconnect.AuthentificationError as e:
        LOG.critical('There was a problem when authenticating with WeConnect: %s', e)
    except weconnect.APICompatibilityError as e:
        LOG.critical('There was a problem when communicating with WeConnect.'
                     ' If this problem persists please open a bug report: %s', e)
    finally:
        mqttCLient.disconnect()


class WeConnectMQTTClient(paho.mqtt.client.Client):
    def __init__(self, interval, prefix='weconnect/0'):
        super().__init__()
        self.weConnect = None
        self.prefix = prefix
        self.interval = interval

        self.on_connect = self.on_connect_callback
        self.on_message = self.on_message_callback

    def disconnect(self, reasoncode=None, properties=None):
        self.publish(topic=f'{self.prefix}/mqtt/weconnectConnected', payload=False)
        super().disconnect(reasoncode, properties)

    def connectWeConnect(self, username, password):
        LOG.info('Connect to WeConnect')
        self.weConnect = weconnect.WeConnect(username=username, password=password, updateAfterLogin=False)
        self.weConnect.addObserver(self.onWeConnectEvent, addressable.AddressableLeaf.ObserverEvent.VALUE_CHANGED)

    def updateWeConnect(self):
        LOG.info('Update data from WeConnect')
        self.weConnect.update()
        self.publish(topic=f'{self.prefix}/mqtt/weconnectUpdated',
                     payload=datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc).isoformat())

    def onWeConnectEvent(self, element, flags):
        if flags & addressable.AddressableLeaf.ObserverEvent.VALUE_CHANGED:
            if isinstance(element.value, (str, int, float)) or element.value is None:
                convertedValue = element.value
            elif isinstance(element.value, Enum):
                convertedValue = element.value.value
            else:
                convertedValue = str(element.value)
            LOG.debug('%s%s, value changed: new value is: %s', self.prefix, element.getGlobalAddress(), convertedValue)
            self.publish(topic=f'{self.prefix}{element.getGlobalAddress()}', payload=convertedValue)

    def on_connect_callback(self, mqttc, obj, flags, rc):
        del mqttc  # unused
        del obj  # unused
        del flags  # unused

        if rc == 0:
            LOG.info('Connected to MQTT broker')
            self.publish(topic=f'{self.prefix}/mqtt/weconnectForceUpdate', payload=False)
            self.subscribe(f'{self.prefix}/mqtt/weconnectForceUpdate')

            self.publish(topic=f'{self.prefix}/mqtt/weconnectUpdateInterval_s', payload=self.interval)
            self.subscribe(f'{self.prefix}/mqtt/weconnectUpdateInterval_s')

            self.publish(topic=f'{self.prefix}/mqtt/weconnectConnected', payload=True)

            self.updateWeConnect()
        elif rc == 1:
            LOG.error('Could not connect (%d): incorrect protocol version', rc)
        elif rc == 2:
            LOG.error('Could not connect (%d): invalid client identifier', rc)
        elif rc == 3:
            LOG.error('Could not connect (%d): server unavailable. Retrying in 10 seconds', rc)
            time.sleep(10)
            self.reconnect()
        elif rc == 4:
            LOG.error('Could not connect (%d): bad username or password', rc)
        elif rc == 5:
            LOG.error('Could not connect (%d): not authorised', rc)
        else:
            print('Could not connect: %d', rc, file=sys.stderr)
            sys.exit(1)

    def on_message_callback(self, mqttc, obj, msg):
        del mqttc  # unused
        del obj  # unused

        if msg.topic == f'{self.prefix}/mqtt/weconnectForceUpdate':
            if msg.payload.lower() == b'True'.lower():
                LOG.info('Update triggered by MQTT message')
                self.updateWeConnect()
                self.publish(topic=f'{self.prefix}/mqtt/weconnectForceUpdate', payload=False)
        elif msg.topic == f'{self.prefix}/mqtt/weconnectUpdateInterval_s':
            if str(msg.payload.decode()).isnumeric():
                newInterval = int(msg.payload)
                if newInterval < 300:
                    LOG.error('New intervall of %ss by MQTT message is too low. Minimum is 300s', msg.payload.decode())
                    self.publish(topic=f'{self.prefix}/mqtt/weconnectUpdateInterval_s', payload=self.interval)
                elif newInterval > 3500:
                    LOG.error('New intervall of %ss by MQTT message is too large. Maximum is 3500s',
                              msg.payload.decode())
                    self.publish(topic=f'{self.prefix}/mqtt/weconnectUpdateInterval_s', payload=self.interval)
                else:
                    self.interval = newInterval
                    LOG.info('New intervall set to %ds by MQTT message', self.interval)
            else:
                LOG.error('MQTT message for new interval does not contain a number: %s', msg.payload.decode())
        else:
            LOG.error('I don\'t understand message %s: %s', msg.topic, msg.payload)

    def run(self):
        self.loop_start()
        try:
            while True:
                time.sleep(self.interval)
                self.updateWeConnect()
        except KeyboardInterrupt:
            self.loop_stop(force=False)
