import sys
import argparse
from concurrent.futures import ProcessPoolExecutor
from functools import partial
import logging

import lmdb
import dotenv
from tqdm import tqdm

dotenv.load_dotenv()

from vsm_datascience_common.cycle_database_connection.cycle_data import get_cycle_by_id
from vsm_datascience_common.cycle_database_connection.db_utils import get_cycles_collection

from utils.dataset_creation import get_features, train_scalers, save_scalers, scale_item
from utils.lmdb_utils import save_to_lmdb, load_from_lmdb
from utils.utils import get_variable_from_module

MAX_LMDB_SIZE_IN_MB = 200_000

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(sys.stdout))


def process_id_wrapper(cycle_id: str, feature_config: dict, env: lmdb.Environment):
    try:
        cycle = get_cycle_by_id(cycle_id)
        features = get_features(cycle, feature_config)
        save_to_lmdb(env, key=str(cycle_id), dataset=features)
    except:
        pass


def scaling_wrapper(key_batch: str,
                    scalers: dict,
                    max_lmdb_size_in_mb: int,
                    lmdb_env_dir: str):
    env = lmdb.open(lmdb_env_dir, readonly=True, lock=False)
    scaled_items = list()
    for key in key_batch:
        item = load_from_lmdb(env, key)
        scaled_item = scale_item(item, scalers)
        scaled_items.append(scaled_item)
    env.close()
    env = lmdb.open(lmdb_env_dir, map_size=max_lmdb_size_in_mb * 1024 * 1024)
    for key, scaled_item in zip(key_batch, scaled_items):
        save_to_lmdb(env, key=key, dataset=scaled_item)
    env.close()


def create_dataset(model_configuration: dict,
                   lmdb_root_dir: str,
                   max_lmdb_size_in_mb: int,
                   max_workers: int) -> None:
    feature_config = model_configuration["feature_config"]

    logger.info(f"Creating dataset for feature set {feature_config['feature_set_name']}")

    # fetch valid cycle ids from database
    valid_cycle_ids = [x["_id"] for x in get_cycles_collection().aggregate(
        feature_config["filter_criteria_pipeline"] + [
            {
                "$project": {
                    "_id": 1
                }
            }
        ]
    )]

    logger.info(f"Fetched {len(valid_cycle_ids)} valid cycle ids from database")

    env = lmdb.open(f"{lmdb_root_dir}/{feature_config['feature_set_name']}", map_size=max_lmdb_size_in_mb * 1024 * 1024)

    # create features for items
    logger.info(f"Creating features for {len(valid_cycle_ids)} cycles")
    with ProcessPoolExecutor(max_workers=max_workers) as executor:
        list(tqdm(executor.map(partial(process_id_wrapper, feature_config=feature_config, env=env), valid_cycle_ids),
                  total=len(valid_cycle_ids)))

    # convert ids (here bson objectids) to keys for use in lmdb
    keys = [str(cycle_id) for cycle_id in valid_cycle_ids]

    # train scalers for featues
    logger.info("Training scalers for features")
    all_scalers = dict()
    sample = load_from_lmdb(env, keys[0])
    for feature_set in tqdm(feature_config["feature_sets"]):
        for feature in feature_config[feature_set]:
            print(f"Training scalers for feature {feature['name']} in feature set {feature_set}")
            scaler = train_scalers(feature_set, feature["name"], feature["scaler"], sample, env)
            if feature_set not in all_scalers:
                all_scalers[feature_set] = dict()
            all_scalers[feature_set] = all_scalers[feature_set] | scaler

    # save scalers
    logger.info("Saving scalers to disk")
    scaler_dir = f"{lmdb_root_dir}/{feature_config['feature_set_name']}/scalers"
    save_scalers(all_scalers, scaler_dir)

    # creat scaling batches for less burden on lmdb
    batch_size = 1_000
    key_batches = [keys[i:i + batch_size] for i in range(0, len(keys), batch_size)]

    # scale items
    logger.info("Scaling items")
    with ProcessPoolExecutor(max_workers=max_workers) as executor:
        list(tqdm(executor.map(partial(scaling_wrapper, scalers=all_scalers,
                                       max_lmdb_size_in_mb=max_lmdb_size_in_mb,
                                       lmdb_env_dir=f"{lmdb_root_dir}/{feature_config['feature_set_name']}"),
                               key_batches),
                  total=len(key_batches)))

    logger.info(f"Dataset creation finished. LMDB saved in {lmdb_root_dir}/{feature_config['feature_set_name']}")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Dataset Creation Wrapper")
    parser.add_argument("--model_config_module",
                        type=str,
                        required=True,
                        help="Path to the model config module")
    parser.add_argument("--model_config_variable",
                        type=str,
                        required=False,
                        default="model_configuration",
                        help="Name of the model config variable, default is 'model_configuration'")
    parser.add_argument("--lmdb_dir",
                        type=str,
                        required=False,
                        default="./lmdb_datasets",
                        help="Path to the lmdb directory")
    parser.add_argument("--lmdb_size",
                        type=int,
                        required=False,
                        default=MAX_LMDB_SIZE_IN_MB,
                        help="Size of the lmdb in MB, default is 200_000")
    parser.add_argument("--max_workers",
                        type=int,
                        required=False,
                        default=None,
                        help="Number of workers for multiprocessing, default is None (use all available cores)")

    args = parser.parse_args()

    # import and load the model config
    model_configuration = get_variable_from_module(
        module_path=args.model_config_module,
        variable_name=args.model_config_variable
    )

    create_dataset(model_configuration,
                   lmdb_root_dir=args.lmdb_dir,
                   max_lmdb_size_in_mb=args.lmdb_size,
                   max_workers=args.max_workers)
