using Dapper;
using EVCB_OCPP.WEBAPI.Helpers;
using EVCB_OCPP.WEBAPI.Models;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic;
using NLog.Fluent;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

namespace EVCB_OCPP.WEBAPI.Services
{
    public class ApiLogDbService
    {
        public ApiLogDbService(
            SqlConnectionFactory<APILogDBContext> apiLogDbConnectionFactory,
            ILogger<ApiLogDbService> logger)
        {
            this.apiLogDbConnectionFactory = apiLogDbConnectionFactory;
            this.logger = logger;

            InitInsertApiLogHandler();
        }

        private readonly SqlConnectionFactory<APILogDBContext> apiLogDbConnectionFactory;
        private readonly ILogger<ApiLogDbService> logger;

        private readonly Queue<string> _existTables = new();

        private GroupSingleHandler<ApiLogEntry> insertApiLogHandler;

        public Task Write(ApiLogEntry apiLogEntry)
        {
            if (apiLogEntry == null) return Task.CompletedTask;

            try
            {
                return insertApiLogHandler.HandleAsync(apiLogEntry);
                //return WriteLogWithDapperCatch(apiLogEntry);
                //return WriteLogWithEf(apiLogEntry);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                logger.LogError(ex.Message);
                logger.LogError(ex.StackTrace);

                return Task.CompletedTask;
            }
        }

        private void InitInsertApiLogHandler()
        {
            if (insertApiLogHandler is not null)
            {
                throw new Exception($"{nameof(InitInsertApiLogHandler)} should only called once");
            }

            insertApiLogHandler = new GroupSingleHandler<ApiLogEntry>(
                BundleInsertWithDapperCatch,
                logger,
                workerCnt: 20
                );
        }

        private async Task WriteLogWithEf(ApiLogEntry log)
        {
            using (var conn = await apiLogDbConnectionFactory.CreateAsync())
            using (var cmd = new SqlCommand("[dbo].[uspInsertApiLog]", conn))
            {

                cmd.CommandType = System.Data.CommandType.StoredProcedure;
                cmd.Parameters.Add(new SqlParameter("Application", log.Application));
                cmd.Parameters.Add(new SqlParameter("User", (string.IsNullOrEmpty(log.User) ? "" : log.User)));
                cmd.Parameters.Add(new SqlParameter("Machine", log.Machine));
                cmd.Parameters.Add(new SqlParameter("RequestIpAddress", log.RequestIpAddress));
                cmd.Parameters.Add(new SqlParameter("RequestContentType", (string.IsNullOrEmpty(log.RequestContentType) ? "" : log.RequestContentType)));
                cmd.Parameters.Add(new SqlParameter("RequestUri", log.RequestUri));
                cmd.Parameters.Add(new SqlParameter("RequestMethod", log.RequestMethod));
                //cmd.Parameters.Add(new SqlParameter("RequestRouteTemplate", log.RequestRouteTemplate));
                cmd.Parameters.Add(new SqlParameter("RequestRouteTemplate", ""));
                cmd.Parameters.Add(new SqlParameter("RequestRouteData", ""));//log.RequestRouteData
                cmd.Parameters.Add(new SqlParameter("RequestHeaders", log.RequestHeaders));
                cmd.Parameters.Add(new SqlParameter("RequestTimestamp", log.RequestTimestamp));

                cmd.Parameters.Add(new SqlParameter("ResponseContentType", log.ResponseContentType));
                cmd.Parameters.Add(new SqlParameter("ResponseStatusCode", log.ResponseStatusCode));
                cmd.Parameters.Add(new SqlParameter("ResponseHeaders", log.ResponseHeaders));
                cmd.Parameters.Add(new SqlParameter("ResponseTimestamp", log.ResponseTimestamp));

                cmd.Parameters.Add(new SqlParameter("IsOutData", log.IsOutData));
                cmd.Parameters.Add(new SqlParameter("ErrorMsg", log.ErrorMsg));
                cmd.Parameters.Add(new SqlParameter("ErrorOn", log.ErrorOn));
                cmd.Parameters.Add(new SqlParameter("RequestContentBody", log.RequestContentBody));
                cmd.Parameters.Add(new SqlParameter("ResponseContentBody", log.ResponseContentBody));

                var result = await cmd.ExecuteNonQueryAsync();
            }
        }

