from __future__ import absolute_import

import ast
import base64
import logging
import os
from glob import iglob
from lib2to3.pgen2.parse import ParseError

from configobj import ConfigObj

import libtorrent as lt

from six import PY3
from six.moves.configparser import DuplicateSectionError, MissingSectionHeaderError, NoSectionError, RawConfigParser

from Tribler.Core.Config.tribler_config import TriblerConfig
from Tribler.Core.Utilities.configparser import CallbackConfigParser
from Tribler.Core.Utilities.unicode import recursive_ungarble_metainfo
from Tribler.Core.exceptions import InvalidConfigException
from Tribler.Core.simpledefs import STATEDIR_CHECKPOINT_DIR

logger = logging.getLogger(__name__)


def convert_config_to_tribler71(current_config, state_dir=None):
    """
    Convert the Config files libtribler.conf and tribler.conf to the newer triblerd.conf and cleanup the files
    when we are done.

    :param: current_config: the current config in which we merge the old config files.
    :return: the newly edited TriblerConfig object with the old data inserted.
    """
    state_dir = state_dir or TriblerConfig.get_default_root_state_dir()
    libtribler_file_loc = os.path.join(state_dir, "libtribler.conf")
    if os.path.exists(libtribler_file_loc):
        libtribler_cfg = RawConfigParser()
        libtribler_cfg.read(libtribler_file_loc)
        current_config = add_libtribler_config(current_config, libtribler_cfg)
        os.remove(libtribler_file_loc)

    tribler_file_loc = os.path.join(state_dir, "tribler.conf")
    if os.path.exists(tribler_file_loc):
        tribler_cfg = RawConfigParser()
        tribler_cfg.read(tribler_file_loc)
        current_config = add_tribler_config(current_config, tribler_cfg)
        os.remove(tribler_file_loc)

    # We also have to update all existing downloads, in particular, rename the section 'downloadconfig' to
    # 'download_defaults'.
    for filename in iglob(os.path.join(state_dir, STATEDIR_CHECKPOINT_DIR, '*.state')):
        download_cfg = RawConfigParser()
        try:
            with open(filename) as cfg_file:
                download_cfg.readfp(cfg_file, filename=filename)
        except MissingSectionHeaderError:
            logger.error("Removing download state file %s since it appears to be corrupt", filename)
            os.remove(filename)

        try:
            download_items = download_cfg.items("downloadconfig")
            download_cfg.add_section("download_defaults")
            for download_item in download_items:
                download_cfg.set("download_defaults", download_item[0], download_item[1])
            download_cfg.remove_section("downloadconfig")
            with open(filename, "w") as output_config_file:
                download_cfg.write(output_config_file)
        except (NoSectionError, DuplicateSectionError):
            # This item has already been converted
            pass

    return current_config


def add_tribler_config(new_config, old_config):
    """
    Add the old values of the tribler.conf file to the newer Config file.

    :param new_config: The Config file to which the old data can be written
    :param old_config: A RawConfigParser containing the old tribler.conf Config file
    :return: the edited Config file
    """
    config = new_config.copy()
    for section in old_config.sections():
        for (name, string_value) in old_config.items(section):
            if string_value == "None":
                continue

            # Attempt to interpret string_value as a string, number, tuple, list, dict, boolean or None
            try:
                value = ast.literal_eval(string_value)
            except (ValueError, SyntaxError):
                value = string_value

            temp_config = config.copy()
            if section == "Tribler" and name == "default_anonymity_enabled":
                temp_config.set_default_anonymity_enabled(value)
            if section == "Tribler" and name == "default_number_hops":
                temp_config.set_default_number_hops(value)
            if section == "downloadconfig" and name == "saveas":
                temp_config.config["download_defaults"]["saveas"] = value
            if section == "downloadconfig" and name == "seeding_mode":
                temp_config.config["download_defaults"]["seeding_mode"] = value
            if section == "downloadconfig" and name == "seeding_ratio":
                temp_config.config["download_defaults"]["seeding_ratio"] = value
            if section == "downloadconfig" and name == "seeding_time":
                temp_config.config["download_defaults"]["seeding_time"] = value
            if section == "downloadconfig" and name == "version":
                temp_config.config["download_defaults"]["version"] = value

            try:
                temp_config.validate()
                config = temp_config
            except InvalidConfigException as exc:
                logger.debug("The following field in the old tribler.conf was wrong: %s", exc.args)
    return config


