using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Http;

using AyaNova.Models;
using AyaNova.Util;
using AyaNova.Generator;
using AyaNova.Biz;

using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using Swashbuckle.AspNetCore.SwaggerUI;

using System.IO;
using System.Reflection;
using System.Linq;
using System;



namespace AyaNova
{

    public class Startup
    {


        /////////////////////////////////////////////////////////////
        //
        public Startup(ILogger<Startup> logger, ILoggerFactory logFactory, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment)
        {
            _log = logger;
            _hostingEnvironment = hostingEnvironment;
            AyaNova.Util.ApplicationLogging.LoggerFactory = logFactory;
            //this must be set here
            ServerBootConfig.AYANOVA_CONTENT_ROOT_PATH = hostingEnvironment.ContentRootPath;

        }

        private readonly ILogger<Startup> _log;
        private string _connectionString = "";
        private readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment _hostingEnvironment;

        ////////////////////////////////////////////////////////////
        // This method gets called by the runtime. Use this method to add services to the container.
        //
        public void ConfigureServices(IServiceCollection services)
        {
            _log.LogDebug("BOOT: initializing services...");

            //Server state service for shutting people out of api
            _log.LogDebug("BOOT: init ApiServerState service");
            services.AddSingleton(new AyaNova.Api.ControllerHelpers.ApiServerState());

            // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
            // note: the specified format code will format the version as "'v'major[.minor][-status]"
            _log.LogDebug("BOOT: init ApiExplorer service");
            services.AddMvcCore().AddVersionedApiExplorer(o => o.GroupNameFormat = "'v'VVV");

            _log.LogDebug("BOOT: ensuring user and backup folders exist and are separate locations...");
            FileUtil.EnsureUserAndUtilityFoldersExistAndAreNotIdentical(_hostingEnvironment.ContentRootPath);

            #region DATABASE
            _connectionString = ServerBootConfig.AYANOVA_DB_CONNECTION;

            //Check DB server exists and can be connected to
            _log.LogDebug("BOOT: Testing database server connection...");

            //parse the connection string properly
            DbUtil.ParseConnectionString(_log, _connectionString);

            //Probe for database server
            //Will retry every 10 seconds for up to 5 minutes before bailing
            if (!DbUtil.DatabaseServerExists(_log, "BOOT: waiting for db server "))
            {
                var err = $"BOOT: E1000 - AyaNova can't connect to the database server after trying for 5 minutes (connection string is:\"{DbUtil.DisplayableConnectionString}\")";
                _log.LogCritical(err);
                throw new System.ApplicationException(err);
            }



            _log.LogInformation("BOOT: Connected to database server - {0}", DbUtil.DisplayableConnectionString);


            //ensure database is ready and present
            DbUtil.EnsureDatabaseExists(_log);

            bool LOG_SENSITIVE_DATA = false;

#if (DEBUG)
            //LOG_SENSITIVE_DATA = true;

#endif

            _log.LogDebug("BOOT: init EF service");

            services.AddEntityFrameworkNpgsql().AddDbContext<AyContext>(
                options => options.UseNpgsql(_connectionString
                //,opt => opt.EnableRetryOnFailure()//REMOVED THIS BECAUSE IT WAS INTEFERING WITH TRANSACTIONS BUT THEN DIDN'T USE THE TRANSACTION BUT IT SEEMS FASTER WITHOUT IT AS WELL SO...??
                )//http://www.npgsql.org/efcore/misc.html?q=execution%20strategy#execution-strategy
                            .ConfigureWarnings(warnings => //https://livebook.manning.com/#!/book/entity-framework-core-in-action/chapter-12/v-10/85
                                    warnings.Throw( //Throw an exception on client eval, not necessarily an error but a smell
                                    Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.QueryClientEvaluationWarning))
                            .EnableSensitiveDataLogging(LOG_SENSITIVE_DATA)
                );




            #endregion

            _log.LogDebug("BOOT: init ApiVersioning service");
            // services.AddApiVersioning(o => o.ReportApiVersions = true);
            services
                 .AddApiVersioning(options =>
                 {
                     options.AssumeDefaultVersionWhenUnspecified = true;
                     options.DefaultApiVersion = Microsoft.AspNetCore.Mvc.ApiVersion.Parse("8.0");
                     options.ReportApiVersions = true;
                 });


            // Add service and create Policy with options
            _log.LogDebug("BOOT: init CORS service");
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder.AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .AllowCredentials());
            });


            _log.LogDebug("BOOT: init MVC service");
            _log.LogDebug("BOOT: init Metrics service");


            services.AddMvc(config =>
            {
                //was this but needed logging, not certain about the new way of adding so keeping this in case it all goes sideways in testing
                //config.Filters.Add(typeof(AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter));
                config.Filters.Add(new AyaNova.Api.ControllerHelpers.ApiCustomExceptionFilter(AyaNova.Util.ApplicationLogging.LoggerFactory));

            }).AddMetrics();


            #region Swagger
            //https://docs.microsoft.com/en-us/aspnet/core/tutorials/web-api-help-pages-using-swagger?tabs=visual-studio-code
            //https://swagger.io/
            //https://github.com/domaindrivendev/Swashbuckle.AspNetCore

            _log.LogDebug("BOOT: init API explorer service");
            services.AddSwaggerGen(
                           c =>
                           {
                               // resolve the IApiVersionDescriptionProvider service
                               // note: that we have to build a temporary service provider here because one has not been created yet
                               var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();

                               // add a swagger document for each discovered API version
                               // note: you might choose to skip or document deprecated API versions differently
                               foreach (var description in provider.ApiVersionDescriptions)
                               {
                                   c.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
                               }

                               // add a custom operation filter which sets default values
                               c.OperationFilter<SwaggerDefaultValues>();

                               // integrate xml comments
                               c.IncludeXmlComments(XmlCommentsFilePath);



                               //this is required to allow authentication when testing secure routes via swagger UI
                               c.AddSecurityDefinition("Bearer", new ApiKeyScheme
                               {
                                   Description = "JWT Authorization header using the Bearer scheme. Get your token by logging in via the Auth route then enter it here with the \"Bearer \" prefix. Example: \"Bearer {token}\"",
                                   Name = "Authorization",
                                   In = "header",
                                   Type = "apiKey"

                               });


                               c.AddSecurityRequirement(new System.Collections.Generic.Dictionary<string, System.Collections.Generic.IEnumerable<string>>
                                   {
                                       { "Bearer", new string[] { } }
                                   });



                           });


            #endregion


            #region JWT AUTHENTICATION
            //get the key if specified
            var secretKey = ServerBootConfig.AYANOVA_JWT_SECRET;

            //If no key specified make a unique one based on license ID which is unique to each customer or trialler
            if (string.IsNullOrWhiteSpace(secretKey))
            {
                // This ensures a key can't work on another AyaNova installation
                if (AyaNova.Core.License.ActiveKey.TrialLicense)
                    secretKey = AyaNova.Core.License.ActiveKey.Id + "5G*QQJ8#bQ7$Xr_@sXfHq4";
                else
                    secretKey = AyaNova.Core.License.ActiveKey.RegisteredTo + "5G*QQJ8#bQ7$Xr_@sXfHq4";
            }

            //If secretKey is less than 32 characters, pad it
            if (secretKey.Length < 32)
            {
                secretKey = secretKey.PadRight(32, '-');
            }

            ServerBootConfig.AYANOVA_JWT_SECRET = secretKey;
            var signingKey = new SymmetricSecurityKey(System.Text.Encoding.ASCII.GetBytes(ServerBootConfig.AYANOVA_JWT_SECRET));

            _log.LogDebug("BOOT: init Authorization service");
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(options =>
                {
                    // options.AutomaticAuthenticate = true;
                    // options.AutomaticChallenge = true;
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        // Token signature will be verified using a private key.
                        ValidateIssuerSigningKey = true,
                        RequireSignedTokens = true,
                        IssuerSigningKey = signingKey,
                        ValidateIssuer = true,
                        ValidIssuer = "ayanova.com",
                        ValidateAudience = false,
                        //ValidAudience = "http://localhost:7575/"

                        // Token will only be valid if not expired yet, with 5 minutes clock skew.
                        ValidateLifetime = true,
                        RequireExpirationTime = true,
                        ClockSkew = new TimeSpan(0, 5, 0),
                    };
                });



            #endregion

            _log.LogDebug("BOOT: init Generator service");
            services.AddSingleton<IHostedService, GeneratorService>();


        }



        ////////////////////////////////////////////////////////////
        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        //
        public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHostingEnvironment env,
        AyContext dbContext, IApiVersionDescriptionProvider provider, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, IServiceProvider serviceProvider)
        {
            _log.LogDebug("BOOT: configuring request pipeline...");


            //Store a reference to the dependency injection service for static classes 
            ServiceProviderProvider.Provider = app.ApplicationServices;

            //Enable ability to handle reverse proxy
            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto
            });


            #region SWAGGER

            _log.LogDebug("BOOT: pipeline - api explorer");
            // Enable middleware to serve generated Swagger as a JSON endpoint.
            app.UseSwagger();

            app.UseSwaggerUI(c =>
            {
                // build a swagger endpoint for each discovered API version
                foreach (var description in provider.ApiVersionDescriptions)
                {
                    c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
                }

                //clean up the swagger explorer UI page and remove the branding
                //via our own css
                //NOTE: this broke when updated to v2.x of swagger and it can be fixed according to docs:
                //https://github.com/domaindrivendev/Swashbuckle.AspNetCore#inject-custom-css
                // c.InjectStylesheet("/api/sw.css");

                c.DefaultModelsExpandDepth(-1);
                c.DocumentTitle = "AyaNova API explorer";
                c.RoutePrefix = "api-docs";
            });
            #endregion swagger

            #region STATIC FILES
            _log.LogDebug("BOOT: pipeline - static files");
            app.UseDefaultFiles();
            app.UseStaticFiles();
            //Might need the following if the page doesn't update in the client properly
            //however the vue build process will automatically uniquify each build file names so maybe not required
            // app.UseStaticFiles(new StaticFileOptions
            // {
            //     OnPrepareResponse = context =>
            //     {
            //         if (context.File.Name == "index.html")
            //         {
            //             context.Context.Response.Headers.Add("Cache-Control", "no-cache, no-store");
            //             context.Context.Response.Headers.Add("Expires", "-1");
            //         }
            //     }
            // });
            #endregion

            #region AUTH / ROLES
            _log.LogDebug("BOOT: pipeline - authentication");
            //Use authentication middleware
            app.UseAuthentication();

            //Custom middleware to get user roles and put them into the request so 
            //they can be authorized in routes
            app.Use(async (context, next) =>
            {
                if (!context.User.Identity.IsAuthenticated)
                {
                    context.Request.HttpContext.Items["AY_ROLES"] = 0;
                }
                else
                {
                    //Get user ID from claims
                    long userId = Convert.ToInt64(context.User.FindFirst(c => c.Type == "id").Value);

                    //Get the database context
                    var ct = context.RequestServices.GetService<AyContext>();

                    //get the user record
                    var u = ct.User.AsNoTracking().Where(a => a.Id == userId).Select(m => new { roles = m.Roles, name = m.Name, id = m.Id, localeId = m.LocaleId }).First();
                    context.Request.HttpContext.Items["AY_ROLES"] = u.roles;
                    context.Request.HttpContext.Items["AY_USERNAME"] = u.name;
                    context.Request.HttpContext.Items["AY_USER_ID"] = u.id;
                    context.Request.HttpContext.Items["AY_LOCALE_ID"] = u.localeId;



                }
                await next.Invoke();
            });

            #endregion

            _log.LogDebug("BOOT: pipeline - CORS");
            app.UseCors("CorsPolicy");

            //USE MVC
            _log.LogDebug("BOOT: pipeline - MVC");
            app.UseMvc();

            // ******************************************************************
            // ******************** TESTING WIPE DB *****************************
            //
            //Set this to true to wipe the db and reinstall a trial license and re-seed the data
            var TESTING_REFRESH_DB = true;//#############################################################################################

