import sys
from utils.dl.common.env import set_random_seed
set_random_seed(1)

from typing import List
from data.dataloader import build_dataloader
from data import Scenario, CLScenario
from methods.elasticdnn.api.online_model_v2 import ElasticDNN_OnlineModel

import torch
import sys
from torch import nn
from methods.elasticdnn.api.model import ElasticDNN_OfflineSegFMModel, ElasticDNN_OfflineSegMDModel
from methods.elasticdnn.api.algs.md_pretraining_wo_fbs import ElasticDNN_MDPretrainingWoFBSAlg
from methods.elasticdnn.model.base import ElasticDNNUtil
from methods.elasticdnn.pipeline.offline.fm_to_md.base import FM_to_MD_Util
from methods.elasticdnn.pipeline.offline.fm_to_md.vit import FM_to_MD_ViT_Util
from methods.elasticdnn.pipeline.offline.fm_lora.base import FMLoRA_Util
from methods.elasticdnn.pipeline.offline.fm_lora.vit import FMLoRA_ViT_Util
from methods.elasticdnn.model.vit import ElasticViTUtil
from utils.common.file import ensure_dir
from utils.dl.common.model import LayerActivation, get_module, get_parameter
from utils.common.exp import save_models_dict_for_init, get_res_save_dir
from data import build_scenario
from utils.dl.common.loss import CrossEntropyLossSoft
import torch.nn.functional as F
from utils.dl.common.env import create_tbwriter
import os
from utils.common.log import logger
from utils.common.data_record import write_json
# from methods.shot.shot import OnlineShotModel
from methods.feat_align.main import OnlineFeatAlignModel, FeatAlignAlg
import tqdm
from methods.feat_align.mmd import mmd_rbf
from methods.base.alg import BaseAlg
from methods.base.model import BaseModel
import shutil


