// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/log.go in the NATS server Go source.

using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.NatsNet.Server.Internal;

namespace ZB.MOM.NatsNet.Server;

public sealed partial class NatsServer
{
    private readonly object _loggingLock = new();
    private INatsLogger? _natsLogger;

    /// <summary>
    /// Configures and sets the server logger.
    /// Mirrors Go <c>Server.ConfigureLogger()</c>.
    /// </summary>
    public void ConfigureLogger()
    {
        var opts = GetOpts();
        if (opts.NoLog)
        {
            return;
        }

        var syslog = opts.Syslog;
        if (ServiceManager.IsWindowsService() && string.IsNullOrEmpty(opts.LogFile))
        {
            syslog = true;
        }

        if (!string.IsNullOrEmpty(opts.LogFile))
        {
            var fileLogger = new FileNatsLogger(opts.LogFile, opts.Logtime, opts.LogtimeUtc);
            if (opts.LogSizeLimit > 0)
            {
                fileLogger.SetSizeLimit(opts.LogSizeLimit);
            }

            if (opts.LogMaxFiles > 0)
            {
                var maxFiles = opts.LogMaxFiles > int.MaxValue ? 0 : (int)opts.LogMaxFiles;
                fileLogger.SetMaxNumFiles(maxFiles);
            }

            SetLoggerV2(fileLogger, opts.Debug, opts.Trace, opts.TraceVerbose);
            return;
        }

        // Syslog/remote-syslog-specific sinks are not yet ported.
        // Keep parity with Go option precedence and wire to the current ILogger backend.
        if (!string.IsNullOrEmpty(opts.RemoteSyslog) || syslog)
        {
            SetLoggerV2(new MicrosoftLoggerAdapter(_logger), opts.Debug, opts.Trace, opts.TraceVerbose);
            return;
        }

        SetLoggerV2(new MicrosoftLoggerAdapter(_logger), opts.Debug, opts.Trace, opts.TraceVerbose);
    }

    /// <summary>
    /// Sets the server logger.
    /// Mirrors Go <c>Server.SetLogger()</c>.
    /// </summary>
    public void SetLogger(INatsLogger? logger, bool debugFlag, bool traceFlag)
        => SetLoggerV2(logger, debugFlag, traceFlag, false);

    /// <summary>
    /// Sets the server logger and trace flags.
    /// Mirrors Go <c>Server.SetLoggerV2()</c>.
    /// </summary>
    public void SetLoggerV2(INatsLogger? logger, bool debugFlag, bool traceFlag, bool sysTrace)
    {
        Interlocked.Exchange(ref _debugEnabled, debugFlag ? 1 : 0);
        Interlocked.Exchange(ref _traceEnabled, traceFlag ? 1 : 0);
        Interlocked.Exchange(ref _traceSysAcc, sysTrace ? 1 : 0);

        INatsLogger? previous;
        lock (_loggingLock)
        {
            previous = _natsLogger;
            _natsLogger = logger;
            _logger = ToMicrosoftLogger(logger);
        }

        if (previous is IDisposable disposable)
        {
            try
            {
                disposable.Dispose();
            }
            catch (Exception ex)
            {
                _logger.LogError("Error closing logger: {Error}", ex.Message);
            }
        }
    }

    /// <summary>
    /// Re-opens file logger when file logging is enabled.
    /// Mirrors Go <c>Server.ReOpenLogFile()</c>.
    /// </summary>
    public void ReOpenLogFile()
    {
        INatsLogger? activeLogger;
        lock (_loggingLock)
        {
            activeLogger = _natsLogger;
        }

        if (activeLogger == null)
        {
            Noticef("File log re-open ignored, no logger");
            return;
        }

        var opts = GetOpts();
        if (string.IsNullOrEmpty(opts.LogFile))
        {
            Noticef("File log re-open ignored, not a file logger");
            return;
        }

        var fileLogger = new FileNatsLogger(opts.LogFile, opts.Logtime, opts.LogtimeUtc);
        if (opts.LogSizeLimit > 0)
        {
            fileLogger.SetSizeLimit(opts.LogSizeLimit);
        }

        if (opts.LogMaxFiles > 0)
        {
            var maxFiles = opts.LogMaxFiles > int.MaxValue ? 0 : (int)opts.LogMaxFiles;
            fileLogger.SetMaxNumFiles(maxFiles);
        }

        SetLogger(fileLogger, opts.Debug, opts.Trace);
        Noticef("File log re-opened");
    }

