// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Web;
using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Extensions;
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;

namespace Foxnouns.Backend.Services.Auth;

public partial class FediverseAuthService
{
    private string MastodonRedirectUri(string instance) =>
        $"{config.BaseUrl}/auth/callback/mastodon/{instance}";

    private async Task<FediverseApplication> CreateMastodonApplicationAsync(
        string instance,
        Snowflake? existingAppId = null
    )
    {
        HttpResponseMessage resp = await client.PostAsJsonAsync(
            $"https://{instance}/api/v1/apps",
            new CreateMastodonApplicationRequest(
                $"pronouns.cc (+{config.BaseUrl})",
                MastodonRedirectUri(instance),
                "read read:accounts",
                config.BaseUrl
            )
        );
        resp.EnsureSuccessStatusCode();

        PartialMastodonApplication? mastodonApp =
            await resp.Content.ReadFromJsonAsync<PartialMastodonApplication>();
        if (mastodonApp == null)
        {
            throw new FoxnounsError(
                $"Application created on Mastodon-compatible instance {instance} was null"
            );
        }

        FediverseApplication app;

        if (existingAppId == null)
        {
            app = new FediverseApplication
            {
                Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(),
                ClientId = mastodonApp.ClientId,
                ClientSecret = mastodonApp.ClientSecret,
                Domain = instance,
                InstanceType = FediverseInstanceType.MastodonApi,
            };

            db.Add(app);
        }
        else
        {
            app =
                await db.FediverseApplications.FindAsync(existingAppId)
                ?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null");

            app.ClientId = mastodonApp.ClientId;
            app.ClientSecret = mastodonApp.ClientSecret;
            app.InstanceType = FediverseInstanceType.MastodonApi;
        }

        await db.SaveChangesAsync();

        return app;
    }

    private async Task<FediverseUser> GetMastodonUserAsync(
        FediverseApplication app,
        string code,
        string? state = null
    )
    {
        if (state != null)
            await keyCacheService.ValidateAuthStateAsync(state);

        HttpResponseMessage tokenResp = await client.PostAsync(
            MastodonTokenUri(app.Domain),
            new FormUrlEncodedContent(
                new Dictionary<string, string>
                {
                    { "grant_type", "authorization_code" },
                    { "code", code },
                    { "scope", "read:accounts" },
                    { "client_id", app.ClientId },
                    { "client_secret", app.ClientSecret },
                    { "redirect_uri", MastodonRedirectUri(app.Domain) },
                }
            )
        );
        if (tokenResp.StatusCode == HttpStatusCode.Unauthorized)
        {
            throw new FoxnounsError($"Application for instance {app.Domain} was invalid");
        }

        tokenResp.EnsureSuccessStatusCode();
        string? token = (
            await tokenResp.Content.ReadFromJsonAsync<MastodonTokenResponse>()
        )?.AccessToken;
        if (token == null)
        {
            throw new FoxnounsError($"Token response from instance {app.Domain} was invalid");
        }

        var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain));
        req.Headers.Add("Authorization", $"Bearer {token}");

        HttpResponseMessage currentUserResp = await client.SendAsync(req);
        currentUserResp.EnsureSuccessStatusCode();
        FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync<FediverseUser>();
        if (user == null)
        {
            throw new FoxnounsError($"User response from instance {app.Domain} was invalid");
        }

        return user;
    }

    private record MastodonTokenResponse([property: J("access_token")] string AccessToken);

    private async Task<string> GenerateMastodonAuthUrlAsync(
        FediverseApplication app,
        bool forceRefresh,
        string? state = null
    )
    {
        if (forceRefresh)
        {
            _logger.Information(
                "An app credentials refresh was requested for {ApplicationId}, creating a new application",
                app.Id
            );
            app = await CreateMastodonApplicationAsync(app.Domain, app.Id);
        }

        state ??= HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync());

        return $"https://{app.Domain}/oauth/authorize?response_type=code"
            + $"&client_id={app.ClientId}"
            + $"&scope={HttpUtility.UrlEncode("read:accounts")}"
            + $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}"
            + $"&state={state}";
    }

    private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token";

    private static string MastodonCurrentUserUri(string instance) =>
        $"https://{instance}/api/v1/accounts/verify_credentials";

    [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
    private record PartialMastodonApplication(
        [property: J("name")] string Name,
        [property: J("client_id")] string ClientId,
        [property: J("client_secret")] string ClientSecret
    );

    private record CreateMastodonApplicationRequest(
        [property: J("client_name")] string ClientName,
        [property: J("redirect_uris")] string RedirectUris,
        [property: J("scopes")] string Scopes,
        [property: J("website")] string Website
    );
}