def elasticfm_cl(apps_name: List[str],
                 scenarios: List[CLScenario], 
                 elasticfm_models: List[ElasticDNN_OnlineModel], 
                 da_algs: List[BaseAlg], 
                 da_alg_hyps: List[dict],
                 da_models: List[BaseModel],
                 device,
                 settings,
                 __entry_file__,
                 tag=None):
    
    assert len(apps_name) == 1
    
    involve_fm = settings['involve_fm']
    
    tasks_name = apps_name
    online_models = elasticfm_models
    
    log_dir = get_res_save_dir(__entry_file__, tag=tag)
    tb_writer = create_tbwriter(os.path.join(log_dir, 'tb_log'), False)
    res = []
    global_avg_after_acc = 0.
    global_iter = 0
    
    last_da_model_attrs = {}
    
    for domain_index, _ in enumerate(scenarios[0].target_tasks_order):
        avg_before_acc, avg_after_acc = 0., 0.
        cur_res = {}
        
        for task_name, online_model, scenario, da_alg, da_model, da_alg_hyp in zip(tasks_name, online_models, scenarios, da_algs, da_models, da_alg_hyps):

            cur_target_domain_name = scenario.target_tasks_order[scenario.cur_task_index]
            if cur_target_domain_name in da_alg_hyp:
                da_alg_hyp = da_alg_hyp[cur_target_domain_name]
                logger.info(f'use dataset-specific hyps')
                
            if domain_index == 0: # add last layer
                logger.info(f'add num of classes in the last layer')
                online_model.add_cls_in_head(scenario.num_tasks_to_be_learn)
            
            online_model.set_sd_sparsity(da_alg_hyp['sd_sparsity'])
            sd, unpruned_indexes_of_layers = online_model.generate_sd_by_target_samples(scenario.get_online_cur_task_samples_for_training(da_alg_hyp['train_batch_size']))

            tmp_sd_path = os.path.join(log_dir, 'tmp_sd_model.pt')
            torch.save({'main': sd}, tmp_sd_path)
            
            if 'cls' not in task_name and 'pos' not in task_name and 'vqa' not in task_name:
                da_model_args = [f'{task_name}/{domain_index}', 
                        tmp_sd_path, 
                        device,
                        scenario.num_classes]
            else:
                da_model_args = [f'{task_name}/{domain_index}', 
                        tmp_sd_path, 
                        device]
                
            da_model_ent = da_model(*da_model_args)
            for k, v in last_da_model_attrs.items():
                logger.info(f'set attr of last model: {k}')
                setattr(da_model_ent, k, v)
            
            da_metrics, after_da_model = da_alg(
                {'main': da_model_ent}, 
                os.path.join(log_dir, f'{task_name}/{domain_index}')
            ).run(scenario, {_k: _v for _k, _v in da_alg_hyp.items() if _k != 'sd_sparsity'})
            
            last_da_model_attrs = {k: getattr(after_da_model['main'], k) for k in dir(after_da_model['main']) if str(k).startswith('_el_data_')}
            logger.info(f'collect attrs in last model: {last_da_model_attrs.keys()}')
            
            os.remove(tmp_sd_path)
            if domain_index > 0:
                shutil.rmtree(os.path.join(log_dir, f'{task_name}/{domain_index}/backup_codes'))

            
            online_model.last_trained_cls_indexes = torch.LongTensor([i for i in range(scenario.get_cur_class_offset(), 
                                                                      scenario.get_cur_class_offset() + scenario.get_cur_num_class())])

            online_model.sd_feedback_to_md(after_da_model['main'].models_dict['main'], unpruned_indexes_of_layers)
            online_model.md_feedback_to_self_fm()
            
            accs = da_metrics['accs']
            before_acc = accs[0]['acc']
            after_acc = accs[-1]['acc']
            
            avg_before_acc += before_acc
            avg_after_acc += after_acc
            
            for _acc in accs:
                tb_writer.add_scalar(f'total_acc', _acc['acc'], _acc['iter'] + global_iter) # TODO: bug here
            global_iter += _acc['iter'] + 1
            
            tb_writer.add_scalars(f'accs/{task_name}', dict(before=before_acc, after=after_acc), domain_index)
            tb_writer.add_scalar(f'times/{task_name}', da_metrics['time'], domain_index)
            
            scenario.next_task()
            
            logger.info(f"task: {task_name}, domain {domain_index}, acc: {before_acc:.4f} -> "
                        f"{after_acc:.4f} ({da_metrics['time']:.2f}s)")
            cur_res[task_name] = da_metrics
        
        if involve_fm:
            for online_model in online_models:
                online_model.aggregate_fms_to_self_fm([m.models_dict['fm'] for m in online_models])
            for online_model in online_models:
                online_model.fm_feedback_to_md()
        
        avg_before_acc /= len(tasks_name)
        avg_after_acc /= len(tasks_name)
        tb_writer.add_scalars(f'accs/apps_avg', dict(before=avg_before_acc, after=avg_after_acc), domain_index)
        logger.info(f"--> domain {domain_index}, avg_acc: {avg_before_acc:.4f} -> "
                        f"{avg_after_acc:.4f}")
        res += [cur_res]
        
        global_avg_after_acc += avg_after_acc
        
        write_json(os.path.join(log_dir, 'res.json'), res, backup=False)

    global_avg_after_acc /= (domain_index + 1)
    logger.info(f'-----> final metric: {global_avg_after_acc:.4f}')
    write_json(os.path.join(log_dir, f'res_{global_avg_after_acc:.4f}.json'), res, backup=False)
    
    
    
def init_online_model(fm_models_dict_path, md_models_dict_path, task_name, __entry_file__):
    fm_models = torch.load(fm_models_dict_path)
    md_models = torch.load(md_models_dict_path)
    
    online_models_dict_path = save_models_dict_for_init({
        'fm': fm_models['main'],
        'md': md_models['main'],
        'sd': None,
        'indexes': md_models['indexes'],
        'bn_stats': md_models['bn_stats']
    }, __entry_file__, task_name)
    return online_models_dict_path