import logging
import re
import threading
import traceback
import os
from contextlib import contextmanager
from threading import Lock


_LOG_FIELDS_ATTR = "_edp_log_fields"


def set_log_fields(**fields):
    """Merge fields into the current thread's dynamic log-fields dict.

    The :class:`DynamicLogFieldsFilter` copies every entry of this dict onto
    each emitted ``LogRecord``, so they ship as GELF additional fields.
    """
    thread = threading.current_thread()
    current = getattr(thread, _LOG_FIELDS_ATTR, None)
    if current is None:
        current = {}
        setattr(thread, _LOG_FIELDS_ATTR, current)
    current.update(fields)


def clear_log_fields(*names):
    """Remove the named fields, or all of them when no names are given."""
    thread = threading.current_thread()
    current = getattr(thread, _LOG_FIELDS_ATTR, None)
    if current is None:
        return
    if not names:
        delattr(thread, _LOG_FIELDS_ATTR)
        return
    for name in names:
        current.pop(name, None)


@contextmanager
def log_fields(**fields):
    """Scoped helper that sets ``fields`` for the duration of the with-block.

    On exit, the previous state of the current thread's dynamic log-fields
    dict is restored (including the case where no dict existed before).
    """
    thread = threading.current_thread()
    had_previous = hasattr(thread, _LOG_FIELDS_ATTR)
    previous_snapshot = dict(getattr(thread, _LOG_FIELDS_ATTR, {})) if had_previous else None
    set_log_fields(**fields)
    try:
        yield
    finally:
        if had_previous:
            setattr(thread, _LOG_FIELDS_ATTR, previous_snapshot)
        elif hasattr(thread, _LOG_FIELDS_ATTR):
            delattr(thread, _LOG_FIELDS_ATTR)


# Default ``ThreadPoolExecutor`` worker names look like ``ThreadPoolExecutor-{N}_{M}``.
# When renaming a worker we keep that trailing ``{N}_{M}`` id so each worker
# is still individually identifiable in the logs.
_THREADPOOL_WORKER_ID_RE = re.compile(r"\d+_\d+$")


def build_thread_name_initializer(current_thread):
    """Build a ``ThreadPoolExecutor`` initializer that preserves Graylog context.

    Captures the calling (parent) thread's name and ``_edp_context_name`` now
    and returns a no-arg callable suitable for
    ``ThreadPoolExecutor(initializer=…)``. When each worker starts, the
    initializer renames it as
    ``MessageProcessorThreadPool_{N}_{M}$<processor_name>$<request_id>`` —
    splicing ``processor_name`` / ``request_id`` from the parent's name onto
    the worker's auto-assigned id, so :class:`MessageProcessorThreadNameFilter`
    parses the same fields out of the worker logs as it would for the parent.
    If the parent thread has an ``_edp_context_name`` attribute, it is also
    copied to the worker so :class:`ContextLogFilter` keeps emitting the same
    ``context_name`` field from within the pool.

    Usage::

        with ThreadPoolExecutor(
            max_workers=N,
            initializer=build_thread_name_initializer(
                current_thread=threading.current_thread()
            ),
        ) as executor:
            ...
    """
    parent_thread_name = current_thread.name
    parent_parts = parent_thread_name.split("$")
    processor_name = parent_parts[1] if len(parent_parts) > 1 else parent_thread_name
    request_id = parent_parts[2] if len(parent_parts) > 2 else ""
    has_context_name = hasattr(current_thread, "_edp_context_name")
    context_name = getattr(current_thread, "_edp_context_name", None)
    has_log_fields = hasattr(current_thread, _LOG_FIELDS_ATTR)
    log_fields_snapshot = dict(getattr(current_thread, _LOG_FIELDS_ATTR, {}))

    def _initializer():
        worker = threading.current_thread()
        match = _THREADPOOL_WORKER_ID_RE.search(worker.name)
        worker_id = match.group(0) if match else worker.name
        worker.name = f"MessageProcessorThreadPool_{worker_id}${processor_name}${request_id}"
        if has_context_name:
            worker._edp_context_name = context_name
        if has_log_fields:
            setattr(worker, _LOG_FIELDS_ATTR, dict(log_fields_snapshot))

    return _initializer


