using System.Collections.Generic;
using System.Net;
using Hangfire;
using Hangfire.MemoryStorage;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Web.Framework;
using StardewModdingAPI.Web.Framework.Caching.Mods;
using StardewModdingAPI.Web.Framework.Caching.Wiki;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.Compression;
using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.Framework.RedirectRules;
using StardewModdingAPI.Web.Framework.Storage;

namespace StardewModdingAPI.Web
{
    /// <summary>The web app startup configuration.</summary>
    internal class Startup
    {
        /*********
        ** Accessors
        *********/
        /// <summary>The web app configuration.</summary>
        public IConfigurationRoot Configuration { get; }


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="env">The hosting environment.</param>
        public Startup(IWebHostEnvironment env)
        {
            this.Configuration = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables()
                .Build();
        }

        /// <summary>The method called by the runtime to add services to the container.</summary>
        /// <param name="services">The service injection container.</param>
        public void ConfigureServices(IServiceCollection services)
        {
            // init basic services
            services
                .Configure<ApiClientsConfig>(this.Configuration.GetSection("ApiClients"))
                .Configure<BackgroundServicesConfig>(this.Configuration.GetSection("BackgroundServices"))
                .Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList"))
                .Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
                .Configure<SiteConfig>(this.Configuration.GetSection("Site"))
                .Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
                .AddLogging()
                .AddMemoryCache();

            // init MVC
            services
                .AddControllers()
                .AddNewtonsoftJson(options => this.ConfigureJsonNet(options.SerializerSettings))
                .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()));
            services
                .AddRazorPages();

            // init storage
            services.AddSingleton<IModCacheRepository>(new ModCacheMemoryRepository());
            services.AddSingleton<IWikiCacheRepository>(new WikiCacheMemoryRepository());

            // init Hangfire
            services
                .AddHangfire((_, config) =>
                {
                    config
                        .SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
                        .UseSimpleAssemblyNameTypeSerializer()
                        .UseRecommendedSerializerSettings()
                        .UseMemoryStorage();
                });

            // init background service
            {
                BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get<BackgroundServicesConfig>();
                if (config.Enabled)
                    services.AddHostedService<BackgroundService>();
            }

            // init API clients
            {
                ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
                string version = this.GetType().Assembly.GetName().Version!.ToString(3);
                string userAgent = string.Format(api.UserAgent, version);

                services.AddSingleton<IChucklefishClient>(new ChucklefishClient(
                    userAgent: userAgent,
                    baseUrl: api.ChucklefishBaseUrl,
                    modPageUrlFormat: api.ChucklefishModPageUrlFormat
                ));

                services.AddSingleton<ICurseForgeClient>(new CurseForgeClient(
                    userAgent: userAgent,
                    apiUrl: api.CurseForgeBaseUrl
                ));

                services.AddSingleton<IGitHubClient>(new GitHubClient(
                    baseUrl: api.GitHubBaseUrl,
                    userAgent: userAgent,
                    acceptHeader: api.GitHubAcceptHeader,
                    username: api.GitHubUsername,
                    password: api.GitHubPassword
                ));

                services.AddSingleton<IModDropClient>(new ModDropClient(
                    userAgent: userAgent,
                    apiUrl: api.ModDropApiUrl,
                    modUrlFormat: api.ModDropModPageUrl
                ));

                if (!string.IsNullOrWhiteSpace(api.NexusApiKey))
                {
                    services.AddSingleton<INexusClient>(new NexusClient(
                        webUserAgent: userAgent,
                        webBaseUrl: api.NexusBaseUrl,
                        webModUrlFormat: api.NexusModUrlFormat,
                        webModScrapeUrlFormat: api.NexusModScrapeUrlFormat,
                        apiAppVersion: version,
                        apiKey: api.NexusApiKey
                    ));
                }
                else
                {
                    services.AddSingleton<INexusClient>(new DisabledNexusClient());
                }

                services.AddSingleton<IPastebinClient>(new PastebinClient(
                    baseUrl: api.PastebinBaseUrl,
                    userAgent: userAgent
                ));
            }