#if (DEBUG)
            //TESTING
            if (TESTING_REFRESH_DB)
                ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE = TESTING_REFRESH_DB;
            //TESTING
#endif


            if (ServerBootConfig.AYANOVA_PERMANENTLY_ERASE_DATABASE)
            {
                _log.LogWarning("BOOT: AYANOVA_PERMANENTLY_ERASE_DATABASE is true, dropping and recreating database");
                Util.DbUtil.DropAndRecreateDb(_log);
                AySchema.CheckAndUpdate(dbContext, _log);
            }

            //Check schema
            _log.LogDebug("BOOT: db schema check");
            AySchema.CheckAndUpdate(dbContext, _log);

            //Check database integrity
            _log.LogDebug("BOOT: db integrity check");
            DbUtil.CheckFingerPrint(AySchema.EXPECTED_COLUMN_COUNT, AySchema.EXPECTED_INDEX_COUNT, _log);

            //Initialize license
            AyaNova.Core.License.Initialize(apiServerState, dbContext, _log);

            //Ensure locales are present, not missing any keys and that there is a server default locale that exists          
            LocaleBiz lb = new LocaleBiz(dbContext, 1, ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID, AuthorizationRoles.OpsAdminFull);
            lb.ValidateLocales();



#if (DEBUG)
            //TESTING
            if (TESTING_REFRESH_DB)
            {
                AyaNova.Core.License.Fetch(apiServerState, dbContext, _log);
                Util.Seeder.SeedDatabase(Util.Seeder.SeedLevel.SmallOneManShopTrialDataSet, -8);//#############################################################################################
            }
            //TESTING
