using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Winbooks.Apis.Services;
using Winbooks.Apis.Services.v2;
using Winbooks.TORM.OM;
using Winbooks.TORM.OM.Accounting;
using Winbooks.TORM.DAL;
using Winbooks.TORM.DAL.Accounting;

namespace Winbooks.Apis.Linker
{
    /// <summary>
    /// This class allows to post Sales/Credit notes
    /// </summary>
    public class SalesPurchasesLinker : CommonLinker
    {

        public SalesPurchasesLinker(EntitiesServices folder, Settings settings, Cache cache)
            : base(folder, settings, cache)
        { }

        /// <summary>
        /// Post the invoice in the accounting
        /// Throw an error (System.ArgumentException) if something is wrong
        /// </summary>
        /// <param name="invoiceHeader"></param>
        /// <param name="bookPeriod"></param>
        /// <returns>The Id of the transaction on the customer or supplier</returns>
        public Guid PostInvoice(InvoiceHeader invoiceHeader, BookPeriod bookPeriod)
        {
            if (_settings == null || string.IsNullOrEmpty(_settings.FolderCurrency))
                throw new ArgumentException("Settings was not initialized");
            CheckParameters(invoiceHeader, bookPeriod);
            CheckJournal(invoiceHeader.Journal, bookPeriod);
            int dec = invoiceHeader.Currency.NumberOfDecimals == null ? 2 : (int)(invoiceHeader.Currency.NumberOfDecimals);
            GLTransHeader header = CreateGLTransHeader(invoiceHeader, bookPeriod, dec);
            return Guid.Empty;
        }

        private void CheckParameters(InvoiceHeader invoiceHeader, BookPeriod bookPeriod)
        {
            if (invoiceHeader == null)
                throw new System.ArgumentException("Cannot PostInvoice with invoiceHeader == null");
            if (bookPeriod == null)
                throw new System.ArgumentException("Cannot PostInvoice with bookPeriod == null");

            if (string.IsNullOrEmpty(invoiceHeader.CurrencyCode) || invoiceHeader.CurrencyRate == 0 || invoiceHeader.DocNumber <= 0)
                throw new System.ArgumentException("Currency, Currency rate and DocNumber must be defined");
            if (invoiceHeader.CurrencyCode.Equals(_settings.FolderCurrency) && invoiceHeader.CurrencyRate != 1.00m)
                throw new System.ArgumentException(string.Format("The currency {0} is the same as the folder currency, the exchange rate should be 1.00", invoiceHeader.CurrencyCode));
            if (invoiceHeader.CurrencyRate == 0)
                throw new ArgumentException("The currency rate cannot be 0");

            if (string.IsNullOrEmpty(invoiceHeader.JournalCode))
                throw new ArgumentException("The Journal is not defined");
            if (invoiceHeader.Journal.Type == JournalType.Sales || invoiceHeader.Journal.Type == JournalType.SalesCreditNote)
            {
                if (invoiceHeader.Customer == null)
                    throw new System.ArgumentException("Customer is not defined");
                if (bookPeriod.IsSalesClosed)
                    throw new System.ArgumentException(string.Format("Journal {0}: the period {1} is closed for sales", invoiceHeader.Journal.Code, bookPeriod.Name));
            }
            else if (invoiceHeader.Journal.Type == JournalType.Purchase || invoiceHeader.Journal.Type == JournalType.PurchaseCreditNote)
            {
                if (invoiceHeader.Supplier == null)
                    throw new System.ArgumentException("Supplier is not defined");
                if (bookPeriod.IsPurchaseClosed)
                    throw new System.ArgumentException(string.Format("Journal {0}: the period {1} is closed for purchases", invoiceHeader.Journal.Code, bookPeriod.Name));
                throw new System.ArgumentException(string.Format("Journal {0}: the purchase and credit note on purchase are not yet supported", invoiceHeader.Journal.Code));
            }
            else
                throw new System.ArgumentException(string.Format("The journal {0} is not a sales or purchase journal", invoiceHeader.Journal.Code));
        }

