from dataclasses import dataclass, field
from dataclasses_json import dataclass_json
from typing import Dict
from datetime import datetime, timedelta
from time import sleep
from psygnal import evented

from .api import API
from .brands import Brand
from .command import Command, COMMANDS_BY_NAME


def convert(v):
    if not isinstance(v, str):
        return v

    if v == 'null':
        return None

    try:
        v = int(v)
    except:
        try:
            v = float(v)
        except:
            pass

    return v


def sg(dct: dict, *keys):
    if not isinstance(dct, dict):
        return None

    for key in keys:
        try:
            dct = dct[key]
        except KeyError:
            return None

    return convert(dct)


def sg_eq(dct: dict, expect, *keys):
    v = sg(dct, *keys)

    if v is None:
        return None

    return v == expect


CHARGING_LEVELS = {
    'DEFAULT': 0,
    'LEVEL_1': 1,
    'LEVEL_2': 2,
    'LEVEL_3': 3,
}

CHARGING_LEVEL_PREFS = {
    'LEVEL_ONE': 1,
    'LEVEL_TWO': 2,
    'LEVEL_THREE': 3,
    'LEVEL_FOUR': 4,
    'LEVEL_FIVE': 5,
}


@dataclass_json
@dataclass
class Location:
    longitude: float = None
    latitude: float = None
    altitude: float = None
    bearing: float = None
    is_approximate: bool = None
    updated: datetime = None

    def __repr__(self):
        return f'lat: {self.latitude}, lon: {self.longitude} (updated {self.updated})'


@dataclass_json
@dataclass
class Vehicle:
    vin: str

    # General info
    nickname: str
    make: str
    model: str
    year: str
    region: str

    # Status
    ignition_on: bool = None
    trunk_locked: bool = None

    odometer: float = None
    odometer_unit: str = None
    days_to_service: int = None
    distance_to_service: int = None
    distance_to_service_unit: str = None
    distance_to_empty: int = None
    distance_to_empty_unit: str = None
    battery_voltage: float = None
    oil_level: int = None
    fuel_low: bool = None
    fuel_amount: int = None

    # EV related
    plugged_in: bool = None
    ev_running: bool = None
    charging: bool = None
    charging_level: int = None
    charging_level_preference: int = None
    state_of_charge: int = None
    time_to_fully_charge_l3: int = None
    time_to_fully_charge_l2: int = None

    # Wheels
    wheel_front_left_pressure: float = None
    wheel_front_left_pressure_unit: str = None
    wheel_front_left_pressure_warning: bool = None
    wheel_front_right_pressure: float = None
    wheel_front_right_pressure_unit: str = None
    wheel_front_right_pressure_warning: bool = None
    wheel_rear_left_pressure: float = None
    wheel_rear_left_pressure_unit: str = None
    wheel_rear_left_pressure_warning: bool = None
    wheel_rear_right_pressure: float = None
    wheel_rear_right_pressure_unit: str = None
    wheel_rear_right_pressure_warning: bool = None

    # Doors
    door_driver_locked: bool = None
    door_passenger_locked: bool = None
    door_rear_left_locked: bool = None
    door_rear_right_locked: bool = None

    # Windows
    window_driver_closed: bool = None
    window_passenger_closed: bool = None

    location: Location = None
    supported_commands: list[str] = field(default_factory=list)
    last_full_update: datetime = None

    def __repr__(self):
        return f'{self.vin} (nick: {self.nickname})'


def _update_vehicle(v: Vehicle, p: dict) -> Vehicle:
    vi = sg(p, 'vehicleInfo')
    ev = sg(p, 'evInfo')
    batt = sg(ev, 'battery')

    v.battery_voltage = sg(vi, 'batteryInfo', 'batteryVoltage', 'value')
    v.charging = sg_eq(batt, 'CHARGING', 'chargingStatus')
    v.charging_level = CHARGING_LEVELS.get(sg(batt, 'chargingLevel'), None)
    v.charging_level_preference = CHARGING_LEVEL_PREFS.get(
        sg(ev, 'chargePowerPreference'), None)
    v.plugged_in = sg(batt, 'plugInStatus')
    v.state_of_charge = sg(batt, 'stateOfCharge')

    v.days_to_service = sg(vi, 'daysToService')
    v.distance_to_service = sg(
        vi, 'distanceToService', 'distanceToService', 'value')
    v.distance_to_service_unit = sg(
        vi, 'distanceToService', 'distanceToService', 'unit')
    v.distance_to_empty = sg(vi, 'fuel', 'distanceToEmpty', 'value')
    v.distance_to_empty_unit = sg(vi, 'fuel', 'distanceToEmpty', 'unit')
    v.fuel_low = sg(vi, 'fuel', 'isFuelLevelLow')
    v.fuel_amount = sg(vi, 'fuel', 'fuelAmountLevel')
    v.oil_level = sg(vi, 'oilLevel', 'oilLevel')

    v.ignition_on = sg_eq(ev, 'ON', 'ignitionStatus')
    v.time_to_fully_charge_l3 = sg(batt, 'timeToFullyChargeL3')
    v.time_to_fully_charge_l2 = sg(batt, 'timeToFullyChargeL2')
    v.odometer = sg(vi, 'odometer', 'odometer', 'value')
    v.odometer_unit = sg(vi, 'odometer', 'odometer', 'unit')

    if isinstance(vi, dict) and 'tyrePressure' in vi:
        tp = {x['type']: x for x in vi['tyrePressure']}

        v.wheel_front_left_pressure = sg(tp, 'FL', 'pressure', 'value')
        v.wheel_front_left_pressure_unit = sg(tp, 'FL', 'pressure', 'unit')
        v.wheel_front_left_pressure_warning = sg(tp, 'FL', 'warning')

        v.wheel_front_right_pressure = sg(tp, 'FR', 'pressure', 'value')
        v.wheel_front_right_pressure_unit = sg(tp, 'FR', 'pressure', 'unit')
        v.wheel_front_right_pressure_warning = sg(tp, 'FR', 'warning')

        v.wheel_rear_left_pressure = sg(tp, 'RL', 'pressure', 'value')
        v.wheel_rear_left_pressure_unit = sg(tp, 'RL', 'pressure', 'unit')
        v.wheel_rear_left_pressure_warning = sg(tp, 'RL', 'warning')

        v.wheel_rear_right_pressure = sg(tp, 'RR', 'pressure', 'value')
        v.wheel_rear_right_pressure_unit = sg(tp, 'RR', 'pressure', 'unit')
        v.wheel_rear_right_pressure_warning = sg(tp, 'RR', 'warning')

    return v


