using Coscine.Database.DataModel;
using Coscine.Database.ReturnObjects;
using Coscine.Database.Util;
using LinqKit;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.RegularExpressions;

namespace Coscine.Database.Models
{
    public class ProjectModel : DatabaseModel<Project>
    {
        public bool IsDeleted(Guid id)
        {
            return DatabaseConnection.ConnectToDatabase((db) =>
            {
                return
                    (from tableEntry in GetITableFromDatabase(db)
                     where tableEntry.Id == id
                            && tableEntry.Deleted
                     select tableEntry).Count() == 1;
            });
        }

        public override Project GetById(Guid id)
        {
            var expression = GetIdFromObject();
            return DatabaseConnection.ConnectToDatabase((db) =>
            {
                return
                    (from tableEntry in GetITableFromDatabase(db).AsExpandable()
                     where expression.Invoke(tableEntry) == id
                        && !tableEntry.Deleted
                     select tableEntry).FirstOrDefault();
            });
        }

        public Project GetBySlug(String slug)
        {
            return GetWhere((tableEntry) => tableEntry.Slug == slug);
        }

        public Project GetByIdIncludingDeleted(Guid id)
        {
            var expression = GetIdFromObject();
            return DatabaseConnection.ConnectToDatabase((db) =>
            {
                return
                    (from tableEntry in GetITableFromDatabase(db).AsExpandable()
                     where expression.Invoke(tableEntry) == id
                     select tableEntry).FirstOrDefault();
            });
        }

        public override Project GetWhere(Expression<Func<Project, bool>> whereClause)
        {
            return DatabaseConnection.ConnectToDatabase((db) =>
            {
                return
                    (from tableEntry in GetITableFromDatabase(db).AsExpandable()
                     where whereClause.Invoke(tableEntry)
                        && !tableEntry.Deleted
                     select tableEntry).FirstOrDefault();
            });
        }

        public override IEnumerable<Project> GetAll()
        {
            return DatabaseConnection.ConnectToDatabase((db) =>
            {
                return
                    (from tableEntry in GetITableFromDatabase(db)
                     where !tableEntry.Deleted
                     select tableEntry).ToList();
            });
        }

        public override IEnumerable<Project> GetAllWhere(Expression<Func<Project, bool>> whereClause)
        {
            return DatabaseConnection.ConnectToDatabase((db) =>
            {
                return
                    (from tableEntry in GetITableFromDatabase(db).AsExpandable()
                     where whereClause.Invoke(tableEntry)
                        && !tableEntry.Deleted
                     select tableEntry).ToList();
            });
        }

        public override int Update(Project databaseObject)
        {
            if (!databaseObject.Deleted)
            {
                return DatabaseConnection.ConnectToDatabase((db) =>
                {
                    return (int)db.Update(databaseObject).State;
                });
            }
            else
            {
                return 0;
            }
        }

        public override int Delete(Project databaseObject)
        {
            databaseObject.Deleted = true;
            return DatabaseConnection.ConnectToDatabase((db) => (int)db.Update(databaseObject).State);
        }

        public int HardDelete(Project databaseObject)
        {
            return DatabaseConnection.ConnectToDatabase((db) => (int)db.Remove(databaseObject).State);
        }

        public int HardDelete(Expression<Func<Project, bool>> whereClause)
        {
            return DatabaseConnection.ConnectToDatabase((db) =>
            {
                return (int)db.Remove(from tableEntry in GetITableFromDatabase(db).AsExpandable()
                                      where whereClause.Invoke(tableEntry)
                                      select tableEntry).State;
            });
        }

        public Project StoreFromObject(ProjectObject projectObject, User user, IEnumerable<ProjectQuota> defaultProjectQuotas)
        {
            if (!projectObject.Disciplines.Any() || !projectObject.Organizations.Any())
            {
                throw new ArgumentException("Discipline and Institute are necessary!");
            }

            Project project = new Project()
            {
                Description = projectObject.Description,
                DisplayName = projectObject.DisplayName,
                StartDate = projectObject.StartDate,
                EndDate = projectObject.EndDate,
                Keywords = projectObject.Keywords,

                ProjectName = projectObject.ProjectName,
                PrincipleInvestigators = projectObject.PrincipleInvestigators,
                GrantId = projectObject.GrantId,
                Slug = GenerateSlug(projectObject),
                VisibilityId = projectObject.Visibility.Id,
                // DateCreated is skipped here. Value set automatically by the database.
                Creator = user.Id,
            };

            Insert(project);
            try
            {
                SetDisciplines(project, projectObject.Disciplines);
                SetOrganizations(project, projectObject.Organizations);
                SetQuotas(project, defaultProjectQuotas);
            }
            catch (Exception)
            {
                HardDelete(project);
                throw;
            }
            SetOwner(project, user);
            return project;
        }