        private void CheckJournal(Journal journal, BookPeriod bookPeriod)
        {
            if (journal.Type == JournalType.Cashbook || journal.Type == JournalType.CostAccounting || journal.Type == JournalType.MiscellaneousOperation)

                if (journal.Type == JournalType.Purchase || journal.Type == JournalType.PurchaseCreditNote)

                    if ((journal.Type == JournalType.Sales || journal.Type == JournalType.SalesCreditNote) && bookPeriod.IsSalesClosed)
                        throw new System.ArgumentException(string.Format("Journal {0}: the period {1} is closed for sales", journal.Code, bookPeriod.Name));
        }

        private void ComputeTotals(InvoiceHeader invoiceHeader, int numberOfDecimals,
                                        out decimal totalVatBasesCur, out decimal totalVatTaxesCur, out decimal totalAmount,
                                        out decimal totalVatBases, out decimal totalVatTaxes, out decimal turnOver)
        {
            Journal journal = invoiceHeader.Journal;
            decimal totalGLAccounts = 0;
            if (invoiceHeader.GLAccounts != null)  // Left part of the encoding form
            {
                foreach (InvoiceGLAccount invoiceGLAccount in invoiceHeader.GLAccounts)
                {
                    if (invoiceGLAccount.BaseAmount < 0)
                        throw new ArgumentException("The amount allocated on the general accounts cannot be negative");
                    totalGLAccounts += SignAmountGLAccount(journal.Type, CheckRounded(invoiceGLAccount.BaseAmount, numberOfDecimals), invoiceGLAccount.Sign);
                }
            }

            totalVatBasesCur = 0;
            totalVatTaxesCur = 0;
            decimal totalAmountCur = 0; // see comment on totalAmount

            totalVatBases = 0;
            totalVatTaxes = 0;
            totalAmount = 0; // Total amount to pay. For purchase with vat code EM21, if base = 1000, taxe = 210 but total amount to pay = 1000
            turnOver = 0;
            if (invoiceHeader.Vats != null) // Right part of the encoding form
            {
                foreach (InvoiceVat invoiceVat in invoiceHeader.Vats)
                {
                    totalVatBasesCur += CheckRounded(invoiceVat.BaseAmount, numberOfDecimals);
                    totalVatTaxesCur += CheckRounded(invoiceVat.TaxAmount, numberOfDecimals);

                    decimal baseAmount = decimal.Round(invoiceVat.BaseAmount / invoiceHeader.CurrencyRate, numberOfDecimals);   // in the folder currency
                    decimal taxAmount = decimal.Round(invoiceVat.TaxAmount / invoiceHeader.CurrencyRate, numberOfDecimals);     // in the folder currency
                    totalVatBases += baseAmount;
                    totalVatTaxes += taxAmount;
                    if (invoiceVat.Vat.CentralAccountType2_Id == Guid.Empty)    // Regular Vat to pay (sales) or to recover (purchase)
                    {
                        totalAmountCur += invoiceVat.BaseAmount + invoiceVat.TaxAmount;
                        totalAmount += baseAmount + taxAmount;
                    }
                    else                                                        // Vat to pay AND recover (like for purchase with vat code EM21/EH21)
                    {
                        totalAmountCur += invoiceVat.BaseAmount;
                        totalAmount += baseAmount;
                    }
                    if (invoiceVat.Vat.IsInTurnOver)
                        turnOver += decimal.Round(invoiceVat.BaseAmount / invoiceHeader.CurrencyRate, numberOfDecimals);
                }
            }
            if (totalGLAccounts != totalVatBasesCur)
                throw new ArgumentException("The total amount on the general accounts (GLAccounts[].BaseAmount) is not equal to the total of VAT base amount (Vats[].BaseAmount");
            if (totalAmount != CheckRounded(invoiceHeader.TotalAmount, numberOfDecimals))
                throw new ArgumentException("The total of base amounts + the total of taxe amounts to pay/recover is not equal to the total to pay");
        }

        private decimal SignAmountGLAccount(JournalType jnlType, decimal amount, string sign)
        {
            if (string.IsNullOrEmpty(sign))
                throw new System.ArgumentException("The GLAccount.Sign cannot be empty");
            if (!sign.Equals("C") && !sign.Equals("D"))
                throw new System.ArgumentException("The GLAccount.Sign should be 'D' or 'C'");
            if (jnlType == JournalType.Sales || jnlType == JournalType.PurchaseCreditNote)
                return sign.Equals("C") ? amount : amount * -1;
            else
                return sign.Equals("D") ? amount : amount * -1;
        }

