using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using MiGu.Server.Persistence;

namespace MiGu.Server.Wms;

public sealed class WmsTransportPlanner
{
    private readonly PlatformDbContext _db;
    private readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web);

    public WmsTransportPlanner(PlatformDbContext db) => _db = db;

    public async Task<TransportCandidatePreview> PreviewAsync(WmsTransportRequest request)
    {
        if (!WmsTransportTriggerTypes.IsDefined(request.TriggerType))
            throw new InvalidOperationException("触发类型无效");

        var trigger = WmsTransportTriggerTypes.ParseOr(request.TriggerType);
        var ctx = await BuildContextAsync();
        var rules = await LoadRulesAsync(request, trigger);
        var ruleResults = new List<RulePreviewResult>();
        var candidates = new List<TransportCandidatePair>();

        foreach (var rule in rules)
        {
            var sourceSelector = TransportSelectorParser.ParseSelector(rule.SourceSelectorJson);
            var targetSelector = TransportSelectorParser.ParseSelector(rule.TargetSelectorJson);
            var options = TransportSelectorParser.ParseTaskOptions(rule.TaskOptionsJson);
            var ruleCandidates = BuildCandidates(ctx, request, trigger, rule, sourceSelector, targetSelector, options);
            var rejectReasons = ruleCandidates.Count == 0
                ? new List<string> { "未找到满足条件的起点/终点组合" }
                : new List<string>();
            ruleResults.Add(new RulePreviewResult(rule.Id, rule.Code, rule.Name, ruleCandidates.Count, rejectReasons));
            candidates.AddRange(ruleCandidates);
        }

        candidates = candidates
            .OrderByDescending(x => x.Score)
            .ThenBy(x => x.SourceStorageCode, StringComparer.Ordinal)
            .ThenBy(x => x.ContainerCode, StringComparer.Ordinal)
            .Take(rules.FirstOrDefault() is { } r
                ? TransportSelectorParser.ParseTaskOptions(r.TaskOptionsJson).MaxCandidateCount
                : 20)
            .ToList();

        return new TransportCandidatePreview(request, candidates, ruleResults);
    }

    public async Task<TransportCandidatePair?> PickBestAsync(WmsTransportRequest request)
    {
        var preview = await PreviewAsync(request);
        return preview.Candidates.FirstOrDefault();
    }

    private async Task<List<WmsTransportRule>> LoadRulesAsync(WmsTransportRequest request, WmsTransportTriggerType trigger)
    {
        var query = _db.WmsTransportRules.AsNoTracking()
            .Where(x => x.Enabled && x.TriggerType == trigger);
        if (request.RuleId.HasValue)
            query = query.Where(x => x.Id == request.RuleId.Value);
        return await query.OrderByDescending(x => x.Priority).ThenBy(x => x.Code).ToListAsync();
    }

    private async Task<PlannerContext> BuildContextAsync()
    {
        var storages = await _db.Storages.AsNoTracking().Where(x => x.Enabled).ToListAsync();
        var containers = await _db.Containers.AsNoTracking().Where(x => x.Enabled).ToListAsync();
        var locations = await _db.ContainerLocations.AsNoTracking()
            .Where(x => x.LocationType == ContainerLocationTypes.Storage).ToListAsync();
        var materials = await _db.Materials.AsNoTracking().Where(x => x.Enabled).ToListAsync();
        var containerMaterials = await _db.ContainerMaterials.AsNoTracking().ToListAsync();
        var now = DateTimeOffset.UtcNow;
        var activeReservations = await _db.WmsTransportReservations.AsNoTracking()
            .Where(x => x.Status == WmsReservationStatuses.Active
                        && (x.ExpiresAt == null || x.ExpiresAt > now))
            .ToListAsync();
        var activeTasks = await _db.WmsTransportTasks.AsNoTracking()
            .Where(x => WmsTransportTaskStatuses.Active.Contains(x.Status)).ToListAsync();

        var storageById = storages.ToDictionary(x => x.Id);
        var containerById = containers.ToDictionary(x => x.Id);
        var materialById = materials.ToDictionary(x => x.Id);
        var occupiedStorageIds = locations
            .Where(x => Guid.TryParse(x.LocationId, out _))
            .Select(x => Guid.Parse(x.LocationId))
            .ToHashSet();
        var reservedContainers = activeReservations.Select(x => x.ContainerId).ToHashSet();
        var reservedTargetStorages = activeReservations.Select(x => x.TargetStorageId).ToHashSet();
        var taskReservedContainers = activeTasks.Select(x => x.ContainerId).ToHashSet();

        return new PlannerContext(
            storages, containers, locations, containerMaterials, materials,
            storageById, containerById, materialById,
            occupiedStorageIds, reservedContainers, reservedTargetStorages, taskReservedContainers);
    }

    private List<TransportCandidatePair> BuildCandidates(
        PlannerContext ctx,
        WmsTransportRequest request,
        WmsTransportTriggerType trigger,
        WmsTransportRule rule,
        TransportSelector sourceSelector,
        TransportSelector targetSelector,
        TransportTaskOptions options)
    {
        var results = new List<TransportCandidatePair>();
        var sources = BuildSourceCandidates(ctx, request, trigger, sourceSelector, options);

        foreach (var src in sources)
        {
            var targets = BuildTargetCandidates(ctx, request, trigger, targetSelector, options, src.StorageId);
            foreach (var tgt in targets)
            {
                var score = rule.Priority * 1000 + src.Score + tgt.Score;
                results.Add(new TransportCandidatePair(
                    rule.Id, rule.Code,
                    src.StorageId, src.StorageCode,
                    tgt.StorageId, tgt.StorageCode,
                    src.ContainerId, src.ContainerCode,
                    src.MaterialId, src.MaterialCode, src.Quantity,
                    score, []));
            }
        }

        return results;
    }

    private List<SourceCandidate> BuildSourceCandidates(
        PlannerContext ctx,
        WmsTransportRequest request,
        WmsTransportTriggerType trigger,
        TransportSelector selector,
        TransportTaskOptions options)
    {
        var list = new List<SourceCandidate>();
        foreach (var loc in ctx.Locations)
        {
            if (!Guid.TryParse(loc.LocationId, out var storageId)) continue;
            if (request.SourceStorageId.HasValue && request.SourceStorageId.Value != storageId) continue;
            if (!ctx.StorageById.TryGetValue(storageId, out var storage)) continue;
            if (request.ContainerId.HasValue && request.ContainerId.Value != loc.ContainerId) continue;
            if (!ctx.ContainerById.TryGetValue(loc.ContainerId, out var container)) continue;

            if (!MatchesStorage(storage, selector.Storage, ctx, requireOccupied: true)) continue;
            if (!MatchesContainer(container, selector.Container, ctx)) continue;

            var cm = FindMatchingMaterial(ctx, loc.ContainerId, request, selector.Material);
            if (selector.Material is { MaterialIds.Count: > 0 } or { Categories.Count: > 0 } or { MinQuantity: not null }
                && cm == null)
                continue;

            if (request.RequestSiteId is { Length: > 0 } siteId &&
                trigger == WmsTransportTriggerTypes.FinishedGoodsOffline &&
                !string.Equals(storage.SiteId, siteId, StringComparison.OrdinalIgnoreCase))
                continue;

            list.Add(new SourceCandidate(
                storageId, storage.Code, loc.ContainerId, container.Code,
                cm?.MaterialId, cm == null ? null : ctx.MaterialById.GetValueOrDefault(cm.MaterialId)?.Code,
                cm?.Quantity,
                ScoreSource(storage, container, loc, cm, selector)));
        }

        return SortSources(list, selector);
    }

    private List<TargetCandidate> BuildTargetCandidates(
        PlannerContext ctx,
        WmsTransportRequest request,
        WmsTransportTriggerType trigger,
        TransportSelector selector,
        TransportTaskOptions options,
        Guid sourceStorageId)
    {
        var list = new List<TargetCandidate>();
        foreach (var storage in ctx.Storages)
        {
            if (request.TargetStorageId.HasValue && request.TargetStorageId.Value != storage.Id) continue;
            if (!options.AllowSameStorage && storage.Id == sourceStorageId) continue;
            if (!MatchesStorage(storage, selector.Storage, ctx, requireOccupied: false)) continue;
            if (ctx.OccupiedStorageIds.Contains(storage.Id)) continue;
            if (ctx.ReservedTargetStorages.Contains(storage.Id)) continue;

            if (request.RequestSiteId is { Length: > 0 } siteId &&
                trigger == WmsTransportTriggerTypes.MaterialCall &&
                selector.Storage?.SiteIds is { Count: 0 } &&
                !string.Equals(storage.SiteId, siteId, StringComparison.OrdinalIgnoreCase))
                continue;

            list.Add(new TargetCandidate(storage.Id, storage.Code, ScoreTarget(storage, selector)));
        }

        return SortTargets(list, selector);
    }

    private static ContainerMaterial? FindMatchingMaterial(
        PlannerContext ctx,
        Guid containerId,
        WmsTransportRequest request,
        MaterialSelectorFilter? filter)
    {
        var rows = ctx.ContainerMaterials.Where(x => x.ContainerId == containerId).ToList();
        if (rows.Count == 0) return null;

        foreach (var row in rows)
        {
            if (request.MaterialId.HasValue && row.MaterialId != request.MaterialId.Value) continue;
            if (filter?.MaterialIds is { Count: > 0 } ids && !ids.Contains(row.MaterialId.ToString("D"))) continue;
            if (filter?.StatusIn is { Count: > 0 } st &&
                !st.Contains(row.Status.ToString(), StringComparer.OrdinalIgnoreCase)) continue;
            if (filter?.MinQuantity is { } min && row.Quantity < min) continue;
            if (request.Quantity is { } reqQty && row.Quantity < reqQty) continue;
            if (request.BatchNo is { Length: > 0 } batch && !string.Equals(row.BatchNo, batch, StringComparison.OrdinalIgnoreCase)) continue;
            if (filter?.Categories is { Count: > 0 } cats &&
                ctx.MaterialById.TryGetValue(row.MaterialId, out var mat) &&
                !cats.Contains(mat.Category, StringComparer.OrdinalIgnoreCase))
                continue;
            return row;
        }

        return request.MaterialId.HasValue || filter?.MaterialIds is { Count: > 0 } ? null : rows.FirstOrDefault();
    }

    private static bool MatchesStorage(Storage storage, StorageSelectorFilter? filter, PlannerContext ctx, bool requireOccupied)
    {
        if (filter == null) return true;
        if (filter.AreaIds is { Count: > 0 } && !filter.AreaIds.Contains(storage.AreaId.ToString("D"))) return false;
        if (filter.StorageTypes is { Count: > 0 } && !filter.StorageTypes.Contains(storage.StorageType, StringComparer.OrdinalIgnoreCase)) return false;
        if (filter.ZoneCodes is { Count: > 0 } && !filter.ZoneCodes.Contains(storage.ZoneCode, StringComparer.OrdinalIgnoreCase)) return false;
        if (filter.StatusIn is { Count: > 0 } &&
            !filter.StatusIn.Contains(storage.Status.ToString(), StringComparer.OrdinalIgnoreCase)) return false;
        if (filter.SiteIds is { Count: > 0 } && !filter.SiteIds.Contains(storage.SiteId, StringComparer.OrdinalIgnoreCase)) return false;
        if (filter.AllowInbound == true && !storage.AllowInbound) return false;
        if (filter.AllowOutbound == true && !storage.AllowOutbound) return false;
        if (filter.RequireSiteId && string.IsNullOrWhiteSpace(storage.SiteId)) return false;
        if (filter.ExcludeStorageIds is { Count: > 0 } && filter.ExcludeStorageIds.Contains(storage.Id.ToString("D"))) return false;
        var occupied = ctx.OccupiedStorageIds.Contains(storage.Id);
        if (filter.RequireEmpty && occupied) return false;
        if ((filter.RequireOccupied || requireOccupied) && !occupied && requireOccupied) return false;
        return true;
    }

    private static bool MatchesContainer(Container container, ContainerSelectorFilter? filter, PlannerContext ctx)
    {
        if (filter == null) return true;
        if (filter.ContainerTypes is { Count: > 0 } && !filter.ContainerTypes.Contains(container.ContainerType, StringComparer.OrdinalIgnoreCase)) return false;
        if (filter.StatusIn is { Count: > 0 } &&
            !filter.StatusIn.Contains(container.Status.ToString(), StringComparer.OrdinalIgnoreCase)) return false;
        if (filter.ExcludeReserved &&
            (ctx.ReservedContainers.Contains(container.Id) || ctx.TaskReservedContainers.Contains(container.Id)))
            return false;
        return true;
    }

    private static int ScoreSource(Storage storage, Container container, ContainerLocation loc, ContainerMaterial? cm, TransportSelector selector) =>
        storage.Priority * 10 + (cm?.Quantity is > 0 ? 5 : 0);

    private static int ScoreTarget(Storage storage, TransportSelector selector) => storage.Priority * 10;

    private static List<SourceCandidate> SortSources(List<SourceCandidate> list, TransportSelector selector)
    {
        IOrderedEnumerable<SourceCandidate>? ordered = null;
        foreach (var rule in selector.Ranking)
        {
            ordered = ordered == null
                ? ApplySourceSort(list, rule)
                : ApplySourceSort(ordered, rule);
        }
        return (ordered ?? list.OrderBy(x => x.StorageCode)).ToList();
    }

    private static List<TargetCandidate> SortTargets(List<TargetCandidate> list, TransportSelector selector)
    {
        IOrderedEnumerable<TargetCandidate>? ordered = null;
        foreach (var rule in selector.Ranking)
        {
            ordered = ordered == null
                ? ApplyTargetSort(list, rule)
                : ApplyTargetSort(ordered, rule);
        }
        return (ordered ?? list.OrderBy(x => x.StorageCode)).ToList();
    }

    private static IOrderedEnumerable<SourceCandidate> ApplySourceSort(IEnumerable<SourceCandidate> list, RankingRule rule) =>
        rule.Field.ToLowerInvariant() switch
        {
            "storagepriority" or "priority" => rule.Direction.Equals("desc", StringComparison.OrdinalIgnoreCase)
                ? list.OrderByDescending(x => x.Score)
                : list.OrderBy(x => x.Score),
            "quantity" => rule.Direction.Equals("desc", StringComparison.OrdinalIgnoreCase)
                ? list.OrderByDescending(x => x.Quantity ?? 0)
                : list.OrderBy(x => x.Quantity ?? 0),
            _ => list.OrderBy(x => x.StorageCode)
        };

    private static IOrderedEnumerable<SourceCandidate> ApplySourceSort(IOrderedEnumerable<SourceCandidate> list, RankingRule rule) =>
        rule.Field.ToLowerInvariant() switch
        {
            "quantity" => rule.Direction.Equals("desc", StringComparison.OrdinalIgnoreCase)
                ? list.ThenByDescending(x => x.Quantity ?? 0)
                : list.ThenBy(x => x.Quantity ?? 0),
            _ => list.ThenBy(x => x.StorageCode)
        };

    private static IOrderedEnumerable<TargetCandidate> ApplyTargetSort(IEnumerable<TargetCandidate> list, RankingRule rule) =>
        rule.Field.ToLowerInvariant() switch
        {
            "storagepriority" or "priority" => rule.Direction.Equals("desc", StringComparison.OrdinalIgnoreCase)
                ? list.OrderByDescending(x => x.Score)
                : list.OrderBy(x => x.Score),
            _ => list.OrderBy(x => x.StorageCode)
        };

    private static IOrderedEnumerable<TargetCandidate> ApplyTargetSort(IOrderedEnumerable<TargetCandidate> list, RankingRule rule) =>
        list.ThenBy(x => x.StorageCode);

    private sealed record PlannerContext(
        List<Storage> Storages,
        List<Container> Containers,
        List<ContainerLocation> Locations,
        List<ContainerMaterial> ContainerMaterials,
        List<Material> Materials,
        Dictionary<Guid, Storage> StorageById,
        Dictionary<Guid, Container> ContainerById,
        Dictionary<Guid, Material> MaterialById,
        HashSet<Guid> OccupiedStorageIds,
        HashSet<Guid> ReservedContainers,
        HashSet<Guid> ReservedTargetStorages,
        HashSet<Guid> TaskReservedContainers);

    private sealed record SourceCandidate(
        Guid StorageId, string StorageCode,
        Guid ContainerId, string ContainerCode,
        Guid? MaterialId, string? MaterialCode, decimal? Quantity, int Score);

    private sealed record TargetCandidate(Guid StorageId, string StorageCode, int Score);
}
