using CashLineERP.Application.Sales;
using CashLineERP.Contracts.Sales;
using CashLineERP.Domain.Finance;
using CashLineERP.Domain.Inventory;
using CashLineERP.Domain.Sales;
using CashLineERP.Domain.Workflow;
using CashLineERP.SharedKernel.Workflow;
using Microsoft.EntityFrameworkCore;

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class SalesReturnRepository : ISalesReturnRepository
{
    private readonly CashLineErpDbContext _dbContext;

    public SalesReturnRepository(CashLineErpDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public IReadOnlyCollection<SalesReturnDetailView> GetSalesReturns() =>
        _dbContext.SalesReturns
            .AsNoTracking()
            .Include(salesReturn => salesReturn.Lines)
            .OrderByDescending(salesReturn => salesReturn.ReturnDate)
            .ThenByDescending(salesReturn => salesReturn.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public SalesReturnDetailView? GetSalesReturn(long salesReturnId)
    {
        var salesReturn = _dbContext.SalesReturns
            .AsNoTracking()
            .Include(existingReturn => existingReturn.Lines)
            .SingleOrDefault(existingReturn => existingReturn.Id == salesReturnId);

        return salesReturn is null ? null : Map(salesReturn);
    }

    public SalesReturnDetailView Create(CreateSalesReturnCommand command)
    {
        var dispatch = _dbContext.DispatchOrders
            .Include(existingDispatch => existingDispatch.Lines)
            .SingleOrDefault(existingDispatch => existingDispatch.Id == command.DispatchOrderId);

        if (dispatch is null)
        {
            throw new InvalidOperationException("Sales return requires a valid dispatch order.");
        }

        if (dispatch.SalesOrderId != command.SalesOrderId)
        {
            throw new InvalidOperationException("Sales return sales order must match the source dispatch.");
        }

        var salesOrder = _dbContext.SalesOrders
            .Include(existingSalesOrder => existingSalesOrder.Lines)
            .Single(existingSalesOrder => existingSalesOrder.Id == command.SalesOrderId);

        if (salesOrder.CustomerId != command.CustomerId)
        {
            throw new InvalidOperationException("Sales return customer must match the source sales order customer.");
        }

        var nextSequence = (_dbContext.SalesReturns.Max(salesReturn => (long?)salesReturn.Id) ?? 0L) + 1L;
        var salesReturn = new SalesReturn(
            salesReturnNumber: $"SRT-{nextSequence:D6}",
            branchId: command.BranchId,
            customerId: command.CustomerId,
            salesOrderId: command.SalesOrderId,
            dispatchOrderId: command.DispatchOrderId,
            returnDate: command.ReturnDate,
            returnWarehouseId: command.ReturnWarehouseId,
            reasonCode: command.ReasonCode,
            notes: command.Notes);

        var lineNumber = 1;
        foreach (var line in command.Lines)
        {
            if (line.QuantityReturned <= 0m)
            {
                throw new InvalidOperationException("Sales return quantity must be greater than zero.");
            }

            var dispatchLine = dispatch.Lines.SingleOrDefault(existingLine => existingLine.Id == line.DispatchOrderLineId);
            if (dispatchLine is null)
            {
                throw new InvalidOperationException("Sales return line must reference a valid dispatch line.");
            }

            if (dispatchLine.SalesOrderLineId != line.SalesOrderLineId || dispatchLine.ItemId != line.ItemId)
            {
                throw new InvalidOperationException("Sales return line must match the source dispatch line and sales order line.");
            }

            if (line.QuantityReturned > dispatchLine.QuantityDispatched)
            {
                throw new InvalidOperationException("Sales return quantity cannot exceed source dispatch quantity.");
            }

            salesReturn.AddLine(
                lineNumber++,
                line.DispatchOrderLineId,
                line.SalesOrderLineId,
                line.ItemId,
                line.QuantityReturned,
                line.WarehouseLocationId,
                line.BatchNumber,
                line.SerialNumber,
                line.Notes);
        }

        _dbContext.SalesReturns.Add(salesReturn);
        _dbContext.SaveChanges();
        return GetSalesReturn(salesReturn.Id)!;
    }

    public SalesReturnDetailView Submit(long salesReturnId, long actorUserId)
    {
        var salesReturn = _dbContext.SalesReturns
            .Include(existingReturn => existingReturn.Lines)
            .Single(existingReturn => existingReturn.Id == salesReturnId);

        salesReturn.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "SALES",
            documentType: "SALES_RETURN",
            documentId: salesReturn.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Sales return submitted for review.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetSalesReturn(salesReturn.Id)!;
    }

    public SalesReturnDetailView Post(long salesReturnId, long actorUserId)
    {
        var salesReturn = _dbContext.SalesReturns
            .Include(existingReturn => existingReturn.Lines)
            .Single(existingReturn => existingReturn.Id == salesReturnId);

        if (salesReturn.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetSalesReturn(salesReturn.Id)!;
        }

        salesReturn.Post(actorUserId);

        var nextSequence = (_dbContext.InventoryMovements.Max(movement => (long?)movement.Id) ?? 0L) + 1L;
        foreach (var line in salesReturn.Lines.OrderBy(line => line.LineNumber))
        {
            var movement = new InventoryMovement(
                movementNumber: $"IM-{nextSequence + line.LineNumber - 1:D6}",
                movementType: "SALES_RETURN_RECEIPT",
                branchId: salesReturn.BranchId,
                itemId: line.ItemId,
                fromWarehouseId: null,
                fromWarehouseLocationId: null,
                toWarehouseId: salesReturn.ReturnWarehouseId,
                toWarehouseLocationId: line.WarehouseLocationId,
                batchNumber: line.BatchNumber,
                serialNumber: line.SerialNumber,
                stockStatus: "AVAILABLE",
                quantityMoved: line.QuantityReturned,
                unitCost: null,
                movedAtUtc: salesReturn.ReturnDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc));

            _dbContext.InventoryMovements.Add(movement);
        }

        _dbContext.SaveChanges();
        return GetSalesReturn(salesReturn.Id)!;
    }

    public SalesReturnDetailView IssueCreditNote(long salesReturnId, IssueSalesReturnCreditNoteRequest request)
    {
        var salesReturn = _dbContext.SalesReturns
            .Include(existingReturn => existingReturn.Lines)
            .Single(existingReturn => existingReturn.Id == salesReturnId);

        if (salesReturn.StatusId != WorkflowStatusSeedIds.Posted)
        {
            throw new InvalidOperationException("Credit note issuance requires a posted sales return.");
        }

        if (salesReturn.CreditNoteInvoiceId.HasValue)
        {
            return GetSalesReturn(salesReturn.Id)!;
        }

        var salesOrder = _dbContext.SalesOrders
            .Include(existingSalesOrder => existingSalesOrder.Lines)
            .Single(existingSalesOrder => existingSalesOrder.Id == salesReturn.SalesOrderId);

        var nextSequence = (_dbContext.AccountsReceivableInvoices.Max(invoice => (long?)invoice.Id) ?? 0L) + 1L;
        var creditNote = new AccountsReceivableInvoice(
            invoiceNumber: $"ARINV-{nextSequence:D6}",
            customerId: salesReturn.CustomerId,
            salesOrderId: salesReturn.SalesOrderId,
            dispatchOrderId: salesReturn.DispatchOrderId,
            invoiceType: "CREDIT_NOTE",
            invoiceDate: request.InvoiceDate,
            taxDate: request.TaxDate,
            dueDate: request.DueDate,
            currencyCode: salesOrder.CurrencyCode);

        var lineNumber = 1;
        foreach (var line in salesReturn.Lines.OrderBy(line => line.LineNumber))
        {
            var salesOrderLine = salesOrder.Lines.Single(existingLine => existingLine.Id == line.SalesOrderLineId);
            creditNote.AddLine(
                lineNumber++,
                salesOrderLineId: salesOrderLine.Id,
                itemId: line.ItemId,
                quantityInvoiced: line.QuantityReturned,
                unitOfMeasureCode: salesOrderLine.UnitOfMeasureCode,
                unitPrice: salesOrderLine.UnitPrice,
                taxCode: salesOrderLine.TaxCode);
        }

        _dbContext.AccountsReceivableInvoices.Add(creditNote);
        _dbContext.SaveChanges();

        salesReturn.LinkCreditNote(request.ActorUserId, creditNote.Id);
        _dbContext.SaveChanges();
        return GetSalesReturn(salesReturn.Id)!;
    }

    private static SalesReturnDetailView Map(SalesReturn salesReturn) =>
        new(
            salesReturn.Id,
            salesReturn.SalesReturnNumber,
            salesReturn.BranchId,
            salesReturn.CustomerId,
            salesReturn.SalesOrderId,
            salesReturn.DispatchOrderId,
            salesReturn.ReturnDate,
            salesReturn.ReturnWarehouseId,
            salesReturn.ReasonCode,
            salesReturn.Notes,
            WorkflowStatusSeedIds.ToCode(salesReturn.StatusId),
            salesReturn.TotalReturnedQuantity,
            salesReturn.CreditNoteInvoiceId,
            salesReturn.CreditNoteIssuedAtUtc,
            salesReturn.Lines.OrderBy(line => line.LineNumber).Select(line => new SalesReturnLineView(
                line.Id,
                line.LineNumber,
                line.DispatchOrderLineId,
                line.SalesOrderLineId,
                line.ItemId,
                line.QuantityReturned,
                line.WarehouseLocationId,
                line.BatchNumber,
                line.SerialNumber,
                line.Notes)).ToArray());
}
