"""Provides functionality for exporting alfred data to .csv via the
command line.

Easiest use case is to call it from within your experiment directory.
It will extract the necessary information about filepaths from your
config.conf and secrets.conf files.::

    python -m alfred3.export

Optionally, specify the source manually by giving the name of a
config.conf or a secrets.conf section. The function will then export data
that was saved with the saving agent defined in that section::

    python -m alfred3.export --src="mongo_saving_agent"

You can also use it to convert .json files from the current working
directory to .csv, saving the resulting file in the current working
directory aswell::

    python -m alfred3.export -h

For more detailed usage information, see::

    python -m alfred3.export --help

.. moduleauthor:: Johannes Brachem <jbrachem@posteo.de>
"""

import csv
import os
import random
from collections.abc import Iterator
from itertools import chain
from pathlib import Path

from .config import ExperimentConfig
from .data_manager import DataManager, decrypt_recursively


class Exporter:
    """
    Handles data export from json to csv.

    The exporter obeys the follwoing rules for all export functions:

    1. Check if a file of the designated name exists in the designated
       directory.
    2. If yes, append the data from the current session to this file.
    3. If not, scan all available .json files and produce a new,
       complete csv file.
    """

    def __init__(self, experiment):
        self.experiment = experiment
        self.exp = experiment
        self.csv_dir = self.exp.subpath(self.exp.config.get("data", "csv_directory"))
        self.delimiter = self.exp.config.get("data", "csv_delimiter")
        self.save_dir = self.exp.subpath(
            self.exp.config.get("local_saving_agent", "path")
        )

    def export(self, data_type: str):
        """
        Calls the appropriate specialized export function, depending
        on data type.

        Args:
            data_type (str): One of 'exp_data', 'unlinked', 'codebook',
                'move_history'.

        """
        self.csv_dir.mkdir(parents=True, exist_ok=True)
        if data_type == DataManager.CODEBOOK_DATA:
            self.export_codebook()
        elif data_type == DataManager.HISTORY:
            self.export_move_history()
        elif data_type == DataManager.UNLINKED_DATA:
            self.export_unlinked()
        elif data_type == DataManager.EXP_DATA:
            self.export_exp_data()

    def _load(self, path: str | Path) -> list:
        """
        Returns a list of dictonaries with session data, read from an
        existing csv file.

        This version of 'load' uses the experiment-specific delimiter
        defined in config.conf.
        """
        return self.load(path, self.delimiter)

    @staticmethod
    def load(path: str | Path, delimiter: str) -> list:
        """
        Returns a list of dictonaries with session data, read from an
        existing csv file.

        This version of 'load' is available as a public static method.
        """
        with open(path, encoding="utf-8", newline="") as csvfile:
            reader = csv.DictReader(csvfile, delimiter=delimiter)
            existing_data = [dict(row) for row in reader]
        return existing_data

    def _write(self, data: Iterator[dict], fieldnames: list[str], path: Path):
        """
        Writes a list of session data dictionaries to a csv file.

        This version of 'write' uses the experiment-specific delimiter
        defined in config.conf.
        """
        self.write(data, fieldnames, path, self.delimiter)

    @staticmethod
    def write(data: Iterator[dict], fieldnames: list[str], path: Path, delimiter: str):
        """
        Writes a list of session data dictionaries to a csv file.

        This version of 'write' is available as a public static method.
        """
        with open(path, "w", encoding="utf-8", newline="") as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter=delimiter)
            writer.writeheader()
            writer.writerows(data)

    def export_exp_data(self):
        csv_name = "exp_data.csv"
        path = self.csv_dir / csv_name

        if path.exists() and path.read_text(encoding="utf-8"):
            alldata = self._load(path)
            sessiondata = self.exp.data_manager.flat_session_data
            alldata.append(sessiondata)

            # get fieldnames from list of flat datasets
            metadata = list(self.exp.data_manager.metadata.keys())
            client_info = list(self.exp.data_manager.client_data.keys())
            element_names = []
            for row in alldata:
                for colname in row:
                    if colname not in metadata + client_info:
                        if colname not in element_names:
                            element_names.append(colname)
            fieldnames = metadata + client_info + sorted(element_names)
        else:
            data = list(
                DataManager.iterate_local_data(
                    data_type=DataManager.EXP_DATA, directory=self.save_dir
                )
            )
            fieldnames = DataManager.extract_ordered_fieldnames(data)
            alldata = [DataManager.flatten(d) for d in data]
        self._write(alldata, fieldnames, path)
        self.exp.log.info(
            f"Exported main experiment data to {path.parent.name}/{path.name}."
        )

    def export_move_history(self):
        csv_name = "move_history.csv"
        data = self.exp.data_manager.move_history
        fieldnames = DataManager.extract_fieldnames(data)
        path = self.csv_dir / csv_name

        if path.exists() and path.read_text(encoding="utf-8"):
            history = self._load(path)
            history += data
            fieldnames = DataManager.extract_fieldnames(history)
        else:
            existing_data = DataManager.iterate_local_data(
                data_type=DataManager.EXP_DATA, directory=self.save_dir
            )
            history = [d["exp_move_history"] for d in existing_data]
            fieldnames = DataManager.extract_fieldnames(chain(*history))
            history = chain(*history)

        self._write(history, fieldnames, path)
        self.exp.log.info(
            f"Exported movement history to {path.parent.name}/{path.name}."
        )

    def export_unlinked(self):
        csv_name = "unlinked.csv"
        agent = self.exp.data_saver.unlinked.agents["local_unlinked"]
        data = self.exp.data_manager.unlinked_data_with(agent)
        data = self.exp.data_manager.flatten(data)
        fieldnames = list(data.keys())
        data = [data]

        path = self.csv_dir / csv_name

        if path.exists() and path.read_text(encoding="utf-8"):
            ul_data = self._load(path)
            ul_data += data
            random.shuffle(ul_data)
            fieldnames = DataManager.extract_fieldnames(ul_data)
            data = ul_data
        else:
            unlinked_dir = self.exp.config.get("local_saving_agent_unlinked", "path")
            existing_data = list(
                DataManager.iterate_local_data(
                    data_type=DataManager.UNLINKED_DATA, directory=unlinked_dir
                )
            )
            data = [DataManager.flatten(d) for d in existing_data]
            fieldnames = DataManager.extract_fieldnames(data)

        if self.exp.config.getboolean(
            "local_saving_agent_unlinked", "decrypt_csv_export"
        ):
            if self.exp.secrets.get("encryption", "key"):
                key = self.exp.secrets.get("encryption", "key").encode()
                data = decrypt_recursively(data, key=key)

        self._write(data, fieldnames, path)
        self.exp.log.info(f"Exported unlinked data to {path.parent.name}/{path.name}.")

    def export_codebook(self):
        data = self.exp.data_manager.codebook_data

        version = self.exp.config.get("metadata", "version")
        csv_name = f"{DataManager.CODEBOOK_DATA}_{version}.csv"

        path = self.csv_dir / csv_name

        if path.exists() and path.read_text(encoding="utf-8"):
            with open(path, encoding="utf-8") as csvfile:
                reader = csv.DictReader(csvfile, delimiter=self.delimiter)
                existing_codebook = {dict(row)["name"]: dict(row) for row in reader}

            for name, cb in existing_codebook.items():
                for lab in [
                    "label_top",
                    "label_left",
                    "label_right",
                    "label_bottom",
                    "placeholder",
                ]:
                    oldlab = cb.get(lab, "")
                    new = data.get(name, "")
                    newlab = new.get(lab, "") if new else ""
                    if not new or (not oldlab == newlab):
                        self.exp.log.warning(
                            f"{lab} of '{name}' has changed from '{oldlab}' to"
                            f" '{newlab}'. This introduces inconsistencies into the"
                            " codebook. Do you have dynamic labels that do not match"
                            " their elements' names? To change a label, increase the"
                            " experiment version."
                        )

            existing_codebook.update(data)
            data = existing_codebook

        fieldnames = DataManager.extract_fieldnames(data.values())
        fieldnames = DataManager.sort_codebook_fieldnames(fieldnames)
        self._write(data.values(), fieldnames, path)
        self.exp.log.info(f"Exported codebook to {path.parent.name}/{path.name}.")


def find_unique_name(directory, filename, exp_version=None, index: int = 1):
    filename = Path(filename)
    name = filename.stem
    ext = filename.suffix
    exp_version = "_" + exp_version if exp_version else ""

    normal_name = name + exp_version + ext
    idx_name = name + exp_version + f"_{index}" + ext

    if normal_name not in os.listdir(directory):
        return normal_name
    elif idx_name not in os.listdir(directory):
        return idx_name
    else:
        i = index + 1
        return find_unique_name(directory=directory, filename=filename, index=i)


def find_data_directory(expdir, saving_agent):
    config = ExperimentConfig(expdir=expdir)
    path = Path(config.get(saving_agent, "path")).resolve()
    if not path.is_absolute():
        path = expdir.resolve() / path
    return path


def find_csv_dir(expdir):
    config = ExperimentConfig(expdir=expdir)
    path = Path(config.get("data", "csv_directory")).resolve()
    if not path.is_absolute():
        path = expdir.resolve() / path
    return path


def find_csv_name(expdir, data_type):
    directory = find_csv_dir(expdir)
    filename = data_type + ".csv"
    return find_unique_name(directory, filename)
