#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019.
# Built in San Francisco.

# This software is distributed under a commercial license for personal,
# educational, corporate or any other use.
# The software as a whole or any parts of it is prohibited for distribution or
# use without obtaining a license from 3D Control Systems, Inc.

# All software licenses are subject to the 3DPrinterOS terms of use
# (available at https://www.3dprinteros.com/terms-and-conditions/),
# and privacy policy (available at https://www.3dprinteros.com/privacy-policy/)

import os
import sys
import time
import errno
import zipfile
import logging
import logging.handlers
import traceback
import platform
import os.path
import simplejson as json

import paths
import http_client
import config
import version
import subprocess

try:
    paths.remove_pyc_files()
    import requests
except ImportError:
    paths.remove_pyc_files()
    import requests

#TODO refactor to reduce the mess with paths and filenames here
SETTINGS_FOLDER = paths.current_settings_folder()
LOG_FILE = os.path.join(SETTINGS_FOLDER, "3dprinteros_client.log")
CAMERA_LOG_FILE = os.path.join(SETTINGS_FOLDER, "3dprinteros_camera.log")
EXCEPTIONS_LOG_FILE = os.path.join(SETTINGS_FOLDER, 'critical_errors.log')
REPORT_FILE_NAME = 'problem_report.txt'
REPORT_FILE_PATH = os.path.join(SETTINGS_FOLDER, REPORT_FILE_NAME)

LOG_FILE_SIZE = 1024*1024*50#(50MB)
LOG_BACKUPS = 1 #total max long 0 + 1 = 50MB + 50MB = 100MB
REQUEST_SKIP = config.get_settings()['logging']['request_skip']

TAIL_LINES = 100
AVERAGE_LINE_LENGTH = 200 


class SkipRequestsFilter(logging.Filter):

    REQUEST_RECORD_START = "Request:"

    def __init__(self):
        self.counter = 0

    def filter(self, record):
        if record.getMessage().startswith(self.REQUEST_RECORD_START):
            if self.counter < REQUEST_SKIP:
                self.counter += 1
                return False
            else:
                self.counter = 0
        return True

def create_logger(logger_name, log_file_name=None):
    logger = logging.getLogger(logger_name)
    logger.setLevel(logging.DEBUG)
    std_handler = logging.StreamHandler(stream=sys.stdout)
    std_handler.setLevel(logging.DEBUG)
    logger.addHandler(std_handler)
    if log_file_name and config.get_settings().get('logging') and config.get_settings()['logging']['enabled']:
        if config.get_settings()['logging']['erase_on_start']:
            clear_logs()
        try:
            file_handler = logging.handlers.RotatingFileHandler(log_file_name, 'a', LOG_FILE_SIZE, LOG_BACKUPS)
            file_handler.setFormatter(logging.Formatter('%(asctime)s\t%(threadName)s/%(funcName)s\t%(message)s'))
            file_handler.setLevel(logging.DEBUG)
            if REQUEST_SKIP:
                skip_requests_filter = SkipRequestsFilter()
                file_handler.addFilter(skip_requests_filter)
            logger.addHandler(file_handler)
            print("File logger created: " + log_file_name)
        except Exception as e:
            print('Could not create log file because' + str(e) + '\n.No log mode.')
    return logger

def log_exception(func_or_method):
    def decorator(*args, **kwargs):
        try:
            result = func_or_method(*args, **kwargs)
        except SystemExit:
            pass
        except IOError as e:
            if e.errno != errno.EINTR:
                report_exception()
        except:
            report_exception()
        else:
            return result
    return decorator

def report_exception():
    trace = traceback.format_exc()
    try:
        logging.getLogger(__name__).error(trace)
    except:
        print(trace)
    with open(EXCEPTIONS_LOG_FILE, "a") as f:
        f.write(form_log_title() + time.ctime() + "\n" + trace + "\n")
    if config.get_settings()['logging']['auto_report_exceptions']:
        try:
            send_logs()
        except:
            logging.getLogger(__name__).error("Error while trying to send logs")
    sys.exit(1)

def compress_logs():
    logger = logging.getLogger(__name__)
    try:
        log_file_names = os.listdir(SETTINGS_FOLDER)
    except:
        logger.warning('No logs to pack')
    else:
        for name in log_file_names[:]:
            if not (name.startswith(os.path.basename(LOG_FILE))
                    or name == os.path.basename(EXCEPTIONS_LOG_FILE)
                    or name == REPORT_FILE_NAME
                    or name == os.path.basename(CAMERA_LOG_FILE)): log_file_names.remove(name)
        if log_file_names:
            zip_file_name = time.strftime("%Y_%m_%d___%H_%M_%S", time.localtime()) + ".zip"
            zip_file_name_path = os.path.abspath(os.path.join(SETTINGS_FOLDER, zip_file_name))
            logger.info('Creating zip file : ' + zip_file_name)
            try:
                zf = zipfile.ZipFile(zip_file_name_path, mode='w')
                for name in log_file_names:
                    zf.write(os.path.join(SETTINGS_FOLDER, name), name, compress_type=zipfile.ZIP_DEFLATED)
                zf.close()
            except Exception as e:
                logger.warning("Error while creating logs archive " + zip_file_name + ': ' + str(e))
            else:
                return zip_file_name_path

