"""
Module holding the Converter class, which converts from XBRL-XML to XBRL-CSV
taking as input the taxonomy object and the instance object
"""

import copy
import csv
import json
from pathlib import Path
from tempfile import TemporaryDirectory
from zipfile import ZipFile

import pandas as pd

from xbridge.modules import Module
from xbridge.xml_instance import Instance

INDEX_FILE = Path(__file__).parent / "modules" / "index.json"
MAPPING_FILE = Path(__file__).parent / "modules" / "dim_dom_mapping.json"

if not INDEX_FILE.exists():
    raise ValueError(
        "Cannot find the index file for the modules. "
        "Please make sure that the index file and the "
        "JSON files with the mappings exist in the modules folder."
    )

with open(INDEX_FILE, "r", encoding="utf-8") as fl:
    index = json.load(fl)


class Converter:
    """
    Converter different types of files into others, using the EBA :obj:`taxonomy <xbridge.taxonomy.Taxonomy>` and XBRL-instance. Each file is extracted and saved in a temporary directory.
    Then, these files are converted into JSON and again saved in a compressed folder such as ZIP or 7z.

    :obj:`Variables <xbridge.taxonomy.Variable>` are generated by combining the open keys column with the attributes found within the taxonomy :obj:`modules <xbridge.taxonomy.Module>`. Then, variable's dimension is extracted
    from it with the purpose to be used with the context coming from the ``XML_instance``, to create the :obj:`scenario <xbridge.xml_instance.Scenario>`.

    Finally, an inner join is done between the variables created and the values from the facts of the ``XML_instance`` :obj:`context <xbridge.xml_instance.Context>`.

    """

    def __init__(self, instance_path: str | Path) -> None:
        self.instance = Instance(instance_path)
        module_ref = self.instance.module_ref

        if module_ref not in index:
            raise ValueError(f"Module {module_ref} not found in the taxonomy index")

        module_path = Path(__file__).parent / "modules" / index[module_ref]
        self.module = Module.from_serialized(module_path)
        self._reported_tables = []

    def convert(self, output_path: str | Path) -> Path:
        """
        Convert the ``XML Instance`` to a CSV file
        """
        if not output_path:
            raise ValueError("Output path not provided")

        if isinstance(output_path, str):
            output_path = Path(output_path)
        if not self.instance:
            raise ValueError("Instance not provided")

        if self.module is None:
            raise ValueError("Module of the instance file not found in the taxonomy")

        temp_dir = TemporaryDirectory()
        temp_dir_path = Path(temp_dir.name)

        meta_inf_dir = temp_dir_path / "META-INF"
        report_dir = temp_dir_path / "reports"

        meta_inf_dir.mkdir()
        report_dir.mkdir()

        with open(meta_inf_dir / "reports.json", "w", encoding="UTF-8") as fl:
            json.dump(
                {
                    "documentInfo": {
                        "documentType": "http://xbrl.org/PWD/2020-12-09/report-package"
                    }
                },
                fl,
            )

        with open(report_dir / "report.json", "w", encoding="UTF-8") as fl:
            json.dump(
                {
                    "documentInfo": {
                        "documentType": "https://xbrl.org/CR/2021-02-03/xbrl-csv",
                        "extends": [self.module.url],
                    }
                },
                fl,
            )

        self._convert_filing_indicator(report_dir)
        with open(MAPPING_FILE, "r", encoding="utf-8") as fl:
            mapping_dict = json.load(fl)
        self._convert_tables(report_dir, mapping_dict)
        self._convert_parameters(report_dir)

        instance_path = self.instance.path

        if not isinstance(instance_path, str):
            instance_path = str(instance_path)

        instance_name = Path(instance_path).name

        if ".xbrl" in instance_name:
            file_name = instance_name.replace(".xbrl", ".zip")
        elif ".xml" in instance_name:
            file_name = instance_name.replace(".xml", ".zip")
        else:
            file_name = instance_name + ".zip"

        zip_file_path = output_path / file_name

        with ZipFile(zip_file_path, "w") as zip_fl:
            for file in meta_inf_dir.iterdir():
                zip_fl.write(file, arcname=f"META-INF/{file.name}")
            for file in report_dir.iterdir():
                zip_fl.write(file, arcname=f"reports/{file.name}")

        temp_dir.cleanup()

        return zip_file_path

    def _variable_generator(self, table):
        """Returns the dataframe with the CSV file for the table

        :param table: The table we use.

        """

        variable_columns = set(table.variable_columns)
        open_keys = set(table.open_keys)
        attributes = set(table.attributes)
        instance_columns = set(self.instance.instance_df.columns)

        # If any open key is not in the instance, then the table cannot have
        # any datapoint
        if not open_keys.issubset(instance_columns):
            return pd.DataFrame(columns=["datapoint", "value"] + list(open_keys))

        # Determine the not relevant dims
        not_relevant_dims = instance_columns - variable_columns - open_keys - attributes
        not_relevant_dims = not_relevant_dims - {"value", "unit", "decimals"}

        instance_df = copy.copy(self.instance.instance_df)
        for col in ["unit", "decimals"]:
            if col not in attributes and col in instance_df.columns:
                del instance_df[col]

        # Drop datapoints that have non-null values in not relevant dimensions
        # And drop the not relevant columns
        instance_df = instance_df[
            instance_df[list(not_relevant_dims)].isnull().all(axis=1)
        ]
        for dim in not_relevant_dims:
            del instance_df[dim]

        # Do the intersection and drop from datapoints the columns and records
        intersect_cols = variable_columns & instance_columns
        datapoint_df = table.variable_df
        for col in variable_columns - instance_columns:
            datapoint_df = datapoint_df[datapoint_df[col].isnull()]
            del datapoint_df[col]

        # Join the dataframes on the datapoint_columns
        table_df = pd.merge(
            datapoint_df, instance_df, on=list(intersect_cols), how="inner"
        )

        if len(table_df) == 0:
            return pd.DataFrame(columns=["datapoint", "value"])

        for col in intersect_cols:
            del table_df[col]
        open_keys_copy = copy.copy(open_keys)
        for open_key_name in open_keys_copy:
            if open_key_name not in table_df.columns:
                open_keys.remove(open_key_name)
        table_df.dropna(subset=list(open_keys), inplace=True)

        return table_df

    def _convert_tables(self, temp_dir_path, mapping_dict):
        for table in self.module.tables:
            ##Workaround:
            # To calculate the table code for abstract tables, we look whether the name
            # ends with a letter, and if so, we remove the last part of the code
            # Possible alternative: add metadata mapping abstract and concrete tables to
            # avoid doing this kind of corrections
            # Defining the output path and check if the table is reported
            normalised_table_code = table.code.replace("-", ".")
            if normalised_table_code[-1].isalpha():
                normalised_table_code = normalised_table_code.rsplit(".", maxsplit=1)[0]
            if normalised_table_code not in self._reported_tables:
                continue

            datapoints = self._variable_generator(table)
            # Cleaning up the dataframe and sorting it
            datapoints = datapoints.rename(columns={"value": "factValue"})
            #Workaround
            #The enumerated key dimensions need to have a prefix like the one
            #Defined by the EBA in the JSON files. We take them from the taxonomy
            #Because EBA is using exactly those for the JSON files.
            for open_key in table.open_keys:
                dim_name = mapping_dict.get(open_key)
                #For open keys, there are no dim_names (they are not mapped)
                if dim_name and not datapoints.empty:
                    datapoints[open_key] = dim_name + ":" + datapoints[open_key].astype(str)
            datapoints = datapoints.sort_values(by=["datapoint"], ascending=True)
            output_path_table = temp_dir_path / table.url
            datapoints.to_csv(output_path_table, index=False)

    def _convert_filing_indicator(self, temp_dir_path):
        # Workaround;
        # Developed for the EBA structure
        output_path_fi = temp_dir_path / "FilingIndicators.csv"
        filing_indicators = self.instance.filing_indicators

        with open(output_path_fi, "w", newline="", encoding="utf-8") as fl:
            csv_writer = csv.writer(fl)
            csv_writer.writerow(["templateID", "reported"])
            for fil_ind in filing_indicators:
                value = "true" if fil_ind.value else "false"
                csv_writer.writerow([fil_ind.table, value])
                if fil_ind.value:
                    self._reported_tables.append(fil_ind.table)

    def _convert_parameters(self, temp_dir_path):
        # Workaround;
        # Developed for the EBA structure
        output_path_parameters = temp_dir_path / "parameters.csv"
        parameters = {
            "entityID": self.instance.entity,
            "refPeriod": self.instance.period,
            "baseCurrency": self.instance.base_currency,
            "decimalsInteger": 0,
            "decimalsMonetary": (
                self.instance.decimals_monetary
                if self.instance.decimals_monetary
                else 0
            ),
            "decimalsPercentage": (
                self.instance.decimals_percentage
                if self.instance.decimals_percentage
                else 4
            ),
        }
        with open(output_path_parameters, "w", newline="", encoding="utf-8") as fl:
            csv_writer = csv.writer(fl)
            csv_writer.writerow(["name", "value"])
            for k, v in parameters.items():
                csv_writer.writerow([k, v])
