using Coscine.Action;
using Coscine.Action.EventArgs;
using Coscine.ApiCommons;
using Coscine.Configuration;
using Coscine.Database.DataModel;
using Coscine.Database.Models;
using Coscine.Database.ReturnObjects;
using Coscine.Database.Util;
using Coscine.Logging;
using Coscine.ResourceTypes;
using Coscine.ResourceTypes.ResourceTypeConfigs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Coscine.Api.Resources.Controllers
{
    /// <summary>
    /// This controller represents the actions which can be taken with a resource object.
    /// </summary>
    [Authorize]
    public class ResourceController : Controller
    {
        private readonly Authenticator _authenticator;
        private readonly ResourceModel _resourceModel;
        private readonly IConfiguration _configuration;
        private readonly Emitter _emitter;
        private readonly ProjectRoleModel _projectRoleModel;
        private readonly ProjectResourceModel _projectResourceModel;
        private readonly CoscineLogger _coscineLogger;
        private readonly ResourceTypeModel _resourceTypeModel;
        private readonly VisibilityModel _visibilityModel;
        private readonly LicenseModel _licenseModel;

        /// <summary>
        /// ResourceController constructor specifying a ResourceModel.
        /// </summary>
        public ResourceController(ILogger<ResourceController> logger)
        {
            _authenticator = new Authenticator(this, Program.Configuration);
            _configuration = Program.Configuration;
            _resourceModel = new ResourceModel();
            _emitter = new Emitter(_configuration);
            _projectRoleModel = new ProjectRoleModel();
            _projectResourceModel = new ProjectResourceModel();
            _coscineLogger = new CoscineLogger(logger);
            _resourceTypeModel = new ResourceTypeModel();
            _visibilityModel = new VisibilityModel();
            _licenseModel = new LicenseModel();
        }

        /// <summary>
        /// Returns a list of all resources the current user has access to.
        /// </summary>
        /// <returns>List of Resources</returns>
        [Route("[controller]")]
        public IActionResult Index()
        {
            var user = _authenticator.GetUser();
            return Json(_resourceModel.GetAllWhere((resource) =>
                (from projectResource in resource.ProjectResources
                 where (from projectRole in projectResource.Project.ProjectRoles
                        where projectRole.User == user
                        && (projectRole.Role.DisplayName == "Owner" || projectRole.Role.DisplayName == "Member")
                        && !projectRole.Project.Deleted
                        select projectRole).Any()
                 select projectResource).Any()
            ).Select((resource) => Helpers.CreateResourceReturnObject(resource)));
        }

        /// <summary>
        /// Returns the resource with a specified id.
        /// </summary>
        /// <param name="id">The resource id.</param>
        /// <returns>ResourceObject if OK, 401 if not authorized</returns>
        [HttpGet("[controller]/{id}")]
        public IActionResult Get(Guid id)
        {
            var resource = _resourceModel.GetById(id);
            var user = _authenticator.GetUser();

            // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md)
            // - Resource: View Resource (RCV, Metadatamanager)
            if (_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member, UserRoles.Guest))
            {
                _resourceModel.SetType(resource);

                var returnObject = Helpers.CreateResourceReturnObject(resource);

                if (_resourceModel.HasAccess(user, resource, UserRoles.Guest))
                {
                    returnObject.ResourceTypeOption = null;
                }

                return Json(returnObject);
            }
            else
            {
                return Unauthorized("User does not own this resource!");
            }
        }

        /// <summary>
        /// Returns whether or not the current user is creator of a specified resource.
        /// </summary>
        /// <param name="id">The resource id.</param>
        /// <returns>JSON object.</returns>
        [HttpGet("[controller]/{id}/isCreator")]
        public IActionResult IsUserResourceCreator(Guid id)
        {
            var resource = _resourceModel.GetById(id);
            var user = _authenticator.GetUser();
            return Json(new JObject
            {
                ["isResourceCreator"] = resource.Creator.Equals(user.Id)
            });
        }

        /// <summary>
        /// Updates a resource.
        /// </summary>
        /// <param name="id">The resource id.</param>
        /// <param name="resourceObject">Entry representing the user</param>
        /// <returns>JSON object.</returns>
        [HttpPost("[controller]/{id}")]
        public IActionResult Update(Guid id, [FromBody] ResourceObject resourceObject)
        {
            var resource = _resourceModel.GetById(id);
            var resourceTypeModel = new ResourceTypeModel();
            var resourceType = resourceTypeModel.GetById(resource.TypeId);
            if (resource.Archived == "0")
            {
                // TODO: Consider making all those checks automatically as annotations over the Object<ResourceObject>
                if (!(string.IsNullOrWhiteSpace(resourceObject.DisplayName) ||
                    string.IsNullOrWhiteSpace(resourceObject.ResourceName) ||
                    string.IsNullOrWhiteSpace(resourceObject.Description) ||
                    resourceObject.Disciplines?.Any() != true ||
                    resourceObject.Visibility == null))
                {
                    var user = _authenticator.GetUser();

                    // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md)
                    // - Resource: Change Resource Settings
                    if (_resourceModel.HasAccess(user, resource, UserRoles.Owner) ||
                        (_resourceModel.HasAccess(user, resource, UserRoles.Member) && resource.Creator.Equals(user.Id)))
                    {
                        LogAnalyticsEditResource(resource, _resourceModel.GetMetadataCompleteness(resourceObject), resourceObject.Disciplines, user);

                        var result = SetResourceTypeOptions(resource, resourceType, resourceObject);
                        if (result != null)
                        {
                            return result;
                        }

                        var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource);
                        if (resourceTypeDefinition.GetResourceTypeInformation().Result.CanUpdateResource)
                        {
                            resourceTypeDefinition.UpdateResource(id.ToString());
                        }
                        return Json(_resourceModel.UpdateByObject(resource, resourceObject));
                    }
                    else
                    {
                        return Unauthorized("The user is not authorized to perform an update on the selected resource!");
                    }
                }
                else
                {
                    return BadRequest("Mandatory information of the resource is missing!");
                }
            }
            else
            {
                return BadRequest("The resource is archived and cannot be modified!");
            }
        }

        /// <summary>
        /// Sets a read only status of a given resource.
        /// </summary>
        /// <param name="id">A GUID as a string that identifies the resource.</param>
        /// <param name="status">A boolean value that specifies if the resource is archived.</param>
        /// <returns>JSON object.</returns>
        [HttpPost("[controller]/{id}/setReadonly")]
        public IActionResult SetResourceReadonly(Guid id, bool status)
        {
            var resource = _resourceModel.GetById(id);
            var user = _authenticator.GetUser();

            // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md)
            // - Resource: Change Resource Settings
            if (_resourceModel.HasAccess(user, resource, UserRoles.Owner) ||
                (_resourceModel.HasAccess(user, resource, UserRoles.Member) && resource.Creator.Equals(user.Id)))
            {
                var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource);
                if (resourceTypeDefinition.GetResourceTypeInformation().Result.CanSetResourceReadonly)
                {
                    resourceTypeDefinition.SetResourceReadonly(id.ToString(), status);
                }

                // update archived status of the resource
                resource.Archived = status ? "1" : "0";
                _resourceModel.Update(resource);

                var returnObject = Helpers.CreateResourceReturnObject(resource);

                if (Request.Query != null && Request.Query["noanalyticslog"] != "true")
                {
                    if (status)
                    {
                        LogAnalyticsArchiveResource(resource, user);
                    }
                    else
                    {
                        LogAnalyticsUnarchiveResource(resource, user);
                    }
                }
                return Json(returnObject);
            }
            else
            {
                return Unauthorized("The user is not authorized to perform an update on the selected resource!");
            }
        }

        /// <summary>
        /// Deletes a resource.
        /// </summary>
        /// <param name="id">A GUID as a string that identifies the resource.</param>
        /// <returns>Deleted ResourceObject if OK, 401 if not</returns>
        [HttpDelete("[controller]/{id}")]
        public IActionResult Delete(Guid id)
        {
            var resource = _resourceModel.GetById(id);
            var user = _authenticator.GetUser();

            // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md)
            // - Resource: Change Resource Settings
            if (_resourceModel.HasAccess(user, resource, UserRoles.Owner) ||
                (_resourceModel.HasAccess(user, resource, UserRoles.Member) && resource.Creator.Equals(user.Id)))
            {
                var resourceObject = Helpers.CreateResourceReturnObject(resource);

                LogAnalyticsDeleteResource(resource, _resourceModel.GetMetadataCompleteness(resourceObject), resourceObject.Disciplines, user);

                _emitter.EmitResourceDelete(new ResourceEventArgs(_configuration)
                {
                    Resource = resource
                });

                _resourceModel.DeleteResource(resource);

                return Json(resourceObject);
            }
            else
            {
                return Unauthorized("The user is not authorized to perform an update on the selected resource!");
            }
        }

        /// <summary>
        /// Stores the provided resource object in a specified project.
        /// </summary>
        /// <param name="projectId">A GUID as a string that identifies the resource.</param>
        /// <param name="resourceObject">Entry representing the user</param>
        /// <returns>JSON object.</returns>
        [HttpPost("[controller]/project/{projectId}")]
        public async Task<IActionResult> StoreToProject(Guid projectId, [FromBody] ResourceObject resourceObject)
        {
            var projectModel = new ProjectModel();
            var resourceTypeModel = new ResourceTypeModel();
            var resourceType = resourceTypeModel.GetById(resourceObject.Type.Id);
            var project = projectModel.GetById(projectId);
            var user = _authenticator.GetUser();

            if (resourceType.Type == "gitlab")
            {
                var parseTosAccepted = bool.TryParse(resourceObject.ResourceTypeOption["TosAccepted"]?.ToString(), out var gitlabTosAccepted);

                if (!parseTosAccepted)
                {
                    return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""TosAccepted"".");
                }

                if (!gitlabTosAccepted)
                {
                    return BadRequest("Gitlab Terms of Service not accepted!");
                }
            }

            if (string.IsNullOrWhiteSpace(user.EmailAddress))
            {
                return Unauthorized("Access denied!");
            }

            // Rights Matrix (https://git.rwth-aachen.de/coscine/docs/private/internal-wiki/-/blob/master/coscine/Definition%20of%20rights%20Matrix.md)
            // - Resource: Create Resource
            if (projectModel.HasAccess(user, project, UserRoles.Owner, UserRoles.Member))
            {
                var orgs = Util.OrganizationsHelper.GetOrganization(user);

                if (orgs?.Any() != true)
                {
                    orgs = new List<string> { "*" };
                }

                if (!Util.ResourceTypeHelper.IsResourceTypeUsable(resourceType, projectId, orgs))
                {
                    return Unauthorized("The user is not authorized to add a new resource of this type to the selected project!");
                }

                var totalReservedQuota = Helpers.CalculateTotalReservedQuota(resourceType, projectId);
                var maximumQuota = Helpers.GetMaximumQuota(projectId, resourceType.Id);

                var success = long.TryParse(resourceObject.ResourceTypeOption["Size"]?.ToString(), out long desiredResourceQuota);

                var freeQuota = new QuotaDimObject
                {
                    Value = Helpers.ConvertCapacityUnits(maximumQuota, QuotaUnit.GibiBYTE) - Helpers.ConvertCapacityUnits(totalReservedQuota, QuotaUnit.GibiBYTE),
                    Unit = QuotaUnit.GibiBYTE
                };

                if (success && desiredResourceQuota > freeQuota.Value)
                {
                    return BadRequest($"You can not create a resource with a quota value of {desiredResourceQuota} GB. You have already reserved {Helpers.ConvertCapacityUnits(totalReservedQuota, QuotaUnit.GibiBYTE)} GB for {resourceObject.Type.DisplayName} resources and have only {freeQuota.Value} GB free.");
                }

                var resource = _resourceModel.StoreFromObject(resourceObject, user);

                var result = SetResourceTypeOptions(resource, resourceType, resourceObject);
                if (result != null)
                {
                    return result;
                }

                var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource);

                await resourceTypeDefinition.CreateResource(resource.Id.ToString(), desiredResourceQuota);
                projectModel.AddResource(project, resource);

                _emitter.EmitResourceCreate(new ResourceEventArgs(_configuration)
                {
                    Resource = resource
                });

                var resourceReturnObject = Helpers.CreateResourceReturnObject(resource);
                LogAnalyticsAddResource(resource, _resourceModel.GetMetadataCompleteness(resourceReturnObject), resourceReturnObject.Disciplines, user);
                return Json(resourceReturnObject);
            }
            else
            {
                return Unauthorized("The user is not authorized to add a new resource to the selected project!");
            }
        }

        private IActionResult SetResourceTypeOptions(Database.DataModel.Resource resource, Database.DataModel.ResourceType resourceType, ResourceObject resourceObject)
        {
            GetResourceTypeConfigOptions getConfig = null;

            if (resourceType.Type == "rdss3" || resourceType.Type == "rdss3worm")
            {
                getConfig = new GetRdsResourceTypeConfigOptions { Bucketname = resource.Id.ToString() };
            }
            else if (resourceType.Type == "gitlab")
            {
                var accessToken = resourceObject.ResourceTypeOption["AccessToken"]?.ToString();

                if (string.IsNullOrWhiteSpace(accessToken))
                {
                    return BadRequest(@"""ResourceTypeOption"" does not contain an valid ""AccessToken"".");
                }

                var parseResult = int.TryParse(resourceObject.ResourceTypeOption["ProjectId"]?.ToString(), out var gitlabProjectId);

                if (!parseResult)
                {
                    return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""ProjectId"".");
                }

                var repoUrl = resourceObject.ResourceTypeOption["RepoUrl"]?.ToString();

                if (string.IsNullOrWhiteSpace(repoUrl))
                {
                    return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""RepoUrl"".");
                }

                var branch = resourceObject.ResourceTypeOption["Branch"]?.ToString();

                if (string.IsNullOrWhiteSpace(branch))
                {
                    return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""Branch"".");
                }

                var parseTosAccepted = bool.TryParse(resourceObject.ResourceTypeOption["TosAccepted"]?.ToString(), out var gitlabTosAccepted);

                if (!parseTosAccepted)
                {
                    return BadRequest(@"""ResourceTypeOption"" does not contain a valid ""TosAccepted"".");
                }

                getConfig = new GetGitLabResourceTypeConfigOptions { AccessToken = accessToken, ProjectId = gitlabProjectId, RepoUrl = repoUrl, Branch = branch, TosAccepted = gitlabTosAccepted };
            }
            var config = ResourceTypeFactory.Instance.GetResourceTypeConfig(resourceType.Type, resourceType.SpecificType, getConfig);
            ResourceTypeFactory.Instance.SaveResourceTypeToDatabase(resourceType.Type, config, resource);

            return null;
        }

        private async Task LogAnalyticsEditResource(Database.DataModel.Resource resource, string metadataCompleteness, IEnumerable<DisciplineObject> disciplines, User user)
        {
            await Task.Run(() =>
            {
                var resourceTypeDisplayName = _resourceTypeModel.GetById(resource.TypeId).DisplayName;
                var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value;
                var reserved = GetReservedResourceQuota(resource);
                var maximum = GetMaximumResourceQuota(resource, projectId);
                var analyticsLogObject = new AnalyticsLogObject
                {
                    Operation = "Edit Resource",
                    Type = "Action",
                    RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(),
                    ProjectId = projectId.ToString(),
                    QuotaSize = new List<string> { $"{resourceTypeDisplayName}: {reserved}/{maximum}" },
                    ResourceId = resource.Id.ToString(),
                    ApplicationsProfile = resource.ApplicationProfile,
                    MetadataCompleteness = metadataCompleteness,
                    License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null,
                    Disciplines = disciplines.Select(x => x.DisplayNameEn).ToList(),
                    Visibility = resource.VisibilityId.HasValue ? _visibilityModel.GetById(resource.VisibilityId.Value)?.DisplayName : null,
                };

                _coscineLogger.AnalyticsLog(analyticsLogObject);
            });
        }

        private async Task LogAnalyticsAddResource(Database.DataModel.Resource resource, string metadataCompleteness, IEnumerable<DisciplineObject> disciplines, User user)
        {
            await Task.Run(() =>
            {
                var resourceTypeDisplayName = _resourceTypeModel.GetById(resource.TypeId).DisplayName;
                var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value;
                var reserved = GetReservedResourceQuota(resource);
                var maximum = GetMaximumResourceQuota(resource, projectId);
                var analyticsLogObject = new AnalyticsLogObject
                {
                    Operation = "Add Resource",
                    Type = "Action",
                    RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(),
                    ProjectId = projectId.ToString(),
                    QuotaSize = new List<string> { $"{resourceTypeDisplayName}: {reserved}/{maximum}" },
                    ResourceId = resource.Id.ToString(),
                    ApplicationsProfile = resource.ApplicationProfile,
                    MetadataCompleteness = metadataCompleteness,
                    License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null,
                    Disciplines = disciplines.Select(x => x.DisplayNameEn).ToList(),
                    Visibility = resource.VisibilityId.HasValue ? _visibilityModel.GetById(resource.VisibilityId.Value)?.DisplayName : null,
                };

                _coscineLogger.AnalyticsLog(analyticsLogObject);
            });
        }

        private async Task LogAnalyticsDeleteResource(Database.DataModel.Resource resource, string metadataCompleteness, IEnumerable<DisciplineObject> disciplines, User user)
        {
            await Task.Run(() =>
            {
                var resourceTypeDisplayName = _resourceTypeModel.GetById(resource.TypeId).DisplayName;
                var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value;
                var reserved = GetReservedResourceQuota(resource);
                var maximum = GetMaximumResourceQuota(resource, projectId);
                var analyticsLogObject = new AnalyticsLogObject
                {
                    Operation = "Delete Resource",
                    Type = "Action",
                    RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(),
                    ProjectId = projectId.ToString(),
                    QuotaSize = new List<string> { $"{resourceTypeDisplayName}: {reserved}/{maximum}" },
                    ResourceId = resource.Id.ToString(),
                    ApplicationsProfile = resource.ApplicationProfile,
                    MetadataCompleteness = metadataCompleteness,
                    License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null,
                    Disciplines = disciplines.Select(x => x.DisplayNameEn).ToList(),
                    Visibility = resource.VisibilityId.HasValue ? _visibilityModel.GetById(resource.VisibilityId.Value)?.DisplayName : null,
                };

                _coscineLogger.AnalyticsLog(analyticsLogObject);
            });
        }

        private async Task LogAnalyticsArchiveResource(Database.DataModel.Resource resource, User user)
        {
            await Task.Run(() =>
            {
                var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value;
                var analyticsLogObject = new AnalyticsLogObject
                {
                    Operation = "Archive Resource",
                    Type = "Action",
                    RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(),
                    ProjectId = projectId.ToString(),
                    ResourceId = resource.Id.ToString(),
                    ApplicationsProfile = resource.ApplicationProfile,
                    License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null
                };

                _coscineLogger.AnalyticsLog(analyticsLogObject);
            });
        }

        private async Task LogAnalyticsUnarchiveResource(Database.DataModel.Resource resource, User user)
        {
            await Task.Run(() =>
            {
                var projectId = _projectResourceModel.GetProjectForResource(resource.Id).Value;
                var analyticsLogObject = new AnalyticsLogObject
                {
                    Operation = "Unarchive Resource",
                    Type = "Action",
                    RoleId = _projectRoleModel.GetGetUserRoleForProject(projectId, user.Id).ToString(),
                    ProjectId = projectId.ToString(),
                    ResourceId = resource.Id.ToString(),
                    ApplicationsProfile = resource.ApplicationProfile,
                    License = resource.LicenseId.HasValue ? _licenseModel.GetById(resource.LicenseId.Value)?.DisplayName : null
                };

                _coscineLogger.AnalyticsLog(analyticsLogObject);
            });
        }

        private static int GetReservedResourceQuota(Database.DataModel.Resource resource)
        {
            var resourceTypeDefinition = ResourceTypeFactory.Instance.GetResourceType(resource);
            if (resourceTypeDefinition.GetResourceTypeInformation().Result.IsQuotaAvailable)
            {
                try
                {
                    return (int)resourceTypeDefinition.GetResourceQuotaAvailable(resource.Id.ToString()).Result;
                }
                catch (Exception)
                {
                    // Error communicating with the resource
                }
            }
            return 0;
        }

        private static int GetMaximumResourceQuota(Database.DataModel.Resource resource, Guid parentProjectId)
        {
            var projectQuotaModel = new ProjectQuotaModel();
            var projectQuota =
                projectQuotaModel.GetWhere((x) =>
                    x.ProjectId == parentProjectId &&
                    x.ResourceTypeId == resource.TypeId);
            var reservedForCurrent = GetReservedResourceQuota(resource);
            var resourceTypeModel = new ResourceTypeModel();
            // Get the resource type
            var resourceType = resourceTypeModel.GetById(resource.TypeId);
            if (resourceType == null)
            {
                throw new ArgumentNullException($"Could not find resourceType with id: {resource.TypeId}");
            }
            var totalReserved = Helpers.CalculateTotalReservedQuota(resourceType, parentProjectId);
            return (int)(projectQuota.Quota - (totalReserved.Value - reservedForCurrent));
        }
    }
}