"""
MAL-Toolbox securiCAD Translator Module
"""

import zipfile
import json
import logging
import xml.etree.ElementTree as ET

from maltoolbox.model import model
from maltoolbox.language import specification

logger = logging.getLogger(__name__)

def load_model_from_scad_archive(scad_archive: str,
    lang_spec,
    lang_classes_factory) -> model.Model:
    """
    Reads a '.sCAD' archive generated by securiCAD representing an instance
    model and loads the information into a maltoobox.model.Model object.

    Arguments:
    scad_archive            - the path to a '.sCAD' archive
    lang_spec               - a dictionary containing the MAL language
                              specification
    lang_classes_factory    - a language classes factory that contains
                              the same classes defined by the
                              language specification

    Return:
    A maltoobox.model.Model object containing the instance model.
    """
    with zipfile.ZipFile(scad_archive, 'r') as archive:
        filelist = archive.namelist()
        model_file = next(filter(lambda x: ( x[-4:] == '.eom'), filelist))
        scad_model = archive.read(model_file)
        root = ET.fromstring(scad_model)

    instance_model = model.Model(scad_archive,
        lang_spec,
        lang_classes_factory)

    for child in root.iter('objects'):
        logger.debug(f'Loading asset from \"{scad_archive}\":\n' \
            + json.dumps(child.attrib, indent = 2))
        if child.attrib['metaConcept'] == 'Attacker':
            attacker_id = int(child.attrib['id'])
            attacker = model.Attacker()
            attacker.entry_points = []
            instance_model.add_attacker(attacker, attacker_id = attacker_id)
            continue

        if not hasattr(lang_classes_factory.ns,
            child.attrib['metaConcept']):
            logger.error(f'Failed to find {child.attrib["metaConcept"]} '
                'asset in language specification!')
            return None
        asset = getattr(lang_classes_factory.ns,
            child.attrib['metaConcept'])(name = child.attrib['name'])
        asset_id = int(child.attrib['id'])
        for subchild in child.iter('evidenceAttributes'):
            defense_name = subchild.attrib['metaConcept']
            defense_name = defense_name[0].lower() + defense_name[1:]
            for distrib in subchild.iter('evidenceDistribution'):
                distrib_type = distrib.attrib['type']
                for d in distrib.iter('parameters'):
                    if 'value' in d.attrib:
                        dist_value = d.attrib['value']
                        setattr(asset, defense_name, float(dist_value))
        instance_model.add_asset(asset, asset_id)

    for child in root.iter('associations'):
        logger.debug(f'Load association '
            f'(\"{child.attrib["sourceObject"]}\",'
            f'\"{child.attrib["targetObject"]}",'
            f'\"{child.attrib["targetProperty"]}\",'
            f'\"{child.attrib["sourceProperty"]}\") '
            f'from \"{scad_archive}\"')

        # Note: This is not a bug in the code. The fields and assets are
        # listed incorrectly in the securiCAD format where the source asset
        # matches the target field and vice versa.
        left_id = int(child.attrib['targetObject'])
        right_id = int(child.attrib['sourceObject'])
        attacker_id = None
        if child.attrib['sourceProperty'] == 'firstSteps':
            attacker_id = right_id
            target_id = left_id
            target_prop = child.attrib['targetProperty']
        elif child.attrib['targetProperty'] == 'firstSteps':
            attacker_id = left_id
            target_id = right_id
            target_prop = child.attrib['sourceProperty']

        if attacker_id:
            attacker = instance_model.get_attacker_by_id(attacker_id)
            if not attacker:
                logger.error(f'Failed to find attacker with id {attacker_id} '
                    'in model!')
                return None
            target_asset = instance_model.get_asset_by_id(target_id)
            if not target_asset:
                logger.error(f'Failed to find asset with id {target_id} '
                    'in model!')
                return None
            attacker.entry_points.append((target_asset,
                [target_prop.split('.')[0]]))
            continue

        left_asset = instance_model.get_asset_by_id(left_id)
        if not left_asset:
            logger.error(f'Failed to find asset with id {left_id} '
                'in model!')
            return None
        right_asset = instance_model.get_asset_by_id(right_id)
        if not right_asset:
            logger.error(f'Failed to find asset with id {right_id} '
                'in model!')
            return None

        # Note: This is not a bug in the code. The fields and assets are
        # listed incorrectly in the securiCAD format where the source asset
        # matches the target field and vice versa.
        left_field = child.attrib['sourceProperty']
        right_field = child.attrib['targetProperty']
        assoc_name = specification.get_association_by_fields_and_assets(
            lang_spec,
            left_field,
            right_field,
            left_asset.metaconcept,
            right_asset.metaconcept)
        logger.debug(f'Found \"{assoc_name}\" association.')

        if not assoc_name:
            logger.error(f'Failed to find '
                f'(\"{left_asset.metaconcept}\",'
                f'\"{right_asset.metaconcept}",'
                f'\"{left_field}\",'
                f'\"{right_field}\") '
                'association in language specification!')
            return None

        if not hasattr(lang_classes_factory.ns,
            assoc_name):
            logger.error(f'Failed to find {assoc_name} '
                'association in language specification!')
            return None

        assoc = getattr(lang_classes_factory.ns, assoc_name)()
        setattr(assoc, left_field, [left_asset])
        setattr(assoc, right_field, [right_asset])
        instance_model.add_association(assoc)

    return instance_model
