using System;
using MaxLib.WebServer;
using MaxLib.WebServer.Services;
using Serilog;
using Serilog.Events;
using System.Net;
using System.Threading.Tasks;
using Serilog.Core;

namespace Dacryptero
{
    class Program
    {
        static bool noHost;
        static ReadOnlyMemory<byte>? dbKey;
        static string? config;

        static async Task Main(string[] args)
        {
            if (!TakeArgs(args))
            {
                Console.Error.WriteLine("Invalid args");
                return;
            }
            if (config is null)
            {
                if (System.IO.File.Exists("config.ini"))
                    Config.Default.Load("config.ini");
            }
            else
            {
                if (!System.IO.File.Exists(config))
                {
                    Console.Error.WriteLine($"Config file not found: {config}");
                    return;
                }
                Config.Default.Load(config);
            }

            TaskScheduler.UnobservedTaskException += (sender, eventArgs) =>
            {
                Serilog.Log.Error(eventArgs.Exception, "Unobserved exception");
                eventArgs.SetObserved();
            };
            AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
            {
                Serilog.Log.Error((Exception)eventArgs.ExceptionObject, "Unhandled Exception, {sender}", sender);
            };

            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Verbose()
                .WriteTo.Console(Config.Default.Log.Console,
                    outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
                .WriteTo.File(
                    path: System.IO.Path.Combine(Config.Default.Log.FileDir, "log.txt"),
                    restrictedToMinimumLevel: Config.Default.Log.File,
                    outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}",
                    fileSizeLimitBytes: null,
                    rollingInterval: RollingInterval.Day
                )
                .CreateLogger();
            WebServerLog.LogPreAdded += WebServerLog_LogPreAdded;

            await Data.Schema.SchemaCollection.LoadSchemas(
                Config.Default.Schema.BaseDir
                // System.IO.Path.Combine(Environment.CurrentDirectory, "schemas")
            ).ConfigureAwait(false);

            // var namegen = await Data.Generator.NameGenerator.GetNameGeneratorAsync().ConfigureAwait(false);
            // Log.Verbose("Generated names: {names}", string.Join(", ", namegen.GenerateNames(null, 10)));
            // var wikimarkow = await Data.Generator.WikiMarkow.GetWikiMarkowAsync("Germany").ConfigureAwait(false);
            // await System.IO.File.WriteAllTextAsync("test.txt", wikimarkow.Generate(5000))
            //     .ConfigureAwait(false);

            var controller = new DB.DBMultiController();
            await controller.LoadController(
                Config.Default.DB.BaseDir
                // System.IO.Path.Combine(Environment.CurrentDirectory, "db")
            ).ConfigureAwait(false);
#if DEBUG
            if (dbKey is not null)
            {
                if (!Data.Schema.SchemaCollection.Schemas.TryGetValue(
                    "/internal/schemas/large-test.json",
                    out Data.Schema.SchemaFile? file
                ))
                {
                    var gen = new Data.Generator.SchemaGenerator();
                    file = gen.GenerateFile(System.IO.Path.Combine(
                        Config.Default.Schema.BaseDir,
                        "internal/schemas/large-test.json"
                    ));
                    var fi = new System.IO.FileInfo(file.LocalPath);
                    if (!fi.Directory?.Exists ?? false)
                        fi.Directory?.Create();
                    await file.SaveConfig().ConfigureAwait(false);
                    Data.Schema.SchemaCollection.Schemas[file.Name] = file;
                }
                var single = controller.GetController("eth-test") ?? 
                    controller.AddController(DB.DBConfig.GetTestConfig(
                        Config.Default.DB.BaseDir,
                        "eth",
                        "/eth/dev/v1.json"
                    ));
                await single.Config.SaveAsync().ConfigureAwait(false);
                single.Decrypt(dbKey.Value, true);
                var genLockPath = System.IO.Path.Combine(
                    Config.Default.DB.BaseDir,
                    "eth-test", "generated"
                );
                _ = Task.Run(async () =>
                {
                    try
                    {
                        if (!Data.Schema.SchemaCollection.Schemas.TryGetValue(
                            "/eth/dev/v1.json",
                            out Data.Schema.SchemaFile? file
                        ))
                        {
                            Serilog.Log.Warning("Cannot create db for eth-test because of missing schema");
                            return;
                        }
                        if (!System.IO.File.Exists(genLockPath))
                        {
                            var dbGen = new Data.Generator.DbDataGenerator(single, file);
                            await dbGen.GenerateAsync(new Notifications.GlobalTransmitter()).ConfigureAwait(false);
                            await System.IO.File.WriteAllTextAsync(genLockPath, "").ConfigureAwait(false);
                        }
                        await Web.WebSocketConnection.Foreach(
                            x => x.Send(new Web.Events.Send.InfoSend(x))
                        );
                        if (!single.Config.ContainsIndex)
                            await single.RebuildIndex(
                                false,
                                new Notifications.GlobalTransmitter(),
                                System.Threading.CancellationToken.None
                            );
                    }
                    catch (Exception e)
                    {
                        Serilog.Log.Error(e, "cannot generate db");
                    }
                });
            }
#endif


            try
            {
                await MimeType.LoadMimeTypesForExtensions(true).ConfigureAwait(false);
            }
            catch (System.IO.IOException e)
            {
                Log.Warning(e, "Cannot load mime cache");
            }

            Server server;
            if (System.IO.File.Exists(Config.Default.Server.Certificate))
            {
                System.Security.Cryptography.X509Certificates.X509Certificate cert;
                try
                {
                    cert = System.Security.Cryptography.X509Certificates.X509Certificate2.CreateFromPemFile(
                        certPemFilePath: Config.Default.Server.Certificate,
                        keyPemFilePath: System.IO.File.Exists(Config.Default.Server.CertificateKey) ?
                            Config.Default.Server.CertificateKey : null
                    );
                }
                catch (Exception e)
                {
                    Serilog.Log.Fatal(e, "cannot load secure certificate from {file}", Config.Default.Server.Certificate);
                    return;
                }
                server = new MaxLib.WebServer.SSL.SecureWebServer(
                    new MaxLib.WebServer.SSL.SecureWebServerSettings(Config.Default.Server.Port, 5000)
                    {
                        Certificate = cert,
                        IPFilter = Config.Default.Server.Interface,
                        EnableUnsafePort = false,
                    }
                );
            }
            else
            {
                server = new Server(new WebServerSettings(Config.Default.Server.Port, 5000)
                {
                    IPFilter = Config.Default.Server.Interface,
                });
            }
            server.AddWebService(new HttpRequestParser());
            server.AddWebService(new HttpHeaderPostParser());
            server.AddWebService(new HttpHeaderSpecialAction());
            server.AddWebService(new Http404Service());
            server.AddWebService(new HttpResponseCreator());
            server.AddWebService(new HttpSender());
            server.AddWebService(new Web.CorsService());
            server.AddWebService(new Web.UploadService());
            server.AddWebService(new Web.RootService());
            server.AddWebService(new Web.SecurityMasterKey());
            server.AddWebService(new Web.StorageService());
            server.AddWebService(new Web.StorageInvalidService());
            server.AddWebService(new MaxLib.WebServer.Chunked.ChunkedResponseCreator(true));
            server.AddWebService(new MaxLib.WebServer.Chunked.ChunkedSender(true));

            if (!noHost)
            {
                var mapper = new LocalIOMapper()
                {
                    // Priority = WebServicePriority.Low,
                };
                mapper.AddFileMapping("/", Config.Default.Server.UiDir);
                // mapper.AddFileMapping("/index.html", "index.html");
                // mapper.AddFileMapping("/", "index.html");
                // mapper.AddFileMapping("/index.js", "index.min.js");
                // mapper.AddFileMapping("/css", "css");
                // mapper.AddFileMapping("/img", "img");
                server.AddWebService(mapper);
            }

            var ws = new  MaxLib.WebServer.WebSocket.WebSocketService();
            ws.Add(new Web.WebSocketEndpoint(controller));
            ws.CloseEndpoint = new MaxLib.WebServer.WebSocket.WebSocketCloserEndpoint(
                MaxLib.WebServer.WebSocket.CloseReason.NormalClose,
                "not allowed"
            );
            server.AddWebService(ws);

            // MaxLib.WebServer.Post.MultipartFormData.StorageMapper = 
            //     (task, @base) => new Web.ThrottleStream(@base, TimeSpan.FromSeconds(1));

            server.Start();

            await Task.Delay(-1).ConfigureAwait(false);

            server.Stop();
        }