        private async Task WriteLogWithDapperCatch(ApiLogEntry log)
        {
            try
            {
                await WriteLogWithDapper(log);
            }
            catch (Exception e)
            {
                logger.LogCritical(e.Message);
                logger.LogCritical(e.StackTrace);
            }
        }

        private async Task WriteLogWithDapper(ApiLogEntry log)
        {
            var workTime = DateTime.UtcNow;
            if (!await GetTableExist(workTime))
            {
                await WriteLogWithEf(log);
                return;
            }

            var tableName = GetTableName(workTime);

            string command = $"""
                INSERT INTO {tableName}
                (Application, [User], Machine, 
                RequestIpAddress, RequestContentType, RequestUri, RequestMethod, RequestRouteTemplate, RequestRouteData, RequestHeaders, RequestTimestamp,
                ResponseContentType, ResponseStatusCode, ResponseHeaders, ResponseTimestamp,
                IsOutData, ErrorMsg, ErrorOn, RequestContentBody, ResponseContentBody)
                VALUES 
                (@Application, @User, @Machine,
                @RequestIpAddress, @RequestContentType, @RequestUri, @RequestMethod, @RequestRouteTemplate, @RequestRouteData, @RequestHeaders, @RequestTimestamp,
                @ResponseContentType, @ResponseStatusCode, @ResponseHeaders, @ResponseTimestamp,
                @IsOutData, @ErrorMsg, @ErrorOn, @RequestContentBody, @ResponseContentBody);
                """;

            var parameters = new DynamicParameters();
            parameters.Add("@Application", log.Application, DbType.String);
            parameters.Add("@User", string.IsNullOrEmpty(log.User) ? "" : log.User, DbType.String);
            parameters.Add("@Machine", log.Machine, DbType.String);

            parameters.Add("@RequestIpAddress", log.RequestIpAddress, DbType.String);
            parameters.Add("@RequestContentType", (string.IsNullOrEmpty(log.RequestContentType) ? "" : log.RequestContentType), DbType.String);
            parameters.Add("@RequestUri", log.RequestUri, DbType.String);
            parameters.Add("@RequestMethod", log.RequestMethod, DbType.String);
            parameters.Add("@RequestRouteTemplate", "", DbType.String);
            parameters.Add("@RequestRouteData", "", DbType.String, size: 36);
            parameters.Add("@RequestHeaders", log.RequestHeaders, DbType.String);
            parameters.Add("@RequestTimestamp", log.RequestTimestamp, DbType.DateTime);

            parameters.Add("@ResponseContentType", log.ResponseContentType, DbType.String);
            parameters.Add("@ResponseStatusCode", log.ResponseStatusCode, DbType.Int32);
            parameters.Add("@ResponseHeaders", log.ResponseHeaders, DbType.String);
            parameters.Add("@ResponseTimestamp", log.ResponseTimestamp, DbType.DateTime);

            parameters.Add("@IsOutData", log.IsOutData, DbType.Boolean);
            parameters.Add("@ErrorMsg", log.ErrorMsg, DbType.String);
            parameters.Add("@ErrorOn", log.ErrorOn, DbType.DateTime);
            parameters.Add("@RequestContentBody", log.RequestContentBody, DbType.String);
            parameters.Add("@ResponseContentBody", log.ResponseContentBody, DbType.String);

            using var sqlConnection = await apiLogDbConnectionFactory.CreateAsync();
            await sqlConnection.ExecuteAsync(command, parameters);
        }

        private async Task BundleInsertWithDapperCatch(IEnumerable<ApiLogEntry> parms)
        {
            try
            {
                await BundleInsertWithDapper(parms);
            }
            catch (Exception e)
            {
                logger.LogCritical(e.Message);
                logger.LogCritical(e.StackTrace);
            }
        }

