import base64
import csv
import json
import shutil
from configparser import ConfigParser
from multiprocessing import Pool
from pathlib import Path
from typing import Any, Dict

import oead
import yaml
from aamp import yaml_util as ayu
from aamp import ParameterIO, ParameterObject, ParameterList, Writer
from byml import yaml_util as byu
from bcml import util, install
from bcml.mergers.texts import read_msbt
from bcml.mergers.rstable import RstbMerger
from bcml.util import RulesParser


def convert_old_mods(source: Path = None):
    mod_dir = util.get_modpack_dir()
    old_path = source or util.get_cemu_dir() / "graphicPacks" / "BCML"
    print("Copying old mods...")
    shutil.rmtree(mod_dir, ignore_errors=True)
    shutil.copytree(old_path, mod_dir)
    print("Converting old mods...")
    for i, mod in enumerate(
        sorted({d for d in mod_dir.glob("*") if d.is_dir() and d.name != "9999_BCML"})
    ):
        print(f"Converting {mod.name[4:]}")
        try:
            convert_old_mod(mod, True)
        except Exception as err:
            shutil.rmtree(mod)
            install.refresh_merges()
            raise RuntimeError(
                f"BCML was unable to convert {mod.name[4:]}. Error: {str(err)}. Your old "
                f"mods have not been modified. {i} mod(s) were successfully imported."
            ) from err
    shutil.rmtree(old_path, ignore_errors=True)


def convert_old_mod(mod: Path, delete_old: bool = False):
    rules_to_info(mod / "rules.txt", delete_old=delete_old)
    if (mod / "logs").exists():
        convert_old_logs(mod)


def convert_old_settings():
    old_settings = ConfigParser()
    old_settings.read(str(util.get_data_dir() / "settings.ini"))
    cemu_dir = old_settings["Settings"]["cemu_dir"]
    mlc_dir = old_settings["Settings"]["mlc_dir"]
    game_dir = old_settings["Settings"]["game_dir"]
    update_dir = util.guess_update_dir(Path(mlc_dir), Path(game_dir))
    dlc_dir = util.guess_aoc_dir(Path(mlc_dir), Path(game_dir))
    settings = {
        "cemu_dir": cemu_dir,
        "game_dir": game_dir,
        "game_dir_nx": "",
        "load_reverse": old_settings["Settings"]["load_reverse"] == "True",
        "update_dir": str(update_dir or ""),
        "dlc_dir": str(dlc_dir or ""),
        "dlc_dir_nx": "",
        "store_dir": str(util.get_data_dir()),
        "site_meta": old_settings["Settings"]["site_meta"],
        "no_guess": old_settings["Settings"]["guess_merge"] == "False",
        "lang": old_settings["Settings"]["lang"],
        "no_cemu": False,
        "wiiu": True,
    }
    setattr(util.get_settings, "settings", settings)
    (util.get_data_dir() / "settings.ini").unlink()
    util.save_settings()


def parse_rules(rules_path: Path) -> Dict[str, Any]:
    rules = RulesParser()
    rules.read(str(rules_path))
    info = {
        "name": str(rules["Definition"]["name"]).strip("\"' "),
        "desc": str(rules["Definition"].get("description", "")).strip("\"' "),
        "url": str(rules["Definition"].get("url", "")).strip("\"' "),
        "image": str(rules["Definition"].get("image", "")).strip("\"' "),
        "version": "1.0.0",
        "depends": [],
        "options": {},
        "platform": "wiiu",
        "priority": 100,
    }
    id_string = f"{info['name']}=={info['version']}"
    info["id"] = base64.urlsafe_b64encode(id_string.encode("utf8")).decode("utf8")
    try:
        info["priority"] = int(rules["Definition"]["fsPriority"])
    except KeyError:
        info["priority"] = int(getattr(rules["Definition"], "fspriority", 100))
    return info


def rules_to_info(rules_path: Path, delete_old: bool = False):
    print("Converting meta file...")
    info = parse_rules(rules_path)
    (rules_path.parent / "info.json").write_text(
        json.dumps(info, ensure_ascii=False, indent=2)
    )
    if delete_old:
        rules_path.unlink()


def convert_old_logs(mod_dir: Path):
    print("Upgrading old logs...")
    if (mod_dir / "logs" / "packs.log").exists():
        print("Upgrading pack log...")
        _convert_pack_log(mod_dir)
    if (mod_dir / "logs" / "rstb.log").exists():
        print("Upgrading RSTB log...")
        _convert_rstb_log(mod_dir)
    if (mod_dir / "logs").glob("*texts*"):
        print("Upgrading text logs...")
        _convert_text_logs(mod_dir / "logs")
    for log in {l for l in mod_dir.glob("logs/*.yml") if not "texts" in l.stem}:
        if log.name == "deepmerge.yml":
            print("Upgrading deep merge log...")
            _convert_aamp_log(log)
        elif log.name == "gamedata.yml":
            print("Upgrading game data log...")
            _convert_gamedata_log(log)
        elif log.name == "savedata.yml":
            print("Upgrading save data log...")
            _convert_savedata_log(log)
        elif log.name == "map.yml":
            print("Upgrading map log...")
            _convert_map_log(log)
        else:
            pass


