namespace AAIntegration.SimmonsBank.API.Services;

using AutoMapper;
using BCrypt.Net;
using AAIntegration.SimmonsBank.API.Entities;
using AAIntegration.SimmonsBank.API.Config;
using AAIntegration.SimmonsBank.API.Models.Transactions;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System;
using Microsoft.EntityFrameworkCore;
using Internal;
using System.Collections.Immutable;

public interface ITransactionService
{
    IEnumerable<Transaction> GetAll(int ownerId);
    Transaction GetById(int id, int ownerId);
    IEnumerable<Transaction> BulkCreate(List<TransactionCreate> model, int ownerId);
    Transaction Create(TransactionCreate model, int ownerId, bool errorOnFail = true);
    void Update(int id, TransactionUpdateRequest model, int ownerId);
    void Delete(int id, int ownerId);
}

public class TransactionService : ITransactionService
{
    private DataContext _context;
    private readonly IMapper _mapper;
    private readonly ILogger<TransactionService> _logger;

    public TransactionService(
        DataContext context,
        IMapper mapper,
        ILogger<TransactionService> logger)
    {
        _context = context;
        _mapper = mapper;
        _logger = logger;
    }

    public IEnumerable<Transaction> GetAll(int ownerId)
    {
        return _context.Transactions
            .Include(t => t.DebitAccount)
            .Include(t => t.CreditAccount)
            .Include(t => t.Owner)
            .Where(x => x.Owner.Id == ownerId);
    }

    public Transaction GetById(int id, int ownerId)
    {
        return getTransaction(id, ownerId);
    }

    private Account prepareAccount(int? accountId)
    {
        if (accountId == null || accountId.Value == 0)
        {
            return null;
        }
        
        Account account = _context.Accounts.Find(accountId.Value);
        if (account == null)
            throw new AppException("Could not find account with ID of '" + accountId.Value + "'.");

        return account;
    }

    public IEnumerable<Transaction> BulkCreate(List<TransactionCreate> model, int ownerId)
    {
        List<Transaction> transactions = new List<Transaction>();

        foreach (TransactionCreate tr in model)
        {
            var tran = this.Create(tr, ownerId, false);
            if (tran != null)
                transactions.Add(tran);
        }

        return transactions;
    }

    public Transaction Create(TransactionCreate model, int ownerId, bool errorOnFail = true)
    {
        Transaction transaction = new Transaction {
            Description = model.Description,
            Date = model.Date.Date.ToUniversalTime(),
            CreatedOn = DateTime.UtcNow,
            UpdatedOn = DateTime.UtcNow,
            ExternalId = string.IsNullOrWhiteSpace(model.ExternalId) ? "" : model.ExternalId,
            DebitAccount = prepareAccount(model.DebitAccount),
            CreditAccount = prepareAccount(model.CreditAccount),
            Amount = Convert.ToDecimal(model.Amount),
            Owner = this.getOwner(ownerId),
            IsPending = model.IsPending
        };

        if (this.ValidateTransaction(transaction, ownerId, errorOnFail) == false)
        {
            _logger.LogInformation($"Aborted adding transaction '{transaction.Description}'.");
            return null;
        }
        
        // At this point transaction itself is valid

        _context.Transactions.Add(transaction);
        _context.SaveChanges();

        _logger.LogInformation("New transaction successfully created.");

        return transaction;
    }

    public void Update(int id, TransactionUpdateRequest model, int ownerId)
    {
        Transaction transaction = getTransaction(id, ownerId);

        // Transaction.Date
        if (model.Date.HasValue)
            transaction.Date = model.Date.Value;

        // Transaction.ExternalId
        if (model.ExternalId != null)
            transaction.ExternalId = model.ExternalId;

        // Transaction.Description
        if (model.Description != null)
            transaction.Description = model.Description;

        // Transaction.DebitAccount
        if (model.DebitAccount.HasValue)
            transaction.DebitAccount = prepareAccount(model.DebitAccount);

        // Transaction.CreditAccount
        if (model.CreditAccount.HasValue)
            transaction.CreditAccount = prepareAccount(model.CreditAccount.Value);

        // Transaction.Amount
        if (model.Amount.HasValue)
            transaction.Amount = model.Amount.Value;

        // Transaction.IsPending
        if (model.IsPending.HasValue)
            transaction.IsPending = model.IsPending.Value;

        this.ValidateTransaction(transaction, ownerId);

        transaction.UpdatedOn = DateTime.UtcNow;
        
        _context.Transactions.Update(transaction);
        _context.SaveChanges();

        _logger.LogInformation($"Transaction '{id}' successfully updated.");
    }

    public void Delete(int id, int ownerId)
    {
        var transaction = getTransaction(id, ownerId);
        _context.Transactions.Remove(transaction);
        _context.SaveChanges();
    }

    // helpers

    private bool ErrorOrFalse(bool error, string errorMessage)
    {
        if (error)
            throw new AppException(errorMessage);

        _logger.LogWarning(errorMessage);
        return false;
    }

    private bool ValidateTransaction(Transaction transaction, int ownerId, bool errorOnFail = true)
    {
        // There has to be at least 1 specified account
        if (transaction.DebitAccount == null && transaction.CreditAccount == null)
            return ErrorOrFalse(errorOnFail, "There must be an envelope or account chosen for a transaction.");

        // Accounts cannot be the same
        if (transaction.DebitAccount != null && transaction.CreditAccount != null &&
            transaction.DebitAccount.Id == transaction.CreditAccount.Id)
            return ErrorOrFalse(errorOnFail, "The debit and credit accounts of a transaction cannot be the same.");

        // Transaction Duplication Check - External ID
        if (!string.IsNullOrWhiteSpace(transaction.ExternalId)
            && _context.Transactions
                .Include(x => x.Owner)
                .Any(x => x.ExternalId == transaction.ExternalId && x.Owner.Id == ownerId))
            return ErrorOrFalse(errorOnFail, "Transaction with the external ID '" + transaction.ExternalId + "' already exists");

        // Transaction Duplication Check - All other fields
        /*if (_context.Transactions.Any(x => 
            x.Description == transaction.Description
            && x.Date == transaction.Date
            && x.DebitAccount == transaction.DebitAccount
            && x.CreditAccount == transaction.CreditAccount
            && x.Amount == transaction.Amount))
        {
            return ErrorOrFalse(errorOnFail, "Transaction with the same fields already exists");
        }*/

        return true;
    }

    private Transaction getTransaction(int id, int ownerId)
    {
        var transaction = _context.Transactions
            .Include(t => t.DebitAccount)
            .Include(t => t.CreditAccount)
            .Include(t => t.Owner)
            .FirstOrDefault(t => t.Id == id && t.Owner.Id == ownerId);
            
        if (transaction == null)
            throw new KeyNotFoundException("Transaction not found");

        return transaction;
    }

    private User getOwner(int ownerId)
    {
        User? owner = _context.Users.Find(ownerId);

        if (owner == null)
            throw new AppException($"Owner with ID of '{ownerId}' could not be found");

        return owner;
    }
}