import inspect
from typing import Dict, Any, List, Tuple

import numpy as np

from enmapbox.typeguard import typechecked
from enmapboxprocessing.enmapalgorithm import EnMAPProcessingAlgorithm, Group
from enmapboxprocessing.typing import ClassifierDump
from enmapboxprocessing.utils import Utils
from qgis.core import (QgsProcessingContext, QgsProcessingFeedback)


@typechecked
class PrepareClassificationDatasetFromCodeAlgorithm(EnMAPProcessingAlgorithm):
    P_CODE, _CODE = 'code', 'Code'
    P_OUTPUT_DATASET, _OUTPUT_DATASET = 'outputClassificationDataset', 'Output dataset'

    @classmethod
    def displayName(cls) -> str:
        return 'Create classification dataset (from Python code)'

    def shortDescription(self) -> str:
        return 'Create a classification dataset from Python code ' \
               'and store the result as a skops file.'

    def helpParameters(self) -> List[Tuple[str, str]]:
        return [
            (self._CODE, 'Python code specifying the classification dataset.\n'
                         'Note: The Python code provided here is executed locally with the permissions of the '
                         'current user during algorithm execution.'),
            (self._OUTPUT_DATASET, self.SkopsFileDestination)
        ]

    def code(cls):
        from enmapboxprocessing.typing import Number, List, Category, ClassifierDump

        # specify categories and feature names
        categories: List[Category] = [
            Category(value=1, name='class 1', color='#ff0000'),
            Category(value=2, name='class 2', color='#00ff00')
        ]
        features: List[str] = ['Feature 1', 'Feature 2', 'Feature 3']

        # specify features X as 2d-array with shape (samples, features)
        X: List[List[Number]] = [
            [1, 2, 3],
            [4, 5, 6]
        ]
        # specify targets y as 2d-array with shape (samples, 1)
        y: List[List[int]] = [
            [1], [2]
        ]

        return ClassifierDump(categories, features, X, y)

    def defaultCodeAsString(self):
        try:
            lines = [line[8:] for line in inspect.getsource(self.code).split('\n')][1:-2]
        except OSError:
            lines = ['']
        lines = '\n'.join(lines)
        return lines

    def classifierDump(
        self, parameters: Dict[str, Any], context: QgsProcessingContext
    ) -> ClassifierDump:
        namespace = dict()
        code = self.parameterAsString(parameters, self.P_CODE, context)

        # nosec B102 # User-defined code execution by design; equivalent to the QGIS Python Console.
        # The code execution is transparently documented for users (e.g., via the Processing algorithm help).
        exec(code, namespace)  # nosec B102 # User-defined code execution equivalent to the QGIS Python Console.
        categories, features, X, y = [namespace[key] for key in ['categories', 'features', 'X', 'y']]
        X = np.array(X)
        y = np.array(y)
        classifierDump = ClassifierDump(categories, features, X, y)
        return classifierDump

    def group(self):
        return Group.DatasetCreation.value

    def initAlgorithm(self, configuration: Dict[str, Any] = None):
        self.addParameterCode(self.P_CODE, self._CODE, self.defaultCodeAsString())
        self.addParameterFileDestination(self.P_OUTPUT_DATASET, self._OUTPUT_DATASET, self.SkopsFileFilter)

    def processAlgorithm(
        self, parameters: Dict[str, Any], context: QgsProcessingContext, feedback: QgsProcessingFeedback
    ) -> Dict[str, Any]:
        filename = self.parameterAsFileOutput(parameters, self.P_OUTPUT_DATASET, context)

        with open(filename + '.log', 'w') as logfile:
            feedback, feedback2 = self.createLoggingFeedback(feedback, logfile)
            self.tic(feedback, parameters, context)

            classifierDump = self.classifierDump(parameters, context)
            Utils.modelDump(classifierDump.__dict__, filename)

            result = {self.P_OUTPUT_DATASET: filename}
            self.toc(feedback, result)
        return result