class Client:
    def __init__(self, email: str, password: str, pin: str, brand: Brand, disable_tls_verification: bool = False, dev_mode: bool = False, debug: bool = False):
        self.api = API(email, password, pin, brand,
                       disable_tls_verification=disable_tls_verification, dev_mode=dev_mode, debug=debug)
        self.vehicles: Dict[str, Vehicle] = {}

    def set_tls_verification(self, verify: bool):
        '''Enable or disable TLS certificate verification'''

        self.api.set_tls_verification(verify)

    def refresh(self):
        '''Refreshes all the vehicle data and caches it locally'''

        vehicles = self.api.list_vehicles()

        for x in vehicles:
            full_update_done = True
            vin = x['vin']

            if not vin in self.vehicles:
                vehicle = Vehicle(vin=vin, nickname=sg(x, 'nickname'), make=sg(x, 'make'), model=sg(
                    x, 'modelDescription'), year=sg(x, 'tsoModelYear'), region=sg(x, 'soldRegion'))
                self.vehicles[vin] = vehicle
            else:
                vehicle = self.vehicles[vin]

            info = self.api.get_vehicle(vin)
            if not 'vehicleInfo' in info and not 'evInfo' in info:
                full_update_done = False

            _update_vehicle(vehicle, info)

            try:
                loc = self.api.get_vehicle_location(vin)

                vehicle.location = Location(
                    longitude=sg(loc, 'longitude'),
                    latitude=sg(loc, 'latitude'),
                    altitude=sg(loc, 'altitude'),
                    bearing=sg(loc, 'bearing'),
                    is_approximate=sg(loc, 'isLocationApprox'),
                    updated=datetime.fromtimestamp(loc['timeStamp'] / 1000)
                )
            except:
                full_update_done = False

            try:
                s = self.api.get_vehicle_status(vin)

                # Check at least one key is present in the response
                if not set('doors', 'windows', 'trunk', 'evRunning').intersection(set(s.keys())):
                    full_update_done = False

                if 'doors' in s:
                    vehicle.door_driver_locked = sg_eq(
                        s, 'LOCKED', 'doors', 'driver', 'status')
                    vehicle.door_passenger_locked = sg_eq(
                        s, 'LOCKED', 'doors', 'passenger', 'status')
                    vehicle.door_rear_left_locked = sg_eq(
                        s, 'LOCKED', 'doors', 'leftRear', 'status')
                    vehicle.door_rear_right_locked = sg_eq(
                        s, 'LOCKED', 'doors', 'rightRear', 'status')

                if 'windows' in s:
                    vehicle.window_driver_closed = sg_eq(
                        s, 'CLOSED', 'windows', 'driver', 'status')
                    vehicle.window_passenger_closed = sg_eq(
                        s, 'CLOSED', 'windows', 'passenger', 'status')

                vehicle.trunk_locked = sg_eq(s, 'LOCKED', 'trunk', 'status')
                vehicle.ev_running = sg_eq(s, 'ON', 'evRunning', 'status')
            except:
                full_update_done = False

            enabled_services = []
            if 'services' in x:
                enabled_services = [v['service'] for v in x['services']
                                    if sg(v, 'vehicleCapable') and sg(v, 'serviceEnabled')]

            vehicle.supported_commands = [
                v for v in enabled_services if v in COMMANDS_BY_NAME]

            if full_update_done:
                vehicle.last_full_update = datetime.now()

    def get_vehicles(self):
        '''Returns all vehicles data. Must execute refresh method before.'''

        return self.vehicles

    def command(self, vin: str, cmd: Command):
        '''Execute a given command against a car with a given VIN'''

        return self.api.command(vin, cmd)

    def _get_commands_statuses(self, vin: str) -> dict:
        r = self.api.get_vehicle_notifications(vin)

        return {
            x['correlationId']: (x['notification']['data']
                                 ['status'].lower() == "success")
            for x in r['notifications']['items']
        }

    def command_verify(self, vin: str, cmd: Command,
                       timeout: timedelta = timedelta(seconds=60),
                       interval: timedelta = timedelta(seconds=2)):
        '''Execute a given command against a car with a given VIN and poll for the status'''

        id = self.command(vin, cmd)

        start = datetime.now()
        while datetime.now() - start < timeout:
            sleep(interval.seconds)
            r = self._get_commands_statuses(vin)
            if id in r:
                return r[id]

        raise Exception(f'unable to obtain command status: timed out (id {id})')
