using ERPCore.Common.Http;
using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Common;
using ERPCore.Dtos.Production;
using ERPCore.Infra.Auth;
using ERPCore.Infra.UoW;
using ERPCore.Repositories.Interfaces;
using ERPCore.Services.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Services.Production;

/// <summary>
/// Production template CRUD + graph validation (docs/30 §D.1, FR-MFG-01..07).
/// Graph rules live in <see cref="ProductionGraphValidator"/>; everything needing the
/// database (referential checks, the edit-lock, reconciliation) lives here.
/// </summary>
public sealed class ProductionTemplateService : IProductionTemplateService
{
    private readonly IRepository<ProductionTemplate> _templates;
    private readonly IRepository<TemplateStage> _stages;
    private readonly IRepository<StageInput> _inputs;
    private readonly IRepository<StageOutput> _outputs;
    private readonly IRepository<StageEdge> _edges;
    private readonly IRepository<Item> _items;
    private readonly IRepository<Uom> _uoms;
    private readonly IRepository<ProductionRun> _runs;
    private readonly IUnitOfWork _uow;
    private readonly ICurrentUser _currentUser;

    public ProductionTemplateService(
        IRepository<ProductionTemplate> templates, IRepository<TemplateStage> stages,
        IRepository<StageInput> inputs, IRepository<StageOutput> outputs, IRepository<StageEdge> edges,
        IRepository<Item> items, IRepository<Uom> uoms, IRepository<ProductionRun> runs,
        IUnitOfWork uow, ICurrentUser currentUser)
    {
        _templates = templates;
        _stages = stages;
        _inputs = inputs;
        _outputs = outputs;
        _edges = edges;
        _items = items;
        _uoms = uoms;
        _runs = runs;
        _uow = uow;
        _currentUser = currentUser;
    }

    public async Task<PagedResponse<TemplateSummaryDto>> ListAsync(
        PageQuery query, EntityStatus? status, CancellationToken ct = default)
    {
        var q = _templates.Query().AsNoTracking();

        if (!string.IsNullOrWhiteSpace(query.Q))
        {
            var term = query.Q.Trim();
            q = q.Where(t => EF.Functions.ILike(t.Code, $"%{term}%") || EF.Functions.ILike(t.Name, $"%{term}%"));
        }

        if (status is not null) q = q.Where(t => t.Status == status);

        var total = await q.CountAsync(ct);

        // Projected to an anonymous type first, then mapped client-side: EF Core 10 cannot
        // translate a record constructor sitting alongside aggregates and a collection
        // projection (same limitation recorded for WarehouseValuationDto, 2026-07-28).
        var rows = await q
            .OrderBy(t => t.Code)
            .Skip(query.Skip).Take(query.PageSize)
            .Select(t => new
            {
                t.TemplateId,
                t.Code,
                t.Name,
                t.Status,
                StageCount = t.Stages.Count,
                Stages = t.Stages.Select(s => new { s.StageId, s.Name }).ToList(),
                Edges = t.Edges.Select(e => new { e.ParentStageId, e.ChildStageId }).ToList(),
                // Drives the canvas edit-lock banner (docs/21 §2) and mirrors the
                // condition UpdateAsync enforces server-side.
                ActiveRunCount = t.Runs.Count(r => r.Status == ProductionRunStatus.InProgress),
                t.CreatedBy,
                t.CreatedAt
            })
            .ToListAsync(ct);

        var items = rows.Select(t => new TemplateSummaryDto(
            t.TemplateId, t.Code, t.Name, t.Status, t.StageCount,
            FlowOrderedNames(
                t.Stages.Select(s => (s.StageId, s.Name)).ToList(),
                t.Edges.Select(e => (e.ParentStageId, e.ChildStageId)).ToList()),
            t.ActiveRunCount, t.CreatedBy, t.CreatedAt)).ToList();

        return PagedResponse<TemplateSummaryDto>.Create(items, query.Page, query.PageSize, total);
    }

