namespace ZB.MOM.WW.OtOpcUa.Admin.Services;

/// <summary>
///     Pure-function impact preview for UNS structural moves per Phase 6.4 Stream A.2. Given
///     a <see cref="UnsMoveOperation"/> plus a snapshot of the draft's UNS tree and its
///     equipment + tag counts, returns an <see cref="UnsImpactPreview"/> the Admin UI shows
///     in a confirmation modal before committing the move.
/// </summary>
/// <remarks>
///     <para>Stateless + deterministic — testable without EF or a live draft. The caller
///     (Razor page) loads the draft's snapshot via the normal Configuration services, passes
///     it in, and the analyzer counts + categorises the impact. The returned
///     <see cref="UnsImpactPreview.RevisionToken"/> is the token the caller must re-check at
///     confirm time; a mismatch means another operator mutated the draft between preview +
///     confirm and the operation needs to be refreshed (decision on concurrent-edit safety
///     in Phase 6.4 Scope).</para>
///
///     <para>Cross-cluster moves are rejected here (decision #82) — equipment is
///     cluster-scoped; the UI disables the drop target and surfaces an Export/Import workflow
///     toast instead.</para>
/// </remarks>
public static class UnsImpactAnalyzer
{
    /// <summary>Run the analyzer. Returns a populated preview or throws for invalid operations.</summary>
    public static UnsImpactPreview Analyze(UnsTreeSnapshot snapshot, UnsMoveOperation move)
    {
        ArgumentNullException.ThrowIfNull(snapshot);
        ArgumentNullException.ThrowIfNull(move);

        // Cross-cluster guard — the analyzer refuses rather than silently re-homing.
        if (!string.Equals(move.SourceClusterId, move.TargetClusterId, StringComparison.OrdinalIgnoreCase))
            throw new CrossClusterMoveRejectedException(
                "Equipment is cluster-scoped (decision #82). Use Export → Import to migrate equipment " +
                "across clusters; drag/drop rejected.");

        return move.Kind switch
        {
            UnsMoveKind.LineMove => AnalyzeLineMove(snapshot, move),
            UnsMoveKind.AreaRename => AnalyzeAreaRename(snapshot, move),
            UnsMoveKind.LineMerge => AnalyzeLineMerge(snapshot, move),
            _ => throw new ArgumentOutOfRangeException(nameof(move), move.Kind, $"Unsupported move kind {move.Kind}"),
        };
    }

    private static UnsImpactPreview AnalyzeLineMove(UnsTreeSnapshot snapshot, UnsMoveOperation move)
    {
        var line = snapshot.FindLine(move.SourceLineId!)
                   ?? throw new UnsMoveValidationException($"Source line '{move.SourceLineId}' not found in draft {snapshot.DraftGenerationId}.");

        var targetArea = snapshot.FindArea(move.TargetAreaId!)
                         ?? throw new UnsMoveValidationException($"Target area '{move.TargetAreaId}' not found in draft {snapshot.DraftGenerationId}.");

        var warnings = new List<string>();
        if (targetArea.LineIds.Contains(line.LineId, StringComparer.OrdinalIgnoreCase))
            warnings.Add($"Target area '{targetArea.Name}' already contains line '{line.Name}' — dropping a no-op move.");

        // If the target area has a line with the same display name as the mover, warn about
        // visual ambiguity even though the IDs differ (operators frequently reuse line names).
        if (targetArea.LineIds.Any(lid =>
                snapshot.FindLine(lid) is { } sibling &&
                string.Equals(sibling.Name, line.Name, StringComparison.OrdinalIgnoreCase) &&
                !string.Equals(sibling.LineId, line.LineId, StringComparison.OrdinalIgnoreCase)))
        {
            warnings.Add($"Target area '{targetArea.Name}' already has a line named '{line.Name}'. Consider renaming before the move.");
        }

        return new UnsImpactPreview
        {
            AffectedEquipmentCount = line.EquipmentCount,
            AffectedTagCount = line.TagCount,
            CascadeWarnings = warnings,
            RevisionToken = snapshot.RevisionToken,
            HumanReadableSummary =
                $"Moving line '{line.Name}' from area '{snapshot.FindAreaByLineId(line.LineId)?.Name ?? "?"}' " +
                $"to '{targetArea.Name}' will re-home {line.EquipmentCount} equipment + re-parent {line.TagCount} tags.",
        };
    }

    private static UnsImpactPreview AnalyzeAreaRename(UnsTreeSnapshot snapshot, UnsMoveOperation move)
    {
        var area = snapshot.FindArea(move.SourceAreaId!)
                   ?? throw new UnsMoveValidationException($"Source area '{move.SourceAreaId}' not found in draft {snapshot.DraftGenerationId}.");

        var affectedEquipment = area.LineIds
            .Select(lid => snapshot.FindLine(lid)?.EquipmentCount ?? 0)
            .Sum();
        var affectedTags = area.LineIds
            .Select(lid => snapshot.FindLine(lid)?.TagCount ?? 0)
            .Sum();

        return new UnsImpactPreview
        {
            AffectedEquipmentCount = affectedEquipment,
            AffectedTagCount = affectedTags,
            CascadeWarnings = [],
            RevisionToken = snapshot.RevisionToken,
            HumanReadableSummary =
                $"Renaming area '{area.Name}' → '{move.NewName}' cascades to {area.LineIds.Count} lines / " +
                $"{affectedEquipment} equipment / {affectedTags} tags.",
        };
    }

