using GitHub.Runner.Sdk;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;

namespace GitHub.Runner.Common
{
    public sealed class HostTraceListener : TextWriterTraceListener
    {
        private const string _logFileNamingPattern = "{0}_{1:yyyyMMdd-HHmmss}-utc.log";
        private string _logFileDirectory;
        private string _logFilePrefix;
        private bool _enablePageLog = false;
        private bool _enableLogRetention = false;
        private int _currentPageSize;
        private int _pageSizeLimit;
        private int _retentionDays;

        public HostTraceListener(string logFileDirectory, string logFilePrefix, int pageSizeLimit, int retentionDays)
            : base()
        {
            ArgUtil.NotNullOrEmpty(logFileDirectory, nameof(logFileDirectory));
            ArgUtil.NotNullOrEmpty(logFilePrefix, nameof(logFilePrefix));
            _logFileDirectory = logFileDirectory;
            _logFilePrefix = logFilePrefix;

            Directory.CreateDirectory(_logFileDirectory);

            if (pageSizeLimit > 0)
            {
                _enablePageLog = true;
                _pageSizeLimit = pageSizeLimit * 1024 * 1024;
                _currentPageSize = 0;
            }

            if (retentionDays > 0)
            {
                _enableLogRetention = true;
                _retentionDays = retentionDays;
            }

            Writer = CreatePageLogWriter();
        }

        public HostTraceListener(string logFile)
            : base()
        {
            ArgUtil.NotNullOrEmpty(logFile, nameof(logFile));
            Directory.CreateDirectory(Path.GetDirectoryName(logFile));
            Stream logStream = new FileStream(logFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, bufferSize: 4096);
            Writer = new StreamWriter(logStream);
        }

        // Copied and modified slightly from .Net Core source code. Modification was required to make it compile.
        // There must be some TraceFilter extension class that is missing in this source code.
        public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
        {
            if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, message, null, null, null))
            {
                return;
            }

            WriteHeader(source, eventType, id);
            WriteLine(message);
            WriteFooter(eventCache);
        }

        public override void WriteLine(string message)
        {
            base.WriteLine(message);
            if (_enablePageLog)
            {
                int messageSize = UTF8Encoding.UTF8.GetByteCount(message);
                _currentPageSize += messageSize;
                if (_currentPageSize > _pageSizeLimit)
                {
                    Flush();
                    if (Writer != null)
                    {
                        Writer.Dispose();
                        Writer = null;
                    }

                    Writer = CreatePageLogWriter();
                    _currentPageSize = 0;
                }
            }

            Flush();
        }

        public override void Write(string message)
        {
            base.Write(message);
            if (_enablePageLog)
            {
                int messageSize = UTF8Encoding.UTF8.GetByteCount(message);
                _currentPageSize += messageSize;
            }

            Flush();
        }

        internal bool IsEnabled(TraceOptions opts)
        {
            return (opts & TraceOutputOptions) != 0;
        }

        // Altered from the original .Net Core implementation.
        private void WriteHeader(string source, TraceEventType eventType, int id)
        {
            string type = null;
            switch (eventType)
            {
                case TraceEventType.Critical:
                    type = "CRIT";
                    break;
                case TraceEventType.Error:
                    type = "ERR ";
                    break;
                case TraceEventType.Warning:
                    type = "WARN";
                    break;
                case TraceEventType.Information:
                    type = "INFO";
                    break;
                case TraceEventType.Verbose:
                    type = "VERB";
                    break;
                default:
                    type = eventType.ToString();
                    break;
            }

            Write(StringUtil.Format("[{0:u} {1} {2}] ", DateTime.UtcNow, type, source));
        }

        // Copied and modified slightly from .Net Core source code to make it compile. The original code
        // accesses a private indentLevel field. In this code it has been modified to use the getter/setter.
        private void WriteFooter(TraceEventCache eventCache)
        {
            if (eventCache == null)
                return;

            IndentLevel++;
            if (IsEnabled(TraceOptions.ProcessId))
                WriteLine("ProcessId=" + eventCache.ProcessId);

            if (IsEnabled(TraceOptions.ThreadId))
                WriteLine("ThreadId=" + eventCache.ThreadId);

            if (IsEnabled(TraceOptions.DateTime))
                WriteLine("DateTime=" + eventCache.DateTime.ToString("o", CultureInfo.InvariantCulture));

            if (IsEnabled(TraceOptions.Timestamp))
                WriteLine("Timestamp=" + eventCache.Timestamp);

            IndentLevel--;
        }

        private StreamWriter CreatePageLogWriter()
        {
            if (_enableLogRetention)
            {
                DirectoryInfo diags = new(_logFileDirectory);
                var logs = diags.GetFiles($"{_logFilePrefix}*.log");
                foreach (var log in logs)
                {
                    if (log.LastWriteTimeUtc.AddDays(_retentionDays) < DateTime.UtcNow)
                    {
                        try
                        {
                            log.Delete();
                        }
                        catch (Exception)
                        {
                            // catch Exception and continue
                            // we shouldn't block logging and fail the runner if the runner can't delete an older log file.
                        }
                    }
                }
            }

            string fileName = StringUtil.Format(_logFileNamingPattern, _logFilePrefix, DateTime.UtcNow);
            string logFile = Path.Combine(_logFileDirectory, fileName);
            Stream logStream;
            if (File.Exists(logFile))
            {
                logStream = new FileStream(logFile, FileMode.Append, FileAccess.Write, FileShare.Read, bufferSize: 4096);
            }
            else
            {
                logStream = new FileStream(logFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, bufferSize: 4096);
            }

            return new StreamWriter(logStream);
        }
    }
}