def add_libtribler_config(new_config, old_config):
    """
    Add the old values of the libtribler.conf file to the newer Config file.

    :param new_config: the Config file to which the old data can be written
    :param old_config: a RawConfigParser containing the old libtribler.conf Config file
    :return: the edited Config file
    """
    config = new_config.copy()
    for section in old_config.sections():
        for (name, string_value) in old_config.items(section):
            if string_value == "None":
                continue

            # Attempt to interpret string_value as a string, number, tuple, list, dict, boolean or None
            try:
                value = ast.literal_eval(string_value)
            except (ValueError, SyntaxError):
                value = string_value

            temp_config = config.copy()
            if section == "general" and name == "state_dir":
                temp_config.set_root_state_dir(value)
            elif section == "general" and name == "log_dir":
                temp_config.set_log_dir(value)
            elif section == "tunnel_community" and name == "enabled":
                temp_config.set_tunnel_community_enabled(value)
            elif section == "tunnel_community" and name == "socks5_listen_ports":
                if isinstance(value, list):
                    temp_config.set_tunnel_community_socks5_listen_ports(value)
            elif section == "tunnel_community" and name == "exitnode_enabled":
                temp_config.set_tunnel_community_exitnode_enabled(value)
            elif section == "general" and name == "ec_keypair_filename_multichain":
                temp_config.set_trustchain_keypair_filename(value)
            elif section == "torrent_checking" and name == "enabled":
                temp_config.set_torrent_checking_enabled(value)
            elif section == "libtorrent" and name == "lt_proxytype":
                temp_config.config["libtorrent"]["proxy_type"] = value
            elif section == "libtorrent" and name == "lt_proxyserver":
                temp_config.config["libtorrent"]["proxy_server"] = value
            elif section == "libtorrent" and name == "lt_proxyauth":
                temp_config.config["libtorrent"]["proxy_auth"] = value
            elif section == "libtorrent" and name == "max_connections_download":
                temp_config.set_libtorrent_max_conn_download(value)
            elif section == "libtorrent" and name == "max_download_rate":
                temp_config.set_libtorrent_max_download_rate(value)
            elif section == "libtorrent" and name == "max_upload_rate":
                temp_config.set_libtorrent_max_upload_rate(value)
            elif section == "libtorrent" and name == "utp":
                temp_config.set_libtorrent_utp(value)
            elif section == "libtorrent" and name == "anon_listen_port":
                temp_config.set_anon_listen_port(value)
            elif section == "libtorrent" and name == "anon_proxytype":
                temp_config.config["libtorrent"]["anon_proxy_type"] = value
            elif section == "libtorrent" and name == "anon_proxyserver":
                if isinstance(value, tuple) and isinstance(value[1], list):
                    temp_config.config["libtorrent"]["anon_proxy_server_ip"] = value[0]
                    temp_config.config["libtorrent"]["anon_proxy_server_ports"] = [str(port) for port in value[1]]
            elif section == "libtorrent" and name == "anon_proxyauth":
                temp_config.config["libtorrent"]["anon_proxy_auth"] = value
            elif section == "video" and name == "enabled":
                temp_config.set_video_server_enabled(value)
            elif section == "video" and name == "port":
                temp_config.set_video_server_port(value)
            elif section == "watch_folder" and name == "enabled":
                temp_config.set_watch_folder_enabled(value)
            elif section == "watch_folder" and name == "watch_folder_dir":
                temp_config.set_watch_folder_path(value)
            elif section == "http_api" and name == "enabled":
                temp_config.set_http_api_enabled(value)
            elif section == "http_api" and name == "port":
                temp_config.set_http_api_port(value)
            elif section == "credit_mining" and name == "enabled":
                temp_config.set_credit_mining_enabled(value)
            elif section == "credit_mining" and name == "sources":
                temp_config.set_credit_mining_sources(value)

            try:
                temp_config.validate()
                config = temp_config
            except InvalidConfigException as exc:
                logger.debug("The following field in the old libtribler.conf was wrong: %s", exc.args)

    return config


def convert_config_to_tribler74(state_dir=None):
    """
    Convert the download config files to Tribler 7.4 format. The extensions will also be renamed from .state to .conf
    """
    refactoring_tool = None
    if PY3:
        from lib2to3.refactor import RefactoringTool, get_fixers_from_package
        refactoring_tool = RefactoringTool(fixer_names=get_fixers_from_package('lib2to3.fixes'))

    state_dir = state_dir or TriblerConfig.get_default_root_state_dir()
    for _, filename in enumerate(iglob(os.path.join(state_dir, STATEDIR_CHECKPOINT_DIR, '*.state'))):
        convert_state_file_to_conf_74(filename, refactoring_tool=refactoring_tool)


def convert_state_file_to_conf_74(filename, refactoring_tool=None):
    """
    Converts .pstate file (pre-7.4.0) to .conf file.
    :param filename: .pstate file
    :param refactoring_tool: RefactoringTool instance if using Python3
    :return: None
    """
    def _fix_state_config(config):
        for section, option in [('state', 'metainfo'), ('state', 'engineresumedata')]:
            value = config.get(section, option, literal_eval=False)
            ungarbled_dict = None
            try:
                if PY3 and refactoring_tool:
                    value = str(refactoring_tool.refactor_string(value + '\n', option + '_2to3'))
                    ungarbled_dict = recursive_ungarble_metainfo(ast.literal_eval(value))

                value = ungarbled_dict or ast.literal_eval(value)
                config.set(section, option, base64.b64encode(lt.bencode(value)).decode('utf-8'))
            except (ValueError, SyntaxError, ParseError) as ex:
                logger.error("Config could not be fixed, probably corrupted. Exception: %s %s", type(ex), str(ex))
                return None
        return config

    old_config = CallbackConfigParser()
    try:
        old_config.read_file(filename)
    except MissingSectionHeaderError:
        logger.error("Removing download state file %s since it appears to be corrupt", filename)
        os.remove(filename)

    # We first need to fix the .state file such that it has the correct metainfo/resumedata.
    # If the config cannot be fixed, it is likely corrupted in which case we simply remove the file.
    fixed_config = _fix_state_config(old_config)
    if not fixed_config:
        logger.error("Removing download state file %s since it could not be fixed", filename)
        os.remove(filename)
        return

    # Remove dlstate since the same information is already stored in the resumedata
    if fixed_config.has_option('state', 'dlstate'):
        fixed_config.remove_option('state', 'dlstate')

    new_config = ConfigObj(infile=filename[:-6] + '.conf', encoding='utf8')
    for section in fixed_config.sections():
        for key, _ in fixed_config.items(section):
            val = fixed_config.get(section, key)
            if section not in new_config:
                new_config[section] = {}
            new_config[section][key] = val
    new_config.write()
    os.remove(filename)
