using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EonaCat.Logger.EonaCatCoreLogger.Internal;
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 _path;
        private readonly string _fileNamePrefix;
        private readonly int _maxFileSize;
        private readonly int _maxRetainedFiles;
        private readonly int _maxRolloverFiles;
        private readonly int _maxTries;
        private int _rollOverCount = 0;
        private static readonly object WriteLock = new object();
        private static readonly object RollOverLock = new object();
        private string _logFile;
        private bool _rollingOver;

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

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

        /// <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)
        {
            FileLoggerOptions loggerOptions = options.Value;
            _path = loggerOptions.LogDirectory;
            _fileNamePrefix = loggerOptions.FileNamePrefix;
            _maxFileSize = loggerOptions.FileSizeLimit;
            _maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
            _maxRolloverFiles = loggerOptions.MaxRolloverFiles;
            _maxTries = loggerOptions.MaxWriteTries;
        }

        /// <inheritdoc />
        protected override async Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken cancellationToken)
        {
            Directory.CreateDirectory(_path);

            foreach (IGrouping<(int Year, int Month, int Day), LogMessage> group in messages.GroupBy(GetGrouping))
            {
                LogFile = GetFullName(group.Key);
                FileInfo fileInfo = new FileInfo(LogFile);
                if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize)
                {
                    if (_maxRolloverFiles > 0 && _rollOverCount >= 0)
                    {
                        if (_rollOverCount < _maxRolloverFiles)
                        {
                            var rollOverFile = LogFile.Replace(".log", $"_{++_rollOverCount}.log");
                            if (File.Exists(rollOverFile))
                            {
                                File.Delete(rollOverFile);
                            }
                            fileInfo.CopyTo(rollOverFile);
                            await File.WriteAllTextAsync(LogFile, string.Empty, cancellationToken).ConfigureAwait(false);
                        }
                        else
                        {
                            lock (RollOverLock)
                            {
                                _rollingOver = true;
                                MoveRolloverLogFiles();
                                _rollingOver = false;
                            }
                        }
                    }
                }

                while (_rollingOver)
                {
                    await Task.Delay(100, cancellationToken).ConfigureAwait(false);
                }

                lock (WriteLock)
                {
                    int tries = 0;
                    bool completed = false;

                    while (!completed)
                    {
                        try
                        {
                            System.IO.StreamWriter file = new System.IO.StreamWriter(LogFile, true);
                            foreach (LogMessage item in group)
                            {
                                file.Write(item.Message);
                            }
                            file.Close();
                            completed = true;
                        }
                        catch (Exception exc)
                        {
                            tries++;
                            Task.Delay(100);
                            if (tries >= _maxTries)
                                throw;
                        }
                    }
                }

                DeleteOldLogFiles();
            }
        }

        private string GetFullName((int Year, int Month, int Day) group)
        {
            bool hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix);
            if (hasPrefix)
            {
                return Path.Combine(_path, $"{_fileNamePrefix}_{group.Year:0000}{group.Month:00}{group.Day:00}.log");
            }
            else
            {
                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 static void MoveFile(string copyFromPath, string copyToPath)
        {
            var origin = new FileInfo(copyFromPath);
            origin.MoveTo(copyToPath);

            var destination = new FileInfo(copyToPath);
            destination.CreationTime = origin.CreationTime;
            destination.LastWriteTime = origin.LastWriteTime;
            destination.LastAccessTime = origin.LastAccessTime;
        }

        /// <summary>
        /// Rollover logFiles
        /// </summary>
        protected void MoveRolloverLogFiles()
        {
            if (_maxRolloverFiles > 0 && _rollOverCount >= 0)
            {
                if (_rollOverCount >= _maxRolloverFiles)
                {
                    var maxRollover = _rollOverCount;
                    bool hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix);
                    IEnumerable<FileInfo> files;
                    if (hasPrefix)
                    {
                        files = new DirectoryInfo(_path).GetFiles(_fileNamePrefix + "*").OrderBy(x => x.CreationTime);
                    }
                    else
                    {
                        files = new DirectoryInfo(_path).GetFiles("*").OrderBy(x => x.CreationTime);
                    }

                    for (int i = files.Count() -1; i >= 0; i--)
                    {
                        var currentFile = files.ElementAt(i);
                        if (i == 0)
                        {
                            // Temporary move first file
                            var newFilename2 = Path.GetFileName(currentFile.FullName).Replace($".log", $"_{i + 1}.log");
                            MoveFile(currentFile.FullName, $@"{Path.GetDirectoryName(currentFile.FullName)}\{newFilename2}");
                            continue;
                        }

                        if (i == files.Count() - 1)
                        {
                            // Delete the last file
                            File.Delete(currentFile.FullName);
                            continue;
                        }

                        var newFilename = Path.GetFileName(currentFile.FullName).Replace($"_{i}.log", $"_{i + 1}.log");
                        MoveFile(currentFile.FullName, $@"{Path.GetDirectoryName(currentFile.FullName)}\{newFilename}");                        
                    }
                    _rollOverCount = 0;
                }
            }
        }

        /// <summary>
        /// Deletes old log files, keeping a number of files defined by <see cref="FileLoggerOptions.RetainedFileCountLimit" />
        /// </summary>
        protected void DeleteOldLogFiles()
        {
            if (_maxRetainedFiles > 0)
            {
                bool 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 (FileInfo item in files)
                {
                    item.Delete();
                }
            }
        }
    }
}