import logging
import logging.handlers
import os
import sys
from datetime import datetime
from threading import get_ident

logger = logging.getLogger(__name__)

basic_msg_pre = '%(time_iso_8601)s %(local_ip)s %(pid)d.%(tid)d '
full_format_str = basic_msg_pre + '%(levelname)s %(name)s@%(filename)s:%(lineno)d %(message)s'

short_format_str = '%(time_short)s %(levelname)s %(name)s@%(filename_short)s:%(lineno)d %(message)s'


class Colors:
    RESET = '\033[0m'
    BOLD = '\033[1m'
    DEBUG = '\033[36m'
    INFO = '\033[32m'
    WARNING = '\033[33m'
    ERROR = '\033[31m'
    CRITICAL = '\033[35m'


def _supports_color():
    if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty():
        return False
    if sys.platform == 'win32':
        try:
            import ctypes
            kernel32 = ctypes.windll.kernel32
            kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
            return True
        except Exception:
            return False
    term = os.environ.get('TERM', '')
    if term and term != 'dumb':
        return True
    return False


def get_local_ip_address():
    try:
        import socket
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('8.8.8.8', 80))
        ip = s.getsockname()[0]
        s.close()
        if ip:
            return ip
    except Exception:
        pass
    return '127.0.0.1'


_level_abbrev = {
    'DEBUG': 'DBUG',
    'INFO': 'INFO',
    'WARNING': 'WARN',
    'ERROR': 'ERR ',
    'CRITICAL': 'CRIT',
}


class AlignedFormatter(logging.Formatter):

    def format(self, record):
        record.levelname = _level_abbrev.get(record.levelname, record.levelname)
        return super().format(record)


class ColoredFormatter(logging.Formatter):

    def __init__(self, format_str):
        super().__init__(format_str)
        self.color_map = {
            logging.DEBUG: Colors.DEBUG,
            logging.INFO: Colors.INFO,
            logging.WARNING: Colors.WARNING,
            logging.ERROR: Colors.ERROR,
            logging.CRITICAL: Colors.BOLD + Colors.CRITICAL,
        }

    def format(self, record):
        level_color = self.color_map.get(record.levelno, '')
        reset = Colors.RESET
        levelname_abbrev = _level_abbrev.get(record.levelname, record.levelname)
        record.levelname = f'{level_color}{levelname_abbrev}{reset}'
        return super().format(record)


class LogManager:

    def __init__(self):
        self.basic_handlers = []
        self.basic_log_level = logging.NOTSET
        self.disable_stdout_logger = False
        self.disable_file_logger = False
        self._init_from_env_done = False
        self._filelog_init_keys = set()

    def shutdown(self):
        for handler in self.basic_handlers:
            handler.close()

    def init_from_env(self, env_config):
        if self._init_from_env_done:
            return
        explicit_level = env_config.get('LOG.LEVEL')
        if explicit_level is not None:
            level = explicit_level
        else:
            app_env = (env_config.get('APP_ENV') or 'prod').lower()
            if app_env in ('dev', 'test'):
                level = 'debug'
            else:
                level = 'info'

        self.silence_noise_loggers(env_config)
        self.basic_log_level = _map_log_level(level)
        self.disable_stdout_logger = env_config.get_bool('LOG.DISABLE_STDOUT_LOGGER', default=False)
        if not self.disable_stdout_logger:
            self._init_stdout_logger()
        self._init_from_env_done = True
        logger.debug(f'root logger: {logging.getLogger()}, handlers: {self.basic_handlers}')

    def setup_readable_logger_handler(self, handler, use_color=False, use_short_format=False):
        format_str = short_format_str if use_short_format else full_format_str
        if use_color and _supports_color():
            formatter = ColoredFormatter(format_str)
        else:
            formatter = AlignedFormatter(format_str)
        filter_obj = BasicContextFilter(use_short_format=use_short_format)
        handler.setFormatter(formatter)
        handler.setLevel(self.basic_log_level)
        handler.addFilter(filter_obj)
        return handler

    def silence_noise_loggers(self, env_config):
        names = env_config.get_list('LOG.NOISE_LOGGERS')
        level = env_config.get('LOG.NOISE_LEVEL', 'warning')
        log_level = _map_log_level(level)
        for name in names:
            logging.getLogger(name).setLevel(log_level)

    def reset_root_logger_handlers(self):
        root_logger = logging.getLogger()
        root_logger.setLevel(self.basic_log_level)
        root_logger.handlers = []
        for handler in self.basic_handlers:
            root_logger.addHandler(handler)

    def _init_stdout_logger(self):
        stream_handler = logging.StreamHandler(sys.stdout)
        use_color = True
        use_short_format = True
        self.setup_readable_logger_handler(stream_handler, use_color=use_color, use_short_format=use_short_format)
        self.basic_handlers.append(stream_handler)
        self.reset_root_logger_handlers()

    def add_file_logger(self, app_name, logs_dir, use_short_format=False):
        key = (app_name, str(logs_dir), use_short_format)
        if key in self._filelog_init_keys:
            return
        filename = f'{logs_dir}/{app_name}.log'
        app_filelog_handler = _get_rotating_filehandler(filename)
        logger.debug(f'add file logger: {app_name}, {filename}, {use_short_format}')
        self.setup_readable_logger_handler(app_filelog_handler, use_short_format=use_short_format)
        self.basic_handlers.append(app_filelog_handler)
        self._filelog_init_keys.add(key)
        self.reset_root_logger_handlers()


log_manager = LogManager()


class BasicContextFilter(logging.Filter):

    def __init__(self, use_short_format=False):
        self.local_ip = get_local_ip_address()
        self.use_short_format = use_short_format

    def filter(self, record):
        if self.use_short_format:
            record.time_short = datetime.now().strftime('%H:%M:%S')
            record.filename_short = os.path.basename(record.filename)
        else:
            record.time_iso_8601 = datetime.now().astimezone().strftime('%Y-%m-%dT%H:%M:%S%z')
            record.local_ip = self.local_ip
            record.pid = os.getpid()
            record.tid = get_ident()
        return True


def _get_rotating_filehandler(filename):
    os.makedirs(os.path.dirname(filename), exist_ok=True)
    handler = logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=1024 * 1024 * 128, backupCount=3)
    os.chmod(filename, 0o666)
    return handler


def _map_log_level(log_level_str):
    log_level = logging.INFO
    if log_level_str is not None:
        level = log_level_str.lower()
        log_level_conf = {
            'verbose': logging.NOTSET,
            'debug': logging.DEBUG,
            'info': logging.INFO,
            'warning': logging.WARNING,
            'warn': logging.WARNING,
            'error': logging.ERROR,
            'critical': logging.CRITICAL,
        }
        if level in log_level_conf:
            log_level = log_level_conf[level]
    return log_level
