import argparse
import errno
import json
import logging
import os
import sys
from copy import copy
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Union

from epg2xml import __description__, __title__, __url__, __version__
from epg2xml.utils import dump_json

# suppress modules logging
logging.getLogger("requests").setLevel(logging.ERROR)
logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)

logger = logging.getLogger("CONFIG")


def setup_root_logger(
    *,
    handler: logging.Handler = None,
    formatter: logging.Formatter = None,
    level: Union[int, str] = None,
) -> None:
    if level is None:
        level = logging.INFO

    if handler is None:
        # logging to console, stderr by default
        handler = logging.StreamHandler()

    if formatter is None:
        log_fmt = "%(asctime)-15s %(levelname)-8s %(name)-7s %(lineno)4d: %(message)s"
        formatter = logging.Formatter(log_fmt, datefmt="%Y/%m/%d %H:%M:%S")

    handler.setFormatter(formatter)
    logging.getLogger().addHandler(handler)
    logging.getLogger().setLevel(level)


class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)

        return cls._instances[cls]


class Config:
    __metaclass__ = Singleton

    base_config = {
        "GLOBAL": {
            "ENABLED": True,
            "FETCH_LIMIT": 2,
            "ID_FORMAT": "{ServiceId}.{Source.lower()}",
            "ADD_REBROADCAST_TO_TITLE": False,
            "ADD_EPNUM_TO_TITLE": True,
            "ADD_DESCRIPTION": True,
            "ADD_XMLTV_NS": False,
            "GET_MORE_DETAILS": False,
            "ADD_CHANNEL_ICON": True,
            "HTTP_PROXY": None,
        },
        "KT": {
            "MY_CHANNELS": [],
        },
        "LG": {
            "MY_CHANNELS": [],
        },
        "SK": {
            "MY_CHANNELS": [],
        },
        "DAUM": {
            "MY_CHANNELS": [],
        },
        "NAVER": {
            "MY_CHANNELS": [],
        },
        "WAVVE": {
            "MY_CHANNELS": [],
        },
        "TVING": {
            "MY_CHANNELS": [],
        },
        "SPOTV": {
            "MY_CHANNELS": [],
        },
    }

    base_settings = {
        "config": {
            "argv": "--config",
            "env": "EPG2XML_CONFIG",
            "default": str(Path.cwd().joinpath("epg2xml.json")),
        },
        "logfile": {
            "argv": "--logfile",
            "env": "EPG2XML_LOGFILE",
            "default": None,
        },
        "loglevel": {
            "argv": "--loglevel",
            "env": "EPG2XML_LOGLEVEL",
            "default": "INFO",
        },
        "channelfile": {
            "argv": "--channelfile",
            "env": "EPG2XML_CHANNELFILE",
            "default": str(Path.cwd().joinpath("Channel.json")),
        },
        "xmlfile": {
            "argv": "--xmlfile",
            "env": "EPG2XML_XMLFILE",
            "default": None,
        },
        "xmlsock": {
            "argv": "--xmlsock",
            "env": "EPG2XML_XMLSOCK",
            "default": None,
        },
        "parallel": {
            "argv": "--parallel",
            "env": "EPG2XML_PARALLEL",
            "default": False,
        },
        "dbfile": {
            "argv": "--dbfile",
            "env": "EPG2XML_DBFILE",
            "default": None,
        },
    }

    def __init__(self):
        """Initializes config"""
        # Args and settings
        self.args = self.parse_args()
        self.settings = self.get_settings()
        # Configs
        self.configs = None

    @property
    def default_config(self):
        """reserved for adding extra fields"""
        cfg = copy(self.base_config)
        return cfg

    def __inner_upgrade(self, settings1, settings2, key=None, overwrite=False):
        sub_upgraded = False
        merged = copy(settings2)

        if isinstance(settings1, dict):
            for k, v in settings1.items():
                # missing k
                if k not in settings2:
                    merged[k] = v
                    sub_upgraded = True
                    if not key:
                        logger.info("Added %r config option: %s", str(k), str(v))
                    else:
                        logger.info("Added %r to config option %r: %s", str(k), str(key), str(v))
                    continue

                # iterate children
                if isinstance(v, (dict, list)):
                    merged[k], did_upgrade = self.__inner_upgrade(
                        settings1[k], settings2[k], key=k, overwrite=overwrite
                    )
                    sub_upgraded = did_upgrade or sub_upgraded
                elif settings1[k] != settings2[k] and overwrite:
                    merged = settings1
                    sub_upgraded = True
        elif isinstance(settings1, list) and key:
            for v in settings1:
                if v not in settings2:
                    merged.append(v)
                    sub_upgraded = True
                    logger.info("Added to config option %r: %s", str(key), str(v))
                    continue

        return merged, sub_upgraded

    def upgrade_configs(self, currents):
        fields_env = {}

        # ENV gets priority: ENV > config.json
        for name, _ in self.base_config.items():
            if name in os.environ:
                # Use JSON decoder to get same behaviour as config file
                fields_env[name] = json.JSONDecoder().decode(os.environ[name])
                logger.debug("setting from ENV   --%s=%s", name, fields_env[name])

        # Update in-memory config with environment settings
        currents.update(fields_env)

        # Do inner upgrade
        upgraded_configs, upgraded = self.__inner_upgrade(self.base_config, currents)
        return upgraded_configs, upgraded

    def load_with_hidden(self, cfg_old):
        cfg_new = copy(cfg_old)
        for p in cfg_new:
            # push items in GLOBAL as defaults
            for k, v in cfg_old["GLOBAL"].items():
                if k not in cfg_new[p]:
                    cfg_new[p][k] = v
        del cfg_new["GLOBAL"]
        self.configs = cfg_new

    def load(self):
        logger.debug("Loading config...")
        if not Path(self.settings["config"]).exists():
            logger.info("No config file found. Creating a default one...")
            self.save(self.default_config)

        try:
            with open(self.settings["config"], "r", encoding="utf-8") as fp:
                cfg, upgraded = self.upgrade_configs(json.load(fp))

                # Save config if upgraded
                if upgraded:
                    self.save(cfg)
                    sys.exit(0)

            self.load_with_hidden(cfg)
        except (json.decoder.JSONDecodeError, ValueError):
            logger.exception("Please check your config here: %s", self.settings["config"])
            sys.exit(1)

    def save(self, cfg, exitOnSave=True):
        dump_json(self.settings["config"], cfg)
        if exitOnSave:
            logger.info("Your config was upgraded. You may check the changes here: %r", self.settings["config"])

        if exitOnSave:
            sys.exit(0)

    def get_settings(self):
        setts = {}
        for name, data in self.base_settings.items():
            # Argrument priority: cmd < environment < default
            try:
                value = None
                # Command line argument
                if self.args[name]:
                    value = self.args[name]
                    logger.debug("setting from ARG   --%s=%s", name, value)

                # Envirnoment variable
                elif data["env"] in os.environ:
                    value = os.environ[data["env"]]
                    logger.debug("setting from ENV   --%s=%s", data["env"], value)

                # Default
                else:
                    value = data["default"]
                    logger.debug("setting by default %s=%s", data["argv"], value)

                setts[name] = value

            except Exception:
                logger.exception("Exception raised on setting value: %r", name)

        # checking existance of important files' dir
        for argname in ["config", "logfile", "channelfile", "dbfile"]:
            filepath = setts[argname]
            if filepath is not None and not Path(filepath).parent.exists():
                logger.error(FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), filepath))
                sys.exit(1)

        # handling of boolean args
        for argname in ["parallel"]:
            if isinstance(setts[argname], str):
                setts[argname] = setts[argname].lower() in ("y", "yes", "t", "true", "on", "1")

        # logging to file
        if setts["logfile"] is not None:
            fileHandler = RotatingFileHandler(setts["logfile"], maxBytes=2 * 1024**2, backupCount=5, encoding="utf-8")
            setup_root_logger(handler=fileHandler)

        # set configured log level
        logging.getLogger().setLevel(setts["loglevel"])

        return setts

    # Parse command line arguments
    def parse_args(self):
        parser = argparse.ArgumentParser(
            prog=__title__,
            description=__description__,
            epilog=f"Online help: <{__url__}>",
            formatter_class=argparse.RawTextHelpFormatter,
        )

        # Mode
        parser.add_argument(
            "cmd",
            metavar="command",
            choices=("run", "fromdb", "update_channels"),
            help=('"run": XML 형식으로 출력\n' '"fromdb": dbfile로부터 불러오기\n' '"update_channels": 채널 정보 업데이트'),
        )

        # Display version info
        parser.add_argument(
            "-v",
            "--version",
            action="version",
            version=f"{__title__} v{__version__}",
        )

        # Config file
        parser.add_argument(
            self.base_settings["config"]["argv"],
            nargs="?",
            const=None,
            help=f"config file path (default: {self.base_settings['config']['default']})",
        )

        # Log file
        parser.add_argument(
            self.base_settings["logfile"]["argv"],
            nargs="?",
            const=None,
            help=f"log file path (default: {self.base_settings['logfile']['default']})",
        )

        # Log level
        parser.add_argument(
            self.base_settings["loglevel"]["argv"],
            choices=("DEBUG", "INFO", "WARNING", "ERROR"),
            help=f"loglevel (default: {self.base_settings['loglevel']['default']})",
        )

        # Channel file
        parser.add_argument(
            self.base_settings["channelfile"]["argv"],
            nargs="?",
            const=None,
            help=f"channel file path (default: {self.base_settings['channelfile']['default']})",
        )

        # XML file
        parser.add_argument(
            self.base_settings["xmlfile"]["argv"],
            nargs="?",
            const=None,
            help="write output to file if specified",
        )

        # XML socket
        parser.add_argument(
            self.base_settings["xmlsock"]["argv"],
            nargs="?",
            const=None,
            help="send output to unix socket if specified",
        )

        # Run in Parallel
        parser.add_argument(
            self.base_settings["parallel"]["argv"],
            action="store_true",
            help="run in parallel",
        )

        # DB file
        parser.add_argument(
            self.base_settings["dbfile"]["argv"],
            nargs="?",
            const=None,
            help="export/import data to/from db",
        )

        # Print help by default if no arguments
        if len(sys.argv) == 1:
            parser.print_help()

            sys.exit(0)

        else:
            return vars(parser.parse_args())


# logging
setup_root_logger()
