import logging

import requests

from . import Urls, ApiError, Defaults
from ..util import FileUtils

_LOGGER = logging.getLogger('Connector')

_JSON_CONTENT_TYPE_HEADER = {'content-type': 'application/json'}


class ApiConnector:
    """
    This is the low level smart.vaillant.com API connector. This is returning the raw JSON from responses or
    :exc:`vr900connector.api.ApiError` if something goes wrong (basically, when response error code > 399).

    On the first call, the connector will login automatically. If login don't succeed the first time, it will try to
    login a second time (only if response error code is 401) before raising an :exc:`vr900connector.api.ApiError`

    On Other calls, if session is running, the connector will assume the session is ok. If response error code is 401,
    the connector will clear the session and retry to login. This implementation allows the connector to reconnect
    automatically when cookies are outdated.

    All GET against the API works, PUT/POST  are not yet implemented.

    Please use :mod:`vr900connector.api.urls` in order to generate URL to be passed to the connector.

    Args:
        user: User for login
        password: Password for login
        smart_phone_id: Smart phone id required by the API
        file_path: Where to store files created by this connector. Cookies and serial number are saved to avoid doing
        re-login between sessions
    """

    def __init__(self, user: str, password: str, smart_phone_id: str = Defaults.SMART_PHONE_ID,
                 file_path: str = Defaults.FILES_PATH):
        self._user = user
        self._password = password
        self._smart_phone_Id = smart_phone_id
        self._file_path = file_path
        self._serial_number = self._load_serial_number_from_file()
        self._session = self._create_or_load_session()

    def logout(self):
        """
        To get logged out of the API. It means, the connector will have to request a new token and ask for cookies.
        Files generated by the connector are also deleted
        """
        response = None
        try:
            response = self._session.request('POST', Urls.logout())
        except Exception as e:
            raise ApiError("Error during logout", response) from e
        finally:
            self._clear_session()

    def query(self, url: str, method: str = 'GET', payload=None):
        """
        Call the vaillant API url with the chosen method, please use :mod:`vr900connector.api.urls` in order to generate
        URL to be passed to the connector
        """

        return self._secure_call(method, url, payload)

    def get(self, url: str):
        """
        GET call to a vaillant API url please use :mod:`vr900connector.api.urls` in order to generate  URL to be passed
        to the connector
        """
        return self.query(url)

    def put(self, url: str, payload=None):
        """
        PUT call to a vaillant API url please use :mod:`vr900connector.api.urls` in order to generate  URL to be passed
        to the connector
        """
        return self.query(url, 'PUT', payload)

    def post(self, url: str, payload=None):
        """
        POST call to a vaillant API url please use :mod:`vr900connector.api.urls` in order to generate  URL to be passed
        to the connector.
        """
        return self.query(url, 'POST', payload)

    def delete(self, url: str):
        """
        DELETE call to a vaillant API url please use :mod:`vr900connector.api.urls` in order to generate  URL to be
        passed to the connector
        """
        return self.query(url, 'DELETE')

    def _secure_call(self, method: str, url: str, payload=None, re_login: bool = False):
        response = None
        safe_url = None
        try:
            self._login(re_login)

            safe_url = url.format(serial_number=self._serial_number)
            response = self._session.request(method,
                                             safe_url,
                                             json=payload,
                                             headers=None if payload is None else _JSON_CONTENT_TYPE_HEADER)

            if response.status_code > 399:
                if not re_login and response.status_code == 401:
                    _LOGGER.debug('Call to %s failed with HTTP 401, will try to re-login', safe_url)
                    return self._secure_call(method, url, payload, True)
                else:
                    raise ApiError('Received error from server url: ' + safe_url + ' and method ' + method,
                                   response, payload)
            if response.text:
                return response.json()
            else:
                return {"ok": "ok"}
        except ApiError:
            raise
        except Exception as e:
            raise ApiError('Cannot {} url: {}'.format(method, safe_url if safe_url else url), response, payload) from e

    def _login(self, force_login: bool = False):
        try:
            if force_login:
                self._clear_session()
            if not self._session.cookies:
                self._session = self._create_or_load_session()
                self._serial_number = self._load_serial_number_from_file()

                if not self._session.cookies:
                    _LOGGER.info(
                        'No previous session found, will try to logging with username: %s and smartphoneId: %s',
                        self._user, self._smart_phone_Id)

                    authtoken = self._request_token()
                    self._get_cookies(authtoken)

            if not self._serial_number:
                self._get_serial_number()
        except ApiError:
            raise
        except Exception as e:
            raise ApiError('Error during login', None) from e

    def _request_token(self):
        params = {
            "smartphoneId": self._smart_phone_Id,
            "username": self._user,
            "password": self._password
        }

        try:
            response = self._session.post(Urls.new_token(), json=params, headers=_JSON_CONTENT_TYPE_HEADER)
            if response.status_code == 200:
                _LOGGER.debug('Token generation successful')
                return response.json()['body']['authToken']
            else:
                raise ApiError('Authentication failed', response)

        except ApiError:
            raise
        except Exception as e:
            raise ApiError('Error during authentication', None) from e

    def _get_cookies(self, auth_token: str):
        params = {
            "smartphoneId": self._smart_phone_Id,
            "username": self._user,
            "authToken": auth_token
        }

        try:
            response = self._session.post(Urls.authenticate(), json=params, headers=_JSON_CONTENT_TYPE_HEADER)

            if response.status_code == 200:
                self._session.cookies = response.cookies
                _LOGGER.debug('Cookie successfully retrieved %s', self._session.cookies)
                self._save_cookies_to_file()
            else:
                raise ApiError('Cannot get cookies', response)
        except ApiError:
            raise
        except Exception as e:
            raise ApiError('Error while getting cookies', None) from e

    def _get_serial_number(self):
        try:
            response = self._session.get(Urls.facilities_list())

            if response.status_code == 200:
                _LOGGER.debug('Serial number successfully retrieved')
                self._serial_number = response.json()['body']['facilitiesList'][0]['serialNumber']
                self._save_serial_number_to_file()
            else:
                raise ApiError('Cannot get serial number', response)
        except ApiError:
            raise
        except Exception as e:
            raise ApiError('Cannot get serial number', None) from e

    def _create_or_load_session(self):
        session = requests.Session()
        cookies = self._load_cookies_from_file()
        _LOGGER.debug('Found cookies %s', cookies)
        if cookies is not None:
            session.cookies = cookies
        return session

    def _clear_session(self):
        self._clear_cookie()
        self._clear_serial_number()
        self._session.close()
        self._session = requests.session()
        FileUtils.delete_dir(self._file_path)

    def _save_cookies_to_file(self):
        FileUtils.save_to_file(self._session.cookies, self._file_path, Defaults.COOKIE_FILE_NAME)

    def _save_serial_number_to_file(self):
        FileUtils.save_to_file(self._serial_number, self._file_path, Defaults.SERIAL_NUMBER_FILE_NAME)

    def _load_cookies_from_file(self):
        return FileUtils.load_from_file(self._file_path, Defaults.COOKIE_FILE_NAME)

    def _load_serial_number_from_file(self):
        return FileUtils.load_from_file(self._file_path, Defaults.SERIAL_NUMBER_FILE_NAME)

    def _clear_cookie(self):
        FileUtils.delete_file(self._file_path, Defaults.COOKIE_FILE_NAME)

    def _clear_serial_number(self):
        FileUtils.delete_file(self._file_path, Defaults.SERIAL_NUMBER_FILE_NAME)
        self._serial_number = None