    /// <summary>
    /// Stage names in <b>flow order</b> — upstream first — for the overview canvas, which
    /// draws each template as a line left to right (docs/21 §1).
    /// </summary>
    /// <remarks>
    /// Ordering by <c>StageId</c> would be insertion order, which routinely puts the terminal
    /// stage first and renders the line backwards. Kahn's algorithm over the edge set gives the
    /// real sequence, tie-broken by stage id so parallel branches come out deterministically.
    /// Falls back to id order if the graph somehow contains a cycle, so a listing can never
    /// fail because of bad data.
    /// </remarks>
    private static List<string> FlowOrderedNames(
        List<(int StageId, string Name)> stages, List<(int ParentStageId, int ChildStageId)> edges)
    {
        var nameById = stages.ToDictionary(s => s.StageId, s => s.Name);
        var children = stages.ToDictionary(s => s.StageId, _ => new List<int>());
        var indegree = stages.ToDictionary(s => s.StageId, _ => 0);

        foreach (var (parent, child) in edges)
        {
            if (!children.ContainsKey(parent) || !indegree.ContainsKey(child)) continue;
            children[parent].Add(child);
            indegree[child]++;
        }

        // SortedSet keeps the frontier in id order, so the output is stable run to run.
        var frontier = new SortedSet<int>(indegree.Where(kv => kv.Value == 0).Select(kv => kv.Key));
        var ordered = new List<string>(stages.Count);

        while (frontier.Count > 0)
        {
            var next = frontier.Min;
            frontier.Remove(next);
            ordered.Add(nameById[next]);

            foreach (var child in children[next])
                if (--indegree[child] == 0)
                    frontier.Add(child);
        }

        return ordered.Count == stages.Count
            ? ordered
            : stages.OrderBy(s => s.StageId).Select(s => s.Name).ToList();
    }

    public async Task<ETagged<TemplateGraphDto>?> GetAsync(int templateId, CancellationToken ct = default)
    {
        var template = await LoadGraphQuery().AsNoTracking()
            .FirstOrDefaultAsync(t => t.TemplateId == templateId, ct);

        if (template is null) return null;

        // Counted separately rather than Included: the graph query already fans out over four
        // collections, and adding Runs would multiply those rows again for a single integer.
        var activeRuns = await _runs.Query().AsNoTracking()
            .CountAsync(r => r.TemplateId == templateId && r.Status == ProductionRunStatus.InProgress, ct);

        return new ETagged<TemplateGraphDto>(ToGraphDto(template, activeRuns), template.RowVersion);
    }

    public async Task<ETagged<TemplateGraphDto>> CreateAsync(SaveTemplateRequest request, CancellationToken ct = default)
    {
        var code = request.Code.Trim();
        if (await _templates.Query().AnyAsync(t => t.Code == code, ct))
            throw new ConflictException($"A production template with code '{code}' already exists.");

        var template = new ProductionTemplate
        {
            Code = code,
            Name = request.Name.Trim(),
            Description = request.Description?.Trim(),
            Status = EntityStatus.Active,
            CreatedBy = _currentUser.AuditUserId,
            CreatedAt = DateTime.UtcNow
        };

        await ValidateAsync(request, ct);

        await _uow.ExecuteInTransactionAsync(async token =>
        {
            await _templates.AddAsync(template, token);
            BuildGraph(template, request);
        }, ct);

        return await RequireGraphAsync(template.TemplateId, ct);
    }

    public async Task<ETagged<TemplateGraphDto>> UpdateAsync(
        int templateId, SaveTemplateRequest request, uint expectedRowVersion, CancellationToken ct = default)
    {
        await ValidateAsync(request, ct);

        var code = request.Code.Trim();
        if (await _templates.Query().AnyAsync(t => t.Code == code && t.TemplateId != templateId, ct))
            throw new ConflictException($"A production template with code '{code}' already exists.");

        await _uow.ExecuteInTransactionAsync(async token =>
        {
            var template = await LoadGraphQuery().FirstOrDefaultAsync(t => t.TemplateId == templateId, token)
                ?? throw new NotFoundException($"Production template {templateId} was not found.");

            if (template.RowVersion != expectedRowVersion)
                throw new DomainException(ErrorCodes.ConcurrencyConflict,
                    "The template was modified by another request.", 412);

            // FR-MFG-06: edit-lock instead of versioning. Checked inside the transaction to
            // keep the window small; the residual race is benign because a run copies
            // everything it needs at creation and never reads the template again.
            var activeRuns = await _runs.Query().AsNoTracking()
                .CountAsync(r => r.TemplateId == templateId && r.Status == ProductionRunStatus.InProgress, token);
            if (activeRuns > 0)
                throw new DomainException(ErrorCodes.TemplateInUse,
                    $"This template has {activeRuns} run(s) in progress and cannot be edited until they finish.", 409);

            template.Code = code;
            template.Name = request.Name.Trim();
            template.Description = request.Description?.Trim();

            // Two passes, deliberately. Tearing the old graph down and flushing before
            // rebuilding removes any dependence on how EF happens to order a mixed batch of
            // inserts and deletes — which matters because stage_inputs → stage_outputs is a
            // Restrict FK and stage_edges carries a unique (parent, child) index.
            var keptStages = TearDownGraph(template, request);
            await _uow.SaveChangesAsync(token);

            BuildGraph(template, request, keptStages);
        }, ct);

        return await RequireGraphAsync(templateId, ct);
    }

