import numpy as np
import joblib

from lightgbm import LGBMClassifier
from catboost import CatBoostClassifier
from xgboost import XGBClassifier

from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.neural_network import MLPClassifier

from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score
from sklearn.metrics import confusion_matrix
from sklearn.pipeline import Pipeline
from sklearn.pipeline import make_pipeline
from sklearn.feature_selection import RFE

from sklearn.model_selection import cross_val_score
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import cross_validate


SEED = 41


def load_model(**kwargs):
    if kwargs["model"] == "rf":
        return RandomForestClassifier(
            random_state=kwargs["random_state"],
            n_jobs=4,
            criterion="entropy",
            n_estimators=500
        )
    elif kwargs["model"] == "dt":
        return DecisionTreeClassifier(
            random_state=kwargs["random_state"],
            criterion="entropy",
            max_depth=8
        )
    elif kwargs["model"] == "lgb":
        return LGBMClassifier(
            random_state=kwargs["random_state"],
            objective="binary",
            metric="binary_logloss",
            class_weight="balanced",
            max_depth=30,
            num_leaves=1200
        )
    elif kwargs["model"] == "xgb":
        return XGBClassifier(
            random_state=kwargs["random_state"],
            use_label_encoder=False,
            objective="reg:logistic"
        )
    elif kwargs["model"] == "svm":
        return SVC(random_state=kwargs["random_state"])
    elif kwargs["model"] == "lr":
        return LogisticRegression(random_state=kwargs["random_state"], n_jobs=-1)
    elif kwargs["model"] == "knn":
        return KNeighborsClassifier(n_jobs=-1)
    elif kwargs["model"] == "adacat":
        bm = CatBoostClassifier(
            random_seed=kwargs["random_state"],
            logging_level="Silent",
            loss_function="Logloss",
            eval_metric="AUC",
            class_weights=(2.1, 1),
            thread_count=-1,
            early_stopping_rounds=10
        )
        return AdaBoostClassifier(
            base_estimator=bm,
            n_estimators=100,
            random_state=kwargs["random_state"]
        )
    elif kwargs["model"] == "adalgbm":
        bm = LGBMClassifier(
            random_state=kwargs["random_state"],
            objective="binary",
            metric="binary_logloss",
            class_weight="balanced",
            max_depth=30,
            num_leaves=1200
        )
        return AdaBoostClassifier(
            base_estimator=bm,
            n_estimators=100,
            random_state=kwargs["random_state"]
        )
    elif kwargs["model"] == "mlp":
        return MLPClassifier(
            random_state=kwargs["random_state"],
            activation="relu",
            solver="adam",
            batch_size=128,
            learning_rate_init=0.01,
            hidden_layer_sizes=(20, 12)
        )
    elif kwargs["model"] == "cat":
        return CatBoostClassifier(
            random_seed=kwargs["random_state"],
            logging_level="Silent",
            loss_function="Logloss",
            eval_metric="AUC",
            class_weights=(2.1, 1),
            thread_count=-1,
            early_stopping_rounds=10
        )
    elif kwargs["model"] == "voting":
        xgb = load_model(model="xgb", random_state=kwargs["random_state"])
        lgbm = load_model(model="lgb", random_state=kwargs["random_state"])
        cat = load_model(model="cat", random_state=kwargs["random_state"])
        adalgbm = load_model(model="adalgbm", random_state=kwargs["random_state"])
        adacat = load_model(model="adacat", random_state=kwargs["random_state"])
        voters = [
            ("xgb", xgb),
            ("lgbm", lgbm),
            ("catboost", cat),
            ("lgbmXadaboost", adalgbm),
            ("catboostXadaboost", adacat),
        ]
        return VotingClassifier(estimators=voters, voting="soft")
    else:
        print("Unsupported Algorithm")
        return None


def train(X_train, y_train, model):
    '''
        머신러닝 모델을 선택하여 학습을 진행하는 함수

        :param X_train: 학습할 2차원 리스트 특징벡터
        :param y_train: 학습할 1차원 리스트 레이블 벡터
        :param model: 문자열, 선택할 머신러닝 알고리즘
        :return: 학습된 머신러닝 모델 객체
    '''
    clf = load_model(model=model, random_state=SEED)
    clf.fit(X_train, y_train)

    # pipe = make_pipeline(
    #     MinMaxScaler(),
    #     clf
    # )
    # pipe.fit(X_train, y_train)
    return clf


def evaluate(X_test, y_test, model):
    '''
        학습된 머신러닝 모델로 검증 데이터를 검증하는 함수

        :param X_test: 검증할 2차원 리스트 특징 벡터
        :param y_test: 검증할 1차원 리스트 레이블 벡터
        :param model: 학습된 머신러닝 모델 객체
    '''
    predict = model.predict(X_test)
    print("정확도", model.score(X_test, y_test))


def evaluate_kfold(X_train, y_train, model):
    kfold = StratifiedKFold(n_splits=10).split(X_train, y_train)
    scores = []
    for k, (_train, _test) in enumerate(kfold):
        model.fit(X_train[_train], y_train[_train])
        local_prediction = model.predict(X_train[_test])
        score_acc = accuracy_score(local_prediction, y_train[_test])
        score_f1 = f1_score(local_prediction, y_train[_test])
        scores.append(score_acc)
        cf = confusion_matrix(local_prediction, y_train[_test])
        print(f"폴드: {k + 1}, 클래스 분포: {np.bincount(y_train[_test])}, 정확도: {score_acc:.4f}, f1: {score_f1:.4f}")
        print(cf)
    print("CV 정확도: %.4f +/- %.4f" % (np.mean(scores), np.std(scores)))

    # scores = cross_validate(
    #     estimator=model,
    #     X=X_train,
    #     y=y_train,
    #     scoring=["accuracy", "f1"],
    #     cv=10,
    #     n_jobs=-1,
    #     return_train_score=False
    # )

    # scores = cross_val_score(
    #     estimator=model,
    #     X=X_train,
    #     y=y_train,
    #     cv=10,
    #     n_jobs=1
    # )
    # print("CV 정확도 점수: %s" % scores)


def create_pretrained_model(X_train, y_train, model, path: str):
    trained = train(X_train, y_train, model)
    joblib.dump(trained, path)


def load_pretrained_model(path):
    return joblib.load(path)