        private GLTransHeader CreateGLTransHeader(InvoiceHeader invoice, BookPeriod bookPeriod, int dec)
        {
            int lineOrder = 0;
            bool isALTrans = false;

            Guid customer_Id, supplier_Id;
            customer_Id = Guid.Empty;
            supplier_Id = Guid.Empty;
            if (invoice.Journal.Type == JournalType.Sales || invoice.Journal.Type == JournalType.SalesCreditNote)
                customer_Id = invoice.Customer.Id;
            else
                supplier_Id = invoice.Supplier.Id;

            ComputeTotals(invoice, dec, out decimal totalVatBasesCur, out decimal totalVatTaxesCur, out decimal totalAmount,
                                        out decimal totalVatBases, out decimal totalVatTaxes, out decimal turnOver);

            // CREATE HEADER
            GLTransHeader header = new GLTransHeader();
            header.BookPeriod_Id = bookPeriod.Id;
            header.Journal_Id = invoice.Journal.Id;
            header.DocNumber = invoice.DocNumber;
            header.BookDate = invoice.Date;
            header.DueDate = invoice.DueDate;

            // CREATE TRANSACTION FOR CUSTOMER OR SUPPLIER
            GLTrans glTransThird = CreateTransaction(header, bookPeriod.Id, customer_Id, supplier_Id, invoice.Currency.Id, invoice.CurrencyRate);
            glTransThird.LineOrder = lineOrder++;
            if (invoice.Journal.Type == JournalType.Sales || invoice.Journal.Type == JournalType.SalesCreditNote)
            {
                glTransThird.GLAccount_Id = GetCentralGLAccountCustomer(invoice.Customer);                  // 400000
                glTransThird.CentralAccountType_Id = _defaultCentralAccountCustomer.CentralAccountType_Id;
            }
            else
            {
                glTransThird.GLAccount_Id = GetCentralGLAccountSupplier(invoice.Supplier);                  // 440000
                glTransThird.CentralAccountType_Id = _defaultCentralAccountSupplier.CentralAccountType_Id;
            }
            glTransThird.Amount = SignedAmount(invoice.Journal, totalAmount);
            glTransThird.VatBaseAmount = SignedVatTaxAmount(invoice.Journal, totalVatBases);
            glTransThird.VatTaxAmount = SignedVatTaxAmount(invoice.Journal, totalVatTaxes);
            glTransThird.TurnOverAmount = SignedVatTaxAmount(invoice.Journal, turnOver);
            glTransThird.CurrencyRate = invoice.CurrencyRate;
            glTransThird.CurrencyAmount = SignedAmount(invoice.Journal, invoice.TotalAmount);
            glTransThird.CurrencyVatBaseAmount = SignedVatTaxAmount(invoice.Journal, totalVatBasesCur);
            glTransThird.CurrencyVatTaxAmount = SignedVatTaxAmount(invoice.Journal, totalVatTaxesCur);
            glTransThird.ValueDate = header.BookDate;
            glTransThird.OldestMatchedDate = header.BookDate;
            glTransThird.CommentBooking = invoice.Comment;
            glTransThird.CommentExternal = invoice.CommentExt;
            header.GLTransactions.Add(glTransThird);

            // CREATE TRANSACTION FOR SALES/PURCHASES ACCOUNT (700000/600000)
            foreach (InvoiceGLAccount invoiceGLAccount in invoice.GLAccounts)
            {
                GLTrans glTransAlloc = CreateTransaction(header, bookPeriod.Id, customer_Id, supplier_Id, invoice.Currency.Id, invoice.CurrencyRate);
                glTransAlloc.LineOrder = lineOrder++;
                glTransAlloc.GLAccount_Id = invoiceGLAccount.GLAccount.Id;        // 600000/700000
                glTransAlloc.CurrencyAmount = invoiceGLAccount.Sign.Equals("D") ? invoiceGLAccount.BaseAmount : -invoiceGLAccount.BaseAmount;
                glTransAlloc.Amount = decimal.Round((decimal)glTransAlloc.CurrencyAmount / invoice.CurrencyRate, dec);
                glTransAlloc.CurrencyRate = invoice.CurrencyRate;
                glTransAlloc.ValueDate = header.BookDate;
                glTransAlloc.OldestMatchedDate = header.BookDate;
                glTransAlloc.CommentBooking = invoiceGLAccount.Comment;
                glTransAlloc.Vat_Id = invoiceGLAccount.Vat == null ? Guid.Empty : invoiceGLAccount.Vat.Id;
                if (invoiceGLAccount.ALAccounts != null && invoiceGLAccount.ALAccounts.Count > 0)
                {
                    isALTrans = true;
                    // Due to an unknown reason, if we add the cost accounting here, the ALTransactionDimension are not posted
                    // glTransAlloc.ALTransactions = new List<ALTrans>();
                    // ToolsALTransactions.AddALTranss(_settings, glTransAlloc, invoiceGLAccount.ALAccounts, glTransAlloc.ALTransactions);
                }
                header.GLTransactions.Add(glTransAlloc);
                glTransAlloc = null;
            }

            // CREATE TRANSACTION FOR VAT (451000/411000)
            for (int i = 0; i < invoice.Vats.Count; i++)
            {
                InvoiceVat v = invoice.Vats[i];
                for (int j = 1; j <= (v.Vat.CentralAccountType2_Id == Guid.Empty ? 1 : 2); j++)
                {
                    int sign = j == 1 ? 1 : -1;
                    GLTrans glTransVat = CreateTransaction(header, bookPeriod.Id, customer_Id, supplier_Id, invoice.Currency.Id, invoice.CurrencyRate);
                    glTransVat.LineOrder = lineOrder++;
                    glTransVat.CurrencyAmount = -SignedAmount(invoice.Journal, v.TaxAmount) * sign;
                    glTransVat.CurrencyVatBaseAmount = SignedVatTaxAmount(invoice.Journal, v.BaseAmount) * sign;
                    glTransVat.Amount = decimal.Round((decimal)glTransVat.CurrencyAmount, dec);
                    glTransVat.VatBaseAmount = decimal.Round((decimal)glTransVat.CurrencyVatBaseAmount, dec);
                    glTransVat.IsVatAllocation = true;
                    glTransVat.CurrencyRate = invoice.CurrencyRate;
                    glTransVat.ValueDate = header.BookDate;
                    glTransVat.OldestMatchedDate = header.BookDate;
                    glTransVat.CommentBooking = invoice.Comment;
                    if (v.Vat != null)
                    {
                        glTransVat.Vat_Id = v.Vat.Id;
                        Guid centralAccountTypeId = j == 1 ? v.Vat.CentralAccountType_Id : v.Vat.CentralAccountType2_Id;
                        if (centralAccountTypeId != Guid.Empty) // it could be null if VAT rate == 0
                        {
                            GLAccount vatAccount = _cache.GetGLAccountByCentralAccountTypeId(centralAccountTypeId);
                            glTransVat.GLAccount_Id = vatAccount.Id;        // 451000
                            glTransVat.CentralAccountType_Id = centralAccountTypeId;
                        }
                    }
                    header.GLTransactions.Add(glTransVat);
                    glTransVat = null;
                }
            }

            GLTransHeader glTransHeaderPosted;
            try
            {
                glTransHeaderPosted = _folder.Insert<GLTransHeader>(header, 2); // isALTrans ? 4 : 2
            }
            catch (Exception e)
            {
                throw new ArgumentException(string.Format("Cannot insert GLTransHeader {0} -{1}", invoice.Journal.Code, header.DocNumber), e);
            }

            if (isALTrans)
            {
                IList<GLTrans> glTranss = _cache.GetGLTranss(invoice.Journal, header.DocNumber);
                if (glTranss == null)
                    throw new ArgumentException(string.Format("Transactions of {0} -{1} not found", invoice.Journal.Code, header.DocNumber));

                List<ALTrans> alTranss = new List<ALTrans>();
                foreach (GLTrans glTrans in glTranss)
                {
                    // glTrans.LineOrder 0 is for the customer/supplier transaction
                    // glTrans.LineOrder 1 = invoice.GLAccounts[0]
                    int index = (int)glTrans.LineOrder - 1;
                    if (index >= 0 && index < invoice.GLAccounts.Count)
                    {
                        InvoiceGLAccount invGLAccount = invoice.GLAccounts[index];
                        ToolsALTransactions.AddALTranss(_settings, glTrans, invGLAccount.ALAccounts, alTranss);
                    }
                }

                if (alTranss.Count > 0)
                {
                    // Due to an unknown reason, if we add the cost accounting here, the ALTransactionDimension are not posted
                    // _folder.InsertCollection<ALTrans>(alTranss, 2);

                    foreach (var alTrans in alTranss)
                    {
                        ALTrans alTransNoSub = new ALTrans()    // alTrans contains ALTransactionDimension, so we create an alTransNoSub which does not contains ALTransactionDimension
                        {
                            CommentBooking = alTrans.CommentBooking,
                            Currency_Id = alTrans.Currency_Id,
                            GLTrans_Id = alTrans.GLTrans_Id,
                            LineOrder = alTrans.LineOrder
                        };
                        if (_settings.ALBookingMethod == ALBookingMethod.SameAmountForAllDimensions)
                        {
                            alTransNoSub.Amount = alTrans.Amount;
                            alTransNoSub.CurrencyAmount = alTrans.CurrencyAmount;
                        }

                        ALTrans alTransPosted;
                        try
                        {
                            alTransPosted = _folder.Insert(alTransNoSub);
                        }
                        catch (Exception e)
                        {
                            throw new ArgumentException(string.Format("Cannot insert ALTrans {0} -{1} Line order {2}", invoice.Journal.Code, header.DocNumber, alTransNoSub.LineOrder), e);
                        }

                        foreach (var alTransDimension in alTrans.ALTransactionDimensions)
                        {
                            alTransDimension.ALTrans_Id = alTransPosted.Id;
                            try
                            {
                                _folder.Insert(alTransDimension);
                            }
                            catch (Exception e)
                            {
                                throw new ArgumentException(string.Format("Cannot insert ALTransactionDimension", invoice.Journal.Code, header.DocNumber), e);
                            }
                        }
                    }
                }
            }

            return glTransHeaderPosted;
        }