        private async Task BundleInsertWithDapper(IEnumerable<ApiLogEntry> parms)
        {
            var watch = Stopwatch.StartNew();
            List<long> times = new();

            var parmsList = parms.ToList();

            if (parmsList.Count == 0)
            {
                return;
            }

            var workTime = DateTime.UtcNow;
            var candidate = parmsList[0];
            if (!await GetTableExist(workTime))
            {
                await WriteLogWithEf(candidate);
                parmsList.Remove(candidate);
            }

            var tableName = GetTableName(workTime);

            string command = $"""
                INSERT INTO {tableName}
                (Application, [User], Machine, 
                RequestIpAddress, RequestContentType, RequestUri, RequestMethod, RequestRouteTemplate, RequestRouteData, RequestHeaders, RequestTimestamp,
                ResponseContentType, ResponseStatusCode, ResponseHeaders, ResponseTimestamp,
                IsOutData, ErrorMsg, ErrorOn, RequestContentBody, ResponseContentBody)
                VALUES 
                (@Application, @User, @Machine,
                @RequestIpAddress, @RequestContentType, @RequestUri, @RequestMethod, @RequestRouteTemplate, @RequestRouteData, @RequestHeaders, @RequestTimestamp,
                @ResponseContentType, @ResponseStatusCode, @ResponseHeaders, @ResponseTimestamp,
                @IsOutData, @ErrorMsg, @ErrorOn, @RequestContentBody, @ResponseContentBody);
                """;

            using var sqlConnection = await apiLogDbConnectionFactory.CreateAsync();
            using var trans = await sqlConnection.BeginTransactionAsync();

            foreach (var log in parmsList)
            {
                var parameters = new DynamicParameters();
                parameters.Add("@Application", log.Application, DbType.String);
                parameters.Add("@User", string.IsNullOrEmpty(log.User) ? "" : log.User, DbType.String);
                parameters.Add("@Machine", log.Machine, DbType.String);

                parameters.Add("@RequestIpAddress", log.RequestIpAddress, DbType.String);
                parameters.Add("@RequestContentType", (string.IsNullOrEmpty(log.RequestContentType) ? "" : log.RequestContentType), DbType.String);
                parameters.Add("@RequestUri", log.RequestUri, DbType.String);
                parameters.Add("@RequestMethod", log.RequestMethod, DbType.String);
                parameters.Add("@RequestRouteTemplate", "", DbType.String);
                parameters.Add("@RequestRouteData", "", DbType.String, size: 36);
                parameters.Add("@RequestHeaders", log.RequestHeaders, DbType.String);
                parameters.Add("@RequestTimestamp", log.RequestTimestamp, DbType.DateTime);

                parameters.Add("@ResponseContentType", log.ResponseContentType, DbType.String);
                parameters.Add("@ResponseStatusCode", log.ResponseStatusCode, DbType.Int32);
                parameters.Add("@ResponseHeaders", log.ResponseHeaders, DbType.String);
                parameters.Add("@ResponseTimestamp", log.ResponseTimestamp, DbType.DateTime);

                parameters.Add("@IsOutData", log.IsOutData, DbType.Boolean);
                parameters.Add("@ErrorMsg", log.ErrorMsg, DbType.String);
                parameters.Add("@ErrorOn", log.ErrorOn, DbType.DateTime);
                parameters.Add("@RequestContentBody", log.RequestContentBody, DbType.String);
                parameters.Add("@ResponseContentBody", log.ResponseContentBody, DbType.String);

                await sqlConnection.ExecuteAsync(command, parameters, trans);
            }

            await trans.CommitAsync();
        }

        private async ValueTask<bool> GetTableExist(DateTime tableDateTime)
        {
            var tableName = GetTableName(tableDateTime);
            if (_existTables.Contains(tableName))
            {
                return true;
            }

            string checkTableSql = $"SELECT Count(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{tableName}'";

            using var conn = await apiLogDbConnectionFactory.CreateAsync();
            var result = await conn.ExecuteScalarAsync<int>(checkTableSql);

            if (result > 0)
            {
                _existTables.Enqueue(tableName);
                if (_existTables.Count > 30)
                {
                    _existTables.TryDequeue(out _);
                }
                return true;
            }

            return false;
        }

        private static string GetTableName(DateTime dateTime)
            => $"ApiLogEntry{dateTime:yyMMdd}";
    }
}
