using Coscine.ApiCommons;
using Coscine.Configuration;
using Coscine.Database.DataModel;
using Coscine.Database.Models;
using Coscine.Database.Util;
using Coscine.Logging;
using Coscine.Metadata;
using Coscine.ResourceLoader;
using Coscine.ResourceTypeBase;
using Coscine.WaterbutlerHelper.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;

namespace Coscine.Api.Blob.Controllers
{
    /// <summary>
    /// This controller represents the actions which can be taken with a Blob object.
    /// </summary>
    [Authorize]
    public class BlobController : Controller
    {
        private readonly IConfiguration _configuration;
        private readonly Authenticator _authenticator;
        private readonly ResourceModel _resourceModel;
        private readonly ProjectResourceModel _projectResourceModel;
        private readonly ProjectRoleModel _projectRoleModel;
        private readonly CoscineLogger _coscineLogger;
        private readonly AnalyticsLogObject _analyticsLogObject;
        private readonly RdfStoreConnector _rdfStoreConnector;
        private readonly string _prefix;

        /// <summary>
        /// Blob controller constructor
        /// </summary>
        /// <param name="logger">Logger</param>
        /// <param name="dataSourceService">Source service for data</param>
        public BlobController(ILogger<BlobController> logger, IDataSourceService dataSourceService)
        {
            _configuration = Program.Configuration;
            _authenticator = new Authenticator(this, _configuration);
            _resourceModel = new ResourceModel();
            _projectResourceModel = new ProjectResourceModel();
            _projectRoleModel = new ProjectRoleModel();
            _rdfStoreConnector = new RdfStoreConnector(_configuration.GetStringAndWait("coscine/local/virtuoso/additional/url"));

            _coscineLogger = new CoscineLogger(logger);
            _analyticsLogObject = new AnalyticsLogObject();
            _prefix = _configuration.GetStringAndWait("coscine/global/epic/prefix");
        }

        /// <summary>
        /// Generates Id
        /// </summary>
        /// <param name="resourceId">Id of the resource</param>
        /// <param name="path"> Path to file</param>
        /// <returns> Uri </returns>
        public Uri GenerateId(string resourceId, string path)
        {
            return new Uri($"https://hdl.handle.net/{_prefix}/{resourceId}@path={Uri.EscapeDataString(path)}");
        }

        /// <summary>
        /// This method returns the amount of allocated space for the given resource
        /// </summary>
        /// <param name="resourceId">Id of a resource</param>
        /// <returns>Data, file count and byte size used or Status Code 400, 404, 401 or 500  </returns>
        [HttpGet("[controller]/{resourceId}/quota")]
        public IActionResult GetQuota(string resourceId)
        {
            if (!Guid.TryParse(resourceId, out Guid resourceGuid))
            {
                return BadRequest($"{resourceId} is not a GUID.");
            }

            var user = _authenticator.GetUser();
            Resource resource;
            try
            {
                resource = _resourceModel.GetById(resourceGuid);
                if (resource == null)
                {
                    return NotFound($"Could not find resource with id: {resourceId}");
                }
            }
            catch (Exception)
            {
                return NotFound($"Could not find resource with id: {resourceId}");
            }

            if (resource.Type == null)
            {
                ResourceTypeModel resourceTypeModel = new ResourceTypeModel();
                resource.Type = resourceTypeModel.GetById(resource.TypeId);
            }

            if (user == null || !_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member))
            {
                return BadRequest("User does not have permission to the resource.");
            }

            var resourceTypeOptions = _resourceModel.GetResourceTypeOptions(resource.Id);
            var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(resource.Type.DisplayName, _configuration);
            if (resourceTypeDefinition == null)
            {
                return BadRequest($"No provider for: \"{resource.Type.DisplayName}\".");
            }

            var resourceTypeInformation = resourceTypeDefinition.GetResourceTypeInformation().Result;

            if (resource.ResourceTypeOptionId.HasValue && resourceTypeInformation.IsQuotaAvailable)
            {
                try
                {
                    var totalFileSize = resourceTypeDefinition.GetResourceQuotaUsed(resourceId, resourceTypeOptions).Result;
                    return Ok($"{{ \"data\": {{ \"usedSizeByte\": {totalFileSize} }}}}");
                }
                catch (Exception e)
                {
                    _coscineLogger.Log("Get Quota failed", e);
                    return BadRequest("Error in communication with the resource");
                }
            }
            else
            {
                return BadRequest("The resource quota must be adjustable.");
            }
        }

