﻿// Copyright 2015-2025 Finsa S.p.A. <finsa@finsa.it>
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
// in compliance with the License. You may obtain a copy of the License at:
//
// "http://www.apache.org/licenses/LICENSE-2.0"
//
// Unless required by applicable law or agreed to in writing, software distributed under the License
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
// or implied. See the License for the specific language governing permissions and limitations under
// the License.

using Common.Logging;
using Ninject;
using PommaLabs.Thrower;
using System;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Finsa.Caravan.WebApi.Filters
{
    /// <summary>
    ///   Blocca l'accesso a un controller oppure a una action se l'utente correntemente loggato non
    ///   ha almeno uno dei ruoli richiesti.
    /// </summary>
    public sealed class RequireUserInRoleAttribute : ActionFilterAttribute
    {
        private readonly string[] _roles;
        private readonly string _expectedRoles;

        /// <summary>
        ///   Registra l'elenco dei ruoli richiesti.
        /// </summary>
        /// <param name="roles">I ruoli richiesti.</param>
        public RequireUserInRoleAttribute(params string[] roles)
        {
            // Preconditions
            Raise.ArgumentNullException.IfIsNull(roles, nameof(roles));
            Raise.ArgumentOutOfRangeException.IfIsEqual(roles.Length, 0, nameof(roles));
            Raise.ArgumentException.If(roles.Any(r => string.IsNullOrWhiteSpace(r)), nameof(roles));

            _roles = roles;
            _expectedRoles = string.Join(", ", _roles.Select(r => $"'{r}'"));
        }

        /// <summary>
        ///   Il log utilizzato dal filtro.
        /// </summary>
        [Inject]
        public ILog Log { get; set; }

        /// <summary>
        ///   Occurs before the action method is invoked.
        /// </summary>
        /// <param name="actionContext">The action context.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
        {
            var user = actionContext.RequestContext.Principal;

            // Se non trovo un utente loggato, non autorizzo la request ed esco immediatamente.
            if (user == null || !user.Identity.IsAuthenticated)
            {
                const string errMsg = "There is no logged in user, cannot verify user roles. Did you forget the authorize attribute?";
                await HandleErrorAsync(actionContext, new Exception(errMsg));
                return;
            }

            // Se l'utente non appartiene ad alcun ruolo indicato, non autorizzo la request ed esco immediatamente.
            if (_roles.All(r => !user.IsInRole(r)))
            {
                Log.Warn($"User '{user.Identity.Name}' does NOT have at least one of the following roles: {_expectedRoles}");

                const string errMsg = "User has not sufficient rights to perform current request";
                await HandleErrorAsync(actionContext, new Exception(errMsg));
                return;
            }

            // L'utente ha almeno un ruolo richiesto, autorizzo la request.
            Log.Debug($"User '{user.Identity.Name}' has at least one of the following roles: {_expectedRoles}");
        }

        /// <summary>
        ///   Gestisce un errore avvenuto durante la validazione dell'accesso. Questa variante deve
        ///   gestire anche una eventuale eccezione.
        /// </summary>
        /// <param name="actionContext">L'azione per cui si stava validando l'accesso.</param>
        /// <param name="exception">L'eccezione che è stata lanciata.</param>
        public Task HandleErrorAsync(HttpActionContext actionContext, Exception exception)
        {
            var controllerName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName;
            var actionName = actionContext.ActionDescriptor.ActionName;
            exception = exception.GetBaseException();
            var errorMessage = $"Access denied to {controllerName}.{actionName}. Reason: InvalidRole. Exception: {exception.Message}";
            Log.Error(errorMessage, exception);
            throw new HttpException(HttpStatusCode.Unauthorized, errorMessage, new HttpExceptionInfo(CaravanErrorCodes.CVE00000, errorMessage));
        }
    }
}