    private static UnsImpactPreview AnalyzeLineMerge(UnsTreeSnapshot snapshot, UnsMoveOperation move)
    {
        var src = snapshot.FindLine(move.SourceLineId!)
                  ?? throw new UnsMoveValidationException($"Source line '{move.SourceLineId}' not found.");
        var dst = snapshot.FindLine(move.TargetLineId!)
                  ?? throw new UnsMoveValidationException($"Target line '{move.TargetLineId}' not found.");

        var warnings = new List<string>();
        if (!string.Equals(snapshot.FindAreaByLineId(src.LineId)?.AreaId,
                           snapshot.FindAreaByLineId(dst.LineId)?.AreaId,
                           StringComparison.OrdinalIgnoreCase))
        {
            warnings.Add($"Lines '{src.Name}' and '{dst.Name}' are in different areas. The merge will re-parent equipment + tags into '{dst.Name}'s area.");
        }

        return new UnsImpactPreview
        {
            AffectedEquipmentCount = src.EquipmentCount,
            AffectedTagCount = src.TagCount,
            CascadeWarnings = warnings,
            RevisionToken = snapshot.RevisionToken,
            HumanReadableSummary =
                $"Merging line '{src.Name}' into '{dst.Name}': {src.EquipmentCount} equipment + {src.TagCount} tags re-parent. " +
                $"The source line is deleted at commit.",
        };
    }
}

/// <summary>Kind of UNS structural move the analyzer understands.</summary>
public enum UnsMoveKind
{
    /// <summary>Drag a whole line from one area to another.</summary>
    LineMove,

    /// <summary>Rename an area (cascades to the UNS paths of every equipment + tag below it).</summary>
    AreaRename,

    /// <summary>Merge two lines into one; source line's equipment + tags are re-parented.</summary>
    LineMerge,
}

/// <summary>One UNS structural move request.</summary>
/// <param name="Kind">Move variant — selects which source + target fields are required.</param>
/// <param name="SourceClusterId">Cluster of the source node. Must match <see cref="TargetClusterId"/> (decision #82).</param>
/// <param name="TargetClusterId">Cluster of the target node.</param>
/// <param name="SourceAreaId">Source area id for <see cref="UnsMoveKind.AreaRename"/>.</param>
/// <param name="SourceLineId">Source line id for <see cref="UnsMoveKind.LineMove"/> / <see cref="UnsMoveKind.LineMerge"/>.</param>
/// <param name="TargetAreaId">Target area id for <see cref="UnsMoveKind.LineMove"/>.</param>
/// <param name="TargetLineId">Target line id for <see cref="UnsMoveKind.LineMerge"/>.</param>
/// <param name="NewName">New display name for <see cref="UnsMoveKind.AreaRename"/>.</param>
public sealed record UnsMoveOperation(
    UnsMoveKind Kind,
    string SourceClusterId,
    string TargetClusterId,
    string? SourceAreaId = null,
    string? SourceLineId = null,
    string? TargetAreaId = null,
    string? TargetLineId = null,
    string? NewName = null);

/// <summary>Snapshot of the UNS tree + counts the analyzer walks.</summary>
public sealed class UnsTreeSnapshot
{
    public required long DraftGenerationId { get; init; }
    public required DraftRevisionToken RevisionToken { get; init; }
    public required IReadOnlyList<UnsAreaSummary> Areas { get; init; }
    public required IReadOnlyList<UnsLineSummary> Lines { get; init; }

    public UnsAreaSummary? FindArea(string areaId) =>
        Areas.FirstOrDefault(a => string.Equals(a.AreaId, areaId, StringComparison.OrdinalIgnoreCase));

    public UnsLineSummary? FindLine(string lineId) =>
        Lines.FirstOrDefault(l => string.Equals(l.LineId, lineId, StringComparison.OrdinalIgnoreCase));

    public UnsAreaSummary? FindAreaByLineId(string lineId) =>
        Areas.FirstOrDefault(a => a.LineIds.Contains(lineId, StringComparer.OrdinalIgnoreCase));
}

public sealed record UnsAreaSummary(string AreaId, string Name, IReadOnlyList<string> LineIds);

public sealed record UnsLineSummary(string LineId, string Name, int EquipmentCount, int TagCount);

/// <summary>
///     Opaque per-draft revision fingerprint. Preview fetches the current token + stores it
///     in the <see cref="UnsImpactPreview.RevisionToken"/>. Confirm compares the token against
///     the draft's live value; mismatch means another operator mutated the draft between
///     preview + commit — raise <c>409 Conflict / refresh-required</c> in the UI.
/// </summary>
public sealed record DraftRevisionToken(string Value)
{
    /// <summary>Compare two tokens for equality; null-safe.</summary>
    public bool Matches(DraftRevisionToken? other) =>
        other is not null &&
        string.Equals(Value, other.Value, StringComparison.Ordinal);
}

/// <summary>Output of <see cref="UnsImpactAnalyzer.Analyze"/>.</summary>
public sealed class UnsImpactPreview
{
    public required int AffectedEquipmentCount { get; init; }
    public required int AffectedTagCount { get; init; }
    public required IReadOnlyList<string> CascadeWarnings { get; init; }
    public required DraftRevisionToken RevisionToken { get; init; }
    public required string HumanReadableSummary { get; init; }
}

/// <summary>Thrown when a move targets a different cluster than the source (decision #82).</summary>
public sealed class CrossClusterMoveRejectedException(string message) : Exception(message);

/// <summary>Thrown when the move operation references a source / target that doesn't exist in the draft.</summary>
public sealed class UnsMoveValidationException(string message) : Exception(message);
