using CSRedis;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

public static class LogHelper
{
    private static AsyncLocal<string> _asyncLocal;
    private static BlockingCollection<Log> _queue;
    private static object _lockObject;
    private static string _secretKey;
    private static IHttpContextAccessor _contextAccessor;

    public static void Initialize(string redisConnectionString, string secretKey)
    {
        _secretKey = secretKey;
        var client = new CSRedisClient(redisConnectionString);
        _queue = new BlockingCollection<Log>();
        _asyncLocal = new AsyncLocal<string>();
        _lockObject = new object();
        _contextAccessor = new HttpContextAccessor();

        Task.Factory.StartNew(() =>
        {
            while (!_queue.IsCompleted)
            {
                var log = _queue.Take();
                client.LPush("log.queue", log);
            }
        });
    }

    public static void Trace(string message, params string[] tag)
    {
        WriteLog(message, tag, LogLevel.Trace);
    }
    public static void Debug(string message, params string[] tag)
    {
        WriteLog(message, tag, LogLevel.Debug);
    }
    public static void Information(string message, params string[] tag)
    {
        WriteLog(message, tag, LogLevel.Information);
    }
    public static void Warning(string message, params string[] tag)
    {
        WriteLog(message, tag, LogLevel.Warning);
    }
    public static void Error(string message, params string[] tag)
    {
        WriteLog(message, tag, LogLevel.Error);
    }
    public static void Fatal(string message, params string[] tag)
    {
        WriteLog(message, tag, LogLevel.Fatal);
    }

    private static void WriteLog(string message, string[] tag, LogLevel logLevel)
    {
        try
        {
            if (_contextAccessor.HttpContext == null || string.IsNullOrEmpty(_contextAccessor.HttpContext.TraceIdentifier))
            {
                if (string.IsNullOrEmpty(_asyncLocal.Value))
                    lock (_lockObject)
                        Guid.NewGuid().ToString("N");
            }
            else
            {
                _asyncLocal.Value = _contextAccessor.HttpContext.TraceIdentifier;
            }

            Log log = new Log
            {
                Id = SnowflakeId.Default().NextId(),
                Level = logLevel,
                LogTime = DateTime.Now,
                Message = message,
                Stack = GetStackInfo(),
                TraceId = _asyncLocal.Value,
                Tag = tag.ToList(),
                SecretKey = _secretKey
            };
            _queue.Add(log);
        }
        catch { }
    }

    private static List<string> GetStackInfo()
    {
        StackTrace trace = new StackTrace(true);

        List<string> result = new List<string>();
        for (int i = 0; i < trace.FrameCount; i++)
        {
            StackFrame frame = trace.GetFrame(i);
            MethodBase method = frame.GetMethod();
            if (method.DeclaringType == null
                || method.DeclaringType.FullName.StartsWith("System")
                || method.DeclaringType.FullName.StartsWith("Microsoft")
                || method.DeclaringType.FullName.StartsWith("Online.Log"))
                continue;

            var paras = method.GetParameters().Select(t => $"{t.ParameterType.Name} {t.Name}");

            result.Add($"At {method.DeclaringType.FullName}.{method.Name}({string.Join(",", paras)}) Number({frame.GetFileLineNumber()})");
        }
        return result;
    }
}

internal class Log
{
    public long Id { get; set; }
    public DateTime LogTime { get; set; }
    public LogLevel Level { get; set; }
    public string SecretKey { get; set; }
    public string TraceId { get; set; }
    public string Message { get; set; }
    public List<string> Stack { get; set; }
    public List<string> Tag { get; set; }
}

internal enum LogLevel
{
    All = 0,
    Trace = 1,
    Debug = 2,
    Information = 3,
    Warning = 4,
    Error = 5,
    Fatal = 6
}

internal class SnowflakeId
{
    public const long Twepoch = 1288834974657L;

    private const int WorkerIdBits = 5;
    private const int DatacenterIdBits = 5;
    private const int SequenceBits = 12;
    private const long MaxWorkerId = -1L ^ (-1L << WorkerIdBits);
    private const long MaxDatacenterId = -1L ^ (-1L << DatacenterIdBits);

    private const int WorkerIdShift = SequenceBits;
    private const int DatacenterIdShift = SequenceBits + WorkerIdBits;
    public const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
    private const long SequenceMask = -1L ^ (-1L << SequenceBits);

    private static SnowflakeId _snowflakeId;

    private readonly object _lock = new object();
    private static readonly object SLock = new object();
    private long _lastTimestamp = -1L;

    public SnowflakeId(long workerId, long datacenterId, long sequence = 0L)
    {
        WorkerId = workerId;
        DatacenterId = datacenterId;
        Sequence = sequence;

        // sanity check for workerId
        if (workerId > MaxWorkerId || workerId < 0)
            throw new ArgumentException($"worker Id can't be greater than {MaxWorkerId} or less than 0");

        if (datacenterId > MaxDatacenterId || datacenterId < 0)
            throw new ArgumentException($"datacenter Id can't be greater than {MaxDatacenterId} or less than 0");
    }

    public long WorkerId { get; protected set; }
    public long DatacenterId { get; protected set; }

    public long Sequence { get; internal set; }

    public static SnowflakeId Default()
    {
        lock (SLock)
        {
            if (_snowflakeId != null)
            {
                return _snowflakeId;
            }

            var random = new Random();

            if (!int.TryParse(Environment.GetEnvironmentVariable("CAP_WORKERID", EnvironmentVariableTarget.Machine), out var workerId))
            {
                workerId = random.Next((int)MaxWorkerId);
            }

            if (!int.TryParse(Environment.GetEnvironmentVariable("CAP_DATACENTERID", EnvironmentVariableTarget.Machine), out var datacenterId))
            {
                datacenterId = random.Next((int)MaxDatacenterId);
            }

            return _snowflakeId = new SnowflakeId(workerId, datacenterId);
        }
    }

    public virtual long NextId()
    {
        lock (_lock)
        {
            var timestamp = TimeGen();

            if (timestamp < _lastTimestamp)
                throw new Exception(
                    $"InvalidSystemClock: Clock moved backwards, Refusing to generate id for {_lastTimestamp - timestamp} milliseconds");

            if (_lastTimestamp == timestamp)
            {
                Sequence = (Sequence + 1) & SequenceMask;
                if (Sequence == 0) timestamp = TilNextMillis(_lastTimestamp);
            }
            else
            {
                Sequence = 0;
            }

            _lastTimestamp = timestamp;
            var id = ((timestamp - Twepoch) << TimestampLeftShift) |
                     (DatacenterId << DatacenterIdShift) |
                     (WorkerId << WorkerIdShift) | Sequence;

            return id;
        }
    }

    protected virtual long TilNextMillis(long lastTimestamp)
    {
        var timestamp = TimeGen();
        while (timestamp <= lastTimestamp) timestamp = TimeGen();
        return timestamp;
    }

    protected virtual long TimeGen()
    {
        return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
    }
}

