# Copyright (c) 2013 Calin Crisan
# This file is part of motionEye.
#
# motionEye is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import json
import logging
import re
from time import time
from typing import Optional
from urllib.parse import urlencode

from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPResponse

from motioneye import settings, utils
from motioneye.utils.authstate import generate_hmac_signature, generate_nonce

_DOUBLE_SLASH_REGEX = re.compile('//+')


def _make_request(
    scheme,
    host,
    port,
    remote_secret,
    path,
    method='GET',
    data=None,
    query=None,
    timeout=None,
    content_type=None,
):
    path = _DOUBLE_SLASH_REGEX.sub('/', path)
    query = dict(query or {})
    qs = urlencode(query)
    uri = (path + ('&' if '?' in path else '?') + qs) if qs else path

    url = '{scheme}://{host}{port}{uri}'.format(
        scheme=scheme, host=host, port=':' + str(port) if port else '', uri=uri or ''
    )

    # Use HMAC authentication instead of query parameters
    headers = {}
    if content_type:
        headers['Content-Type'] = content_type

    if remote_secret:
        timestamp = str(int(time()))
        nonce = generate_nonce()
        signature = generate_hmac_signature(
            remote_secret, method, uri, timestamp, nonce, data
        )

        headers['X-HMAC-Signature'] = signature
        headers['X-Timestamp'] = timestamp
        headers['X-Nonce'] = nonce
    if timeout is None:
        timeout = settings.REMOTE_REQUEST_TIMEOUT

    return HTTPRequest(
        url,
        method,
        body=data,
        connect_timeout=timeout,
        request_timeout=timeout,
        headers=headers,
        validate_cert=settings.VALIDATE_CERTS,
    )


async def _send_request(request: HTTPRequest) -> HTTPResponse:
    try:
        # The raise_error=False argument only affects the HTTPError raised when a non-200 response
        # code is used, instead of suppressing all errors.
        response = await AsyncHTTPClient().fetch(request, raise_error=False)

        if response.code != 200:
            decoded = json.loads(response.body)
            if decoded['error'] == 'unauthorized':
                response.error = Exception('Authentication Error')

            elif decoded['error']:
                response.error = Exception(decoded['error'])
    except Exception as e:
        logging.error(f"_send_request: {e}")
        response = HTTPResponse(request, 599)
        response.error = e

    return response


def pretty_camera_url(local_config, camera=True):
    scheme = local_config.get('@scheme', local_config.get('scheme')) or 'http'
    host = local_config.get('@host', local_config.get('host'))
    port = local_config.get('@port', local_config.get('port'))
    path = local_config.get('@path', local_config.get('path')) or ''

    url = scheme + '://' + host
    if port and str(port) not in ['80', '443']:
        url += ':' + str(port)

    if path:
        url += path

    if url.endswith('/'):
        url = url[:-1]

    if camera:
        if camera is True:
            url += '/config/' + str(
                local_config.get(
                    '@remote_camera_id', local_config.get('remote_camera_id')
                )
            )

        else:
            url += '/config/' + str(camera)

    return url


def _remote_params(local_config):
    params = [
        local_config.get('@scheme', local_config.get('scheme')) or 'http',
        local_config.get('@host', local_config.get('host')),
        local_config.get('@port', local_config.get('port')),
        local_config.get('@remote_secret', local_config.get('remote_secret')),
        local_config.get('@path', local_config.get('path')) or '',
        local_config.get('@remote_camera_id', local_config.get('remote_camera_id')),
    ]

    if params[3] is not None:
        params[3] = str(params[3])

    if params[4] is not None:
        params[4] = str(params[4])

    if params[5] is not None:
        params[5] = str(params[5])

    return params


def make_camera_response(c):
    return {'id': c['id'], 'name': c['name']}