def _convert_rstb_log(mod: Path):
    (mod / "logs" / "rstb.log").unlink()
    with Pool(maxtasksperchild=500) as pool:
        files = install.find_modded_files(mod, pool=pool)
        merger = RstbMerger()
        merger.set_pool(pool)
        merger.log_diff(mod, files)


def _convert_pack_log(mod: Path):
    packs = {}
    with (mod / "logs" / "packs.log").open("r") as rlog:
        csv_loop = csv.reader(rlog)
        for row in csv_loop:
            if "logs" in str(row[1]) or str(row[0]) == "name":
                continue
            packs[str(row[0])] = Path(str(row[1])).as_posix().replace("\\", "/")
    (mod / "logs" / "packs.log").unlink()
    (mod / "logs" / "packs.json").write_text(
        json.dumps(packs, ensure_ascii=False, indent=2), encoding="utf-8"
    )


def _convert_aamp_log(log: Path):
    loader = yaml.CLoader
    ayu.register_constructors(loader)
    doc = yaml.load(log.read_text("utf-8"), Loader=loader)
    pio = ParameterIO("log", 0)
    root = ParameterList()
    for file, plist in doc.items():
        if not plist.lists:
            continue
        root.set_list(file, plist.list("param_root"))
    file_table = ParameterObject()
    for i, file in enumerate(doc):
        if not doc[file].lists:
            continue
        file_table.set_param(f"File{i}", file)
    root.set_object("FileTable", file_table)
    pio.set_list("param_root", root)
    log.unlink()
    log.with_suffix(".aamp").write_bytes(Writer(pio).get_bytes())


def _convert_text_log(log: Path) -> dict:
    lang = log.stem[6:]
    data = yaml.safe_load(log.read_text("utf-8"))
    log.unlink()
    return {lang: {file: data[file]["entries"] for file in data}}


def _convert_text_logs(logs_path: Path):
    diffs = {}
    with Pool(maxtasksperchild=500) as pool:
        for diff in pool.imap_unordered(
            _convert_text_log, logs_path.glob("texts_*.yml")
        ):
            diffs.update(diff)
    fails = set()
    for text_pack in logs_path.glob("newtexts_*.sarc"):
        lang = text_pack.stem[9:]
        sarc = oead.Sarc(text_pack.read_bytes())
        for file in sarc.get_files():
            if lang not in diffs:
                diffs[lang] = {}
            try:
                diffs[lang].update({file.name: read_msbt(bytes(file.data))["entries"]})
            except RuntimeError:
                print(
                    f"Warning: {file.name} could not be processed and will not be used"
                )
                fails.add(file.name)
                continue
        util.vprint(f"{len(fails)} text files failed to process:\n{fails}")
        text_pack.unlink()
    (logs_path / "texts.json").write_text(
        json.dumps(diffs, ensure_ascii=False, indent=2), encoding="utf-8"
    )


def _convert_gamedata_log(log: Path):
    diff = oead.byml.from_text(log.read_text("utf-8"))
    log.write_text(
        oead.byml.to_text(
            oead.byml.Hash(
                {
                    data_type: {"add": data, "del": oead.byml.Array()}
                    for data_type, data in diff.items()
                }
            )
        ),
        encoding="utf-8",
    )


def _convert_savedata_log(log: Path):
    diff = oead.byml.from_text(log.read_text("utf-8"))
    log.write_text(
        oead.byml.to_text(oead.byml.Hash({"add": diff, "del": oead.byml.Array()})),
        encoding="utf-8",
    )


def _convert_map_log(log: Path):
    loader = yaml.CLoader
    byu.add_constructors(loader)
    diff = yaml.load(log.read_text("utf-8"), Loader=loader)
    new_diff = {}
    for unit, changes in diff.items():
        new_changes = {
            "add": changes["add"],
            "del": changes["del"],
            "mod": {str(hashid): actor for hashid, actor in changes["mod"].items()},
        }
        new_diff[unit] = new_changes
    dumper = yaml.CDumper
    byu.add_representers(dumper)
    log.write_text(
        yaml.dump(new_diff, Dumper=dumper, allow_unicode=True), encoding="utf-8"
    )
