namespace MyOffice.Services.Account
{
	using AutoMapper;
	using Core;
	using Core.Extensions;
	using Core.Helpers;
	using Data.Models.Accounts;
	using Data.Models.Items;
	using Data.Repositories.Account;
	using Data.Repositories.Currency;
	using Data.Repositories.Item;
	using Item;
	using Item.Domain;
	using Microsoft.Extensions.Logging;
	using MyOffice.Services.Account.Domain;

	public class AccountService
	{
		private ILogger<AccountService> _logger;
		private readonly IMapper _mapper;
		private readonly IAccountCategoryRepository _accountCategoryRepository;
		private readonly IAccountAccessRepository _accountAccessRepository;
		private readonly IAccountRepository _accountRepository;
		private readonly ICurrencyRepository _currencyRepository;
		private readonly IAccountAccountCategoryRepository _accountAccountCategoryRepository;
		private readonly IMotionRepository _motionRepository;
		private readonly IItemRepository _itemRepository;
		private readonly IItemGlobalRepository _itemGlobalRepository;
		private readonly ItemService _itemService;

		public AccountService(
			ILogger<AccountService> logger,
			IMapper mapper,
			IAccountCategoryRepository accountCategoryRepository,
			IAccountAccessRepository accountAccessRepository,
			IAccountRepository accountRepository,
			ICurrencyRepository currencyRepository,
			IAccountAccountCategoryRepository accountAccountCategoryRepository,
			IMotionRepository motionRepository,
			IItemRepository itemRepository,
			IItemGlobalRepository itemGlobalRepository,
			ItemService itemService
		)
		{
			_logger = logger;
			_mapper = mapper;
			_accountCategoryRepository = accountCategoryRepository;
			_accountAccessRepository = accountAccessRepository;
			_accountRepository = accountRepository;
			_currencyRepository = currencyRepository;
			_accountAccountCategoryRepository = accountAccountCategoryRepository;
			_motionRepository = motionRepository;
			_itemRepository = itemRepository;
			_itemGlobalRepository = itemGlobalRepository;
			_itemService = itemService;
		}

		public List<AccountCategory> GetAllCategories(Guid userId)
		{
			return _accountCategoryRepository.GetAll(userId);
		}

		public Exec<AccountCategory, GeneralExecStatus> GetCategory(Guid userId, Guid id)
		{
			var result = new Exec<AccountCategory, GeneralExecStatus>(GeneralExecStatus.success);
			
			var category =  _accountCategoryRepository.Get(userId, id);

			if (category == null)
			{
				return result.Set(GeneralExecStatus.not_found);
			}

			return result.Set(category);
		}

		public Exec<AccountCategory, GeneralExecStatus> CategoryAdd(Guid userId, AccountCategory category)
		{
			if (category == null)
				throw new ArgumentNullException(nameof(category));

			var result = new Exec<AccountCategory, GeneralExecStatus>(GeneralExecStatus.success);

			category.Id = Guid.NewGuid();
			category.UserId = userId;

			if (!_accountCategoryRepository.Add(category))
			{
				return result.Set(GeneralExecStatus.failure);
			}

			return result.Set(category);
		}

		public Exec<AccountCategory, GeneralExecStatus> CategoryUpdate(Guid userId, Guid id, AccountCategory category)
		{
			if (category == null)
				throw new ArgumentNullException(nameof(category));

			var result = new Exec<AccountCategory, GeneralExecStatus>(GeneralExecStatus.success);

			var exists = _accountCategoryRepository.Get(userId, id);
			if (exists == null)
			{
				return result.Set(GeneralExecStatus.not_found);
			}

			exists.Name = category.Name;

			if (!_accountCategoryRepository.Update(exists))
			{
				return result.Set(GeneralExecStatus.failure);
			}

			return result.Set(exists);
		}

		public Exec<AccountCategory, AccountCategoryRemoveResult> CategoryRemove(Guid userId, Guid id)
		{
			var result = new Exec<AccountCategory, AccountCategoryRemoveResult>(AccountCategoryRemoveResult.success);

			var exists = _accountCategoryRepository.Get(userId, id);
			if (exists == null)
			{
				return result.Set(AccountCategoryRemoveResult.not_found);
			}

			if (exists.Accounts?.Any() == true)
			{
				return result.Set(AccountCategoryRemoveResult.accounts_exists);
			}
			if (!_accountCategoryRepository.Remove(exists))
			{
				return result.Set(AccountCategoryRemoveResult.failure);
			}

			return result.Set(exists);
		}

		public List<AccountDto> GetAllAccounts(Guid userId)
		{
			return _mapper.Map<List<AccountDto>>(_accountRepository.GetAll(userId));
		}

		public List<AccountDto> GetByCategory(Guid userId, Guid categoryId)
		{
			return _mapper.Map<List<AccountDto>>(_accountRepository.GetByCategory(userId, categoryId));
		}

		public List<AccountDetailedDto> GetByCategoryDetailed(Guid userId, Guid categoryId)
		{
			return _mapper.Map<List<AccountDetailedDto>>(_accountRepository.GetByCategoryDetailed(userId, categoryId));
		}

