from typing import Any
import argparse
import sys
import webbrowser
from pathlib import Path
from importlib import metadata

import logging
logger = logging.getLogger('millconnect.gui')

from PyQt6.QtCore import QTimer ,QRunnable, QThreadPool, pyqtSlot, pyqtSignal, QTranslator, QDir, QStandardPaths, Qt
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout, QProgressBar, QStackedWidget, QFileDialog, QPushButton
from PyQt6.QtGui import QIcon

from .project import Project
from .updater import newVersionAvailable, update
from .server import listen, handleConnection

class ProjectSelector(QWidget):
    folderSelected = pyqtSignal(Path)
    canceledSelection = pyqtSignal()

    def __init__(self, parent=None):
        super().__init__(parent)

        self.projectFolder: Path = None

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.label = QLabel(self, text=self.tr('Select a project folder.'))
        layout.addWidget(self.label)

    def showEvent(self, a0):
        super().showEvent(a0)
        QTimer.singleShot(0, self.selectFolder)

    def selectFolder(self):
        defaultFolder = QDir.toNativeSeparators(QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DesktopLocation))
        self.projectFolder = QFileDialog.getExistingDirectory(self, self.tr('Open project folder'), defaultFolder)
        logger.debug(f'selected folder: "{self.projectFolder}".')

        if not self.projectFolder:
            self.canceledSelection.emit()
        else:
            self.folderSelected.emit(Path(self.projectFolder))

class UpdateRunner(QRunnable):

    def __init__(self, parent=None):
        super().__init__()
        self.parent = parent

    def run(self):
        self.checkForUpdate()

    def checkForUpdate(self):
        logger.info('Checking for update.')
        if newVersionAvailable():
            logger.info('New version available.')
            self.parent.downloadInitialiated()
            update()
            self.parent.restartSignal.emit()
        else:
            logger.info('No new version available.')
            self.parent.noUpdate()
            self.parent.noUpdateSignal.emit()