        private static bool TakeArgs(ReadOnlySpan<string> args)
        {
            for (int i = 0; i < args.Length; ++i)
            {
                if (!args[i].StartsWith("--"))
                    return false;
                switch (args[i])
                {
                    // The path of the config file to use
                    case "--config":
                        if (i + 1 >= args.Length)
                            return false;
                        config = args[++i];
                        break;
#if DEBUG
                    // [DEV] This server will not host the content in the ui dir
                    case "--no-host":
                        noHost = true;
                        break;
                    // [DEV] This server will use a fixed db named "test" and use the provided key
                    // in base64 to access it. This db is available for all connections.
                    case "--db-key":
                    {
                        if (i + 1 >= args.Length)
                            return false;
                        Memory<byte> buffer = new byte[args[i + 1].Length * 3 / 4];
                        if (!Convert.TryFromBase64String(args[i + 1], buffer.Span, out int written))
                            return false;
                        dbKey = buffer[..written];
                        i++;
                        break;
                    }
#endif
                    default: return false;
                }
            }
            return true;
        }
		
        private static readonly MessageTemplate serilogMessageTemplate =
            new Serilog.Parsing.MessageTemplateParser().Parse(
                "{infoType}: {info}"
            );

        private static void WebServerLog_LogPreAdded(ServerLogArgs e)
        {
            e.Discard = true;
            var info = e.LogItem.Information;
            if (e.LogItem.InfoType == "Header" && e.LogItem.Type == ServerLogType.Debug &&
                e.LogItem.Information.StartsWith("GET /storage/")
            )
            {
                var index = e.LogItem.Information.LastIndexOf(' ');
                var suffix = index >= 0 ? e.LogItem.Information.Substring(index) : "";
                info = "GET /storage/<**confidential path removed**>" + suffix;
            }
            Log.Write(new LogEvent(
                e.LogItem.Date,
                e.LogItem.Type switch
                {
                    ServerLogType.Debug => LogEventLevel.Verbose,
                    ServerLogType.Information => LogEventLevel.Debug,
                    ServerLogType.Error => LogEventLevel.Error,
                    ServerLogType.FatalError => LogEventLevel.Fatal,
                    _ => LogEventLevel.Information,
                },
                null,
                serilogMessageTemplate,
                new[]
                {
                    new LogEventProperty("infoType", new ScalarValue(e.LogItem.InfoType)),
                    new LogEventProperty("info", new ScalarValue(info))
                }
            ));
        }
    }
}
