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 InventoryAdjustmentRepository : IInventoryAdjustmentRepository
{
    private const long InventoryAssetAccountId = 1401L;
    private const long InventoryAdjustmentLossAccountId = 5105L;
    private const long InventoryAdjustmentGainAccountId = 4105L;

    private readonly CashLineErpDbContext _dbContext;

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

    public IReadOnlyCollection<InventoryAdjustmentDetailView> GetInventoryAdjustments() =>
        _dbContext.Set<InventoryAdjustment>()
            .AsNoTracking()
            .Include(adjustment => adjustment.Lines)
            .OrderByDescending(adjustment => adjustment.AdjustmentDate)
            .ThenByDescending(adjustment => adjustment.Id)
            .ToArray()
            .Select(Map)
            .ToArray();

    public InventoryAdjustmentDetailView? GetInventoryAdjustment(long inventoryAdjustmentId)
    {
        var adjustment = _dbContext.Set<InventoryAdjustment>()
            .AsNoTracking()
            .Include(existingAdjustment => existingAdjustment.Lines)
            .SingleOrDefault(existingAdjustment => existingAdjustment.Id == inventoryAdjustmentId);

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

    public InventoryAdjustmentDetailView Create(CreateInventoryAdjustmentCommand command)
    {
        ValidateCreateCommand(command);
        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, command.AdjustmentDate, "Inventory adjustment");

        var nextSequence = (_dbContext.Set<InventoryAdjustment>().Max(adjustment => (long?)adjustment.Id) ?? 0L) + 1L;
        var adjustment = new InventoryAdjustment(
            adjustmentNumber: $"INVADJ-{nextSequence:D6}",
            branchId: command.BranchId,
            warehouseId: command.WarehouseId,
            adjustmentDate: command.AdjustmentDate,
            reasonCode: command.ReasonCode,
            notes: command.Notes);

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

        _dbContext.Add(adjustment);
        _dbContext.SaveChanges();
        return GetInventoryAdjustment(adjustment.Id)!;
    }

    public InventoryAdjustmentDetailView Submit(long inventoryAdjustmentId, long actorUserId)
    {
        var adjustment = _dbContext.Set<InventoryAdjustment>()
            .Include(existingAdjustment => existingAdjustment.Lines)
            .Single(existingAdjustment => existingAdjustment.Id == inventoryAdjustmentId);

        AccountingPeriodLockGuard.EnsureOpenPeriod(_dbContext, adjustment.AdjustmentDate, "Inventory adjustment");
        adjustment.Submit(actorUserId);

        var workflowInstance = new WorkflowInstance(
            moduleCode: "INVENTORY",
            documentType: "INV_ADJUSTMENT",
            documentId: adjustment.Id,
            makerUserId: actorUserId,
            workflowRuleId: 1L);
        workflowInstance.Submit(actorUserId, "Inventory adjustment submitted for checker approval.");

        _dbContext.WorkflowInstances.Add(workflowInstance);
        _dbContext.SaveChanges();
        return GetInventoryAdjustment(adjustment.Id)!;
    }

    public InventoryAdjustmentDetailView Approve(long inventoryAdjustmentId, long actorUserId)
    {
        var adjustment = _dbContext.Set<InventoryAdjustment>()
            .Include(existingAdjustment => existingAdjustment.Lines)
            .Single(existingAdjustment => existingAdjustment.Id == inventoryAdjustmentId);

        adjustment.Approve(actorUserId);

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

        workflowInstance?.Approve(actorUserId, "Inventory adjustment approved by checker.");

        _dbContext.SaveChanges();
        return GetInventoryAdjustment(adjustment.Id)!;
    }

    public InventoryAdjustmentDetailView Post(long inventoryAdjustmentId, long actorUserId)
    {
        var adjustment = _dbContext.Set<InventoryAdjustment>()
            .Include(existingAdjustment => existingAdjustment.Lines)
            .Single(existingAdjustment => existingAdjustment.Id == inventoryAdjustmentId);

        if (adjustment.StatusId == WorkflowStatusSeedIds.Posted)
        {
            return GetInventoryAdjustment(adjustment.Id)!;
        }

        var postingPeriodId = AccountingPeriodLockGuard.RequireOpenPeriod(_dbContext, adjustment.AdjustmentDate, "Inventory adjustment");
        using var transaction = _dbContext.Database.IsRelational()
            ? _dbContext.Database.BeginTransaction()
            : null;

        var nextBatchSequence = (_dbContext.GeneralLedgerBatches.Max(batch => (long?)batch.Id) ?? 0L) + 1L;
        var batch = new GeneralLedgerBatch(
            batchNumber: $"GLB-{nextBatchSequence:D6}",
            batchType: "INV_ADJUSTMENT",
            branchId: adjustment.BranchId,
            periodId: postingPeriodId,
            descriptionText: $"Inventory adjustment posting for {adjustment.AdjustmentNumber}",
            sourceModule: "INVENTORY",
            sourceDocumentId: adjustment.Id);

        var journal = new GeneralLedgerJournal(
            generalLedgerBatchId: batch.Id,
            journalNumber: $"JRNL-{nextBatchSequence:D6}",
            journalType: "INV_ADJUSTMENT",
            postingDate: adjustment.AdjustmentDate,
            taxDate: adjustment.AdjustmentDate,
            documentDate: adjustment.AdjustmentDate,
            descriptionText: $"Inventory adjustment {adjustment.AdjustmentNumber}");

        var nextMovementSequence = (_dbContext.InventoryMovements.Max(movement => (long?)movement.Id) ?? 0L) + 1L;
        var gainAmount = 0m;
        var lossAmount = 0m;

        foreach (var line in adjustment.Lines.OrderBy(line => line.LineNumber))
        {
            var absoluteQuantity = Math.Abs(line.QuantityDelta);
            var lineAmount = line.LineAmount;
            var isIncrease = line.QuantityDelta > 0m;

            if (isIncrease)
            {
                gainAmount += lineAmount;
            }
            else
            {
                lossAmount += lineAmount;
            }

            var movement = new InventoryMovement(
                movementNumber: $"IM-{nextMovementSequence + line.LineNumber - 1:D6}",
                movementType: isIncrease ? "ADJUSTMENT_GAIN" : "ADJUSTMENT_LOSS",
                branchId: adjustment.BranchId,
                itemId: line.ItemId,
                fromWarehouseId: isIncrease ? null : adjustment.WarehouseId,
                fromWarehouseLocationId: isIncrease ? null : line.WarehouseLocationId,
                toWarehouseId: isIncrease ? adjustment.WarehouseId : null,
                toWarehouseLocationId: isIncrease ? line.WarehouseLocationId : null,
                batchNumber: line.BatchNumber,
                serialNumber: line.SerialNumber,
                stockStatus: line.StockStatus,
                quantityMoved: absoluteQuantity,
                unitCost: line.UnitCost,
                movedAtUtc: adjustment.AdjustmentDate.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc));

            _dbContext.InventoryMovements.Add(movement);
        }

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

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

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

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

        batch.AddJournal(journal);
        _dbContext.GeneralLedgerBatches.Add(batch);
        adjustment.Post(actorUserId, batch.Id);
        _dbContext.SaveChanges();
        transaction?.Commit();
        return GetInventoryAdjustment(adjustment.Id)!;
    }

    private static InventoryAdjustmentDetailView Map(InventoryAdjustment adjustment) =>
        new(
            adjustment.Id,
            adjustment.AdjustmentNumber,
            adjustment.BranchId,
            adjustment.WarehouseId,
            adjustment.AdjustmentDate,
            adjustment.ReasonCode,
            adjustment.Notes,
            WorkflowStatusSeedIds.ToCode(adjustment.StatusId),
            adjustment.TotalAdjustmentAmount,
            adjustment.SubmittedAtUtc,
            adjustment.SubmittedByUserId,
            adjustment.ApprovedAtUtc,
            adjustment.ApprovedByUserId,
            adjustment.PostedAtUtc,
            adjustment.PostedByUserId,
            adjustment.GeneralLedgerBatchId,
            adjustment.Lines
                .OrderBy(line => line.LineNumber)
                .Select(line => new InventoryAdjustmentLineView(
                    line.Id,
                    line.LineNumber,
                    line.ItemId,
                    line.WarehouseLocationId,
                    line.QuantityDelta,
                    line.UnitOfMeasureCode,
                    line.UnitCost,
                    line.LineAmount,
                    line.StockStatus,
                    line.BatchNumber,
                    line.SerialNumber,
                    line.Notes))
                .ToArray());

    private static void ValidateCreateCommand(CreateInventoryAdjustmentCommand command)
    {
        if (command.Lines.Count == 0)
        {
            throw new InvalidOperationException("Inventory adjustment must contain at least one line.");
        }

        if (string.IsNullOrWhiteSpace(command.ReasonCode))
        {
            throw new InvalidOperationException("Inventory adjustment reason code is required.");
        }
    }
}