class UpdatingDialog(QWidget):
    noUpdateSignal = pyqtSignal()
    restartSignal = pyqtSignal()

    def __init__(self, parent=None, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.label = QLabel(self, text=self.tr('Checking for updates.'))
        layout.addWidget(self.label)

        self.progressBar = QProgressBar(self, minimum=0, maximum=100, value=0)
        layout.addWidget(self.progressBar)


    @pyqtSlot()
    def initializeUpdate(self):
        self.threadpool = QThreadPool()
        self.threadpool.start(UpdateRunner(self))

    @pyqtSlot()
    def downloadInitialiated(self):
        self.label.setText(self.tr('Downloading...'))
        self.progressBar.setValue(50)
        self.update()

    @pyqtSlot()
    def restartInitiated(self):
        self.label.setText(self.tr('Update successfull. Restarting...'))
        self.progressBar.setValue(100)
        self.update()

    @pyqtSlot()
    def noUpdate(self):
        self.label.setText(self.tr('Application is up-to-date.'))
        self.progressBar.setValue(100)
        self.update()

class WebsocketServer(QRunnable):

    def __init__(self, parent, callback):
        super().__init__()
        self.parent = parent
        self.server = None
        self.callback = callback

    @pyqtSlot()
    def run(self):
        logger.info('Starting websocket server.')
        try:
            self.server = listen('localhost', 8765, self.callback)
            self.server.serve_forever()
        except OSError:
            logger.error('Could not start websocket server.')
            self.parent.closeWebsocketServer(False)

    def stopServer(self):
        logger.info('Stopping websocket server.')
        if self.server:
            self.server.shutdown()

class BrowserDialog(QWidget):
    uploadFinished = pyqtSignal()
    uploadFailed = pyqtSignal()

    def __init__(self, parent=None, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

        self.worker: WebsocketServer = None

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.label = QLabel(self, text=self.tr('Please follow the instructions in the browser.'))
        layout.addWidget(self.label)

        self.threadpool = QThreadPool()

    @pyqtSlot(str, int, Any)
    def createWebsocketServer(self, apiHost: str, project: Project):
        self.worker = WebsocketServer(self, lambda websocket: handleConnection(websocket, project, apiHost, self))
        self.threadpool.start(self.worker)

    def closeWebsocketServer(self, success=True):
        if self.worker:
            self.worker.stopServer()
        if success:
            self.uploadFinished.emit()
        else:
            self.uploadFailed.emit()

class ErrorDialog(QWidget):
    buttonPressed = pyqtSignal()

    def __init__(self, parent=None, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.label = QLabel(self, text=self.tr('An Error occurt during uploading of the project. Please contact the support.'))
        layout.addWidget(self.label)

        self.button = QPushButton(self.tr('OK'), self)
        layout.addWidget(self.button)

        self.button.clicked.connect(self.handleButtonPressed)

    @pyqtSlot()
    def handleButtonPressed(self):
        self.buttonPressed.emit()

class MainWindow(QWidget):
    def __init__(self, parent=None, projectPath: Path=None, host: str=None, uploadURL: str=None, no_update: bool=False, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

        self.projectPath = projectPath
        self.host = host
        self.uploadURL = uploadURL

        self.resize(600, 60)
        filePath = Path(__file__).parent / 'assets' / 'millhouse.png'
        self.setWindowIcon(QIcon(str(filePath)))

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.stackedWidget = QStackedWidget(self)
        layout.addWidget(self.stackedWidget)
        
        self.updatingDialog = UpdatingDialog(self)
        self.stackedWidget.addWidget(self.updatingDialog)

        self.browserDialog = BrowserDialog(self)
        self.stackedWidget.addWidget(self.browserDialog)

        self.projectSelector = ProjectSelector(self)
        self.stackedWidget.addWidget(self.projectSelector)

        self.errorDialog = ErrorDialog(self)
        self.stackedWidget.addWidget(self.errorDialog)

        self.updatingDialog.restartSignal.connect(self.close)
        self.errorDialog.buttonPressed.connect(self.close)

        if self.projectPath:
            self.updatingDialog.noUpdateSignal.connect(self.showBrowserDialog)
            self.updatingDialog.noUpdateSignal.connect(self.startWebsocket)
        else:
            self.updatingDialog.noUpdateSignal.connect(self.showProjectSelector)
            self.projectSelector.canceledSelection.connect(self.close)
            self.projectSelector.folderSelected.connect(self.setProjectPath)

        self.browserDialog.uploadFinished.connect(self.close)
        self.browserDialog.uploadFailed.connect(self.showErrorDialog)

        if no_update:
            if self.projectPath:
                self.stackedWidget.setCurrentWidget(self.browserDialog)
            else:
                self.stackedWidget.setCurrentWidget(self.projectSelector)

            self.updatingDialog.noUpdateSignal.emit()
        else:
            self.stackedWidget.setCurrentWidget(self.updatingDialog)
            self.updatingDialog.initializeUpdate()

    @pyqtSlot()
    def showBrowserDialog(self):
        logger.info(f'Opening browser with URL: {self.uploadURL}.')
        webbrowser.open_new_tab(self.uploadURL)

        self.stackedWidget.setCurrentWidget(self.browserDialog)
        self.update()

    @pyqtSlot()
    def startWebsocket(self):
        self.project = Project(self.projectPath)
        self.browserDialog.createWebsocketServer(self.host, self.project)

    @pyqtSlot()
    def showProjectSelector(self):
        self.stackedWidget.setCurrentWidget(self.projectSelector)
        self.update()

    @pyqtSlot(Path)
    def setProjectPath(self, projectPath: Path):
        self.projectPath = projectPath
        self.showBrowserDialog()
        self.startWebsocket()

    @pyqtSlot()
    def showErrorDialog(self):
        self.stackedWidget.setCurrentWidget(self.errorDialog)
        self.update()
        self.setWindowState( (self.windowState() & ~Qt.WindowState.WindowMinimized) | Qt.WindowState.WindowActive)
        self.raise_()  # for MacOS
        self.activateWindow() # for Windows

    def closeEvent(self, event):
        self.browserDialog.closeWebsocketServer()
        return super().closeEvent(event)

def handle_exception(exc_type, exc_value, exc_traceback):
    logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))

    sys.__excepthook__(exc_type, exc_value, exc_traceback)
    return

sys.excepthook = handle_exception

def main():
    parser = argparse.ArgumentParser(description='millConnect order creator.')
    parser.add_argument('projectPath', nargs='?', type=Path, default=None, help='The path to a project folder.')
    parser.add_argument('--host', nargs='?', type=str, default='https://api.connect.millhouse.de/v1', help='The URL of the api, default: https://api.connect.millhouse.de/v1')
    parser.add_argument('--uploadURL', nargs='?', type=str, default='https://connect.millhouse.de/uploadOrder', help='The URL to open in the browser, default: https://connect.millhouse.de/uploadOrder')
    parser.add_argument('--no-update', action='store_true', help='Do not check for updates.')
    parser.add_argument('-v','--verbose', action='store_true', help='Verbose logging.')
    args = parser.parse_args()

    logFormatter = logging.Formatter('%(asctime)s %(name)s %(levelname)s: %(message)s')

    logPath = QDir.home()
    fileHandler = logging.FileHandler(logPath.filePath("millconnect-client.log"), mode='w')
    fileHandler.setFormatter(logFormatter)

    consoleHandler = logging.StreamHandler()
    consoleHandler.setFormatter(logFormatter)

    logLevel = logging.DEBUG if args.verbose else logging.INFO
    logger.setLevel(logLevel)

    logging.basicConfig(level=logLevel, handlers=[fileHandler, consoleHandler])

    app = QApplication(sys.argv)
    app.setOrganizationName('millhouse GmbH')
    app.setApplicationName('millCONNECT Client')

    if args.projectPath and args.projectPath.is_file():
        args.projectPath = args.projectPath.parent

    try:
        my_version = metadata.version('millconnect')
        app.setApplicationVersion(my_version)
        logger.info(f'Starting millConnect client {my_version}.')
    except metadata.PackageNotFoundError:
        logger.info('Running from source code. Disabled update check.')
        args.no_update = True

    translator = QTranslator()
    filePath = Path(__file__).parent / 'assets'
    if translator.load('de_DE', str(filePath)):
        app.installTranslator(translator)

    mainWindow = MainWindow(projectPath=args.projectPath, host=args.host, uploadURL=args.uploadURL, no_update=args.no_update)
    mainWindow.show()

    ret = app.exec()
    sys.exit(ret)

if __name__ == '__main__':
    main()



    