        /// <summary>
        ///  This method checks if the given file exists and returns it
        /// </summary>
        /// <param name="resourceId">Id of the resource</param>
        /// <param name="path"> Path to the file </param>
        /// <returns>File if file exists otherwise status code 204, 400, 401 or 404 </returns>
        [HttpGet("[controller]/{resourceId}/{*path}")]
        [DisableRequestSizeLimit]
        [ApiExplorerSettings(IgnoreApi = true)]
        public async Task<IActionResult> GetFileWithPath(string resourceId, string path)
        {
            return await GetFile(resourceId, path);
        }

        /// <summary>
        ///  This method checks if the given file exists and returns it
        /// </summary>
        /// <param name="resourceId">Id of the resource</param>
        /// <param name="path"> Path to the file </param>
        /// <returns>File if file exists otherwise status code 204, 400, 401 or 404 </returns>
        [HttpGet("[controller]/{resourceId}/")]
        [DisableRequestSizeLimit]
        public async Task<IActionResult> GetFileWithParameter(string resourceId, [System.Web.Http.FromUri] string path)
        {
            // Strip the first slash, to reuse the previous implementation.
            if (path.StartsWith("/"))
            {
                path = path[1..];
            }

            return await GetFile(resourceId, path);
        }

        /// <summary>
        ///  This method checks if the given file exists and returns it
        /// </summary>
        /// <param name="resourceId">Id of the resource</param>
        /// <param name="path"> Path to the file </param>
        /// <returns>File if file exists otherwise status code 204, 400, 401 or 404 </returns>
        public async Task<IActionResult> GetFile(string resourceId, string path)
        {
            var user = _authenticator.GetUser();
            path = $"/{path}";
            var checkPath = CheckPath(path);
            if (checkPath != null)
            {
                return checkPath;
            }
            var checkResourceId = CheckResource(resourceId, out Resource resource);
            if (checkResourceId != null)
            {
                return checkResourceId;
            }
            var checkUser = CheckUser(user, resource);
            if (checkUser != null)
            {
                return checkUser;
            }
            var resourceTypeOptions = _resourceModel.GetResourceTypeOptions(resource.Id);
            try
            {
                var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(resource.Type.DisplayName, _configuration);
                if (resourceTypeDefinition == null)
                {
                    return BadRequest($"No provider for: \"{resource.Type.DisplayName}\".");
                }
                var infos = await resourceTypeDefinition.GetEntry(resource.Id.ToString(), path, null, resourceTypeOptions);
                var response = await resourceTypeDefinition.LoadEntry(resource.Id.ToString(), path, null, resourceTypeOptions);
                new FileExtensionContentTypeProvider().TryGetContentType(path[path.LastIndexOf("/")..], out string contentType);
                LogAnalytics("Download File", resourceId, path[1..], user);
                return File(response, contentType ?? "application/octet-stream");
            }
            catch (Exception e)
            {
                _coscineLogger.Log("Get File failed", e);
                return BadRequest("Error in communication with the resource");
            }
        }

        /// <summary>
        /// This method uploads a given File
        /// </summary>
        /// <param name="resourceId">Id of the resource </param>
        /// <param name="path">Path to the file</param>
        /// <param name="files">List of files.</param>
        /// <returns>status code 204 if file is uploaded otherwise status code 400 or 403</returns>
        [DisableRequestSizeLimit]
        [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
        [HttpPut("[controller]/{resourceId}/{*path}")]
        [ApiExplorerSettings(IgnoreApi = true)]
        public async Task<IActionResult> UploadFileWithPath(string resourceId, string path, List<IFormFile> files)
        {
            return await UploadFile(resourceId, path, files);
        }

        /// <summary>
        /// This method uploads a given File
        /// </summary>
        /// <param name="resourceId">Id of the resource </param>
        /// <param name="path">Path to the file</param>
        /// <param name="files">List of files.</param>
        /// <returns>status code 204 if file is uploaded otherwise status code 400 or 403</returns>
        [DisableRequestSizeLimit]
        [RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue)]
        [HttpPut("[controller]/{resourceId}/")]
        public async Task<IActionResult> UploadFileWithParameter(string resourceId, [System.Web.Http.FromUri] string path, List<IFormFile> files)
        {
            // Strip the first slash, to reuse the previous implementation.
            if (path.StartsWith("/"))
            {
                path = path[1..];
            }

            return await UploadFile(resourceId, path, files);
        }

