using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;

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

public sealed class UnsService(OtOpcUaConfigDbContext db)
{
    public Task<List<UnsArea>> ListAreasAsync(long generationId, CancellationToken ct) =>
        db.UnsAreas.AsNoTracking()
            .Where(a => a.GenerationId == generationId)
            .OrderBy(a => a.Name)
            .ToListAsync(ct);

    public Task<List<UnsLine>> ListLinesAsync(long generationId, CancellationToken ct) =>
        db.UnsLines.AsNoTracking()
            .Where(l => l.GenerationId == generationId)
            .OrderBy(l => l.Name)
            .ToListAsync(ct);

    public async Task<UnsArea> AddAreaAsync(long draftId, string clusterId, string name, string? notes, CancellationToken ct)
    {
        var area = new UnsArea
        {
            GenerationId = draftId,
            UnsAreaId = $"area-{Guid.NewGuid():N}"[..20],
            ClusterId = clusterId,
            Name = name,
            Notes = notes,
        };
        db.UnsAreas.Add(area);
        await db.SaveChangesAsync(ct);
        return area;
    }

    public async Task<UnsLine> AddLineAsync(long draftId, string unsAreaId, string name, string? notes, CancellationToken ct)
    {
        var line = new UnsLine
        {
            GenerationId = draftId,
            UnsLineId = $"line-{Guid.NewGuid():N}"[..20],
            UnsAreaId = unsAreaId,
            Name = name,
            Notes = notes,
        };
        db.UnsLines.Add(line);
        await db.SaveChangesAsync(ct);
        return line;
    }

    /// <summary>
    ///     Build the full UNS tree snapshot for the analyzer. Walks areas + lines in the draft
    ///     and counts equipment + tags per line. Returns the snapshot plus a deterministic
    ///     revision token computed by SHA-256'ing the sorted (kind, id, parent, name) tuples —
    ///     stable across processes + changes whenever any row is added / modified / deleted.
    /// </summary>
    public async Task<UnsTreeSnapshot> LoadSnapshotAsync(long generationId, CancellationToken ct)
    {
        var areas = await db.UnsAreas.AsNoTracking()
            .Where(a => a.GenerationId == generationId)
            .OrderBy(a => a.UnsAreaId)
            .ToListAsync(ct);

        var lines = await db.UnsLines.AsNoTracking()
            .Where(l => l.GenerationId == generationId)
            .OrderBy(l => l.UnsLineId)
            .ToListAsync(ct);

        var equipmentCounts = await db.Equipment.AsNoTracking()
            .Where(e => e.GenerationId == generationId)
            .GroupBy(e => e.UnsLineId)
            .Select(g => new { LineId = g.Key, Count = g.Count() })
            .ToListAsync(ct);
        var equipmentByLine = equipmentCounts.ToDictionary(x => x.LineId, x => x.Count, StringComparer.OrdinalIgnoreCase);

        var lineSummaries = lines.Select(l =>
            new UnsLineSummary(
                LineId: l.UnsLineId,
                Name: l.Name,
                EquipmentCount: equipmentByLine.GetValueOrDefault(l.UnsLineId),
                TagCount: 0)).ToList();

        var areaSummaries = areas.Select(a =>
            new UnsAreaSummary(
                AreaId: a.UnsAreaId,
                Name: a.Name,
                LineIds: lines.Where(l => string.Equals(l.UnsAreaId, a.UnsAreaId, StringComparison.OrdinalIgnoreCase))
                    .Select(l => l.UnsLineId).ToList())).ToList();

        return new UnsTreeSnapshot
        {
            DraftGenerationId = generationId,
            RevisionToken = ComputeRevisionToken(areas, lines),
            Areas = areaSummaries,
            Lines = lineSummaries,
        };
    }

    /// <summary>
    ///     Atomic re-parent of a line to a new area inside the same draft. The caller must pass
    ///     the revision token it observed at preview time — a mismatch raises
    ///     <see cref="DraftRevisionConflictException"/> so the UI can show the 409 concurrent-edit
    ///     modal instead of silently overwriting a peer's work.
    /// </summary>
    public async Task MoveLineAsync(
        long generationId,
        DraftRevisionToken expected,
        string lineId,
        string targetAreaId,
        CancellationToken ct)
    {
        ArgumentNullException.ThrowIfNull(expected);
        ArgumentException.ThrowIfNullOrWhiteSpace(lineId);
        ArgumentException.ThrowIfNullOrWhiteSpace(targetAreaId);

        var supportsTx = db.Database.IsRelational();
        Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction? tx = null;
        if (supportsTx) tx = await db.Database.BeginTransactionAsync(ct).ConfigureAwait(false);

        try
        {
            var areas = await db.UnsAreas
                .Where(a => a.GenerationId == generationId)
                .OrderBy(a => a.UnsAreaId)
                .ToListAsync(ct);

            var lines = await db.UnsLines
                .Where(l => l.GenerationId == generationId)
                .OrderBy(l => l.UnsLineId)
                .ToListAsync(ct);

            var current = ComputeRevisionToken(areas, lines);
            if (!current.Matches(expected))
                throw new DraftRevisionConflictException(
                    $"Draft {generationId} changed since preview. Expected revision {expected.Value}, saw {current.Value}. " +
                    "Refresh + redo the move.");

            var line = lines.FirstOrDefault(l => string.Equals(l.UnsLineId, lineId, StringComparison.OrdinalIgnoreCase))
                       ?? throw new InvalidOperationException($"Line '{lineId}' not found in draft {generationId}.");

            if (!areas.Any(a => string.Equals(a.UnsAreaId, targetAreaId, StringComparison.OrdinalIgnoreCase)))
                throw new InvalidOperationException($"Target area '{targetAreaId}' not found in draft {generationId}.");

            if (string.Equals(line.UnsAreaId, targetAreaId, StringComparison.OrdinalIgnoreCase))
                return; // no-op drop — same area

            line.UnsAreaId = targetAreaId;
            await db.SaveChangesAsync(ct);
            if (tx is not null) await tx.CommitAsync(ct).ConfigureAwait(false);
        }
        catch
        {
            if (tx is not null) await tx.RollbackAsync(ct).ConfigureAwait(false);
            throw;
        }
        finally
        {
            if (tx is not null) await tx.DisposeAsync().ConfigureAwait(false);
        }
    }

    private static DraftRevisionToken ComputeRevisionToken(IReadOnlyList<UnsArea> areas, IReadOnlyList<UnsLine> lines)
    {
        var sb = new StringBuilder(capacity: 256 + (areas.Count + lines.Count) * 80);
        foreach (var a in areas.OrderBy(a => a.UnsAreaId, StringComparer.Ordinal))
            sb.Append("A:").Append(a.UnsAreaId).Append('|').Append(a.Name).Append('|').Append(a.Notes ?? "").Append(';');
        foreach (var l in lines.OrderBy(l => l.UnsLineId, StringComparer.Ordinal))
            sb.Append("L:").Append(l.UnsLineId).Append('|').Append(l.UnsAreaId).Append('|').Append(l.Name).Append('|').Append(l.Notes ?? "").Append(';');

        var hash = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString()));
        return new DraftRevisionToken(Convert.ToHexStringLower(hash)[..16]);
    }
}

/// <summary>Thrown when a UNS move's expected revision token no longer matches the live draft
/// — another operator mutated the draft between preview + commit. Caller surfaces a 409-style
/// "refresh required" modal in the Admin UI.</summary>
public sealed class DraftRevisionConflictException(string message) : Exception(message);
