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

namespace CashLineERP.Infrastructure.Persistence.Repositories;

public sealed class StockCountRepository : IStockCountRepository
{
    private const long InventoryAssetAccountId = 1401L;
    private const long InventoryAdjustmentLossAccountId = 5105L;
    private const long InventoryAdjustmentGainAccountId = 4105L;

    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<StockCountDetailView> GetStockCounts() =>
        _dbContext.Set<StockCount>()
            .AsNoTracking()
            .Include(stockCount => stockCount.Lines)
            .OrderByDescending(stockCount => stockCount.CountDate)
            .ThenByDescending(stockCount => stockCount.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public StockCountDetailView? GetStockCount(long stockCountId)
    {
        var stockCount = _dbContext.Set<StockCount>()
            .AsNoTracking()
            .Include(existingCount => existingCount.Lines)
            .SingleOrDefault(existingCount => existingCount.Id == stockCountId);

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

    public StockCountDetailView Create(CreateStockCountCommand command)
    {
        ValidateCreateCommand(command);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, command.CountDate, "Stock count");

        var nextSequence = (_dbContext.Set<StockCount>().Max(stockCount => (long?)stockCount.Id) ?? 0L) + 1L;
        var stockCount = new StockCount(
            countNumber: $"STKCNT-{nextSequence:D6}",
            branchId: command.BranchId,
            warehouseId: command.WarehouseId,
            countDate: command.CountDate,
            countTypeCode: command.CountTypeCode,
            notes: command.Notes);

        var lineNumber = 1;
        foreach (var line in command.Lines)
        {
            stockCount.AddLine(
                lineNumber++,
                line.ItemId,
                line.WarehouseLocationId,
                line.BookQuantity,
                line.CountedQuantity,
                line.UnitOfMeasureCode,
                line.UnitCost,
                line.StockStatus,
                line.BatchNumber,
                line.SerialNumber,
                line.Notes);
        }

        _dbContext.Add(stockCount);
        _dbContext.SaveChanges();
        return GetStockCount(stockCount.Id)!;
    }

    public StockCountDetailView Submit(long stockCountId, long actorUserId)
    {
        var stockCount = _dbContext.Set<StockCount>()
            .Include(existingCount => existingCount.Lines)
            .Single(existingCount => existingCount.Id == stockCountId);

        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, stockCount.CountDate, "Stock count");
        stockCount.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "INVENTORY",
            documentType: "INV_STOCK_COUNT",
            documentId: stockCount.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Stock count submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetStockCount(stockCount.Id)!;
    }

    public StockCountDetailView Approve(long stockCountId, long actorUserId)
    {
        var stockCount = _dbContext.Set<StockCount>()
            .Include(existingCount => existingCount.Lines)
            .Single(existingCount => existingCount.Id == stockCountId);

        stockCount.Approve(actorUserId);

        var workflowInstance = _dbContext.WorkflowInstances
            .Include(instance => instance.Actions)
            .SingleOrDefault(instance =>
                instance.DocumentType == "INV_STOCK_COUNT" &&
                instance.DocumentId == stockCount.Id);

        workflowInstance?.Approve(actorUserId, "Stock count approved by checker.");

        _dbContext.SaveChanges();
        return GetStockCount(stockCount.Id)!;
    }

    public StockCountDetailView Post(long stockCountId, long actorUserId)
    {
        var stockCount = _dbContext.Set<StockCount>()
            .Include(existingCount => existingCount.Lines)
            .Single(existingCount => existingCount.Id == stockCountId);

        if (stockCount.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetStockCount(stockCount.Id)!;
        }

        var postingPeriodId = AccountingPeriodLockGuard.RequireOpenPeriod(_dbContext, stockCount.CountDate, "Stock count");
        long? inventoryAdjustmentId = null;
        long? generalLedgerBatchId = null;
        var varianceLines = stockCount.Lines
            .Where(line => line.VarianceQuantity != 0m)
            .OrderBy(line => line.LineNumber)
            .ToArray();

        if (varianceLines.Length > 0)
        {
            var adjustment = CreateSystemGeneratedAdjustment(stockCount, varianceLines);
            _dbContext.InventoryAdjustments.Add(adjustment);
            _dbContext.SaveChanges();

            var batch = CreateVarianceLedgerBatch(stockCount, adjustment, varianceLines, postingPeriodId);
            _dbContext.GeneralLedgerBatches.Add(batch);

            foreach (var movement in CreateVarianceInventoryMovements(stockCount, varianceLines))
            {
                _dbContext.InventoryMovements.Add(movement);
            }

            _dbContext.SaveChanges();

            adjustment.MarkSystemGeneratedPosted(actorUserId, batch.Id);
            _dbContext.SaveChanges();

            inventoryAdjustmentId = adjustment.Id;
            generalLedgerBatchId = batch.Id;
        }

        stockCount.Post(actorUserId, inventoryAdjustmentId, generalLedgerBatchId);
        _dbContext.SaveChanges();
        return GetStockCount(stockCount.Id)!;
    }

