using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EonaCat.Logger.EonaCatCoreLogger.Internal;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace EonaCat.Logger.EonaCatCoreLogger;
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.

/// <summary>
///     An <see cref="ILoggerProvider" /> that writes logs to a file
/// </summary>
[ProviderAlias("EonaCatFileLogger")]
public class FileLoggerProvider : BatchingLoggerProvider
{
    private readonly string _fileNamePrefix;
    private readonly int _maxFileSize;
    private readonly int _maxRetainedFiles;
    private readonly int _maxRolloverFiles;
    private readonly int _maxTries;
    private readonly string _path;
    private string _logFile;
    private bool _rollingOver;
    private int _rollOverCount;
    private ConcurrentDictionary<string,string> _buffer = new ConcurrentDictionary<string, string>();
    private readonly LoggerScopedContext _context = new();

    public event EventHandler<ErrorMessage> OnError;

    /// <summary>
    ///     Creates an instance of the <see cref="FileLoggerProvider" />
    /// </summary>
    /// <param name="options">The options object controlling the logger</param>
    public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
    {
        var loggerOptions = options.Value;
        _path = loggerOptions.LogDirectory;
        _fileNamePrefix = loggerOptions.FileNamePrefix;
        _maxFileSize = loggerOptions.FileSizeLimit;
        _maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
        _maxRolloverFiles = loggerOptions.MaxRolloverFiles;
        _maxTries = loggerOptions.MaxWriteTries;
        IncludeCorrelationId = loggerOptions.IncludeCorrelationId;
    }

    /// <summary>
    ///     The file to which log messages should be appended.
    /// </summary>
    public string LogFile
    {
        get => _logFile;
        set
        {
            _logFile = value;

            if (!string.IsNullOrEmpty(_logFile))
            {
                var dir = Path.GetDirectoryName(_logFile);
                if (!string.IsNullOrEmpty(dir))
                {
                    if (!Directory.Exists(dir))
                    {
                        Directory.CreateDirectory(dir);
                    }
                }
            }
        }
    }

    public void SetContext(string key, string value) => _context.Set(key, value);
    public void ClearContext() => _context.Clear();
    public string GetContext(string key) => _context.Get(key);

    /// <inheritdoc />
    protected override async Task WriteMessagesAsync(IEnumerable<LogMessage> messages,
        CancellationToken cancellationToken)
    {
        if (IsWriting)
        {
            return;
        }

        IsWriting = true;

        try
        {
            Directory.CreateDirectory(_path);

            foreach (var group in messages.GroupBy(GetGrouping))
            {
                LogFile = GetFullName(group.Key);
                
                var currentMessages = string.Join(string.Empty, group.Select(item =>
                {
                    if (IncludeCorrelationId)
                    {
                        var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
                        _context.Set("CorrelationId", correlationId);
                    }

                    var contextData = _context.GetAll();
                    var contextInfo = contextData.Count > 0
                        ? string.Join(" ", contextData.Select(kvp => $"{kvp.Key}={kvp.Value}"))
                        : string.Empty;

                    if (!string.IsNullOrWhiteSpace(contextInfo))
                    {
                        // if the message ends with a new line, remove it
                        if (item.Message.EndsWith(Environment.NewLine))
                        {
                            item.Message = item.Message.Substring(0, item.Message.Length - Environment.NewLine.Length);
                        }
                        return $"{item.Message} [{contextInfo}]{Environment.NewLine}";
                    }
                    return $"{item.Message}";
                }));

                if (!_buffer.TryAdd(LogFile, currentMessages))
                {
                    _buffer[LogFile] += currentMessages;
                }

                // Check file size
                FileInfo fileInfo = new FileInfo(LogFile);
                if (fileInfo.Exists && fileInfo.Length >= _maxFileSize)
                {
                    // Roll over the log file
                    await RollOverLogFile().ConfigureAwait(false);
                }

                if (await TryWriteToFileAsync(cancellationToken))
                {
                    // Clear buffer on success
                    _buffer.Clear();
                }
                else if (await WriteToTempFileAsync(cancellationToken))
                {
                    // Fallback to temp file
                }

                DeleteOldLogFiles();
            }
        }
        catch (Exception exception)
        {
            OnError?.Invoke(this, new ErrorMessage { Exception = exception, Message = "Cannot write to file"});
        }
        finally
        {
            IsWriting = false;
        }
    }

    public bool IsWriting { get; set; }
    public bool IncludeCorrelationId { get; set; }

