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 TransformerDump
from enmapboxprocessing.utils import Utils
from qgis.core import (QgsProcessingContext, QgsProcessingFeedback)


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

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

    def shortDescription(self) -> str:
        return 'Create an unsupervised 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 unsupervised 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

        # specify feature names
        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]
        ]

        return TransformerDump(features, X)

    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 parameterAsTransformerDump(
        self, parameters: Dict[str, Any], name, context: QgsProcessingContext
    ) -> TransformerDump:
        namespace = dict()
        code = self.parameterAsString(parameters, name, context)

        # nosec B102 # User-defined code execution by design; equivalent to calling sklearn in 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 by design;
        features, X = [namespace[key] for key in ['features', 'X']]
        X = np.array(X)
        transformerDump = TransformerDump(features, X)
        return transformerDump

    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)

            transformerDump = self.parameterAsTransformerDump(parameters, self.P_CODE, context)
            Utils.modelDump(transformerDump.__dict__, filename)

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