#endif


            //AUTOID VALUES INITIALIZATION 
            ServerBootConfig.SetMostRecentAutoIdValuesFromDatabase(dbContext);

            //SPA FALLBACK ROUTE                 
            app.Use(async (context, next) =>
            {
                //to support html5 pushstate routing in spa
                //this ensures that a refresh at the client will not 404 but rather force back to the index.html app page and then handled internally by the client 
                await next();
                if (context.Request.Path.Value != "/docs" && context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value))
                {
                    context.Request.Path = "/index.html";
                    context.Response.StatusCode = 200;
                    context.Response.ContentType = "text/html";
                    await context.Response.SendFileAsync(Path.Combine(env.WebRootPath, "index.html"));
                }
            });


            //Open up the server for visitors
            apiServerState.SetOpen();

            //final startup log
            _log.LogInformation("BOOT: COMPLETED - SERVER IS NOW OPEN");

        }


        #region Swagger and API Versioning utilities

        static string XmlCommentsFilePath
        {
            get
            {
                //Obsolete, used new method: https://developers.de/blogs/holger_vetter/archive/2017/06/30/swagger-includexmlcomments-platformservices-obsolete-replacement.aspx
                //var basePath = PlatformServices.Default.Application.ApplicationBasePath;
                var basePath = AppContext.BaseDirectory;
                var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml";
                return Path.Combine(basePath, fileName);
            }
        }

        static Info CreateInfoForApiVersion(ApiVersionDescription description)
        {
            var info = new Info()
            {
                Title = $"AyaNova API {description.ApiVersion}",
                Version = description.ApiVersion.ToString()
            };

            if (description.IsDeprecated)
            {
                info.Description += " This API version has been deprecated.";
            }

            return info;
        }
        #endregion


    }
}