    private InventoryAdjustment CreateSystemGeneratedAdjustment(
        StockCount stockCount,
        IReadOnlyCollection<StockCountLine> varianceLines)
    {
        var nextAdjustmentSequence = (_dbContext.InventoryAdjustments.Max(adjustment => (long?)adjustment.Id) ?? 0L) + 1L;
        var adjustment = new InventoryAdjustment(
            adjustmentNumber: $"INVADJ-{nextAdjustmentSequence:D6}",
            branchId: stockCount.BranchId,
            warehouseId: stockCount.WarehouseId,
            adjustmentDate: stockCount.CountDate,
            reasonCode: "COUNT_VARIANCE",
            notes: $"Auto-generated from stock count {stockCount.CountNumber}.");

        var lineNumber = 1;
        foreach (var line in varianceLines)
        {
            adjustment.AddLine(
                lineNumber++,
                line.ItemId,
                line.WarehouseLocationId,
                line.VarianceQuantity,
                line.UnitOfMeasureCode,
                line.UnitCost,
                line.StockStatus,
                line.BatchNumber,
                line.SerialNumber,
                line.Notes);
        }

        return adjustment;
    }

    private GeneralLedgerBatch CreateVarianceLedgerBatch(
        StockCount stockCount,
        InventoryAdjustment adjustment,
        IReadOnlyCollection<StockCountLine> varianceLines,
        long postingPeriodId)
    {
        var nextBatchSequence = (_dbContext.GeneralLedgerBatches.Max(batch => (long?)batch.Id) ?? 0L) + 1L;
        var batch = new GeneralLedgerBatch(
            batchNumber: $"GLB-{nextBatchSequence:D6}",
            batchType: "INV_STOCK_COUNT",
            branchId: stockCount.BranchId,
            periodId: postingPeriodId,
            descriptionText: $"Stock count variance posting for {stockCount.CountNumber}",
            sourceModule: "INVENTORY",
            sourceDocumentId: stockCount.Id);

        var journal = new GeneralLedgerJournal(
            generalLedgerBatchId: batch.Id,
            journalNumber: $"JRNL-{nextBatchSequence:D6}",
            journalType: "INV_STOCK_COUNT",
            postingDate: stockCount.CountDate,
            taxDate: stockCount.CountDate,
            documentDate: stockCount.CountDate,
            descriptionText: $"Stock count variance for {stockCount.CountNumber}");

        var gainAmount = varianceLines.Where(line => line.VarianceQuantity > 0m).Sum(line => line.VarianceAmount);
        var lossAmount = varianceLines.Where(line => line.VarianceQuantity < 0m).Sum(line => line.VarianceAmount);
        var journalLineNumber = 1;

        if (gainAmount > 0m)
        {
            journal.AddLine(
                lineNumber: journalLineNumber++,
                generalLedgerAccountId: InventoryAssetAccountId,
                branchId: stockCount.BranchId,
                departmentId: null,
                partyId: null,
                itemId: null,
                debitAmount: gainAmount,
                creditAmount: 0m,
                currencyCode: "EGP",
                fxRate: 1m,
                lineDescription: $"Inventory increase from stock count {stockCount.CountNumber}");

            journal.AddLine(
                lineNumber: journalLineNumber++,
                generalLedgerAccountId: InventoryAdjustmentGainAccountId,
                branchId: stockCount.BranchId,
                departmentId: null,
                partyId: null,
                itemId: null,
                debitAmount: 0m,
                creditAmount: gainAmount,
                currencyCode: "EGP",
                fxRate: 1m,
                lineDescription: $"Stock count gain recognition via {adjustment.AdjustmentNumber}");
        }

        if (lossAmount > 0m)
        {
            journal.AddLine(
                lineNumber: journalLineNumber++,
                generalLedgerAccountId: InventoryAdjustmentLossAccountId,
                branchId: stockCount.BranchId,
                departmentId: null,
                partyId: null,
                itemId: null,
                debitAmount: lossAmount,
                creditAmount: 0m,
                currencyCode: "EGP",
                fxRate: 1m,
                lineDescription: $"Stock count loss recognition via {adjustment.AdjustmentNumber}");

            journal.AddLine(
                lineNumber: journalLineNumber++,
                generalLedgerAccountId: InventoryAssetAccountId,
                branchId: stockCount.BranchId,
                departmentId: null,
                partyId: null,
                itemId: null,
                debitAmount: 0m,
                creditAmount: lossAmount,
                currencyCode: "EGP",
                fxRate: 1m,
                lineDescription: $"Inventory decrease from stock count {stockCount.CountNumber}");
        }

        batch.AddJournal(journal);
        return batch;
    }

