from autohack import __VERSION__
from autohack.core.plugin.checker import *
from autohack.core.constant import *
from autohack.core.exception import *
from autohack.core.path import *
from autohack.core.util.util import *
from autohack.core.util.run import *
from autohack.core.lib.config import *
from autohack.core.lib.logger import *
from autohack.core.lib.i18n import *
import traceback, time, os


class AppCentral:
    def __init__(self, clientID: str, logTime: time.struct_time, debug: bool = False) -> None:
        self.clientID = clientID
        self.logTime = logTime
        self.debug = debug

    def run(self) -> None:
        hideCursor()

        ensureDirExists(CHECKER_FOLDER_PATH)
        ensureDirExists(LOG_FOLDER_PATH)

        # TODO: Debug
        loggerObj = Logger(LOG_FOLDER_PATH, self.debug)
        logger = loggerObj.getLogger().bind(module="autohack")

        I18n = I18N(TRANSLATION_FOLDER_PATH, logger)
        _ = I18n.translate

        if not GLOBAL_CONFIG_FILE_PATH.exists():
            logger.info("Global config file not found. Creating new one.")
            write("Welcome to autohack-next!", endl=1)
            write("A global config file will be created.", endl=1)
            write("Please select your preferred language:", endl=1)
            # for i, (langID) in enumerate(LANGUAGE_MAPS):
            #     write(f"  {i+1}: {langID} / {I18N(TRANSLATION_FOLDER_PATH, logger).translate("language-info", langID)}", 1)
            # showCursor()
            # while True:
            #     result = inputMessage("Enter the number of your preferred language: ", 0, True)
            #     if result.isdigit() and 1 <= int(result) <= len(LANGUAGE_MAPS):
            #         selectedLang = LANGUAGE_MAPS[int(result) - 1]
            #         break
            #     write("Invalid input. Please enter a valid number.")
            #     prevLine()
            selectedLangIndex = selectionMenu(
                [f"{langID} / {I18N(TRANSLATION_FOLDER_PATH, logger).translate("language-info", langID)}" for i, langID in enumerate(LANGUAGE_MAPS)]
            )
            selectedLang = LANGUAGE_MAPS[selectedLangIndex]
            globalConfig = Config(GLOBAL_CONFIG_FILE_PATH, DEFAULT_GLOBAL_CONFIG, logger)
            globalConfig.modifyConfigEntry("language", selectedLang)
            I18n.setDefaultLanguage(selectedLang)
            write(ANSIHelper.CLEAR_LINE)
            write(ANSIHelper.PREV_LINE)
            write(ANSIHelper.CLEAR_LINE)
            write(ANSIHelper.PREV_LINE)
            write(ANSIHelper.CLEAR_LINE)
            writeMessage(I18n, "__main__.language-select.result", _("language-info"), endl=1)
            writeMessage(I18n, "__main__.language-select.info", GLOBAL_CONFIG_FILE_PATH, endl=2)

        globalConfig = Config(GLOBAL_CONFIG_FILE_PATH, DEFAULT_GLOBAL_CONFIG, logger)
        I18n.setDefaultLanguage(globalConfig.getConfigEntry("language"))

        config = Config(
            CONFIG_FILE_PATH,
            DEFAULT_CONFIG,
            logger,
            CONFIG_VALIDATION_EXCLUDE,
            getTranslatedMessage(I18n, "__main__.config-created", CONFIG_FILE_PATH),
        )

        logger.info(f'Data folder path: "{DATA_FOLDER_PATH}"')
        logger.info(f"Client ID: {self.clientID}")
        logger.info(f"Initialized. Version: {__VERSION__}")
        writeMessage(I18n, "__main__.start.version", __VERSION__, self.clientID, endl=2)
        writeMessage(I18n, "__main__.start.data", getHackDataStorageFolderPath(self.clientID, self.logTime), endl=1)
        writeMessage(I18n, "__main__.start.log", loggerObj.getLogFilePath(), endl=1)
        writeMessage(I18n, "__main__.start.export", getExportFolderPath(self.logTime, self.clientID), endl=1)
        writeMessage(I18n, "__main__.start.checker", CHECKER_FOLDER_PATH, endl=2)

        waitTimeBeforeStart = globalConfig.getConfigEntry("wait_time_before_start")
        for i in range(waitTimeBeforeStart, 0, -1):
            writeMessage(I18n, "__main__.countdown", i, clear=True)
            time.sleep(1)

        fileList = [
            [config.getConfigEntry("commands.compile.source"), "__main__.compile.filename.source"],
            [config.getConfigEntry("commands.compile.std"), "__main__.compile.filename.std"],
            [config.getConfigEntry("commands.compile.generator"), "__main__.compile.filename.generator"],
        ]
        for file in fileList:
            writeMessage(I18n, "__main__.compile.doing", _(file[1]), clear=True)
            try:
                compileCode(file[0])
            except autohackRuntimeError as e:
                logger.error(
                    f"{_(file[1], LOGGER_LANGUAGE_ID).capitalize()} compilation failed with return code {e.returnCode} and message:\n{e.output.decode(errors="ignore")}"
                )
                writeMessage(I18n, "__main__.compile.error", _(file[1]).capitalize(), e.returnCode, endl=2, clear=True, highlight=True)
                write(e.output.decode(errors="ignore"))
                exitProgram(1)
            else:
                logger.debug(f"{_(file[1], LOGGER_LANGUAGE_ID).capitalize()} compiled successfully.")
        writeMessage(I18n, "__main__.compile.finish", endl=1, clear=True)

        writeMessage(I18n, "__main__.activate-checker.doing", config.getConfigEntry("checker.name"))
        currentChecker: checkerType = lambda l, o, a, ar: (False, _("__main__.activate-checker.no-checker-message"))
        deactivateFunc: deactivateType = emptyDeactivate
        try:
            getCheckerResult = getChecker(CHECKER_FOLDER_PATH, config.getConfigEntry("checker.name"), config.getConfigEntry("checker.args"))
            currentChecker = getCheckerResult[0]
            deactivateFunc = getCheckerResult[1]
        except Exception as e:
            logger.critical(f"{e}")
            writeMessage(I18n, "__main__.activate-checker.failed", endl=1, clear=True, highlight=True)
            traceback.print_exc()
            exitProgram(1)
        writeMessage(I18n, "__main__.activate-checker.finish", config.getConfigEntry("checker.name"), endl=2, clear=True)

        dataCount, errorDataCount = 0, 0
        lastStatusError = False
        generateCommand = config.getConfigEntry("commands.run.generator")
        stdCommand = config.getConfigEntry("commands.run.std")
        sourceCommand = config.getConfigEntry("commands.run.source")
        timeLimit = config.getConfigEntry("time_limit") / 1000
        memoryLimit = config.getConfigEntry("memory_limit") * 1024 * 1024
        inputFilePath = config.getConfigEntry("paths.input")
        answerFilePath = config.getConfigEntry("paths.answer")
        outputFilePath = config.getConfigEntry("paths.output")
        maximumDataLimit = config.getConfigEntry("maximum_number_of_data")
        errorDataLimit = config.getConfigEntry("error_data_number_limit")
        refreshSpeed = globalConfig.getConfigEntry("refresh_speed")
        checkerArgs = config.getConfigEntry("checker.args")

        timeLimit = None if timeLimit == 0 else timeLimit
        memoryLimit = None if memoryLimit == 0 else memoryLimit

        def updateStatus(total: float, averagePerS: float, averagePerData: float, addtional: str) -> None:
            # write(
            #     f"Time taken: {total:.2f} seconds, average {averagePerS:.2f} data per second, {averagePerData:.2f} second per data.{addtional}",
            #     clear=True,
            # )
            writeMessage(I18n, "__main__.status", f"{total:.2f}", f"{averagePerS:.2f}", f"{averagePerData:.2f}", clear=True)
            write(addtional)

        write(endl=1)
        updateStatus(0.0, 0.0, 0.0, " (0%)" if maximumDataLimit > 0 else "")
        write(ANSIHelper.PREV_LINE)

        startTime = time.time()

        while (maximumDataLimit <= 0 or dataCount < maximumDataLimit) and (errorDataLimit <= 0 or errorDataCount < errorDataLimit):
            dataInput = b""
            dataAnswer = b""

            dataCount += 1

            try:
                # write(f"{dataCount}: Generate input.", clear=True)
                writeMessage(I18n, "__main__.main.generate-input", dataCount, clear=True)
                logger.debug(f"Generating data {dataCount}.")
                dataInput = generateInput(generateCommand)
            except autohackRuntimeError as e:
                logger.error(f"Input generation failed with return code {e.returnCode}.")
                writeMessage(I18n, "__main__.main.generate-input-failed", e.returnCode, endl=1, clear=True, highlight=True)
                inputExportPath = getExportDataPath(getExportFolderPath(self.logTime, self.clientID), "input")
                writeData(inputExportPath, e.output)
                writeMessage(I18n, "__main__.main.save-input-data", inputExportPath, clear=True)
                exitProgram(1)

            try:
                # write(f"{dataCount}: Generate answer.", clear=True)
                writeMessage(I18n, "__main__.main.generate-answer", dataCount, clear=True)
                logger.debug(f"Generating answer for data {dataCount}.")
                dataAnswer = generateAnswer(stdCommand, dataInput)
            except autohackRuntimeError as e:
                logger.error(f"Answer generation failed with return code {e.returnCode}.")
                writeMessage(I18n, "__main__.main.generate-answer-failed", e.returnCode, endl=1, clear=True, highlight=True)
                inputExportPath = getExportDataPath(getExportFolderPath(self.logTime, self.clientID), "input")
                writeData(inputExportPath, dataInput)
                writeMessage(I18n, "__main__.main.save-input-data", inputExportPath, endl=1, clear=True)
                answerExportPath = getExportDataPath(getExportFolderPath(self.logTime, self.clientID), "answer")
                writeData(answerExportPath, e.output)
                writeMessage(I18n, "__main__.main.save-answer-data", answerExportPath, clear=True)
                exitProgram(1)

            # write(f"{dataCount}: Run source code.", clear=True)
            writeMessage(I18n, "__main__.main.run-source", dataCount, clear=True)
            logger.debug(f"Run source code for data {dataCount}.")
            result = runSourceCode(sourceCommand, dataInput, timeLimit, memoryLimit)
            if result.stdout is None:
                result.stdout = b""
            if result.stderr is None:
                result.stderr = b""

            # TODO: Refresh when running exe. Use threading or async?
            if dataCount % refreshSpeed == 0 or lastStatusError:
                lastStatusError = False
                currentTime = time.time()
                write(endl=1)
                # write(
                #     f"Time taken: {currentTime - startTime:.2f} seconds, average {dataCount/(currentTime - startTime):.2f} data per second, {(currentTime - startTime)/dataCount:.2f} second per data.{f" ({dataCount*100/maximumDataLimit:.0f}%)" if maximumDataLimit > 0 else ""}",
                #     clear=True,
                # )
                updateStatus(
                    currentTime - startTime,
                    dataCount / (currentTime - startTime),
                    (currentTime - startTime) / dataCount,
                    f" ({dataCount*100/maximumDataLimit:.0f}%)" if maximumDataLimit > 0 else "",
                )
                write(ANSIHelper.PREV_LINE)

            saveData, termMessage, logMessage, extMessage, exitAfterSave = (False, "", "", None, False)

            if result.memoryOut:
                saveData = True
                logMessage = f"Memory limit exceeded for data {dataCount}."
                termMessage = getTranslatedMessage(I18n, "__main__.main.memory-limit-exceeded", dataCount)
                if result.maxMemory is not None:
                    extMessage = getTranslatedMessage(I18n, "__main__.main.memory-limit-exceeded-extra", f"{result.maxMemory / 1024 / 1024:.4f}")
            elif result.timeOut:
                saveData = True
                logMessage = f"Time limit exceeded for data {dataCount}."
                termMessage = getTranslatedMessage(I18n, "__main__.main.time-limit-exceeded", dataCount)
                if result.totalTime is not None:
                    extMessage = getTranslatedMessage(I18n, "__main__.main.time-limit-exceeded-extra", f"{result.totalTime*1000:.4f}")
            elif result.returnCode != 0:
                saveData = True
                logMessage = f"Runtime error for data {dataCount} with return code {result.returnCode}."
                termMessage = getTranslatedMessage(I18n, "__main__.main.runtime-error", dataCount)
                if result.returnCode is not None:
                    extMessage = getTranslatedMessage(I18n, "__main__.main.runtime-error-extra", f"{result.returnCode}")

            checkerResult = (False, _("__main__.main.checker-not-executed"))
            try:
                checkerResult = currentChecker(dataInput, result.stdout, dataAnswer, checkerArgs)
            except Exception as e:
                saveData = True
                termMessage = getTranslatedMessage(I18n, "__main__.main.checker-error-without-exception", dataCount)
                logMessage = f"Checker error for data {dataCount}. Exception: {e}"
                extMessage = f"{_("__main__.main.checker-error-extra-message")}\n{traceback.format_exc()}"
                checkerResult = (False, _("__main__.main.checker-exception-occurred"))
                exitAfterSave = True

            if not saveData and not checkerResult[0]:
                saveData = True
                termMessage = getTranslatedMessage(I18n, "__main__.main.wrong-answer", dataCount)
                logMessage = f"Wrong answer for data {dataCount}. Checker output: {checkerResult[1]}"
                extMessage = checkerResult[1]

            if saveData:
                lastStatusError = True
                errorDataCount += 1
                writeData(getHackDataFilePath(getHackDataStorageFolderPath(self.clientID, self.logTime), errorDataCount, inputFilePath), dataInput)
                writeData(getHackDataFilePath(getHackDataStorageFolderPath(self.clientID, self.logTime), errorDataCount, answerFilePath), dataAnswer)
                writeData(
                    getHackDataFilePath(getHackDataStorageFolderPath(self.clientID, self.logTime), errorDataCount, outputFilePath), result.stdout
                )
                write(f"[{errorDataCount}]: {termMessage}", endl=1, clear=True)
                if extMessage is not None and extMessage != "":
                    write(f"{(len(f'[{errorDataCount}]: ')-3)*' '} - {extMessage}", endl=1, clear=True)
                logger.info(f"{logMessage}")

            if exitAfterSave:
                writeMessage(I18n, "__main__.main.checker-failed-exit", clear=True, highlight=True)
                exitProgram(0)

        endTime = time.time()

        writeMessage(I18n, "__main__.main.finish", dataCount, errorDataCount, endl=1, clear=True)
        # write(
        #     f"Time taken: {endTime - startTime:.2f} seconds, average {dataCount/(endTime - startTime):.2f} data per second, {(endTime - startTime)/dataCount:.2f} second per data.",
        #     2,
        #     True,
        # )
        updateStatus(endTime - startTime, dataCount / (endTime - startTime), (endTime - startTime) / dataCount, "")
        write(endl=2)

        # if errorDataCount == 0:
        #     shutil.rmtree(getHackDataStorageFolderPath(clientID))
        #     write("No error data found. Hack data folder removed.", 1)
        #     logger.info("No error data found. Hack data folder removed.")

        dataFolderMaxSize = globalConfig.getConfigEntry("data_folder_max_size")
        # print(getFolderSize(HACK_DATA_STORAGE_FOLDER_PATH) / 1024 / 1024, " ", dataFolderMaxSize)
        if HACK_DATA_STORAGE_FOLDER_PATH.exists() and getFolderSize(HACK_DATA_STORAGE_FOLDER_PATH) > dataFolderMaxSize * 1024 * 1024:
            logger.warning(f"Hack data storage folder size exceeds {dataFolderMaxSize} MB: {HACK_DATA_STORAGE_FOLDER_PATH}")
            # write(f"Warning: Hack data storage folder size exceeds {DATA_FOLDER_MAX_SIZE} MB: {HACK_DATA_STORAGE_FOLDER_PATH}", 2)
            writeMessage(I18n, "__main__.data-folder-size-warning", dataFolderMaxSize, HACK_DATA_STORAGE_FOLDER_PATH, endl=2, highlight=True)

        writeMessage(I18n, "__main__.deactivate-checker.doing")
        try:
            deactivateFunc(config.getConfigEntry("checker.args"))
        except Exception as e:
            logger.error(f"Checker deactivation failed with exception: {e}")
            writeMessage(I18n, "__main__.deactivate-checker.failed", endl=1, clear=True, highlight=True)
            traceback.print_exc()
            exitProgram(1)
        writeMessage(I18n, "__main__.deactivate-checker.finish", endl=1, clear=True)

        writeMessage(I18n, "__main__.post-command", endl=1)
        os.system(config.getConfigEntry("command_at_end"))
        logger.info("Finished.")