    public async Task SetStatusAsync(int templateId, EntityStatus status, CancellationToken ct = default)
    {
        var template = await _templates.Query().FirstOrDefaultAsync(t => t.TemplateId == templateId, ct)
            ?? throw new NotFoundException($"Production template {templateId} was not found.");

        template.Status = status;
        await _uow.SaveChangesAsync(ct);
    }

    // --- graph reconciliation ------------------------------------------------

    private IQueryable<ProductionTemplate> LoadGraphQuery() =>
        _templates.Query()
            .Include(t => t.Stages).ThenInclude(s => s.Inputs)
            .Include(t => t.Stages).ThenInclude(s => s.Outputs)
            .Include(t => t.Edges);

    /// <summary>
    /// Runs the pure graph rules then the referential ones. Item/UOM existence needs the
    /// database, so it can't live in the validator — but it has to run before any write.
    /// </summary>
    private async Task ValidateAsync(SaveTemplateRequest request, CancellationToken ct)
    {
        ProductionGraphValidator.Validate(
            request.Stages.Select(s => new ProductionGraphValidator.StageDraft(
                s.Key, s.Name,
                s.Inputs.Select((i, idx) => new ProductionGraphValidator.InputDraft(idx, i.Source, i.ItemId, i.FromOutputKey, i.QtyUnit)).ToList(),
                s.Outputs.Select(o => new ProductionGraphValidator.OutputDraft(o.Key, o.Name, o.ItemId, o.UomId)).ToList())).ToList(),
            request.Edges.Select(e => new ProductionGraphValidator.EdgeDraft(e.ParentKey, e.ChildKey)).ToList());

        var itemIds = request.Stages
            .SelectMany(s => s.Inputs.Select(i => i.ItemId).Concat(s.Outputs.Select(o => o.ItemId)))
            .OfType<int>().Distinct().ToList();

        var contentByItem = new Dictionary<int, decimal?>();
        if (itemIds.Count > 0)
        {
            var found = await _items.Query().AsNoTracking()
                .Where(i => itemIds.Contains(i.ItemId))
                .Select(i => new { i.ItemId, i.Status, i.ContentBaseQty })
                .ToListAsync(ct);

            var missing = itemIds.Except(found.Select(f => f.ItemId)).ToList();
            if (missing.Count > 0)
                throw new DomainException(ErrorCodes.Validation,
                    $"Item(s) {string.Join(", ", missing)} do not exist.", 422);

            var inactive = found.Where(f => f.Status != EntityStatus.Active).Select(f => f.ItemId).ToList();
            if (inactive.Count > 0)
                throw new DomainException(ErrorCodes.Validation,
                    $"Item(s) {string.Join(", ", inactive)} are inactive and cannot be used in a template.", 422);

            contentByItem = found.ToDictionary(f => f.ItemId, f => f.ContentBaseQty);
        }

        // A Content quantity is only meaningful against an item that declares a content size,
        // and it must still land on a storable pack count: quantities persist at (18,4), so
        // anything under 0.0001 packs would round to zero and consume nothing at stage start.
        foreach (var s in request.Stages)
            foreach (var i in s.Inputs.Where(i => i.QtyUnit == StageQtyUnit.Content))
            {
                var contentBaseQty = contentByItem.GetValueOrDefault(i.ItemId ?? 0);
                if (contentBaseQty is not > 0m)
                    throw new DomainException(ErrorCodes.Validation,
                        $"Stage '{s.Name}' has an input in content units, but item {i.ItemId} has no content size.", 422);

                if (i.QtyPerBatch / contentBaseQty.Value < 0.0001m)
                    throw new DomainException(ErrorCodes.Validation,
                        $"Stage '{s.Name}': {i.QtyPerBatch} is less than 0.0001 of item {i.ItemId}'s content size ({contentBaseQty}), which would round to no stock at all.", 422);
            }

        // Only WIP outputs carry a UOM now; the validator has already rejected one on an
        // item-bearing output, so every non-null id here belongs to real work-in-progress.
        var uomIds = request.Stages
            .SelectMany(s => s.Outputs.Select(o => o.UomId))
            .OfType<int>().Distinct().ToList();

        if (uomIds.Count > 0)
        {
            var knownUoms = await _uoms.Query().AsNoTracking()
                .Where(u => uomIds.Contains(u.UomId)).Select(u => u.UomId).ToListAsync(ct);

            var missingUoms = uomIds.Except(knownUoms).ToList();
            if (missingUoms.Count > 0)
                throw new DomainException(ErrorCodes.Validation,
                    $"UOM(s) {string.Join(", ", missingUoms)} do not exist.", 422);
        }

        // Annotations go into jsonb unvalidated by anything else, so pin the one field the
        // client renders off. Unknown kinds would round-trip fine but draw nothing.
        var badKinds = request.Annotations
            .Select(a => a.Kind)
            .Where(k => k is not ("box" or "line"))
            .Distinct().ToList();
        if (badKinds.Count > 0)
            throw new DomainException(ErrorCodes.Validation,
                $"Unknown canvas annotation kind(s): {string.Join(", ", badKinds)}. Expected 'box' or 'line'.", 422);
    }