        private decimal SignedAmount(Journal journal, decimal amount)
        {
            if (journal.Type == JournalType.Sales || journal.Type == JournalType.PurchaseCreditNote)
                return amount;
            return -amount;
        }

        private decimal SignedVatTaxAmount(Journal journal, decimal amount)
        {
            if (journal.Type == JournalType.Sales || journal.Type == JournalType.Purchase)
                return amount;
            return -amount;
        }

        private GLTrans CreateTransaction(GLTransHeader header, Guid bookPeriod_Id, Guid customer_Id, Guid supplier_Id, Guid currency_Id, decimal currencyRate)
        {
            if (header == null) return null;

            GLTrans glTrans = new GLTrans();
            glTrans.GLTransHeader_Id = header.Id;
            glTrans.BookPeriod_Id = bookPeriod_Id;
            glTrans.Currency_Id = currency_Id;
            glTrans.CurrencyRate = currencyRate;
            glTrans.ValueDate = header.BookDate;
            if (customer_Id != Guid.Empty)
                glTrans.Customer_Id = customer_Id;
            if (supplier_Id != Guid.Empty)
                glTrans.Supplier_Id = supplier_Id;
            glTrans.IsCoda = false;
            return glTrans;
        }

        private GLTrans CreateTransaction(GLTransHeader header, Guid bookPeriod_Id, Guid customer_Id, Guid currency_Id)
        {
            if (header == null) return null;

            GLTrans glTrans = new GLTrans();
            glTrans.GLTransHeader_Id = header.Id;
            glTrans.BookPeriod_Id = bookPeriod_Id;
            glTrans.Currency_Id = currency_Id;
            glTrans.CurrencyRate = 1;
            glTrans.ValueDate = header.BookDate;
            if (customer_Id != Guid.Empty)
                glTrans.Customer_Id = customer_Id;
            glTrans.IsCoda = false;
            return glTrans;
        }

    }
}