    private async Task<bool> TryWriteToFileAsync(CancellationToken cancellationToken)
    {
        if (!_buffer.ContainsKey(LogFile))
        {
            return true;
        }

        var tries = 0;
        var completed = false;

        while (!completed)
        {
            try
            {
                using (var file = new StreamWriter(LogFile, true))
                {
                    await file.WriteAsync(_buffer[LogFile]).ConfigureAwait(false);
                }
                completed = true;
                _buffer.TryRemove(LogFile, out _);
                return true;
            }
            catch (Exception)
            {
                tries++;
                await Task.Delay(100, cancellationToken).ConfigureAwait(false);
                if (tries >= _maxTries)
                {
                    OnError?.Invoke(this, new ErrorMessage { Message = "Cannot write to log folder"});
                    return false;
                }
            }
        }
        return false;
    }

    private async Task<bool> WriteToTempFileAsync(CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(LogFile) || !_buffer.ContainsKey(LogFile))
        {
            return false;
        }

        var tempLogFolder = Path.Combine(Path.GetTempPath(), "EonaCatLogs");
        var tempLogFile = $"{Path.Combine(tempLogFolder, Path.GetFileNameWithoutExtension(LogFile))}.log";

        try
        {
            Directory.CreateDirectory(tempLogFolder);

            // Create new temp file
            using (var file = new StreamWriter(tempLogFile, true)) 
            {
                await file.WriteAsync(_buffer[LogFile]).ConfigureAwait(false);
            }

            _buffer.TryRemove(LogFile, out _);
            return true;
        }
        catch (Exception exception)
        {
            OnError?.Invoke(this, new ErrorMessage { Message = "Cannot write to temp folder ", Exception = exception});
            return false;
        }
    }

    private string GetFullName((int Year, int Month, int Day) group)
    {
        var hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix);
        if (hasPrefix)
        {
            return Path.Combine(_path, $"{_fileNamePrefix}_{group.Year:0000}{group.Month:00}{group.Day:00}.log");
        }

        return Path.Combine(_path, $"{group.Year:0000}{group.Month:00}{group.Day:00}.log");
    }

    private (int Year, int Month, int Day) GetGrouping(LogMessage message)
    {
        return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day);
    }

    private readonly object lockObj = new object();

    private async Task RollOverLogFile()
    {
        lock (lockObj)
        {
            try
            {
                string directoryPath = Path.GetDirectoryName(LogFile);
                string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(LogFile);
                string fileExtension = Path.GetExtension(LogFile);

                // Rename existing files
                for (int i = _maxRolloverFiles - 1; i >= 1; i--)
                {
                    string currentFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}.{i}{fileExtension}");
                    string newFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}.{(i + 1)}{fileExtension}");

                    if (File.Exists(currentFilePath))
                    {
                        if (File.Exists(newFilePath))
                        {
                            File.Delete(newFilePath);
                        }
                        File.Move(currentFilePath, newFilePath);
                    }

                    if (i != 1)
                    {
                        continue;
                    }

                    currentFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}{fileExtension}");
                    newFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}.{i}{fileExtension}");
                    File.Move(currentFilePath, newFilePath);
                }

                // Rename current log file
                var rolloverFilePath = Path.Combine(directoryPath, $"{fileNameWithoutExtension}.1{fileExtension}");

                // Write end message and start processing in background
                Task.Run(async () =>
                {
                    await WriteEndMessageAsync(rolloverFilePath).ConfigureAwait(false);
                    await ProcessAsync().ConfigureAwait(false);
                });
            }
            catch (Exception ex)
            {
                // Log or handle the exception
                Console.WriteLine($"Error occurred during log file rollover: {ex.Message}");
            }
        }
    }

    private async Task WriteEndMessageAsync(string logFilePath)
    {
        var stopMessage = LogHelper.GetStopMessage();
        stopMessage = LogHelper.FormatMessageWithHeader(LoggerSettings, ELogType.INFO, stopMessage, CurrentDateTme, Category);

        using (var file = new StreamWriter(logFilePath, true))
        {
            await file.WriteAsync(stopMessage).ConfigureAwait(false);
        }
    }

    private async Task ProcessAsync()
    {
        try
        {
            await WriteStartMessage().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            // Log or handle the exception
            Console.WriteLine($"Error occurred during log file rollover: {ex.Message}");
        }
    }

    /// <summary>
    ///     Deletes old log files, keeping a number of files defined by <see cref="FileLoggerOptions.RetainedFileCountLimit" />
    /// </summary>
    protected void DeleteOldLogFiles()
    {
        if (_maxRetainedFiles > 0)
        {
            var hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix);
            IEnumerable<FileInfo> files = null;

            if (hasPrefix)
            {
                files = new DirectoryInfo(_path).GetFiles(_fileNamePrefix + "*");
            }
            else
            {
                files = new DirectoryInfo(_path).GetFiles("*");
            }

            files = files.OrderByDescending(file => file.Name).Skip(_maxRetainedFiles);

            foreach (var item in files)
            {
                item.Delete();
            }
        }
    }
}