# Part of this file is taken from 

import re
import json
import logging
import traceback
from inspect import istraceback
from datetime import date, datetime, time, timezone


def get_logger(env):
  logger = logging.getLogger("utils")
  lvl = env.NBOX_LOG_LEVEL("info").upper()
  logger.setLevel(getattr(logging, lvl))

  if env.NBOX_JSON_LOG(False):
    logHandler = logging.StreamHandler()
    logHandler.setFormatter(JsonFormatter(
      '%(timestamp)s %(levelname)s %(message)s %(filename)s %(lineno)d',
      timestamp=True
    ))
    logger.addHandler(logHandler)
  else:
    logHandler = logging.StreamHandler()
    logHandler.setFormatter(InlineFormatter(
      '[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s',
      datefmt = "%Y-%m-%dT%H:%M:%S%z"
    ))
    logger.addHandler(logHandler)

  return logger


##############
# custom logger class for simplicity
class _LogObject:
  def __init__(self, log, msg):
    self.log = log
    self.msg = msg

def logobj(msg, /, *args, **kwargs):
  if args:
    msg += " " + " ".join(args)
  return _LogObject(kwargs, msg)

class InlineFormatter(logging.Formatter):
  def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)

  def format(self, record):
    if type(record.msg) == _LogObject:
      _l: _LogObject = record.msg
      if _l.log:
        record.msg = f"{_l.msg}\n" + json_dumps_pretty(_l.log)
      else:
        record.msg = _l.msg
    return super().format(record)


##############


class JsonFormatter(logging.Formatter):
  """
  A custom formatter to format logging records as json strings.
  Extra values will be formatted as str() if not supported by
  json default encoder
  """
  # skip natural LogRecord attributes
  # http://docs.python.org/library/logging.html#logrecord-attributes
  RESERVED_ATTRS = (
    'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename',
    'funcName', 'levelname', 'levelno', 'lineno', 'module',
    'msecs', 'message', 'msg', 'name', 'pathname', 'process',
    'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName')

  def __init__(self, *args, **kwargs):
    """args and kwargs to pass to logging.Formatter constructor"""
    self.json_serializer = json.dumps
    reserved_attrs = kwargs.pop("reserved_attrs", JsonFormatter.RESERVED_ATTRS)
    self.reserved_attrs = dict(zip(reserved_attrs, reserved_attrs))
    self.timestamp = kwargs.pop("timestamp", False)

    logging.Formatter.__init__(self, *args, **kwargs)

    # Parses format string looking for substitutions
    # This method is responsible for returning a list of fields (as strings) to include in all log messages.
    standard_formatters = re.compile(r'\((.+?)\)', re.IGNORECASE)
    self._required_fields = standard_formatters.findall(self._fmt)
    self._skip_fields = dict(zip(self._required_fields, self._required_fields))
    self._skip_fields.update(self.reserved_attrs)

  def format(self, record):
    """Formats a log record and serializes to json"""

    log_record = {
      "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc),
      "levelname": record.levelname,
      "logtype": "nbox_jlv1",
      "log": {
        "code": {
          "file": record.filename,
          "line": record.lineno,
        },
      }
    }

    if isinstance(record.msg, dict):
      log_record["message"] = "click to see data"
      log_record["log"]["data"] = record.msg
    elif isinstance(record.msg, _LogObject):
      _lo: _LogObject = record.msg
      log_record["message"] = _lo.msg
      if _lo.log:
        log_record["log"]["data"] = _lo.log
    elif record.msg:
      lines = record.getMessage().splitlines()
      if len(lines) == 1:
        # you only have one line, don't bother to create lot of data
        log_record["message"] = lines[0]
      else:
        log_record["message"] = lines[0][:10] + "..."
        log_record["log"]["lines"] = lines

    json_str = json_dumps_tight(log_record)
    return json_str


class _JsonEncoder(json.JSONEncoder):
  """
  A custom encoder extending the default JSONEncoder
  """
  def default(self, obj):
    if isinstance(obj, (date, datetime, time)):
      return self.format_datetime_obj(obj)
    elif istraceback(obj):
      return ''.join(traceback.format_tb(obj)).strip()
    elif type(obj) == Exception \
        or isinstance(obj, Exception) \
        or type(obj) == type:
      return str(obj)
    try:
      return super(_JsonEncoder, self).default(obj)
    except TypeError:
      try:
        return str(obj)
      except Exception:
        return None

  def format_datetime_obj(self, obj):
    return obj.isoformat()

json_dumps_tight = lambda log_record: json.dumps(
  log_record,
  cls = _JsonEncoder,
  ensure_ascii = True,
  separators = (',', ':'),
)

json_dumps_pretty = lambda log_record: json.dumps(
  log_record,
  cls = _JsonEncoder,
  ensure_ascii = True,
  indent = 2,
  separators = (',', ': '),
)