        public List<OrganizationCountObject> GetProjectCountByOrganization()
        {
            return DatabaseConnection.ConnectToDatabase((db) =>
            {
                return (from p in db.Projects
                        join pi in db.ProjectInstitutes on p.Id equals pi.ProjectId into joinedPi
                        from jpi in joinedPi.DefaultIfEmpty()

                        where !p.Deleted
                        group jpi by jpi.OrganizationUrl into g
                        select new OrganizationCountObject(g.Key, g.Count())).ToList();
            });
        }

        private string GenerateSlug(ProjectObject projectObject)
        {
            // create slug for project
            var slug = projectObject.DisplayName;
            slug = slug.ToLower();
            slug = Regex.Replace(slug, @"[\s-]+", "-");
            slug = Regex.Replace(slug, "[^a-z0-9-]*|", "");
            slug = Regex.Replace(slug, "^-|-$", "");

            Random r = new Random();
            int rInt = r.Next(0, 9000000) + 1000000;
            string fullSlug = "" + rInt;

            if (slug.Length >= 7)
            {
                rInt = r.Next(0, 9000) + 1000;
                fullSlug = slug;
            }

            if (GetBySlug(fullSlug) != null)
            {
                if (slug.Length >= 7)
                {
                    fullSlug = slug + "-";
                }
                while (GetBySlug(fullSlug + rInt) != null)
                {
                    rInt++;
                }
                fullSlug += rInt;
            }

            return fullSlug;
        }

        private void SetDisciplines(Project project, IEnumerable<DisciplineObject> disciplines)
        {
            ProjectDisciplineModel projectDisciplineModel = new ProjectDisciplineModel();
            foreach (var oldDiscipline in projectDisciplineModel.GetAllWhere((projectDiscipline) => projectDiscipline.ProjectId == project.Id))
            {
                projectDisciplineModel.Delete(oldDiscipline);
            }
            foreach (var discipline in disciplines)
            {
                projectDisciplineModel.Insert(new ProjectDiscipline()
                {
                    ProjectId = project.Id,
                    DisciplineId = discipline.Id
                });
            }
        }

        private void SetOrganizations(Project project, IEnumerable<OrganizationObject> organizations)
        {
            ProjectInstituteModel projectInstituteModel = new ProjectInstituteModel();
            foreach (var oldInstitute in projectInstituteModel.GetAllWhere((projectInstitute) => projectInstitute.ProjectId == project.Id))
            {
                projectInstituteModel.Delete(oldInstitute);
            }
            foreach (var organization in organizations)
            {
                projectInstituteModel.Insert(new ProjectInstitute()
                {
                    ProjectId = project.Id,
                    OrganizationUrl = organization.Url,
                });
            }
        }

        public void SetQuotas(Project project, IEnumerable<ProjectQuota> defaultProjectQuotas)
        {
            ProjectQuotaModel projectQuotaModel = new ProjectQuotaModel();
            ResourceTypeModel resourceTypeModel = new ResourceTypeModel();

            foreach (var resourceType in resourceTypeModel.GetAll())
            {
                int quota = 0;
                int maxQuota = 0;

                var tDefaultQuotas = defaultProjectQuotas.Where(x => x.ResourceTypeId == resourceType.Id);

                if (tDefaultQuotas.Any())
                {
                    quota = tDefaultQuotas.First().Quota;
                    maxQuota = tDefaultQuotas.First().MaxQuota;
                }

                projectQuotaModel.Insert(new ProjectQuota
                {
                    ProjectId = project.Id,
                    ResourceTypeId = resourceType.Id,
                    Quota = quota,
                    MaxQuota = maxQuota
                });
            }
        }

        public ProjectRole SetOwner(Project project, User user)
        {
            ProjectRoleModel projectRoleModel = new ProjectRoleModel();

            ProjectRole projectRole = new ProjectRole()
            {
                RelationId = Guid.NewGuid(),
                ProjectId = project.Id,
                UserId = user.Id,
                RoleId = new RoleModel().GetWhere((x) => x.DisplayName == "Owner").Id
            };
            projectRoleModel.Insert(projectRole);

            return projectRole;
        }