class EdpLogTypeFilter(object):
    def __init__(self, log_source, **kwargs):
        self.inner_filters = [
            LogLevelNameFilter(),
            ThreadIdFilter(),
            CountFilter(),
            MessageProcessorThreadNameFilter(),
            FixedInfoFilter(log_source=log_source, log_type="edp-log-type-gelf", log_verion="1.0", **kwargs),
            EnvironmentVariableLogFilter(),
            ContextLogFilter(),
            DynamicLogFieldsFilter()
        ]

    def filter(self, record):
        for inner_filter in self.inner_filters:
            inner_filter.filter(record)
        return 1


class LogLevelNameFilter(object):
    def filter(self, record):
        record.log_level = logging.getLevelName(record.levelno)


class ThreadIdFilter(object):
    def filter(self, record):
        record.thread_id = record.thread
        return 1


class CountFilter(object):
    def __init__(self):
        self.lock = Lock()
        self.count = 0
        self.max_count = 99

    def get_count(self):
        with self.lock:
            self.count += 1
            if self.count > self.max_count:
                self.count = 1
            return self.count

    def filter(self, record):
        record.count = self.get_count()
        return 1


class FixedInfoFilter(object):
    def __init__(self, **fields):
        self.fields = {}
        for field_name in fields.keys():
            field_value = fields[field_name]
            if not isinstance(field_name, str):
                raise Exception("The field´s name must be a str")
            self.fields[field_name] = field_value

    def filter(self, record):
        for field in self.fields:
            setattr(record, field, self.fields[field])
        return 1


class MessageProcessorThreadNameFilter(object):
    def filter(self, record):
        try:
            if record.threadName is None or not record.threadName.startswith("MessageProcessorThread"):
                return 1

            first_index = record.threadName.index("$")
            second_index = record.threadName.index("$", first_index + 1)

            thread_name = record.threadName[:first_index]
            processor_name = record.threadName[first_index + 1:second_index]

            extra_index = record.threadName.find("$", second_index + 1)
            if extra_index != -1:
                thread_name += record.threadName[extra_index:]
                request_id = record.threadName[second_index + 1:extra_index]
            else:
                request_id = record.threadName[second_index + 1:]

            record.threadName = thread_name
            record.request_id = request_id
            record.processor_name = processor_name
            if processor_name.startswith("HttpMessageProcessor") and processor_name != "HttpMessageProcessor":
                first_index = processor_name.index("$")
                second_index = processor_name.rindex("$")
                url = processor_name[second_index + 1:]
                method = processor_name[first_index + 1:second_index]
                record.url = url
                record.http_method = method
                record.processor_name = "HttpMessageProcessor"
            return 1
        except Exception:
            record.error_processing_thread_name = record.threadName + " - " + traceback.format_exc()


class EnvironmentVariableLogFilter(FixedInfoFilter):
    def __init__(self):
        field_name_pattern = "GRAYLOG_FIELD_"
        pattern_length = len(field_name_pattern)
        fields = {}
        for var in os.environ.keys():
            if var.startswith(field_name_pattern):
                field_name = var[pattern_length:].lower()
                field_value = os.environ[var]
                fields[field_name] = field_value

        super(EnvironmentVariableLogFilter, self).__init__(**fields)


class ContextLogFilter(object):
    def filter(self, record):
        current_thread = threading.current_thread()
        if hasattr(current_thread, "_edp_context_name"):
            record.context_name = threading.current_thread()._edp_context_name


class DynamicLogFieldsFilter(object):
    """Copy every entry of the current thread's ``_edp_log_fields`` dict onto the record.

    The application registers fields with :func:`set_log_fields` (or via the
    :func:`log_fields` context manager); this filter forwards them as GELF
    additional fields. Missing attribute or empty dict is a no-op.
    """

    def filter(self, record):
        fields = getattr(threading.current_thread(), _LOG_FIELDS_ATTR, None)
        if fields:
            for name, value in fields.items():
                setattr(record, name, value)
        return 1