    /// <summary>
    /// Removes everything the incoming payload replaces and returns the surviving stages
    /// by key.
    /// </summary>
    /// <remarks>
    /// Inputs and outputs are always replaced wholesale — nothing outside the template
    /// references them, because a run copies into its own <c>run_stage_input</c>/
    /// <c>run_stage_output</c> rows. Stages are <b>diffed</b>, not replaced, because
    /// <c>run_stages.TemplateStageId</c> points at them; a stage that disappears from the
    /// payload is deleted and that FK is set null for any historical run (see
    /// <c>RunStageConfiguration</c>).
    /// </remarks>
    private Dictionary<string, TemplateStage> TearDownGraph(ProductionTemplate template, SaveTemplateRequest request)
    {
        var payloadKeys = request.Stages.Select(s => s.Key).ToHashSet(StringComparer.Ordinal);

        foreach (var stage in template.Stages)
        {
            foreach (var input in stage.Inputs.ToList()) _inputs.Remove(input);
            foreach (var output in stage.Outputs.ToList()) _outputs.Remove(output);
        }

        var kept = new Dictionary<string, TemplateStage>(StringComparer.Ordinal);
        var doomed = new List<TemplateStage>();
        foreach (var stage in template.Stages.ToList())
        {
            var key = stage.StageId.ToString();
            if (payloadKeys.Contains(key)) kept[key] = stage;
            else doomed.Add(stage);
        }

        // Edges are diffed rather than replaced: stage_edges has a unique (parent, child)
        // index, and dropping then re-adding an unchanged edge in the same round trip can
        // trip it depending on statement order.
        var wanted = request.Edges
            .Select(e => (Parent: ResolveKeptId(kept, e.ParentKey), Child: ResolveKeptId(kept, e.ChildKey)))
            .Where(e => e.Parent is not null && e.Child is not null)
            .Select(e => (e.Parent!.Value, e.Child!.Value))
            .ToHashSet();

        // Edges must go before stages. StageEdge.ParentStage/ChildStage are required
        // relationships, so deleting a stage that a still-live edge points at makes EF throw
        // "the association ... has been severed". Any edge touching a doomed stage is
        // necessarily absent from `wanted` — the payload cannot reference a stage it dropped
        // — so this ordering never orphans an edge the caller wanted to keep.
        foreach (var edge in template.Edges.ToList())
            if (!wanted.Contains((edge.ParentStageId, edge.ChildStageId)))
                _edges.Remove(edge);

        foreach (var stage in doomed)
            _stages.Remove(stage);

        return kept;
    }

    private static int? ResolveKeptId(Dictionary<string, TemplateStage> kept, string key)
        => kept.TryGetValue(key, out var stage) ? stage.StageId : null;