        public bool HasAccess(User user, Guid projectId, params string[] allowedAccess)
        {
            return HasAccess(user, GetById(projectId), allowedAccess);
        }

        public bool HasAccess(User user, Project project, params string[] allowedAccess)
        {
            ProjectRoleModel projectRoleModel = new ProjectRoleModel();
            allowedAccess = allowedAccess.Select(x => x.ToLower().Trim()).ToArray();

            IEnumerable<ProjectRole> projectRoles = projectRoleModel.GetAllWhere(
                (projectRoleRelation) => projectRoleRelation.ProjectId == project.Id &&
                                         projectRoleRelation.UserId == user.Id &&
                                         allowedAccess.Contains(projectRoleRelation.Role.DisplayName.ToLower()));
            return projectRoles.Any();
        }

        private IEnumerable<Project> GetWithAccess(User user, string[] allowedAccess, Func<IEnumerable<Guid>, IEnumerable<Project>> filter)
        {
            ProjectRoleModel projectRoleModel = new ProjectRoleModel();

            allowedAccess = allowedAccess.Select(x => x.ToLower().Trim()).ToArray();
            var allUserProjectRoles = projectRoleModel.GetAllWhere((projectRoleRelation) => projectRoleRelation.UserId == user.Id &&
                                                                                            allowedAccess.Contains(projectRoleRelation.Role.DisplayName.ToLower()));
            var allowedProjectIds = allUserProjectRoles.Select((projectRole) => projectRole.ProjectId);
            var allowedProjects = filter.Invoke(allowedProjectIds);

            return allowedProjects.ToList();
        }

        public IEnumerable<Project> GetWithAccess(User user, params string[] allowedAccess)
        {
            return GetWithAccess(user, allowedAccess, (allowedProjectIds) => GetAllWhere((project) => allowedProjectIds.Contains(project.Id)));
        }

        public IEnumerable<Project> GetTopLevelWithAccess(User user, params string[] allowedAccess)
        {
            return GetWithAccess(user, allowedAccess, (_) => GetAllWhere((project) =>
                (
                        // all accessible projects that have no parents
                        (project.SubProjectSubProjectNavigations.Count == 0)
                    || // all accessible projects that have no accessible parents
                        (
                            project.SubProjectSubProjectNavigations.All(
                                   (parentProjects) =>
                                        parentProjects.Project.ProjectRoles.All((projectRole) => projectRole.UserId != user.Id)
                            )
                        )
                )
                && project.ProjectRoles.Any((projectRole) => projectRole.UserId == user.Id))
            );
        }

        public void AddResource(Project project, Resource resource)
        {
            ProjectResourceModel projectResourceModel = new ProjectResourceModel();
            if (projectResourceModel.GetAllWhere((projectResource) => projectResource.ResourceId == resource.Id && projectResource.ProjectId == project.Id).Any())
            {
                throw new InvalidOperationException("Resource is already assigned to project!");
            }
            ProjectResource newProjectResource = new ProjectResource
            {
                ProjectId = project.Id,
                ResourceId = resource.Id
            };
            projectResourceModel.Insert(newProjectResource);
        }

        public int UpdateByObject(Project project, ProjectObject projectObject)
        {
            if (!projectObject.Disciplines.Any() || !projectObject.Organizations.Any())
            {
                throw new ArgumentException("Discipline and Institute are necessary!");
            }

            project.Description = projectObject.Description;
            project.DisplayName = projectObject.DisplayName;
            project.StartDate = projectObject.StartDate;
            project.EndDate = projectObject.EndDate;
            project.Keywords = projectObject.Keywords;

            project.ProjectName = projectObject.ProjectName;
            project.PrincipleInvestigators = projectObject.PrincipleInvestigators;
            project.GrantId = projectObject.GrantId;

            SetDisciplines(project, projectObject.Disciplines);
            SetOrganizations(project, projectObject.Organizations);
            project.VisibilityId = projectObject.Visibility.Id;
            // Project creator can not be altered after creation
            // Project DateCreated can not be altered after creation
            return Update(project);
        }

        public ProjectObject CreateReturnObjectFromDatabaseObject(Project project)
        {
            return CreateReturnObjectFromDatabaseObject(project, new Guid());
        }