            // init helpers
            services
                .AddSingleton<IGzipHelper>(new GzipHelper())
                .AddSingleton<IStorageProvider>(serv => new StorageProvider(
                    serv.GetRequiredService<IOptions<ApiClientsConfig>>(),
                    serv.GetRequiredService<IPastebinClient>(),
                    serv.GetRequiredService<IGzipHelper>()
                ));
        }

        /// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
        /// <param name="app">The application builder.</param>
        public void Configure(IApplicationBuilder app)
        {
            // basic config
            app.UseDeveloperExceptionPage();
            app
                .UseCors(policy => policy
                    .AllowAnyHeader()
                    .AllowAnyMethod()
                    .WithOrigins("https://smapi.io")
                )
                .UseRewriter(this.GetRedirectRules())
                .UseStaticFiles() // wwwroot folder
                .UseRouting()
                .UseAuthorization()
                .UseEndpoints(p =>
                {
                    p.MapControllers();
                    p.MapRazorPages();
                });

            // enable Hangfire dashboard
            app.UseHangfireDashboard("/tasks", new DashboardOptions
            {
                IsReadOnlyFunc = context => !JobDashboardAuthorizationFilter.IsLocalRequest(context),
                Authorization = new[] { new JobDashboardAuthorizationFilter() }
            });
        }


        /*********
        ** Private methods
        *********/
        /// <summary>Configure a Json.NET serializer.</summary>
        /// <param name="settings">The serializer settings to edit.</param>
        private void ConfigureJsonNet(JsonSerializerSettings settings)
        {
            foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
                settings.Converters.Add(converter);

            settings.Formatting = Formatting.Indented;
            settings.NullValueHandling = NullValueHandling.Ignore;
        }

        /// <summary>Get the redirect rules to apply.</summary>
        private RewriteOptions GetRedirectRules()
        {
            var redirects = new RewriteOptions()
                // shortcut paths
                .Add(new RedirectPathsToUrlsRule(new Dictionary<string, string>
                {
                    // wiki pages
                    [@"^/3\.0\.?$"] = "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0",
                    [@"^/community\.?$"] = "https://stardewvalleywiki.com/Modding:Community",
                    [@"^/docs\.?$"] = "https://stardewvalleywiki.com/Modding:Index",
                    [@"^/help\.?$"] = "https://stardewvalleywiki.com/Modding:Help",
                    [@"^/install\.?$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI",
                    [@"^/troubleshoot(.*)$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1",
                    [@"^/xnb\.?$"] = "https://stardewvalleywiki.com/Modding:Using_XNB_mods",

                    // GitHub docs
                    [@"^/package(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1",
                    [@"^/release(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#$1",

                    // legacy redirects
                    [@"^/compat\.?$"] = "https://smapi.io/mods"
                }))

                // legacy paths
                .Add(new RedirectPathsToUrlsRule(this.GetLegacyPathRedirects()))

                // subdomains
                .Add(new RedirectHostsToUrlsRule(HttpStatusCode.PermanentRedirect, host => host switch
                {
                    "api.smapi.io" => "smapi.io/api",
                    "json.smapi.io" => "smapi.io/json",
                    "log.smapi.io" => "smapi.io/log",
                    "mods.smapi.io" => "smapi.io/mods",
                    _ => host.EndsWith(".smapi.io")
                        ? "smapi.io"
                        : null
                }))

                // redirect to HTTPS (except API for Linux/macOS Mono compatibility)
                .Add(
                    new RedirectToHttpsRule(except: req => req.Host.Host == "localhost" || req.Path.StartsWithSegments("/api"))
                );

            return redirects;
        }

        /// <summary>Get the redirects for legacy paths that have been moved elsewhere.</summary>
        private IDictionary<string, string> GetLegacyPathRedirects()
        {
            var redirects = new Dictionary<string, string>();

            // canimod.com => wiki
            var wikiRedirects = new Dictionary<string, string[]>
            {
                ["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
                ["Modding:Modder_Guide"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod", "^/for-devs/creating-a-smapi-mod-advanced-config" },
                ["Modding:Player_Guide"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods", "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" },

                ["Modding:Editing_XNB_files"] = new[] { "^/for-devs/creating-an-xnb-mod", "^/guides/creating-an-xnb-mod" },
                ["Modding:Event_data"] = new[] { "^/for-devs/events", "^/guides/events" },
                ["Modding:Gift_taste_data"] = new[] { "^/for-devs/npc-gift-tastes", "^/guides/npc-gift-tastes" },
                ["Modding:IDE_reference"] = new[] { "^/for-devs/creating-a-smapi-mod-ide-primer" },
                ["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" },
                ["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" }
            };
            foreach ((string page, string[] patterns) in wikiRedirects)
            {
                foreach (string pattern in patterns)
                    redirects.Add(pattern, "https://stardewvalleywiki.com/" + page);
            }

            return redirects;
        }
    }
}