package io.sentry.logger;

import io.sentry.HostnameCache;
import io.sentry.IScope;
import io.sentry.ISpan;
import io.sentry.PropagationContext;
import io.sentry.Scopes;
import io.sentry.SentryAttribute;
import io.sentry.SentryAttributeType;
import io.sentry.SentryAttributes;
import io.sentry.SentryDate;
import io.sentry.SentryLevel;
import io.sentry.SentryLogEvent;
import io.sentry.SentryLogEventAttributeValue;
import io.sentry.SentryLogLevel;
import io.sentry.SentryOptions;
import io.sentry.SpanId;
import io.sentry.protocol.SdkVersion;
import io.sentry.protocol.SentryId;
import io.sentry.protocol.User;
import io.sentry.util.Platform;
import io.sentry.util.TracingUtils;
import java.util.HashMap;
import java.util.Map;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public final class LoggerApi implements ILoggerApi {

  private final @NotNull Scopes scopes;

  public LoggerApi(final @NotNull Scopes scopes) {
    this.scopes = scopes;
  }

  @Override
  public void trace(final @Nullable String message, final @Nullable Object... args) {
    log(SentryLogLevel.TRACE, message, args);
  }

  @Override
  public void debug(final @Nullable String message, final @Nullable Object... args) {
    log(SentryLogLevel.DEBUG, message, args);
  }

  @Override
  public void info(final @Nullable String message, final @Nullable Object... args) {
    log(SentryLogLevel.INFO, message, args);
  }

  @Override
  public void warn(final @Nullable String message, final @Nullable Object... args) {
    log(SentryLogLevel.WARN, message, args);
  }

  @Override
  public void error(final @Nullable String message, final @Nullable Object... args) {
    log(SentryLogLevel.ERROR, message, args);
  }

  @Override
  public void fatal(final @Nullable String message, final @Nullable Object... args) {
    log(SentryLogLevel.FATAL, message, args);
  }

  @Override
  public void log(
      final @NotNull SentryLogLevel level,
      final @Nullable String message,
      final @Nullable Object... args) {
    captureLog(level, SentryLogParameters.create(null, null), message, args);
  }

  @Override
  public void log(
      final @NotNull SentryLogLevel level,
      final @Nullable SentryDate timestamp,
      final @Nullable String message,
      final @Nullable Object... args) {
    captureLog(level, SentryLogParameters.create(timestamp, null), message, args);
  }

  @Override
  public void log(
      final @NotNull SentryLogLevel level,
      final @NotNull SentryLogParameters params,
      final @Nullable String message,
      final @Nullable Object... args) {
    captureLog(level, params, message, args);
  }

  @SuppressWarnings("AnnotateFormatMethod")
  private void captureLog(
      final @NotNull SentryLogLevel level,
      final @NotNull SentryLogParameters params,
      final @Nullable String message,
      final @Nullable Object... args) {
    final @NotNull SentryOptions options = scopes.getOptions();
    try {
      if (!scopes.isEnabled()) {
        options
            .getLogger()
            .log(SentryLevel.WARNING, "Instance is disabled and this 'logger' call is a no-op.");
        return;
      }

      if (!options.getLogs().isEnabled()) {
        options
            .getLogger()
            .log(SentryLevel.WARNING, "Sentry Log is disabled and this 'logger' call is a no-op.");
        return;
      }

      if (message == null) {
        return;
      }

      final @Nullable SentryDate timestamp = params.getTimestamp();
      final @NotNull SentryDate timestampToUse =
          timestamp == null ? options.getDateProvider().now() : timestamp;
      final @NotNull String messageToUse = maybeFormatMessage(message, args);

      final @NotNull IScope combinedScope = scopes.getCombinedScopeView();
      final @NotNull PropagationContext propagationContext = combinedScope.getPropagationContext();
      final @Nullable ISpan span = combinedScope.getSpan();
      if (span == null) {
        TracingUtils.maybeUpdateBaggage(combinedScope, options);
      }
      final @NotNull SentryId traceId =
          span == null ? propagationContext.getTraceId() : span.getSpanContext().getTraceId();
      final @NotNull SpanId spanId =
          span == null ? propagationContext.getSpanId() : span.getSpanContext().getSpanId();
      final SentryLogEvent logEvent =
          new SentryLogEvent(traceId, timestampToUse, messageToUse, level);
      logEvent.setSpanId(spanId);
      logEvent.setAttributes(createAttributes(params, message, args));
      logEvent.setSeverityNumber(level.getSeverityNumber());

      scopes.getClient().captureLog(logEvent, combinedScope);
    } catch (Throwable e) {
      options.getLogger().log(SentryLevel.ERROR, "Error while capturing log event", e);
    }
  }

  private @NotNull String maybeFormatMessage(
      final @NotNull String message, final @Nullable Object[] args) {
    if (args == null || args.length == 0) {
      return message;
    }

    try {
      return String.format(message, args);
    } catch (Throwable t) {
      scopes
          .getOptions()
          .getLogger()
          .log(SentryLevel.ERROR, "Error while running log through String.format", t);
      return message;
    }
  }

  private @NotNull HashMap<String, SentryLogEventAttributeValue> createAttributes(
      final @NotNull SentryLogParameters params,
      final @NotNull String message,
      final @Nullable Object... args) {
    final @NotNull HashMap<String, SentryLogEventAttributeValue> attributes = new HashMap<>();

    final @NotNull Map<String, SentryAttribute> scopeAttributes =
        scopes.getCombinedScopeView().getAttributes();
    for (SentryAttribute scopeAttribute : scopeAttributes.values()) {
      attributes.put(
          scopeAttribute.getName(), SentryLogEventAttributeValue.fromAttribute(scopeAttribute));
    }

    final @NotNull String origin = params.getOrigin();
    if (!"manual".equalsIgnoreCase(origin)) {
      attributes.put(
          "sentry.origin", new SentryLogEventAttributeValue(SentryAttributeType.STRING, origin));
    }

    final @Nullable SentryAttributes incomingAttributes = params.getAttributes();

    if (incomingAttributes != null) {
      for (SentryAttribute attribute : incomingAttributes.getAttributes().values()) {
        attributes.put(attribute.getName(), SentryLogEventAttributeValue.fromAttribute(attribute));
      }
    }

    if (args != null) {
      int i = 0;
      for (Object arg : args) {
        final @NotNull SentryAttributeType type = SentryAttributeType.inferFrom(arg);
        attributes.put(
            "sentry.message.parameter." + i, new SentryLogEventAttributeValue(type, arg));
        i++;
      }
      if (i > 0) {
        if (attributes.get("sentry.message.template") == null) {
          attributes.put(
              "sentry.message.template",
              new SentryLogEventAttributeValue(SentryAttributeType.STRING, message));
        }
      }
    }

    final @Nullable SdkVersion sdkVersion = scopes.getOptions().getSdkVersion();
    if (sdkVersion != null) {
      attributes.put(
          "sentry.sdk.name",
          new SentryLogEventAttributeValue(SentryAttributeType.STRING, sdkVersion.getName()));
      attributes.put(
          "sentry.sdk.version",
          new SentryLogEventAttributeValue(SentryAttributeType.STRING, sdkVersion.getVersion()));
    }

    final @Nullable String environment = scopes.getOptions().getEnvironment();
    if (environment != null) {
      attributes.put(
          "sentry.environment",
          new SentryLogEventAttributeValue(SentryAttributeType.STRING, environment));
    }

    final @NotNull SentryId scopeReplayId = scopes.getCombinedScopeView().getReplayId();
    if (!SentryId.EMPTY_ID.equals(scopeReplayId)) {
      attributes.put(
          "sentry.replay_id",
          new SentryLogEventAttributeValue(SentryAttributeType.STRING, scopeReplayId.toString()));
    } else {
      final @NotNull SentryId controllerReplayId =
          scopes.getOptions().getReplayController().getReplayId();
      if (!SentryId.EMPTY_ID.equals(controllerReplayId)) {
        attributes.put(
            "sentry.replay_id",
            new SentryLogEventAttributeValue(
                SentryAttributeType.STRING, controllerReplayId.toString()));
        attributes.put(
            "sentry._internal.replay_is_buffering",
            new SentryLogEventAttributeValue(SentryAttributeType.BOOLEAN, true));
      }
    }

    final @Nullable String release = scopes.getOptions().getRelease();
    if (release != null) {
      attributes.put(
          "sentry.release", new SentryLogEventAttributeValue(SentryAttributeType.STRING, release));
    }

    if (Platform.isJvm()) {
      setServerName(attributes);
    }

    setUser(attributes);

    return attributes;
  }

  private void setServerName(
      final @NotNull HashMap<String, SentryLogEventAttributeValue> attributes) {
    final @NotNull SentryOptions options = scopes.getOptions();
    final @Nullable String optionsServerName = options.getServerName();
    if (optionsServerName != null) {
      attributes.put(
          "server.address",
          new SentryLogEventAttributeValue(SentryAttributeType.STRING, optionsServerName));
    } else if (options.isAttachServerName()) {
      final @Nullable String hostname = HostnameCache.getInstance().getHostname();
      if (hostname != null) {
        attributes.put(
            "server.address",
            new SentryLogEventAttributeValue(SentryAttributeType.STRING, hostname));
      }
    }
  }

  private void setUser(final @NotNull HashMap<String, SentryLogEventAttributeValue> attributes) {
    final @Nullable User user = scopes.getCombinedScopeView().getUser();
    if (user == null) {
      // In case no user is set, we should fallback to the distinct id, known as installation id,
      // which is used on Android as default user id
      final @Nullable String id = scopes.getOptions().getDistinctId();
      if (id != null) {
        attributes.put("user.id", new SentryLogEventAttributeValue(SentryAttributeType.STRING, id));
      }
    } else {
      final @Nullable String id = user.getId();
      if (id != null) {
        attributes.put("user.id", new SentryLogEventAttributeValue(SentryAttributeType.STRING, id));
      }
      final @Nullable String username = user.getUsername();
      if (username != null) {
        attributes.put(
            "user.name", new SentryLogEventAttributeValue(SentryAttributeType.STRING, username));
      }
      final @Nullable String email = user.getEmail();
      if (email != null) {
        attributes.put(
            "user.email", new SentryLogEventAttributeValue(SentryAttributeType.STRING, email));
      }
    }
  }
}
