#
# Copyright 3D Control Systems, Inc. All Rights Reserved 2017-2019. Built in San Francisco.
#
# This software is distributed under commercial non-GPL license for personal, educational,
# corporate or any other use. The software as a whole or any parts of that are prohibited
# for distribution and/or use without obtaining license from 3D Control Systems, Inc.
#
# If you do not have the license to use this software, please delete all software files
# immediately and contact sales to obtain the license: sales@3dprinteros.com.
# If you are unsure about the licensing please contact directly our sales: sales@3dprinteros.com.

import datetime
import json
import logging
import os
import shutil
import subprocess
import threading
from PySide6 import QtCore

import config
import paths
import rights


class FilesModel(QtCore.QObject):

    ROOT_FILES_DIR = "/media"
    ALT_ROOT_FILES_DIR = os.path.expanduser("~/.3dprinteros/user_files")

    SETTINGS_PATCH_NAME = "3dprinteros_settings_patch.json"

    INVALID_PATH_CHARS = ("..", "/", "\\")

    STAT_RESULT_PREFIX = "st_"

    LAST_CLEAN_PRINTHEAD_TIMESTAMP_PATH = os.path.join(paths.CURRENT_SETTINGS_FOLDER, "last_clean_head_time")

    on_fileError = QtCore.Signal()
    on_fileList = QtCore.Signal()
    on_diskSpaceLeft = QtCore.Signal()

    def __init__(self, core_model):
        super().__init__()
        self.logger = logging.getLogger(__name__)
        self._coreModel = core_model
        self._app = core_model._app
        self._printThread = threading.Thread()
        alt_folder = config.get_settings()['qt_interface'].get('alt_local_folder')
        if type(alt_folder) == str:
            self.root_dir = os.path.abspath(os.path.expanduser(alt_folder))
        elif alt_folder:
            self.root_dir = self.ALT_ROOT_FILES_DIR
        else:
            self.root_dir = self.ROOT_FILES_DIR
        self._initFilesDir()

    def _initFilesDir(self):
        self._files_dir = self.root_dir
        try:
            if not os.path.isdir(self._files_dir):
                os.mkdir(self._files_dir)
            #os.chdir(self._files_dir) causes update fail
        except OSError as e:
            self.logger.exception(f"Files directory error. Path:{self._files_dir}")

    def getAbsPath(self, path):
        return os.path.abspath(os.path.join(self._files_dir, path))

    def checkNameForHacks(self, name):
        for forbidden in self.INVALID_PATH_CHARS:
            if forbidden in name:
                self.on_fileError.emit("Invalid characters in path:" + forbidden)
                return False
        return True
    
    def _printFile(self, path):
        self._coreModel.showProcessing.emit("Loading complete", 0.0)
        #self._coreModel.showProcessing.emit("Loading complete", False)
        try:
            self._app.qt_interface.printer_model.loadGcodes(path, print_file=True)
        except Exception:
            message = "Exception while opening file to print"
            self.logger.exception(message)
            self.on_fileError.emit()
        finally:
            self._coreModel.hideProcessing.emit()

    @QtCore.Slot(str)
    def makeDir(self, dirname):
        if self.checkNameForHacks(dirname):
            try:
                os.mkdir(self.getAbsPath(dirname))
            except Exception as e:
                self.logger.exception("Exception while making directory")
                self.on_fileError.emit(str(e))

    @QtCore.Slot(str)
    def printFile(self, path):
        if self._printThread and self._printThread.is_alive():
            message = "File loading blocked by other unfinished loading"
            self.logger.error(message)
            self.emit.on_fileError(message)
        else:
            self._printThread = threading.Thread(target=self._printFile, args=[path])
            self._printThread.start()

    @QtCore.Property('QVariantList', notify=on_fileList)
    def changeFilesDirectory(self, path):
        self._files_dir = os.path.join(self._files_dir, path)
        return self.getFilesList()

    @QtCore.Property('QVariantList', notify=on_fileList)
    def filesList(self):
        file_info_list = []
        if self._files_dir != self.root_dir:
            file_info_list.append({"filename": "..", "size": None, "atime": "", "mtime": "", "isdir": True})
        try:
            for filename in os.listdir(self._files_dir):
                file_info = {"filename": filename}
                stats = os.stat(filename)
                #file_info["size"] = humanize_file_size(stats.st_size)
                file_info["size"] = stats.st_size
                for attr_name in ("atime", "mtime"):
                    seconds = getattr(stats, self.STAT_RESULT_PREFIX + attr_name)
                    date = datetime.datetime.fromtimestamp(seconds).strftime("%A, %B %d, %Y %I:%M:%S")
                    file_info[attr_name] = date
                file_info["isdir"] = os.path.isdir(filename)
                file_info_list.append(file_info)
        except OSError:
            self.logger.exception("OS error on getting file list:")
        return file_info_list

    @QtCore.Property(str, notify=on_diskSpaceLeft)
    def diskSpaceLeft(self):
        _, _, free_space = shutil.disk_usage("/")
        return paths.humanize_disk_size(free_space)

    @QtCore.Slot()
    def copyUpdatePackage(self):
        source_path = self.root_dir
        dest_path = paths.UPDATE_FILE_PATH
        for folder in os.listdir(source_path):
            source_path = os.path.join(self.root_dir, folder, paths.UPDATE_FILE_NAME)
            if os.path.isfile(source_path):
                break
        if not os.path.isfile(source_path):
            self._coreModel.showMessage.emit(None, f"Error: No {paths.UPDATE_FILE_NAME} file found on the USB drive", None)
        else:
            try:
                subprocess.run(['cp', source_path, dest_path], check=True)
            except subprocess.CalledProcessError:
                self._coreModel.showMessage.emit(None, "Error: update to copy update package", None)
                try:
                    subprocess.run(['sync'])
                except:
                    pass
            else:
                self._coreModel.showMessage.emit(None, "Success. Reboot to apply the update", None)

    @QtCore.Slot()
    def loadSettingsFile(self):
        source_path = self.root_dir
        for folder in os.listdir(source_path):
            source_path = os.path.join(self.root_dir, folder, self.SETTINGS_PATCH_NAME)
            if os.path.isfile(source_path):
                break
        if not os.path.isfile(source_path):
            self._coreModel.showMessage.emit(None, f"Error: No {self.SETTINGS_PATCH_NAME} file found on the USB drive", None)
        else:
            try:
                with open(source_path) as f:
                    text = f.read()
                    settings_patch = json.loads(text)
            except:
                self._coreModel.showMessage.emit(None, "Error: corrupted setting patch", None)
            else:
                config.Config.instance().update_settings(settings_patch)
                try:
                    subprocess.run(['sync'])
                except:
                    pass
                self._coreModel.showMessage.emit(None, "Settings patching success", None)

    @QtCore.Slot()
    def updateCACerts(self):
        cert_found = False
        dest_path = paths.CUSTOM_CACERT_PATH
        for root, dirs, files in os.walk(self.root_dir):
            for f in files:
                source_path = os.path.join(root, f)
                if f.endswith('.crt'):
                    self.logger.info(f"Converting crt {source_path} to pem {dest_path}")
                    try:
                        subprocess.run(['openssl', 'x509', "-in", source_path, '-out', dest_path, '-outform', 'PEM'], check=True)
                    except subprocess.CalledProcessError as e:
                        self.logger.info('Certificate conversion crt->pem error:' + str(e))
                    else:
                        self.logger.info('Certificate conversion crt->pem success')
                        cert_found = True
                        break
                elif f.endswith('.pem'):
                    self.logger.info(f"Copying pem from {source_path} to {dest_path}")
                    try:
                        subprocess.run(['cp', source_path, dest_path], check=True)
                    except subprocess.CalledProcessError:
                        self.logger.warning(f'Unable to copy {source_path} to {dest_path}')
                    else:
                        cert_found = True
                        break
            if cert_found:
                break
        if cert_found:
            self.logger.info('Adding custom CA cert to certifi package')
            error = rights.add_cacert_to_certifi(dest_path)
            if error:
                self.logger.warning(error)
                self._coreModel.showMessage.emit(None, error, None)
            # TODO: make logic to uncomment this (code used in Loop3D)
            # else:
            #     self.logger.info('Adding custom CA cert to system')
            #     error = rights.add_cacert_to_trust()
            #     if error:
            #         self.logger.warning(error)
            #         self._coreModel.showMessage.emit(None, error, None)
            #     else:
            #         message = "Success. Custom CA certificate installed"
            #         self.logger.info(message)
            #         self._coreModel.showMessage.emit(None, message, None)
        else:
            message = "Error: No .pem or .crt file found on the USB drive"
            self.logger.warning(message)
            self._coreModel.showMessage.emit(None, message, None)

    def settingsDir(self):
        return paths.CURRENT_SETTINGS_FOLDER
    settingsDir = QtCore.Property(str, fget=settingsDir, constant=True)

    def rootFileDir(self):
        return self.ROOT_FILES_DIR
    rootFileDir = QtCore.Property(str, fget=rootFileDir, constant=True)

    @QtCore.Slot()
    def saveCleanHeadTimestamp(self):
        current_time = datetime.datetime.now()
        timestamp = current_time.strftime("%Y-%m-%d %H:%M:%S")
        with open(self.LAST_CLEAN_PRINTHEAD_TIMESTAMP_PATH, "w") as file:
            file.write(timestamp)

    @QtCore.Slot(result=bool)
    def checkCleanHead(self):
        last_timestamp_str = ""
        if not os.path.exists(self.LAST_CLEAN_PRINTHEAD_TIMESTAMP_PATH):
            return True
        with open(self.LAST_CLEAN_PRINTHEAD_TIMESTAMP_PATH, "r") as file:
            last_timestamp_str = file.read().strip()
        last_timestamp = datetime.datetime.strptime(last_timestamp_str, "%Y-%m-%d %H:%M:%S")
        two_weeks_ago = datetime.datetime.now() - datetime.timedelta(weeks=2)
        return last_timestamp < two_weeks_ago

    @QtCore.Slot(str, result=bool)
    def isLink(self, src):
        return os.path.islink(src)

    @QtCore.Slot(str, result=str)
    def readFile(self, filepath):
        try:
            with open(filepath, 'r') as file:
                content = file.read()
            return content
        except Exception as e:
            return ""

    @QtCore.Slot(str, str, result=bool)
    def writeFile(self, filepath, content):
        try:
            with open(filepath, 'w') as file:
                file.write(content)
            return True
        except Exception:
            return False