    /// <summary>
    /// Executes a logging callback if a logger is present.
    /// Mirrors Go <c>Server.executeLogCall()</c>.
    /// </summary>
    internal void ExecuteLogCall(Action<INatsLogger> action)
    {
        INatsLogger? logger;
        lock (_loggingLock)
        {
            logger = _natsLogger;
        }

        if (logger == null)
        {
            return;
        }

        action(logger);
    }

    /// <summary>
    /// Logs an error with a scope.
    /// Mirrors Go <c>Server.Errors()</c>.
    /// </summary>
    public void Errors(object scope, Exception e)
    {
        ExecuteLogCall(l => l.Errorf("{0} - {1}", scope, ErrorContextHelper.UnpackIfErrorCtx(e)));
    }

    /// <summary>
    /// Logs an error with context.
    /// Mirrors Go <c>Server.Errorc()</c>.
    /// </summary>
    public void Errorc(string ctx, Exception e)
    {
        ExecuteLogCall(l => l.Errorf("{0}: {1}", ctx, ErrorContextHelper.UnpackIfErrorCtx(e)));
    }

    /// <summary>
    /// Logs an error with scope and context.
    /// Mirrors Go <c>Server.Errorsc()</c>.
    /// </summary>
    public void Errorsc(object scope, string ctx, Exception e)
    {
        ExecuteLogCall(l => l.Errorf("{0} - {1}: {2}", scope, ctx, ErrorContextHelper.UnpackIfErrorCtx(e)));
    }

    /// <summary>
    /// Rate-limited warning based on the raw format string.
    /// Mirrors Go <c>Server.rateLimitFormatWarnf()</c>.
    /// </summary>
    internal void RateLimitFormatWarnf(string format, params object[] args)
    {
        if (!_rateLimitLogging.TryAdd(format, DateTime.UtcNow))
        {
            return;
        }

        var statement = string.Format(format, args);
        ExecuteLogCall(l => l.Warnf("{0}", statement));
    }

    /// <summary>
    /// Rate-limited warning based on rendered statement.
    /// Mirrors Go <c>Server.RateLimitWarnf()</c>.
    /// </summary>
    public void RateLimitWarnf(string format, params object[] args)
    {
        var statement = string.Format(format, args);
        if (!_rateLimitLogging.TryAdd(statement, DateTime.UtcNow))
        {
            return;
        }

        ExecuteLogCall(l => l.Warnf("{0}", statement));
    }

    /// <summary>
    /// Rate-limited debug logging based on rendered statement.
    /// Mirrors Go <c>Server.RateLimitDebugf()</c>.
    /// </summary>
    public void RateLimitDebugf(string format, params object[] args)
    {
        var statement = string.Format(format, args);
        if (!_rateLimitLogging.TryAdd(statement, DateTime.UtcNow))
        {
            return;
        }

        if (Interlocked.CompareExchange(ref _debugEnabled, 0, 0) == 0)
        {
            return;
        }

        ExecuteLogCall(l => l.Debugf("{0}", statement));
    }

    private static ILogger ToMicrosoftLogger(INatsLogger? logger)
    {
        return logger switch
        {
            null => NullLogger.Instance,
            MicrosoftLoggerAdapter adapter => adapter.UnderlyingLogger,
            _ => new NatsLoggerBridge(logger)
        };
    }

    private sealed class NatsLoggerBridge(INatsLogger natsLogger) : ILogger
    {
        public IDisposable BeginScope<TState>(TState state) where TState : notnull
            => NoopDisposable.Instance;