		public Exec<AccountDetailedDto, GeneralExecStatus> GetByIdDetailed(Guid userId, Guid id)
		{
			var result = new Exec<AccountDetailedDto, GeneralExecStatus>(GeneralExecStatus.success);

			var account = _accountRepository.GetByIdDetailed(userId, id);
			if (account == null)
			{
				return result.Set(GeneralExecStatus.not_found);
			}

			return result.Set(_mapper.Map<AccountDetailedDto>(account));
		}

		public Exec<Account, AccountAddResult> AccountAdd(Guid userId, AccountAdd input)
		{
			if (input == null)
				throw new ArgumentNullException(nameof(input));

			var result = new Exec<Account, AccountAddResult>(AccountAddResult.success);

			var category = _accountCategoryRepository.Get(userId, input.CategoryId);
			if (category == null)
			{
				return result.Set(AccountAddResult.category_not_found);
			}
			var currency = _currencyRepository.GetByGlobalCurrency(userId, input.CurrencyId);
			if (currency == null)
			{
				return result.Set(AccountAddResult.currency_not_found);
			}

			var account = new Account
			{
				Id = Guid.NewGuid(),
				Name = input.Name,
				CurrencyGlobalId = currency.CurrencyGlobalId,
			};

			account.Categories = new List<AccountAccountCategory>
			{
				new()
				{
					AccountId = account.Id,
					CategoryId = category.Id,
				}
			};
			account.AccessRights = new List<AccountAccess>
			{
				new()
				{
					AccountId = account.Id,
					UserId = userId,
					IsAllowManage = true,
					IsAllowRead = true,
					IsAllowWrite = true,
				}
			};

			if (!_accountRepository.Add(account))
			{
				return result.Set(AccountAddResult.failure);
			}

			var access = account.AccessRights!.FirstOrDefault(x => x.UserId == userId);
			if (access != null && access.Type.ToString() != input.Type)
			{
				if (Enum.TryParse<AccountAccessTypeEnum>(input.Type, out var enumType))
				{
					access.Type = enumType;
					_accountAccessRepository.Update(access);
				}
			}

			return result.Set(account);
		}

		public Exec<Account, GeneralExecStatus> AccountDelete(Guid userId, string id)
		{
			var result = new Exec<Account, GeneralExecStatus>(GeneralExecStatus.success);

			var account = _accountRepository.Get(userId, id.AsGuid());
			if (account == null)
			{
				return result.Set(GeneralExecStatus.not_found);
			}

			if (account.Motions!.Any())
			{
				return result.Set(GeneralExecStatus.not_found);
			}

			_accountRepository.Delete(account);
			result.Set(account);

			return result;
		}

		public Exec<Account, AccountEditResult> AccountUpdate(Guid userId, Guid accountId, AccountEdit input)
		{
			if (input == null)
				throw new ArgumentNullException(nameof(input));

			var result = new Exec<Account, AccountEditResult>(AccountEditResult.success);

			AccountCategory? category = null;
			if (input.CategoryId.HasValue)
			{
				category = _accountCategoryRepository.Get(userId, input.CategoryId.Value);
				if (category == null)
				{
					return result.Set(AccountEditResult.category_not_found);
				}
			}

			var currency = _currencyRepository.GetByGlobalCurrency(userId, input.CurrencyId);
			if (currency == null)
			{
				return result.Set(AccountEditResult.currency_not_found);
			}

			var account = _accountRepository.Get(userId, accountId);
			if (account == null)
			{
				return result.Set(AccountEditResult.not_found);
			}

			account.Name = input.Name;
			if (account.CurrencyGlobalId != currency.CurrencyGlobalId)
			{
				account.CurrencyGlobalId = currency.CurrencyGlobalId;
			}

			if (category != null && account.Categories!.All(x => x.CategoryId != category.Id))
			{
				account.Categories = new List<AccountAccountCategory>
				{
					new()
					{
						AccountId = account.Id,
						CategoryId = category.Id,
					}
				};
			}

			if (!_accountRepository.Update(account))
			{
				return result.Set(AccountEditResult.failure);
			}

			var access = account.AccessRights!.FirstOrDefault(x => x.UserId == userId);
			if (access != null && access.Type.ToString() != input.Type)
			{
				if (Enum.TryParse<AccountAccessTypeEnum>(input.Type, out var enumType))
				{
					access.Type = enumType;
					_accountAccessRepository.Update(access);
				}
			}

			return result.Set(account);
		}

		public Exec<List<AccountAccountCategory>, GeneralExecStatus> AccountCategoryRemove(Guid userId, Guid accountId, Guid categoryId)
		{
			var result = new Exec<List<AccountAccountCategory>, GeneralExecStatus>(GeneralExecStatus.success);

			var categories = _accountAccountCategoryRepository.Get(userId, accountId, categoryId);
			if (categories.Count == 0)
			{
				return result.Set(GeneralExecStatus.not_found);
			}

			foreach (var category in categories)
			{
				_accountAccountCategoryRepository.Remove(category);
			}

			return result.Set(categories);
		}

