import os, sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

import numpy as np
import time
import inspect

from pyod.models.iforest import IForest
from pyod.models.hbos import HBOS
from pyod.models.pca import PCA
from pyod.models.loda import LODA
from pyod.models.lof import LOF
from pyod.models.cblof import CBLOF
from pyod.models.abod import ABOD

from utils import set_seed, cal_metric
from dataloader import TimeSeriesDataset

class PYOD():
    def __init__(self, model_name, seed=42):
        self.seed = seed
        self.model_name = model_name
        self.model_dict = {'IForest': IForest,
                           'HBOS': HBOS,
                           'PCA': PCA,
                           'LODA': LODA,
                           'LOF': LOF,
                           'CBLOF': CBLOF,
                           'ABOD': ABOD
        }

    def fit(self, X_train, y_train=None, **kwargs):
        model_cls = self.model_dict[self.model_name]

        sig = inspect.signature(model_cls.__init__)
        if "random_state" in sig.parameters:
            kwargs["random_state"] = self.seed

        if self.model_name == 'ABOD':
            X_train = X_train + np.random.normal(0, 1e-8, X_train.shape)

        self.model = model_cls(**kwargs).fit(X_train, y_train)

    def predict_score(self, X):
        if self.model_name == 'ABOD':
            X = X + np.random.normal(0, 1e-8, X.shape)
        score = self.model.decision_function(X)
        return score
    

if __name__ == "__main__":
    set_seed(42)

    # dataset name: PSM, SMD, SMAP, MSL, SWaT, WADI
    dataset_name = 'SMD'

    # model type: IForest, HBOS, PCA, LODA, DeepSVDD, LOF, CBLOF, ABOD
    model_name = 'CBLOF'

    # load dataset
    train_set = TimeSeriesDataset(dataset_name=dataset_name, train=True)
    test_set = TimeSeriesDataset(dataset_name=dataset_name, train=False)
    print(f'Dataset & Model name | {dataset_name} - {model_name}')
    print(f'Anomaly ratio        | {test_set.labels.sum() / len(test_set.labels) * 100:.2f}%')
    print(f'Dataset shape        | train: {train_set.data.shape}, test: {test_set.data.shape}')


    hyperparams = {'IForest' : {'n_estimators': 100,
                                'max_samples': 256,
                                'max_features': 1.0,
                                'bootstrap': True},
                               
                   'HBOS'    : {'n_bins': 10,
                                'alpha': 0.1,
                                'tol': 0.5},
                            
                   'PCA'     : {'n_components': 0.5,
                                'n_selected_components': None,
                                'weighted': True,
                                'standardization': True},

                   'LODA'    : {'n_bins': 10,
                                'n_random_cuts': 100},

                   'LOF'     : {'n_neighbors': 20},

                   'CBLOF'   : {'n_clusters': 8,
                               'alpha': 0.9,
                               'beta': 5,
                               'use_weights': False},

                   'ABOD'    : {'n_neighbors': 10,
                                'method': 'fast'},
    }

    pyod = PYOD(model_name=model_name, seed=42)

    # fit
    ts_time = time.time()
    pyod.fit(train_set.data, train_set.labels, **hyperparams[model_name])
    t_time = time.time() - ts_time
    print(f'Training time        | {t_time:.4f} seconds')

    # inference
    is_time = time.time()
    score = pyod.predict_score(test_set.data)
    i_time = time.time() - is_time
    print(f'Inference time       | {i_time:.4f} seconds')

    print(f'Anomaly scores       | Min: {np.min(score):.4f}, Max: {np.max(score):.4f}, Mean: {np.mean(score):.4f}, Std: {np.std(score):.4f}')
    print(f'Metrics              | {cal_metric(y_true=test_set.labels, y_score=score)}')
    print('-' * 100)