    private IReadOnlyCollection<InventoryMovement> CreateVarianceInventoryMovements(
        StockCount stockCount,
        IReadOnlyCollection<StockCountLine> varianceLines)
    {
        var nextMovementSequence = (_dbContext.InventoryMovements.Max(movement => (long?)movement.Id) ?? 0L) + 1L;

        return varianceLines
            .Select((line, index) => new InventoryMovement(
                movementNumber: $"IM-{nextMovementSequence + index:D6}",
                movementType: line.VarianceQuantity > 0m ? "ADJUSTMENT_GAIN" : "ADJUSTMENT_LOSS",
                branchId: stockCount.BranchId,
                itemId: line.ItemId,
                fromWarehouseId: line.VarianceQuantity > 0m ? null : stockCount.WarehouseId,
                fromWarehouseLocationId: line.VarianceQuantity > 0m ? null : line.WarehouseLocationId,
                toWarehouseId: line.VarianceQuantity > 0m ? stockCount.WarehouseId : null,
                toWarehouseLocationId: line.VarianceQuantity > 0m ? line.WarehouseLocationId : null,
                batchNumber: line.BatchNumber,
                serialNumber: line.SerialNumber,
                stockStatus: line.StockStatus,
                quantityMoved: Math.Abs(line.VarianceQuantity),
                unitCost: line.UnitCost,
                movedAtUtc: stockCount.CountDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc)))
            .ToArray();
    }

    private static StockCountDetailView Map(StockCount stockCount) =>
        new(
            stockCount.Id,
            stockCount.CountNumber,
            stockCount.BranchId,
            stockCount.WarehouseId,
            stockCount.CountDate,
            stockCount.CountTypeCode,
            stockCount.Notes,
            WorkflowStatusSeedIds.ToCode(stockCount.StatusId),
            stockCount.TotalVarianceAmount,
            stockCount.SubmittedAtUtc,
            stockCount.SubmittedByUserId,
            stockCount.ApprovedAtUtc,
            stockCount.ApprovedByUserId,
            stockCount.PostedAtUtc,
            stockCount.PostedByUserId,
            stockCount.InventoryAdjustmentId,
            stockCount.GeneralLedgerBatchId,
            stockCount.Lines
                .OrderBy(line => line.LineNumber)
                .Select(line => new StockCountLineView(
                    line.Id,
                    line.LineNumber,
                    line.ItemId,
                    line.WarehouseLocationId,
                    line.BookQuantity,
                    line.CountedQuantity,
                    line.VarianceQuantity,
                    line.UnitOfMeasureCode,
                    line.UnitCost,
                    line.VarianceAmount,
                    line.StockStatus,
                    line.BatchNumber,
                    line.SerialNumber,
                    line.Notes))
                .ToArray());

    private static void ValidateCreateCommand(CreateStockCountCommand command)
    {
        if (command.BranchId <= 0)
        {
            throw new InvalidOperationException("Stock count requires a valid branch.");
        }

        if (command.WarehouseId <= 0)
        {
            throw new InvalidOperationException("Stock count requires a valid warehouse.");
        }

        if (string.IsNullOrWhiteSpace(command.CountTypeCode))
        {
            throw new InvalidOperationException("Stock count requires a count type code.");
        }

        if (command.Lines.Count == 0)
        {
            throw new InvalidOperationException("Stock count requires at least one line.");
        }
    }
}