        public ProjectObject CreateReturnObjectFromDatabaseObject(Project project, Guid parentId)
        {
            IEnumerable<DisciplineObject> disciplines = new List<DisciplineObject>();
            DisciplineModel disciplineModel = new DisciplineModel();
            disciplines = disciplineModel.GetAllWhere((discipline) => (from relation in discipline.ProjectDisciplines where relation.ProjectId == project.Id select relation).Any())
                                                .Select((discipline) => new DisciplineObject(discipline.Id, discipline.Url, discipline.DisplayNameDe, discipline.DisplayNameEn));

            IEnumerable<OrganizationObject> organizations = new List<OrganizationObject>();
            ProjectInstituteModel projectInstituteModel = new ProjectInstituteModel();
            organizations = projectInstituteModel.GetAllWhere((projectInstitute) => (projectInstitute.ProjectId == project.Id))
                                            .Select((projectInstitute) => new OrganizationObject(projectInstitute.OrganizationUrl, projectInstitute.OrganizationUrl));

            if (project.Visibility == null && project.VisibilityId.HasValue)
            {
                VisibilityModel visibilityModel = new VisibilityModel();
                project.Visibility = visibilityModel.GetById(project.VisibilityId.Value);
            }

            return new ProjectObject(project.Id,
                project.Description,
                project.DisplayName,
                project.StartDate,
                project.EndDate,
                project.Keywords,
                project.ProjectName, project.PrincipleInvestigators, project.GrantId,
                disciplines,
                organizations,
                project.Visibility == null ? null : new VisibilityObject(project.Visibility.Id, project.Visibility.DisplayName),
                project.Slug,
                project.DateCreated,
                parentId,
                project.Creator,
                project.Deleted
                );
        }

        public List<Project> ListToRootProject(Project project, User user)
        {
            List<Project> projectList = new List<Project>
            {
                project
            };

            ProjectRoleModel projectRoleModel = new ProjectRoleModel();

            var currentProject = project;
            IEnumerable<Project> list;
            bool continueLoop = true;
            do
            {
                list = GetAllWhere((dbProject) => (from subProject in dbProject.SubProjectProjects
                                                   where subProject.SubProjectId == currentProject.Id
                                                   && !subProject.Project.Deleted
                                                   select subProject).Any());

                if (list.Any())
                {
                    currentProject = list.First();
                    bool authorized = true;
                    if (user != null)
                    {
                        authorized = projectRoleModel.GetAllWhere((dbProjectRole) =>
                                                        dbProjectRole.UserId == user.Id
                                                        && dbProjectRole.ProjectId == currentProject.Id).Any();
                    }
                    if (projectList.Contains(currentProject) || !authorized)
                    {
                        continueLoop = false;
                    }
                    else
                    {
                        projectList.Add(currentProject);
                    }
                }
                else
                {
                    continueLoop = false;
                }
            } while (continueLoop);

            return projectList;
        }

        public override Expression<Func<Project, Guid>> GetIdFromObject()
        {
            return databaseObject => databaseObject.Id;
        }

        public override Microsoft.EntityFrameworkCore.DbSet<Project> GetITableFromDatabase(CoscineDB db)
        {
            return db.Projects;
        }

        public override void SetObjectId(Project databaseObject, Guid id)
        {
            databaseObject.Id = id;
        }

        public string GetMetadataCompleteness(ProjectObject projectObject)
        {
            var maxCount = 0;
            var counted = 0;

            var projectProperties = typeof(ProjectObject).GetProperties();
            foreach (var property in projectProperties)
            {
                if (property == null
                    || property.PropertyType == typeof(Guid)
                    || property.Name == "Slug")
                {
                    continue;
                }

                maxCount++;

                if (property.PropertyType == typeof(string)
                    && property.GetValue(projectObject) != null
                    && !string.IsNullOrEmpty(property.GetValue(projectObject).ToString()))
                {
                    counted++;
                }
                else if (property.PropertyType == typeof(DateTime)
                    && property.GetValue(projectObject) != null)
                {
                    counted++;
                }
                else if (property.PropertyType == typeof(IEnumerable<DisciplineObject>)
                    && property.GetValue(projectObject) != null
                    && ((IEnumerable<DisciplineObject>)property.GetValue(projectObject)).Any())
                {
                    counted++;
                }
                else if (property.PropertyType == typeof(IEnumerable<OrganizationObject>)
                    && property.GetValue(projectObject) != null
                    && ((IEnumerable<OrganizationObject>)property.GetValue(projectObject)).Any())
                {
                    counted++;
                }
                else if (property.PropertyType == typeof(VisibilityObject)
                    && property.GetValue(projectObject) != null)
                {
                    counted++;
                }
            }
            return $"{counted}/{maxCount}";
        }
    }
}