		public Exec<List<Motion>, MotionAddResult> MotionAdd(
			Guid userId,
			Guid accountId,
			MotionAddUpdate motion
		)
		{
			if (motion == null)
				throw new ArgumentNullException(nameof(motion));
			if (motion.Item == null)
				throw new ArgumentNullException(nameof(motion.Item));

			var result = new Exec<List<Motion>, MotionAddResult>(MotionAddResult.success);

			var account = _accountRepository.Get(userId, accountId);
			if (account == null)
			{
				return result.Set(MotionAddResult.account_not_found);
			}

			var itemExec = _itemService.GetOrCreate(userId, motion.Item);
			if (itemExec.Status != ItemGetOrAddResult.success)
			{
				return result.Set(MotionAddResult.failure);
			}

			var motionDb = new Motion
			{
				Id = Guid.NewGuid(),
				CreatedOn = DateTime.UtcNow,
				DateTime = motion.Date,
				AccountId = account.Id,
				ItemId = itemExec.Result!.Id,
				Description = motion.Description,
				AmountPlus = motion.Plus,
				AmountMinus = motion.Minus,
			};

			if (!_motionRepository.Add(motionDb))
			{
				return result.Set(MotionAddResult.failure);
			}

			result.Set(new List<Motion>());
			result.Result!.Add(motionDb);

			if (motion.AccountId.IsPresent() && motion.AmountBalancing != 0)
			{
				var accountBalancing = _accountRepository.Get(userId, motion.AccountId!.AsGuid());
				if (accountBalancing != null)
				{
					var balancingName = $"+{account.Name}";
					var itemBalancingExec = _itemService.GetOrCreate(userId, balancingName);
					var plus = motion.AmountBalancing;
					var minus = 0m;
					if (motion.Plus != 0)
					{
						plus = 0;
						minus = motion.AmountBalancing;
					}
					var motionBalancing = new Motion
					{
						Id = Guid.NewGuid(),
						CreatedOn = DateTime.UtcNow,
						DateTime = motion.Date,
						AccountId = accountBalancing.Id,
						ItemId = itemBalancingExec.Result!.Id,
						Description = motion.Description,
						AmountPlus = plus,
						AmountMinus = minus,
					};

					if (_motionRepository.Add(motionBalancing))
					{
						result.Result!.Add(motionBalancing);
					}
				}
			}

			motionDb.Item = itemExec.Result!;

			return result;
		}

		public Exec<Motion, MotionUpdateResult> MotionUpdate(
			Guid userId,
			Guid accountId,
			Guid motionId,
			MotionAddUpdate motion
		)
		{
			if (motion == null)
				throw new ArgumentNullException(nameof(motion));
			if (motion.Item == null)
				throw new ArgumentNullException(nameof(motion.Item));

			var result = new Exec<Motion, MotionUpdateResult>(MotionUpdateResult.success);

			var exists = _motionRepository.Get(userId, motionId);
			if (exists == null || exists.AccountId != accountId)
			{
				return result.Set(MotionUpdateResult.not_found);
			}

			var itemExec = _itemService.GetOrCreate(userId, motion.Item);
			if (itemExec.Status != ItemGetOrAddResult.success)
			{
				return result.Set(MotionUpdateResult.failure);
			}

			exists.Item.Id = itemExec.Result!.Id;
			exists.DateTime = motion.Date;
			exists.Description = motion.Description;
			exists.AmountPlus = motion.Plus;
			exists.AmountMinus = motion.Minus;
			
			if (!_motionRepository.Update(exists))
			{
				return result.Set(MotionUpdateResult.failure);

			}

			return result.Set(exists);
		}

		public Exec<List<Motion>, GeneralExecStatus> GetMotions(
			Guid userId, 
			Guid accountId, 
			DateTime dateFrom,
			DateTime dateTo
		)
		{
			var result = new Exec<List<Motion>, GeneralExecStatus>(GeneralExecStatus.success);

			var account = _accountRepository.Get(userId, accountId);
			if (account == null)
			{
				return result.Set(GeneralExecStatus.not_found);
			}

			var motions = _motionRepository.GetByAccount(accountId, dateFrom.ToUtc(), dateTo.ToUtc());

			return result.Set(motions);
		}

		public Exec<Motion, MotionDeleteResult> MotionRemove(
			Guid userId,
			Guid accountId,
			Guid motionId
		)
		{
			var result = new Exec<Motion, MotionDeleteResult>(MotionDeleteResult.success);

			var exists = _motionRepository.Get(userId, motionId);
			if (exists == null || exists.AccountId != accountId)
			{
				return result.Set(MotionDeleteResult.not_found);
			}

			if (!_motionRepository.Remove(exists))
			{
				return result.Set(MotionDeleteResult.failure);
			}

			return result.Set(exists);
		}

		public List<Account> FindAccounts(Guid userId, string term)
		{
			return _accountRepository.FindAccounts(userId, term);
		}
	}
}