        /// <summary>
        /// This method uploads a given File
        /// </summary>
        /// <param name="resourceId">Id of the resource </param>
        /// <param name="path">Path to the file</param>
        /// <param name="files">List of files.</param>
        /// <returns>status code 204 if file is uploaded otherwise status code 400 or 403</returns>
        public async Task<IActionResult> UploadFile(string resourceId, string path, List<IFormFile> files)
        {
            var user = _authenticator.GetUser();
            path = $"/{path}";
            var checkPath = CheckPath(path);
            if (checkPath != null)
            {
                return checkPath;
            }
            var checkResourceId = CheckResource(resourceId, out Resource resource);
            if (checkResourceId != null)
            {
                return checkResourceId;
            }
            var checkUser = CheckUser(user, resource);
            if (checkUser != null)
            {
                return checkUser;
            }

            if (resource.Archived == "1")
            {
                return BadRequest("The resource is read only!");
            }

            if (files.Count != 1)
            {
                return BadRequest("Only one file can be uploaded per request.");
            }

            var id = GenerateId(resourceId, path);
            if (!_rdfStoreConnector.HasGraph(id.AbsoluteUri))
            {
                return StatusCode((int)HttpStatusCode.Forbidden,
                    "No metadata set has been added for this file.");
            }

            try
            {
                var resourceTypeOptions = _resourceModel.GetResourceTypeOptions(resource.Id);
                var stream = files[0].OpenReadStream();
                resourceTypeOptions.Add("ContentLength", stream.Length.ToString());
                var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(resource.Type.DisplayName, _configuration);
                if (resourceTypeDefinition == null)
                {
                    return BadRequest($"No provider for: \"{resource.Type.DisplayName}\".");
                }
                ResourceEntry infos = null;
                try
                {
                    infos = await resourceTypeDefinition.GetEntry(resource.Id.ToString(), path, null, resourceTypeOptions);
                }
                catch
                {
                    // do nothing
                }
                await resourceTypeDefinition.StoreEntry(resource.Id.ToString(), path, stream, resourceTypeOptions);
                LogAnalytics(infos == null ? "Upload File" : "Update File", resourceId, path, user);
                return NoContent();
            }
            catch (Exception e)
            {
                _coscineLogger.Log("Upload File failed", e);
                return BadRequest("Error in communication with the resource");
            }
        }

        /// <summary>
        /// This method deletes a given file
        /// </summary>
        /// <param name="resourceId">Id of the resource </param>
        /// <param name="path">Path to the file</param>
        /// <returns>status code 204 if deletion successful otherwise status code 400, 401 or 404 </returns>
        [HttpDelete("[controller]/{resourceId}/{*path}")]
        [ApiExplorerSettings(IgnoreApi = true)]
        public async Task<IActionResult> DeleteFileWithPath(string resourceId, string path)
        {
            return await DeleteFile(resourceId, path);
        }

        /// <summary>
        /// This method deletes a given file
        /// </summary>
        /// <param name="resourceId">Id of the resource </param>
        /// <param name="path">Path to the file</param>
        /// <returns>status code 204 if deletion successful otherwise status code 400, 401 or 404 </returns>
        [HttpDelete("[controller]/{resourceId}/")]
        public async Task<IActionResult> DeleteFileWithParameter(string resourceId, [System.Web.Http.FromUri] string path)
        {
            // Strip the first slash, to reuse the previous implementation.
            if (path.StartsWith("/"))
            {
                path = path[1..];
            }

            return await DeleteFile(resourceId, path);
        }

        /// <summary>
        /// This method deletes a given file
        /// </summary>
        /// <param name="resourceId">Id of the resource </param>
        /// <param name="path">Path to the file</param>
        /// <returns>status code 204 if deletion successful otherwise status code 400, 401 or 404 </returns>
        public async Task<IActionResult> DeleteFile(string resourceId, string path)
        {
            var user = _authenticator.GetUser();
            path = $"/{path}";
            var checkPath = CheckPath(path);
            if (checkPath != null)
            {
                return checkPath;
            }
            var checkResourceId = CheckResource(resourceId, out Resource resource);
            if (checkResourceId != null)
            {
                return checkResourceId;
            }
            var checkUser = CheckUser(user, resource);
            if (checkUser != null)
            {
                return checkUser;
            }

            if (resource.Archived == "1")
            {
                return BadRequest("The resource is read only!");
            }

            try
            {
                var resourceTypeOptions = _resourceModel.GetResourceTypeOptions(resource.Id);
                var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(resource.Type.DisplayName, _configuration);
                if (resourceTypeDefinition == null)
                {
                    return BadRequest($"No provider for: \"{resource.Type.DisplayName}\".");
                }
                await resourceTypeDefinition.DeleteEntry(resource.Id.ToString(), path, resourceTypeOptions);
                LogAnalytics("Delete File", resourceId, path, user);
                return NoContent();
            }
            catch (Exception e)
            {
                _coscineLogger.Log("Delete failed", e);
                return BadRequest("Error in communication with the resource");
            }
        }

