import json
import pickle
import uuid

import pandas
import wizata_dsapi

from .api_dto import ApiDto
from .dataframe_toolkit import DataFrameToolkit
from .ds_dataframe import DSDataFrame
from .mlmodel import MLModel
from .plot import Plot
from .request import Request
from .script import Script


class Execution(ApiDto):

    def __init__(self, execution_id=None):

        # Id
        if execution_id is None:
            execution_id = uuid.uuid4()
        self.execution_id = execution_id

        # Experiment information (required only for queries processed from backend)
        self.experiment_id = None

        # Inputs Properties (load)
        self.script = None
        self.request = None
        self.input_ds_dataframe = None
        self.ml_model = None
        self.function = None
        self.isAnomalyDetection = False

        # Outputs Properties (generated by Execution)
        self.models = []
        self.anomalies = []
        self.plots = []
        self.output_ds_dataframe = None

    def _get_dataframe(self):
        if self.input_ds_dataframe is None:
            return None
        else:
            return self.input_ds_dataframe.dataframe

    def _set_dataframe(self, value):
        if not isinstance(value, pandas.DataFrame):
            raise ValueError("dataframe must be a panda dataframe.")
        self.input_ds_dataframe = DSDataFrame()
        self.input_ds_dataframe.dataframe = value

    def _del_dataframe(self):
        del self.input_ds_dataframe

    dataframe = property(
        fget=_get_dataframe,
        fset=_set_dataframe,
        fdel=_del_dataframe,
        doc="Input Pandas Dataframe (for id fetch 'input_ds_dataframe.df_id')"
    )

    def _get_result_dataframe(self):
        if self.output_ds_dataframe is None:
            return None
        else:
            return self.output_ds_dataframe.dataframe

    def _set_result_dataframe(self, value):
        if not isinstance(value, pandas.DataFrame):
            raise ValueError("dataframe must be a panda dataframe.")
        self.output_ds_dataframe = DSDataFrame()
        self.output_ds_dataframe.dataframe = value

    def _del_result_dataframe(self):
        del self.output_ds_dataframe

    result_dataframe = property(
        fget=_get_result_dataframe,
        fset=_set_result_dataframe,
        fdel=_del_result_dataframe,
        doc="Output Pandas Dataframe (for id fetch 'output_ds_dataframe.df_id')"
    )

    def append_plot(self, figure, name="Unkwown"):
        plot = Plot()
        plot.name = name
        plot.experiment_id = self.experiment_id
        plot.figure = figure
        self.plots.append(plot)
        return plot

    def append_model(self, trained_model, input_columns, output_columns=None, has_anomalies=False, scaler=None):
        ml_model = MLModel()

        ml_model.trained_model = trained_model
        ml_model.scaler = scaler

        ml_model.input_columns = input_columns
        ml_model.output_columns = output_columns

        ml_model.has_anomalies = has_anomalies

        self.models.append(ml_model)
        return ml_model

    def api_id(self) -> str:
        return str(self.execution_id).upper()

    def endpoint(self) -> str:
        return "Executions"

    def to_json(self):
        """
        Convert to a json version of Execution definition.
        By default, use DS API format.
        :param result: if set as True, use format for pushing results.
        """
        obj = {
            "id": str(self.execution_id)
        }
        if self.experiment_id is not None:
            obj["experimentId"] = str(self.experiment_id)
        if self.request is not None:
            obj["request"] = json.dumps(self.request.to_json())
        if self.dataframe is not None:
            obj["dataframe"] = DataFrameToolkit.convert_to_json(self.dataframe)
        if self.script is not None:
            obj["scriptId"] = str(self.script.script_id)
        if self.ml_model is not None:
            obj["mlModelId"] = str(self.ml_model.model_id)
        if self.function is not None:
            obj["function"] = self.function
        if self.isAnomalyDetection:
            obj["isAnomalyDetection"] = str(True)
        if self.plots is not None:
            plots_ids = []
            for plot in self.plots:
                plots_ids.append({"id": str(plot.plot_id)})
            obj["plots"] = plots_ids
        if self.models is not None:
            models_json = []
            for ml_model in self.models:
                models_json.append({"id": str(ml_model.model_id)})
            obj["models"] = models_json
        if self.anomalies is not None:
            obj["anomaliesList"] = self.anomalies
        if self.result_dataframe is not None:
            obj["resultDataframe"] = {
                "id": str(self.output_ds_dataframe.df_id)
            }
        return obj

    def from_json(self, obj):
        if "id" in obj.keys():
            self.execution_id = uuid.UUID(obj["id"])
        if "experimentId" in obj.keys():
            self.experiment_id = uuid.UUID(obj["experimentId"])
        if "request" in obj.keys():
            self.request = Request()
            if isinstance(obj["request"], str):
                self.request.from_json(json.loads(obj["request"]))
            else:
                self.request.from_json(obj["request"])
        if "dataframe" in obj.keys():
            if isinstance(obj["dataframe"], str):
                self.request.from_json(json.loads(obj["dataframe"]))
            else:
                self.dataframe = DataFrameToolkit.convert_from_json(obj["dataframe"])
        if "scriptId" in obj.keys():
            self.script = Script()
            self.script.script_id = uuid.UUID(obj["scriptId"])
        if "mlModelId" in obj.keys():
            self.ml_model = MLModel()
            self.ml_model.model_id = uuid.UUID(obj["mlModelId"])
        if "function" in obj.keys():
            self.function = obj["function"]
        if "isAnomalyDetection" in obj.keys() and obj["isAnomalyDetection"] == 'True':
            self.isAnomalyDetection = True

    def to_pickle(self):
        return pickle.dumps(self)