async def list_cameras(local_config) -> utils.GetCamerasResponse:
    scheme, host, port, remote_secret, path, _ = _remote_params(local_config)

    logging.debug(
        'listing remote cameras on {url}'.format(
            url=pretty_camera_url(local_config, camera=False)
        )
    )

    request = _make_request(scheme, host, port, remote_secret, path + '/config/list/')

    response = await _send_request(request)

    if response.error:
        logging.error(
            'failed to list remote cameras on {url}: {msg}'.format(
                url=pretty_camera_url(local_config, camera=False),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.GetCamerasResponse(None, utils.pretty_http_error(response))

    try:
        response = json.loads(response.body)

    except Exception as e:
        logging.error(
            'failed to decode json answer from {url}: {msg}'.format(
                url=pretty_camera_url(local_config, camera=False), msg=str(e)
            )
        )

        return utils.GetCamerasResponse(None, str(e))

    else:
        cameras = response['cameras']

        # filter out simple mjpeg cameras
        cameras = [
            make_camera_response(c)
            for c in cameras
            if c['proto'] != 'mjpeg' and c.get('enabled')
        ]

        return utils.GetCamerasResponse(cameras, None)


async def get_config(local_config) -> utils.GetConfigResponse:
    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'getting config for remote camera {id} on {url}'.format(
            id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    request = _make_request(
        scheme, host, port, remote_secret, path + f'/config/{camera_id}/get/'
    )
    response = await _send_request(request)

    if response.error:
        logging.error(
            'failed to get config for remote camera {id} on {url}: {msg}'.format(
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.GetConfigResponse(None, error=utils.pretty_http_error(response))

    try:
        response = json.loads(response.body)

    except Exception as e:
        logging.error(
            'failed to decode json answer from {url}: {msg}'.format(
                url=pretty_camera_url(local_config), msg=str(e)
            )
        )

        return utils.GetConfigResponse(None, error=str(e))

    else:
        response['host'] = host
        response['port'] = port

        return utils.GetConfigResponse(remote_ui_config=response, error=None)


async def set_config(local_config, ui_config) -> Optional[str]:
    scheme = local_config.get('@scheme', local_config.get('scheme'))
    host = local_config.get('@host', local_config.get('host'))
    port = local_config.get('@port', local_config.get('port'))
    remote_secret = local_config.get('@remote_secret')
    path = local_config.get('@path', local_config.get('path')) or ''
    camera_id = local_config.get(
        '@remote_camera_id', local_config.get('remote_camera_id')
    )

    logging.debug(
        'setting config for remote camera {id} on {url}'.format(
            id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    ui_config = json.dumps(ui_config)

    p = path + f'/config/{camera_id}/set/'
    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        p,
        method='POST',
        data=ui_config.encode(),
        content_type='application/json',
    )
    response = await _send_request(request)

    result = None

    if response.error:
        logging.error(
            'failed to set config for remote camera {id} on {url}: {msg}'.format(
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        result = utils.pretty_http_error(response)

    return result


async def test(local_config, data) -> utils.CommonExternalResponse:
    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)
    what = data['what']
    logging.debug(
        'testing {what} on remote camera {id}, on {url}'.format(
            what=what, id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    data = json.dumps(data)

    p = path + f'/config/{camera_id}/test/'
    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        p,
        method='POST',
        data=data,
        content_type='application/json',
    )
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to test {what} on remote camera {id}, on {url}: {msg}'.format(
                what=what,
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(
            None, error=utils.pretty_http_error(response)
        )

    return utils.CommonExternalResponse(
        None, None
    )  # it will never return result = True, what the point?


async def get_current_picture(
    local_config, width, height
) -> utils.GetCurrentPictureResponse:
    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'getting current picture for remote camera {id} on {url}'.format(
            id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    query = {}

    if width:
        query['width'] = str(width)

    if height:
        query['height'] = str(height)

    p = path + f'/picture/{camera_id}/current/'

    request = _make_request(scheme, host, port, remote_secret, p, query=query)
    response = await _send_request(request)

    cookies = utils.parse_cookies(response.headers.get_list('Set-Cookie'))
    motion_detected = cookies.get('motion_detected_' + str(camera_id)) == 'true'
    capture_fps_cookie = cookies.get('capture_fps_' + str(camera_id))
    capture_fps = float(capture_fps_cookie) if capture_fps_cookie else 0.0
    monitor_info = cookies.get('monitor_info_' + str(camera_id))

    if response.error:
        logging.error(
            'failed to get current picture for remote camera {id} on {url}: {msg}'.format(
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.GetCurrentPictureResponse(error=utils.pretty_http_error(response))

    return utils.GetCurrentPictureResponse(
        motion_detected=motion_detected,
        capture_fps=capture_fps,
        monitor_info=monitor_info,
        picture=response.body,
    )


async def list_media(
    local_config, media_type, prefix: Optional[str] = None
) -> utils.ListMediaResponse:
    utils.validate_paths(prefix)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'getting media list for remote camera {id} on {url}'.format(
            id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    query = {}
    if prefix is not None:
        query['prefix'] = prefix

    # timeout here is 10 times larger than usual - we expect a big delay when fetching the media list
    p = path + f'/{media_type}/{camera_id}/list/'
    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        p,
        query=query,
        timeout=10 * settings.REMOTE_REQUEST_TIMEOUT,
    )
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to get media list for remote camera {id} on {url}: {msg}'.format(
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.ListMediaResponse(error=utils.pretty_http_error(response))

    try:
        response = json.loads(response.body)

    except Exception as e:
        logging.error(
            'failed to decode json answer from {url}: {msg}'.format(
                url=pretty_camera_url(local_config), msg=str(e)
            )
        )

        return utils.ListMediaResponse(error=str(e))

    return utils.ListMediaResponse(media_list=response)


async def get_media_content(
    local_config, filename: str, media_type
) -> utils.CommonExternalResponse:
    utils.validate_paths(filename)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'downloading file {filename} of remote camera {id} on {url}'.format(
            filename=filename, id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    path += '/{media_type}/{id}/download/{filename}'.format(
        media_type=media_type, id=camera_id, filename=filename
    )

    # timeout here is 10 times larger than usual - we expect a big delay when fetching the media list
    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        path,
        timeout=10 * settings.REMOTE_REQUEST_TIMEOUT,
    )
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to download file {filename} of remote camera {id} on {url}: {msg}'.format(
                filename=filename,
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    return utils.CommonExternalResponse(result=response.body)


async def make_zipped_content(
    local_config, media_type, group: str
) -> utils.CommonExternalResponse:
    utils.validate_paths(group)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'preparing zip file for group "{group}" of remote camera {id} on {url}'.format(
            group=group or 'ungrouped',
            id=camera_id,
            url=pretty_camera_url(local_config),
        )
    )

    prepare_path = path + '/{media_type}/{id}/zipped/{group}/'.format(
        media_type=media_type, id=camera_id, group=group
    )

    # timeout here is 100 times larger than usual - we expect a big delay
    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        prepare_path,
        timeout=100 * settings.REMOTE_REQUEST_TIMEOUT,
    )
    response = await _send_request(request)
    if response.error:
        msg = (
            'failed to prepare zip file for group "%(group)s" '
            'of remote camera %(id)s on %(url)s: %(msg)s'
            % {
                'group': group or 'ungrouped',
                'id': camera_id,
                'url': pretty_camera_url(local_config),
                'msg': utils.pretty_http_error(response),
            }
        )

        logging.error(msg)

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    try:
        key = json.loads(response.body)['key']

    except Exception as e:
        logging.error(
            'failed to decode json answer from {url}: {msg}'.format(
                url=pretty_camera_url(local_config), msg=str(e)
            )
        )

        return utils.CommonExternalResponse(error=str(e))
    else:
        return utils.CommonExternalResponse(result={'key': key})


async def get_zipped_content(
    local_config, media_type, key, group: str
) -> utils.CommonExternalResponse:
    utils.validate_paths(group)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'downloading zip file for remote camera {id} on {url}'.format(
            id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    p = path + '/{media_type}/{id}/zipped/{group}/?key={key}'.format(
        media_type=media_type, group=group, id=camera_id, key=key
    )

    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        p,
        timeout=10 * settings.REMOTE_REQUEST_TIMEOUT,
    )
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to download zip file for remote camera {id} on {url}: {msg}'.format(
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    return utils.CommonExternalResponse(
        result={
            'data': response.body,
            'content_type': response.headers.get('Content-Type'),
            'content_disposition': response.headers.get('Content-Disposition'),
        }
    )


async def make_timelapse_movie(
    local_config, framerate, interval, group: str
) -> utils.CommonExternalResponse:
    utils.validate_paths(group)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    msg = (
        'making timelapse movie for group "%(group)s" of remote camera %(id)s '
        'with rate %(framerate)s/%(int)s on %(url)s'
        % {
            'group': group or 'ungrouped',
            'id': camera_id,
            'framerate': framerate,
            'int': interval,
            'url': pretty_camera_url(local_config),
        }
    )

    logging.debug(msg)

    path += (
        '/picture/{id}/timelapse/{group}/?interval={int}&framerate={framerate}'.format(
            id=camera_id, int=interval, framerate=framerate, group=group
        )
    )

    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        path,
        timeout=100 * settings.REMOTE_REQUEST_TIMEOUT,
    )
    response = await _send_request(request)
    if response.error:
        msg = (
            'failed to make timelapse movie for group "%(group)s" of remote camera %(id)s '
            'with rate %(framerate)s/%(int)s on %(url)s: %(msg)s'
            % {
                'group': group or 'ungrouped',
                'id': camera_id,
                'url': pretty_camera_url(local_config),
                'int': interval,
                'framerate': framerate,
                'msg': utils.pretty_http_error(response),
            }
        )

        logging.error(msg)

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    try:
        response = json.loads(response.body)

    except Exception as e:
        logging.error(
            'failed to decode json answer from {url}: {msg}'.format(
                url=pretty_camera_url(local_config), msg=str(e)
            )
        )

        return utils.CommonExternalResponse(error=str(e))

    else:
        return utils.CommonExternalResponse(result=response)


async def check_timelapse_movie(
    local_config, group: str
) -> utils.CommonExternalResponse:
    utils.validate_paths(group)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'checking timelapse movie status for remote camera {id} on {url}'.format(
            id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    p = path + '/picture/{id}/timelapse/{group}/?check=true'.format(
        id=camera_id, group=group
    )
    request = _make_request(scheme, host, port, remote_secret, p)
    response = await _send_request(request)

    if response.error:
        logging.error(
            'failed to check timelapse movie status for remote camera {id} on {url}: {msg}'.format(
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    try:
        response = json.loads(response.body)

    except Exception as e:
        logging.error(
            'failed to decode json answer from {url}: {msg}'.format(
                url=pretty_camera_url(local_config), msg=str(e)
            )
        )

        return utils.CommonExternalResponse(error=str(e))

    else:
        return utils.CommonExternalResponse(result=response)


async def get_timelapse_movie(
    local_config, key, group: str
) -> utils.CommonExternalResponse:
    utils.validate_paths(group)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'downloading timelapse movie for remote camera {id} on {url}'.format(
            id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    p = path + '/picture/{id}/timelapse/{group}/?key={key}'.format(
        id=camera_id, group=group, key=key
    )

    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        p,
        timeout=10 * settings.REMOTE_REQUEST_TIMEOUT,
    )
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to download timelapse movie for remote camera {id} on {url}: {msg}'.format(
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    return utils.CommonExternalResponse(
        result={
            'data': response.body,
            'content_type': response.headers.get('Content-Type'),
            'content_disposition': response.headers.get('Content-Disposition'),
        }
    )


async def get_media_preview(
    local_config, filename: str, media_type, width, height
) -> utils.CommonExternalResponse:
    utils.validate_paths(filename)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'getting file preview for {filename} of remote camera {id} on {url}'.format(
            filename=filename, id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    path += '/{media_type}/{id}/preview/{filename}'.format(
        media_type=media_type, id=camera_id, filename=filename
    )

    query = {}

    if width:
        query['width'] = str(width)

    if height:
        query['height'] = str(height)

    request = _make_request(scheme, host, port, remote_secret, path, query=query)
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to get file preview for {filename} of remote camera {id} on {url}: {msg}'.format(
                filename=filename,
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    return utils.CommonExternalResponse(result=response.body)


async def del_media_content(
    local_config, filename: str, media_type
) -> utils.CommonExternalResponse:
    utils.validate_paths(filename)

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'deleting file {filename} of remote camera {id} on {url}'.format(
            filename=filename, id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    path += '/{media_type}/{id}/delete/{filename}'.format(
        media_type=media_type, id=camera_id, filename=filename
    )

    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        path,
        method='POST',
        data='{}',
        timeout=settings.REMOTE_REQUEST_TIMEOUT,
        content_type='application/json',
    )
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to delete file {filename} of remote camera {id} on {url}: {msg}'.format(
                filename=filename,
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    return utils.CommonExternalResponse()


async def del_media_group(
    local_config, group: str, media_type
) -> utils.CommonExternalResponse:
    if '..' in group.split('/'):
        raise Exception(f'Path traversal detected in group "{group}"')

    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'deleting group "{group}" of remote camera {id} on {url}'.format(
            group=group or 'ungrouped',
            id=camera_id,
            url=pretty_camera_url(local_config),
        )
    )

    path += '/{media_type}/{id}/delete_all/{group}/'.format(
        media_type=media_type, id=camera_id, group=group
    )

    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        path,
        method='POST',
        data='{}',
        timeout=settings.REMOTE_REQUEST_TIMEOUT,
        content_type='application/json',
    )
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to delete group "{group}" of remote camera {id} on {url}: {msg}'.format(
                group=group or 'ungrouped',
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    return utils.CommonExternalResponse()


async def exec_action(local_config, action) -> utils.CommonExternalResponse:
    scheme, host, port, remote_secret, path, camera_id = _remote_params(local_config)

    logging.debug(
        'executing action "{action}" of remote camera {id} on {url}'.format(
            action=action, id=camera_id, url=pretty_camera_url(local_config)
        )
    )

    path += f'/action/{camera_id}/{action}/'

    request = _make_request(
        scheme,
        host,
        port,
        remote_secret,
        path,
        method='POST',
        data='{}',
        timeout=settings.REMOTE_REQUEST_TIMEOUT,
        content_type='application/json',
    )
    response = await _send_request(request)
    if response.error:
        logging.error(
            'failed to execute action "{action}" of remote camera {id} on {url}: {msg}'.format(
                action=action,
                id=camera_id,
                url=pretty_camera_url(local_config),
                msg=utils.pretty_http_error(response),
            )
        )

        return utils.CommonExternalResponse(error=utils.pretty_http_error(response))

    return utils.CommonExternalResponse()