def upload_compressed_logs(zip_file_path):
    # NOTE this upload should not be moved to http_client, because it only works reliably with requests(lib)
    # otherwise you will need to implement chunk uploading using http client
    # WARNING you will need a valid report_file for logs uploading to work!
    logger = logging.getLogger(__name__)
    connection_class = http_client.get_printerinterface_protocol_connection()
    if connection_class.HTTPS_MODE:
        prefix = 'https://'
    else:
        prefix = 'http://'
    url = prefix \
        + connection_class.URL \
        + connection_class.API_PREFIX \
        + connection_class.TOKEN_SEND_LOGS
    try:
        if connection_class == http_client.HTTPClientPrinterAPIV1:
            url = connection_class.patch_api_prefix(url)
            tokens = config.get_app().user_login.auth_tokens
            if tokens: #FIXME not threadsafe and not taking in account multiple printers with apiprinter
                token = config.get_app().user_login.auth_tokens[-1][1]
            else:
                return 'Error logs uploading failed: no auth token'
        else:
            token = config.get_app().user_login.user_token
        data = {connection_class.SEND_LOGS_TOKE_FIELD_NAME: token}
        logger.info('Sending logs to %s' % url)
        response = None
        with open(zip_file_path, 'rb') as zip_file:
            files = {'file_data': zip_file}
            report_file = None
            if os.path.exists(REPORT_FILE_PATH):
                report_file = open(REPORT_FILE_PATH, 'rb')
                files['report_file'] = report_file
            else:
                return "Empty report file"
            response = requests.post(url, data=data, files=files)
            if report_file:
                report_file.close()
                os.remove(REPORT_FILE_PATH)
    except Exception as e:
        return 'Error while sending logs: ' + str(e)
    else:
        if response != None:
            result = response.text
            logger.debug("Log sending response: " + result)
            if response.status_code != 200:
                return 'Error while uploading logs: response code is not 200 (OK)'
            try:
                answer = json.loads(result)
            except Exception as e:
                return 'Error while uploading logs: ' + str(e)
            if type(answer) == dict and not answer.get('success'):
                return result

def send_logs():
    logger = logging.getLogger(__name__)
    if config.get_settings()['logging']['logs_sending']:
        zip_file_path = compress_logs()
        if not zip_file_path:
            return 'Error while packing logs'
        error = upload_compressed_logs(zip_file_path)
        os.remove(zip_file_path)
        if error:
            logger.warning(error)
            return error
        logger.debug('Logs successfully sent')
        return clear_logs()
    else:
        logger.warning("Can't send logs - disabled in config")

def clear_logs():
    logger = logging.getLogger(__name__)
    remove_old_logs()
    try:
        with open(LOG_FILE, 'w') as f:
            f.write(form_log_title())
        if os.path.exists(EXCEPTIONS_LOG_FILE):
            os.remove(EXCEPTIONS_LOG_FILE)
    except Exception as e:
        error = 'Error while clearing critical errors log: ' + str(e)
        logger.warning(error)
        return error
    try:
        os.remove(os.path.join(SETTINGS_FOLDER, CAMERA_LOG_FILE))
    except (OSError, IOError):
        pass
    logger.info('Logs successfully cleared')

def remove_old_logs():
    logger = logging.getLogger(__name__)
    for handler in logger.handlers:
        handler.do_rollover()
    for number in range(1, 10):
        ending = "." + str(number)
        try:
            os.remove(LOG_FILE + ending)
        except (OSError, IOError):
            break

def get_file_tail(file_path):
    if os.path.isfile(file_path):
        with open(file_path) as f:
            f.seek(0, os.SEEK_END)
            size = f.tell()
            tail_length = min(TAIL_LINES * AVERAGE_LINE_LENGTH, size)
            position = size - tail_length
            f.seek(position)
            tail = f.read(tail_length)
            if tail:
                tail_lines_list = tail.split("\n")[1:]
                tail_lines_list.reverse()
                return tail_lines_list

def form_log_title():
    version_string = "%s.%s_%s" % (version.version, version.build, version.branch)
    return 'Version %s\nOperating system:%s %s\n' % (version_string, platform.system(), platform.release())

def create_report(report_text):
    report_text = form_log_title() + '\n\n' + report_text
    paths.check_and_create_dirs(REPORT_FILE_PATH)
    with open(REPORT_FILE_PATH, 'w') as f:
        f.write(report_text)

def report_problem(report_text):
    create_report(report_text)
    result = send_logs()
    return result

def open_settings_folder():
    path = os.path.abspath(SETTINGS_FOLDER)
    if sys.platform.startswith('darwin'):
        subprocess.Popen(['open', path], close_fds=True)
    elif sys.platform.startswith('linux'):
        subprocess.Popen(['xdg-open', path], close_fds=True)
    elif sys.platform.startswith('win'):
        subprocess.Popen(['explorer', path], close_fds=True)