        public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception? exception,
            Func<TState, Exception?, string> formatter)
        {
            var message = formatter(state, exception);
            if (exception != null)
            {
                message = $"{message}: {exception.Message}";
            }

            switch (logLevel)
            {
                case LogLevel.Trace:
                    natsLogger.Tracef("{0}", message);
                    break;
                case LogLevel.Debug:
                    natsLogger.Debugf("{0}", message);
                    break;
                case LogLevel.Information:
                    natsLogger.Noticef("{0}", message);
                    break;
                case LogLevel.Warning:
                    natsLogger.Warnf("{0}", message);
                    break;
                case LogLevel.Error:
                    natsLogger.Errorf("{0}", message);
                    break;
                case LogLevel.Critical:
                    natsLogger.Fatalf("{0}", message);
                    break;
            }
        }
    }

    private sealed class FileNatsLogger(string filePath, bool includeTimestamp, bool useUtc) : INatsLogger, IDisposable
    {
        private readonly object _sync = new();
        private readonly string _filePath = filePath;
        private readonly bool _includeTimestamp = includeTimestamp;
        private readonly bool _useUtc = useUtc;
        private long _sizeLimit;
        private int _maxNumFiles;
        private bool _disposed;

        public void SetSizeLimit(long sizeLimit)
        {
            _sizeLimit = sizeLimit;
        }

        public void SetMaxNumFiles(int maxNumFiles)
        {
            _maxNumFiles = maxNumFiles < 0 ? 0 : maxNumFiles;
        }

        public void Noticef(string format, params object[] args) => Write("INF", format, args);
        public void Warnf(string format, params object[] args) => Write("WRN", format, args);
        public void Fatalf(string format, params object[] args) => Write("FTL", format, args);
        public void Errorf(string format, params object[] args) => Write("ERR", format, args);
        public void Debugf(string format, params object[] args) => Write("DBG", format, args);
        public void Tracef(string format, params object[] args) => Write("TRC", format, args);

        public void Dispose()
        {
            _disposed = true;
        }

        private void Write(string level, string format, object[] args)
        {
            if (_disposed)
            {
                return;
            }

            var directory = Path.GetDirectoryName(_filePath);
            if (!string.IsNullOrEmpty(directory))
            {
                Directory.CreateDirectory(directory);
            }

            var rendered = string.Format(format, args);
            var line = BuildLine(level, rendered);

            lock (_sync)
            {
                RotateIfNeeded(line);
                File.AppendAllText(_filePath, line, Encoding.UTF8);
            }
        }

        private string BuildLine(string level, string rendered)
        {
            if (!_includeTimestamp)
            {
                return $"[{level}] {rendered}{Environment.NewLine}";
            }

            var now = _useUtc ? DateTime.UtcNow : DateTime.Now;
            return $"[{now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {rendered}{Environment.NewLine}";
        }

        private void RotateIfNeeded(string nextLine)
        {
            if (_sizeLimit <= 0)
            {
                return;
            }

            var currentSize = File.Exists(_filePath) ? new FileInfo(_filePath).Length : 0L;
            var nextSize = Encoding.UTF8.GetByteCount(nextLine);
            if (currentSize + nextSize <= _sizeLimit)
            {
                return;
            }

            RotateFiles();
            File.AppendAllText(_filePath, "Rotated log, backup saved" + Environment.NewLine, Encoding.UTF8);
        }

        private void RotateFiles()
        {
            if (!File.Exists(_filePath))
            {
                return;
            }

            if (_maxNumFiles <= 1)
            {
                var backup = _filePath + ".bak";
                if (File.Exists(backup))
                {
                    File.Delete(backup);
                }

                File.Move(_filePath, backup);
                return;
            }

            for (var i = _maxNumFiles - 1; i >= 1; i--)
            {
                var src = $"{_filePath}.{i}";
                var dst = $"{_filePath}.{i + 1}";

                if (!File.Exists(src))
                {
                    continue;
                }

                if (File.Exists(dst))
                {
                    File.Delete(dst);
                }

                File.Move(src, dst);
            }

            var firstBackup = $"{_filePath}.1";
            if (File.Exists(firstBackup))
            {
                File.Delete(firstBackup);
            }

            File.Move(_filePath, firstBackup);
        }
    }

    private sealed class NoopDisposable : IDisposable
    {
        public static NoopDisposable Instance { get; } = new();

        public void Dispose()
        {
        }
    }
}
