using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace VCommon.Logging
{
    public class FileLogger : ILogger
    {
        // {logTo}\{level}\{level}_yyyyMMdd_{num:000}.log

        // [*]按时间创建
        // [*]续写
        // [*]按大小分卷
        // [*]自动删除旧文件

        public bool LogToConsole { get; set; }
        public int ByteSplit { get; }
        public int? PreserveDays { get; }
        public string LogTo { get; }

        protected enum Level
        {
            All,
            Debug,
            Trace,
            Info,
            Warn,
            Error,
            Fatal
        }

        private readonly Dictionary<Level, FileRoller> _rollers = new Dictionary<Level, FileRoller>();
        private readonly FileRoller _allRoller;

        private Task _cleanOldFileTask;
        private readonly Dictionary<string, DateTime> _files = new Dictionary<string, DateTime>();

        protected FileLogger(int mbSplit = 10, int? preserveDays = null, string logTo = null, bool enableAll = false)
        {
            //init env
            ByteSplit = mbSplit * 1024 * 1024;
            PreserveDays = preserveDays;
            LogTo = logTo ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");

            if (enableAll)
                _allRoller = new FileRoller(this, Level.All);

            if (PreserveDays.HasValue)
            {
                if (Directory.Exists(logTo))
                {
                    var files = Directory.GetFiles(LogTo, "*.log", SearchOption.AllDirectories);
                    foreach (var file in files)
                    {
                        var fn = Path.GetFileNameWithoutExtension(file)?.Split('_');
                        if (fn?.Length == 3)
                        {
                            var dt = DateTime.ParseExact(fn[1], "yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None);
                            _files[file] = dt;
                        }
                    }
                    CleanOldFiles();
                }
            }
        }

        private void NewFileCreated(string fullPath, DateTime dt)
        {
            if (!PreserveDays.HasValue) return;
            lock (_files)
            {
                _files[fullPath] = dt;
            }
        }

        private void CleanOldFiles()
        {
            if (false == PreserveDays.HasValue) return;
            if (false == _cleanOldFileTask?.IsCompleted) return;

            var timeToDelete = DateTime.Now.Date.AddDays(-PreserveDays.Value);

            _cleanOldFileTask = Task.Factory.StartNew(() =>
            {
                KeyValuePair<string, DateTime>[] files;
                lock (_files)
                {
                    files = _files.ToArray();
                }

                var filesToDelete = files.Where(p => p.Value < timeToDelete).Select(p => p.Key);
                foreach (var file in filesToDelete)
                {
                    File.Delete(file);
                    lock (_files)
                    {
                        _files.Remove(file);
                    }
                }
            });
        }

        private void WriteLogInternal(Level level, string summary, object moreInfo)
        {
            lock (this)
            {
                CleanOldFiles();

                if (!_rollers.TryGetValue(level, out var roller)) roller = _rollers[level] = new FileRoller(this, level);

                var today = DateTime.Now;
                var formatted = FormatMessage(today, level, summary, moreInfo);

                roller.WriteLine(today, formatted);
                _allRoller?.WriteLine(today, formatted);

                if (LogToConsole)
                {
                    Console.WriteLine(formatted);
                }
            }
        }

        protected virtual string FormatMessage(DateTime time, Level level, string summary, object moreInfo)
        {
            return $"{time:yyyy-MM-dd HH:mm:ss.fff} {level} {summary} {moreInfo}";
        }

        public void Debug(string summary, object moreInfo = null)
        {
            WriteLogInternal(Level.Debug, summary, moreInfo);
        }

        public void Trace(string summary, object moreInfo = null)
        {
            WriteLogInternal(Level.Trace, summary, moreInfo);
        }

        public void Info(string summary, object moreInfo = null)
        {
            WriteLogInternal(Level.Info, summary, moreInfo);
        }

        public void Warn(string summary, object moreInfo = null)
        {
            WriteLogInternal(Level.Warn, summary, moreInfo);
        }

        public void Error(string summary, object moreInfo = null)
        {
            WriteLogInternal(Level.Error, summary, moreInfo);
        }

        public void Fatal(string summary, object moreInfo = null)
        {
            WriteLogInternal(Level.Fatal, summary, moreInfo);
        }

        private class FileRoller
        {
            private readonly FileLogger _ctx;
            private readonly Level _level;

            private DateTime _fsInstanceDate;
            private int _rollNum;
            private FileStream _fsInstance;

            public FileRoller(FileLogger ctx, Level level)
            {
                _level = level;
                _ctx = ctx;
            }

            private void SwitchFile()
            {
                _fsInstance?.Close();

                var fullPath = Path.Combine(_ctx.LogTo, _level.ToString(), $"{_level}_{_fsInstanceDate:yyyyMMdd}_{_rollNum:000}.log");
                _fsInstance = File.Open(fullPath, FileMode.Append, FileAccess.Write, FileShare.Read);

                _ctx.NewFileCreated(fullPath, _fsInstanceDate);
            }

            private void EnsureFsInstance(DateTime dateTime)
            {
                var date = dateTime.Date;

                if (null != _fsInstance && _fsInstanceDate == date) return;

                _fsInstanceDate = date;

                //ensure dir
                var path = Path.Combine(_ctx.LogTo, _level.ToString());
                if (false == Directory.Exists(path)) Directory.CreateDirectory(path);

                //find last file or start from 0
                var pattern = $"{_level}_{_fsInstanceDate:yyyyMMdd}_???.log";
                var lastFile = Directory.GetFiles(path, pattern).OrderByDescending(p => p).FirstOrDefault();
                _rollNum = null != lastFile
                    ? int.Parse(Path.GetFileNameWithoutExtension(lastFile).Split('_').Last())
                    : 0;

                SwitchFile();
            }

            private void RollNext()
            {
                _rollNum++;
                SwitchFile();
            }

            public void WriteLine(DateTime date, string content)
            {
                EnsureFsInstance(date);

                var buf = Encoding.UTF8.GetBytes(content + Environment.NewLine);

                if (_fsInstance.Length + buf.Length > _ctx.ByteSplit) RollNext();

                _fsInstance.Write(buf, 0, buf.Length);
                _fsInstance.Flush();
            }
        }
    }
}