    /// <summary>
    /// Materialises the payload onto the template.
    /// </summary>
    /// <remarks>
    /// Everything is wired through <b>navigation properties</b> rather than foreign-key
    /// ints, so EF resolves the ids itself during a single <c>SaveChanges</c>. That is what
    /// lets a brand-new Upstream input point at a brand-new output without an intermediate
    /// save to materialise generated keys — outputs are built in the first pass precisely
    /// so the second pass has the entity instances to hand.
    /// </remarks>
    private void BuildGraph(
        ProductionTemplate template, SaveTemplateRequest request,
        Dictionary<string, TemplateStage>? keptStages = null)
    {
        // Replaced wholesale — annotations are opaque client state, not part of the graph, so
        // there is nothing to diff and nothing else can reference them.
        template.Annotations = request.Annotations.Count == 0
            ? null
            : ProductionJson.Serialize(request.Annotations);

        var stagesByKey = new Dictionary<string, TemplateStage>(StringComparer.Ordinal);
        var outputsByKey = new Dictionary<string, StageOutput>(StringComparer.Ordinal);

        // Pass 1 — stages and their outputs.
        foreach (var s in request.Stages)
        {
            if (keptStages is null || !keptStages.TryGetValue(s.Key, out var stage))
            {
                stage = new TemplateStage { Template = template };
                template.Stages.Add(stage);
            }

            stage.Name = s.Name.Trim();
            stage.RoleLabel = string.IsNullOrWhiteSpace(s.RoleLabel) ? null : s.RoleLabel.Trim();
            stage.EstimatedMinutes = s.EstimatedMinutes;
            stage.PosX = s.PosX;
            stage.PosY = s.PosY;
            stage.FieldDefs = ProductionJson.Serialize(s.FieldDefs);

            stagesByKey[s.Key] = stage;

            foreach (var o in s.Outputs)
            {
                var output = new StageOutput
                {
                    Stage = stage,
                    ItemId = o.ItemId,
                    Name = o.Name.Trim(),
                    UomId = o.UomId,
                    QtyPerBatch = o.QtyPerBatch
                };
                stage.Outputs.Add(output);
                outputsByKey[o.Key] = output;
            }
        }

        // Pass 2 — inputs, which may reference any output built above.
        foreach (var s in request.Stages)
        {
            var stage = stagesByKey[s.Key];
            foreach (var i in s.Inputs)
            {
                stage.Inputs.Add(new StageInput
                {
                    Stage = stage,
                    Source = i.Source,
                    ItemId = i.Source == StageInputSource.Stock ? i.ItemId : null,
                    FromOutput = i.Source == StageInputSource.Upstream ? outputsByKey[i.FromOutputKey!] : null,
                    QtyUnit = i.QtyUnit,
                    QtyPerBatch = i.QtyPerBatch
                });
            }
        }

        // Pass 3 — edges not already present.
        var existing = template.Edges
            .Select(e => (e.ParentStageId, e.ChildStageId)).ToHashSet();

        foreach (var e in request.Edges)
        {
            var parent = stagesByKey[e.ParentKey];
            var child = stagesByKey[e.ChildKey];
            if (parent.StageId != 0 && child.StageId != 0 && existing.Contains((parent.StageId, child.StageId)))
                continue;

            template.Edges.Add(new StageEdge
            {
                Template = template,
                ParentStage = parent,
                ChildStage = child
            });
        }
    }

    // --- mapping -------------------------------------------------------------

    private async Task<ETagged<TemplateGraphDto>> RequireGraphAsync(int templateId, CancellationToken ct)
        => await GetAsync(templateId, ct)
           ?? throw new NotFoundException($"Production template {templateId} was not found.");

    private static TemplateGraphDto ToGraphDto(ProductionTemplate t, int activeRunCount)
    {
        // The key a client sends back is just the id as a string; computing it here keeps
        // that contract in exactly one place.
        var outputKeyById = t.Stages
            .SelectMany(s => s.Outputs)
            .ToDictionary(o => o.OutputId, o => o.OutputId.ToString());

        var stages = t.Stages
            .OrderBy(s => s.StageId)
            .Select(s => new TemplateStageDto(
                s.StageId, s.StageId.ToString(), s.Name, s.RoleLabel, s.EstimatedMinutes, s.PosX, s.PosY,
                ProductionJson.Deserialize<List<FieldDefDto>>(s.FieldDefs, []),
                s.Inputs.OrderBy(i => i.InputId).Select(i => new StageInputDto(
                    i.InputId, i.Source, i.ItemId, i.FromOutputId,
                    i.FromOutputId is null ? null : outputKeyById.GetValueOrDefault(i.FromOutputId.Value),
                    i.QtyUnit, i.QtyPerBatch)).ToList(),
                s.Outputs.OrderBy(o => o.OutputId).Select(o => new StageOutputDto(
                    o.OutputId, o.OutputId.ToString(), o.ItemId, o.Name, o.UomId, o.QtyPerBatch)).ToList()))
            .ToList();

        var edges = t.Edges
            .OrderBy(e => e.EdgeId)
            .Select(e => new TemplateEdgeDto(
                e.EdgeId, e.ParentStageId, e.ChildStageId,
                e.ParentStageId.ToString(), e.ChildStageId.ToString()))
            .ToList();

        return new TemplateGraphDto(
            t.TemplateId, t.Code, t.Name, t.Description, t.Status, stages, edges,
            ProductionJson.Deserialize<List<CanvasAnnotationDto>>(t.Annotations, []),
            activeRunCount, t.CreatedBy, t.CreatedAt);
    }
}