        /// <summary>
        /// This method checks if the resource is valid
        /// </summary>
        /// <returns>status code 204 if resource is valid otherwise status code 400 or 404</returns>
        [HttpPost("[controller]/validate")]
        public async Task<IActionResult> IsResourceValid([FromBody] JToken resource)
        {
            var displayName = resource["type"]["displayName"].ToString().ToLower();
            var resourceTypeOptions = new Dictionary<string, string>();
            if (displayName == "s3")
            {
                resourceTypeOptions.Add("accessKey", resource["resourceTypeOption"]["AccessKey"].ToString());
                resourceTypeOptions.Add("secretKey", resource["resourceTypeOption"]["SecretKey"].ToString());
                resourceTypeOptions.Add("bucketname", resource["resourceTypeOption"]["BucketName"].ToString());
                resourceTypeOptions.Add("resourceUrl", resource["resourceTypeOption"]["ResourceUrl"].ToString());
            }
            else if (displayName == "gitlab")
            {
                resourceTypeOptions.Add("token", resource["resourceTypeOption"]["Token"].ToString());
                resourceTypeOptions.Add("repositoryUrl", resource["resourceTypeOption"]["RepositoryUrl"].ToString());
                resourceTypeOptions.Add("repositoryNumber", resource["resourceTypeOption"]["RepositoryNumber"].ToString());
            }
            try
            {
                var resourceTypeDefinition = ResourceTypeFactory.CreateResourceTypeObject(displayName, _configuration);
                if (resourceTypeDefinition == null)
                {
                    return BadRequest($"No provider for: \"{displayName}\".");
                }

                await resourceTypeDefinition.IsResourceCreated("", resourceTypeOptions);
                return NoContent();
            }
            catch (Exception e)
            {
                _coscineLogger.Log("Resource validation failed", e);
                return BadRequest("Error in communication with the resource");
            }
        }

        /// <summary>
        /// Tries to establish connection with resource and validates whether the given file/folder exists
        /// </summary>
        private IActionResult CheckResource(string resourceId, out Resource resource)
        {
            resource = null;

            if (!Guid.TryParse(resourceId, out Guid resourceGuid))
            {
                return BadRequest($"{resourceId} is not a GUID.");
            }

            try
            {
                resource = _resourceModel.GetById(resourceGuid);
                if (resource == null)
                {
                    return NotFound($"Could not find resource with id: {resourceId}");
                }
            }
            catch (Exception)
            {
                return NotFound($"Could not find resource with id: {resourceId}");
            }

            if (resource.Type == null)
            {
                ResourceTypeModel resourceTypeModel = new ResourceTypeModel();
                resource.Type = resourceTypeModel.GetById(resource.TypeId);
            }
            // All good
            return null;
        }

        /// <summary>
        /// Checks if the path is valid
        /// </summary>
        /// <param name="path">path</param>
        /// <returns>status code 400 if the given path is not valid</returns>
        public IActionResult CheckPath(string path)
        {
            if (string.IsNullOrWhiteSpace(path))
            {
                return BadRequest($"Your path \"{path}\" is empty.");
            }

            var rgx = new Regex(@"[\:?*<>|]+");
            if (rgx.IsMatch(path))
            {
                return BadRequest($"Your path \"{path}\" contains bad characters. The following characters are not permissible: {@"\/:?*<>|"}.");
            }

            if (path.Contains("%2F") || path.Contains("%2f"))
            {
                return BadRequest("Path can not contain the sequence %2F.");
            }

            return null;
        }

        /// <summary>
        /// Checks if the user has access to the resource
        /// </summary>
        /// <param name="user">user</param>
        /// <param name="resource">resource</param>
        /// <returns>status code 403 if the user has no access</returns>
        public IActionResult CheckUser(User user, Resource resource)
        {
            if (user == null || !_resourceModel.HasAccess(user, resource, UserRoles.Owner, UserRoles.Member))
            {
                return Forbid("User does not have permission to the resource.");
            }
            return null;
        }

        /// <summary>
        /// Writes an analytics log entry
        /// </summary>
        private void LogAnalytics(string operation, string resourceId, string path, User user)
        {
            if (CoscineLoggerConfiguration.IsLogLevelActivated(LogType.Analytics))
            {
                _analyticsLogObject.Type = "Action";
                _analyticsLogObject.FileId = resourceId + "/" + HttpUtility.UrlDecode(path);
                _analyticsLogObject.ResourceId = resourceId;
                _analyticsLogObject.ProjectId = _projectResourceModel.GetProjectForResource(new Guid(resourceId)).ToString();
                _analyticsLogObject.RoleId = _projectRoleModel.GetGetUserRoleForProject(new Guid(_analyticsLogObject.ProjectId), user.Id).ToString();
                _analyticsLogObject.Operation = operation;
                _coscineLogger.AnalyticsLog(_analyticsLogObject);
            }
        }
    }
}