// 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.Net;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Foxnouns.Backend;

public class FoxnounsError(string message, Exception? inner = null) : Exception(message)
{
    public Exception? Inner => inner;

    public class DatabaseError(string message, Exception? inner = null)
        : FoxnounsError(message, inner);

    public class UnknownEntityError(Type entityType, Exception? inner = null)
        : DatabaseError($"Entity of type {entityType.Name} not found", inner);

    public class RemoteAuthError(string message, string? errorBody = null, Exception? inner = null)
        : FoxnounsError(message, inner)
    {
        public string? ErrorBody => errorBody;

        public override string ToString() =>
            $"{Message}: {ErrorBody} {(Inner != null ? $"({Inner})" : "")}";
    }
}

public class ApiError(
    string message,
    HttpStatusCode? statusCode = null,
    ErrorCode? errorCode = null
) : FoxnounsError(message)
{
    public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError;
    public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError;

    public class Unauthorized(string message, ErrorCode errorCode = ErrorCode.AuthenticationError)
        : ApiError(message, HttpStatusCode.Unauthorized, errorCode);

    public class Forbidden(
        string message,
        IEnumerable<string>? scopes = null,
        ErrorCode errorCode = ErrorCode.Forbidden
    ) : ApiError(message, HttpStatusCode.Forbidden, errorCode)
    {
        public readonly string[] Scopes = scopes?.ToArray() ?? [];
    }

    public class BadRequest(
        string message,
        IReadOnlyDictionary<string, IEnumerable<ValidationError>>? errors = null
    ) : ApiError(message, HttpStatusCode.BadRequest)
    {
        public BadRequest(string message, string field, object? actualValue)
            : this(
                "Error validating input",
                new Dictionary<string, IEnumerable<ValidationError>>
                {
                    { field, [ValidationError.GenericValidationError(message, actualValue)] },
                }
            ) { }

        public JObject ToJson()
        {
            var o = new JObject
            {
                { "status", (int)HttpStatusCode.BadRequest },
                { "message", Message },
                { "code", "BAD_REQUEST" },
            };
            if (errors == null)
                return o;

            var a = new JArray();
            foreach (KeyValuePair<string, IEnumerable<ValidationError>> error in errors)
            {
                var errorObj = new JObject
                {
                    { "key", error.Key },
                    { "errors", JArray.FromObject(error.Value) },
                };
                a.Add(errorObj);
            }

            o.Add("errors", a);
            return o;
        }
    }

    /// <summary>
    /// A special version of BadRequest that ASP.NET generates when it encounters an invalid request.
    /// Any other methods should use <see cref="ApiError.BadRequest" /> instead.
    /// </summary>
    public class AspBadRequest(string message, ModelStateDictionary? modelState = null)
        : ApiError(message, HttpStatusCode.BadRequest)
    {
        public JObject ToJson()
        {
            var o = new JObject
            {
                { "status", (int)HttpStatusCode.BadRequest },
                { "message", Message },
                { "code", "BAD_REQUEST" },
            };
            if (modelState == null)
                return o;

            var a = new JArray();
            foreach (
                KeyValuePair<string, ModelStateEntry?> error in modelState.Where(e =>
                    e.Value is { Errors.Count: > 0 }
                )
            )
            {
                var errorObj = new JObject
                {
                    { "key", error.Key },
                    {
                        "errors",
                        new JArray(
                            error.Value!.Errors.Select(e => new JObject
                            {
                                { "message", e.ErrorMessage },
                            })
                        )
                    },
                };
                a.Add(errorObj);
            }

            o.Add("errors", a);
            return o;
        }
    }

    public class NotFound(string message, ErrorCode? code = null)
        : ApiError(message, HttpStatusCode.NotFound, code);

    public class AuthenticationError(string message) : ApiError(message, HttpStatusCode.BadRequest);
}

public enum ErrorCode
{
    InternalServerError,
    Forbidden,
    BadRequest,
    AuthenticationError,
    AuthenticationRequired,
    MissingScopes,
    GenericApiError,
    UserNotFound,
    MemberNotFound,
    PageNotFound,
    AccountAlreadyLinked,
    LastAuthMethod,
    InvalidReportTarget,
    InvalidWarningTarget,
}

public class ValidationError
{
    public required string Message { get; init; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public int? MinLength { get; init; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public int? MaxLength { get; init; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public int? ActualLength { get; init; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public IEnumerable<object>? AllowedValues { get; init; }

    [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
    public object? ActualValue { get; init; }

    public static ValidationError LengthError(
        string message,
        int minLength,
        int maxLength,
        int actualLength
    ) =>
        new()
        {
            Message = message,
            MinLength = minLength,
            MaxLength = maxLength,
            ActualLength = actualLength,
        };

    public static ValidationError DisallowedValueError(
        string message,
        IEnumerable<object> allowedValues,
        object actualValue
    ) =>
        new()
        {
            Message = message,
            AllowedValues = allowedValues,
            ActualValue = actualValue,
        };

    public static ValidationError GenericValidationError(string message, object? actualValue) =>
        new() { Message = message, ActualValue = actualValue };
}