using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Microsoft.Data.SqlClient;

namespace XLAB2
{
    internal sealed class EkzDirectoryService
    {
        private static readonly string[] CascadingEkzChildTables = { "EKZMK", "EKZMCP" };

        private static readonly string[] CascadingEkzMkChildTables = { "EKZMKFCTVL", "EKZMKDH", "EKZMKEKZK", "EKZMKND", "KSPELEKZMK" };

        public int AddEkzItem(EkzDirectoryItem item)
        {
            var normalizedItem = NormalizeEkzItem(item);

            const string sql = @"
INSERT INTO dbo.EKZ
(
    IDTPRZ,
    IDFRPDV,
    KLSIPR,
    NNZV,
    NNIN,
    DSEKZ,
    GUIDEKZ,
    IsDeleted
)
VALUES
(
    @TypeSizeId,
    @OwnerOrganizationId,
    1,
    @SerialNumber,
    @InventoryNumber,
    @Notes,
    @Guid,
    0
);

SELECT CAST(SCOPE_IDENTITY() AS int);";

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                EnsureEkzIsUnique(connection, normalizedItem.TypeSizeId, normalizedItem.OwnerOrganizationId, normalizedItem.SerialNumber, null);

                command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
                command.Parameters.Add("@TypeSizeId", SqlDbType.Int).Value = normalizedItem.TypeSizeId;
                command.Parameters.Add("@OwnerOrganizationId", SqlDbType.Int).Value = normalizedItem.OwnerOrganizationId;
                command.Parameters.Add("@SerialNumber", SqlDbType.VarChar, EkzDirectoryRules.SerialNumberMaxLength).Value = normalizedItem.SerialNumber;
                ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@InventoryNumber", SqlDbType.VarChar, EkzDirectoryRules.InventoryNumberMaxLength, normalizedItem.InventoryNumber);
                ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@Notes", SqlDbType.VarChar, EkzDirectoryRules.NotesMaxLength, normalizedItem.Notes);
                command.Parameters.Add("@Guid", SqlDbType.UniqueIdentifier).Value = Guid.NewGuid();

                return Convert.ToInt32(command.ExecuteScalar());
            }
        }

        public EkzDeletePreview GetEkzDeletePreview(int id)
        {
            if (id <= 0)
            {
                return new EkzDeletePreview
                {
                    CanDelete = false,
                    ImpactItems = Array.Empty<EkzDeleteImpactItem>(),
                    WarningMessage = "Не выбрана запись EKZ для удаления."
                };
            }

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            {
                connection.Open();
                return BuildEkzDeletePreview(connection, null, id);
            }
        }

        public EkzDeleteResult DeleteEkzItem(int id)
        {
            if (id <= 0)
            {
                throw new InvalidOperationException("Не выбрана запись EKZ для удаления.");
            }

            try
            {
                using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
                {
                    connection.Open();

                    using (var transaction = connection.BeginTransaction())
                    {
                        var preview = BuildEkzDeletePreview(connection, transaction, id);
                        if (!preview.CanDelete)
                        {
                            transaction.Rollback();
                            return new EkzDeleteResult
                            {
                                IsDeleted = false,
                                ImpactItems = preview.ImpactItems ?? Array.Empty<EkzDeleteImpactItem>(),
                                WarningMessage = preview.WarningMessage
                            };
                        }

                        var impactItems = new List<EkzDeleteImpactItem>();
                        AddImpactItem(impactItems, "EKZMKFCTVL", DeleteEkzMkFctvl(connection, transaction, id));
                        AddImpactItem(impactItems, "EKZMKDH", DeleteEkzMkDh(connection, transaction, id));
                        AddImpactItem(impactItems, "EKZMKEKZK", DeleteEkzMkEkzk(connection, transaction, id));
                        AddImpactItem(impactItems, "EKZMKND", DeleteEkzMkNd(connection, transaction, id));
                        AddImpactItem(impactItems, "KSPELEKZMK", DeleteKspelEkzMk(connection, transaction, id));
                        AddImpactItem(impactItems, "DMS", DeleteEkzDms(connection, transaction, id));
                        AddImpactItem(impactItems, "EKZMK", DeleteEkzMk(connection, transaction, id));
                        AddImpactItem(impactItems, "EKZMCP", DeleteEkzMcp(connection, transaction, id));

                        var deletedEkzCount = DeleteEkz(connection, transaction, id);
                        if (deletedEkzCount == 0)
                        {
                            transaction.Rollback();
                            return new EkzDeleteResult
                            {
                                IsDeleted = false,
                                ImpactItems = Array.Empty<EkzDeleteImpactItem>(),
                                WarningMessage = "Запись EKZ для удаления не найдена."
                            };
                        }

                        AddImpactItem(impactItems, "EKZ", deletedEkzCount);
                        transaction.Commit();

                        return new EkzDeleteResult
                        {
                            IsDeleted = true,
                            ImpactItems = OrderImpactItems(impactItems)
                        };
                    }
                }
            }
            catch (SqlException ex) when (ex.Number == 547)
            {
                return new EkzDeleteResult
                {
                    IsDeleted = false,
                    ImpactItems = Array.Empty<EkzDeleteImpactItem>(),
                    WarningMessage = CreateEkzDeleteFailedMessage(ex)
                };
            }
        }

        public IReadOnlyList<EkzDirectoryItem> LoadEkzItems()
        {
            const string sql = @"
SELECT
    z.IDEKZ AS Id,
    z.IDTPRZ AS TypeSizeId,
    areas.NMOI AS MeasurementAreaName,
    names.NMTP AS InstrumentName,
    tips.TP AS TypeName,
    tprz.DPZN AS RangeText,
    tprz.HRTC AS AccuracyText,
    tprz.NNGSRS AS RegistryNumber,
    z.IDFRPDV AS OwnerOrganizationId,
    ownerOrg.NMFRPD AS OwnerOrganizationName,
    z.NNZV AS SerialNumber,
    z.NNIN AS InventoryNumber,
    CAST(z.DSEKZ AS nvarchar(max)) AS Notes
FROM dbo.EKZ z
JOIN dbo.TPRZ tprz ON tprz.IDTPRZ = z.IDTPRZ
JOIN dbo.TIPS tips ON tips.IDTIPS = tprz.IDTIPS
JOIN dbo.SPNMTP names ON names.IDSPNMTP = tips.IDSPNMTP
JOIN dbo.SPOI areas ON areas.IDSPOI = tips.IDSPOI
JOIN dbo.FRPD ownerOrg ON ownerOrg.IDFRPD = z.IDFRPDV
WHERE ISNULL(z.IsDeleted, 0) = 0
ORDER BY ownerOrg.NMFRPD, areas.NMOI, names.NMTP, tips.TP, tprz.DPZN, z.NNZV, z.IDEKZ;";

            var items = new List<EkzDirectoryItem>();

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();

                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        items.Add(new EkzDirectoryItem
                        {
                            Id = ReferenceDirectorySqlHelpers.GetInt32(reader, "Id"),
                            TypeSizeId = ReferenceDirectorySqlHelpers.GetInt32(reader, "TypeSizeId"),
                            MeasurementAreaName = ReferenceDirectorySqlHelpers.GetString(reader, "MeasurementAreaName"),
                            InstrumentName = ReferenceDirectorySqlHelpers.GetString(reader, "InstrumentName"),
                            TypeName = ReferenceDirectorySqlHelpers.GetString(reader, "TypeName"),
                            RangeText = ReferenceDirectorySqlHelpers.GetString(reader, "RangeText"),
                            AccuracyText = ReferenceDirectorySqlHelpers.GetString(reader, "AccuracyText"),
                            RegistryNumber = ReferenceDirectorySqlHelpers.GetString(reader, "RegistryNumber"),
                            OwnerOrganizationId = ReferenceDirectorySqlHelpers.GetInt32(reader, "OwnerOrganizationId"),
                            OwnerOrganizationName = ReferenceDirectorySqlHelpers.GetString(reader, "OwnerOrganizationName"),
                            SerialNumber = ReferenceDirectorySqlHelpers.GetString(reader, "SerialNumber"),
                            InventoryNumber = ReferenceDirectorySqlHelpers.GetString(reader, "InventoryNumber"),
                            Notes = ReferenceDirectorySqlHelpers.GetString(reader, "Notes")
                        });
                    }
                }
            }

            return items;
        }

        public IReadOnlyList<EkzMkDirectoryItem> LoadEkzMkItems(int instrumentId)
        {
            const string sql = @"
SELECT
    m.IDEKZMK AS CardId,
    m.IDEKZ AS InstrumentId,
    verificationType.NMVDMK AS VerificationTypeName,
    organization.NMFRPD AS VerificationOrganizationName,
    m.NNZVPV AS DocumentNumber,
    verificationDocument.NNDMS AS VerificationDocumentNumber,
    verificationDocument.DTDMS AS VerificationDocumentDate,
    m.PRMK AS PeriodMonths,
    m.DTPRM AS AcceptedOn,
    m.DTMKPL AS PlannedOn,
    m.DTMKFK AS PerformedOn,
    m.DTVDM AS IssuedOn,
    m.GDN AS IsPassed,
    CAST(m.DSEKZMK AS nvarchar(max)) AS Notes
FROM dbo.EKZMK m
LEFT JOIN dbo.SPVDMK verificationType ON verificationType.IDSPVDMK = m.IDSPVDMK
LEFT JOIN dbo.FRPD organization ON organization.IDFRPD = m.IDFRPD
OUTER APPLY
(
    SELECT TOP (1)
        d.NND AS NNDMS,
        d.DTD AS DTDMS
    FROM dbo.DMS d
    JOIN dbo.VDODVDD vdd ON vdd.IDVDODVDD = d.IDVDODVDD
    JOIN dbo.FRDMS frdms ON frdms.IDFRDMS = d.IDFRDMS
    JOIN dbo.SPVDD spvdd ON spvdd.IDSPVDD = frdms.IDSPVDD
    WHERE d.IDOD = m.IDEKZMK
      AND vdd.IDSPVDOD = 2
      AND spvdd.IDSPVDD IN (2, 6, 8)
    ORDER BY d.DTD DESC
) verificationDocument
WHERE m.IDEKZ = @InstrumentId
ORDER BY ISNULL(m.DTPRM, CONVERT(datetime, '19000101', 112)) DESC, m.IDEKZMK DESC;";

            var items = new List<EkzMkDirectoryItem>();

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
                command.Parameters.Add("@InstrumentId", SqlDbType.Int).Value = instrumentId;

                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        items.Add(new EkzMkDirectoryItem
                        {
                            CardId = ReferenceDirectorySqlHelpers.GetInt32(reader, "CardId"),
                            InstrumentId = ReferenceDirectorySqlHelpers.GetInt32(reader, "InstrumentId"),
                            VerificationTypeName = ReferenceDirectorySqlHelpers.GetString(reader, "VerificationTypeName"),
                            VerificationOrganizationName = ReferenceDirectorySqlHelpers.GetString(reader, "VerificationOrganizationName"),
                            DocumentNumber = ReferenceDirectorySqlHelpers.GetString(reader, "DocumentNumber"),
                            VerificationDocumentNumber = ReferenceDirectorySqlHelpers.GetString(reader, "VerificationDocumentNumber"),
                            VerificationDocumentDate = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "VerificationDocumentDate"),
                            PeriodMonths = ReferenceDirectorySqlHelpers.GetInt32(reader, "PeriodMonths"),
                            AcceptedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "AcceptedOn"),
                            PlannedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "PlannedOn"),
                            PerformedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "PerformedOn"),
                            IssuedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "IssuedOn"),
                            IsPassed = ReferenceDirectorySqlHelpers.GetNullableBoolean(reader, "IsPassed"),
                            Notes = ReferenceDirectorySqlHelpers.GetString(reader, "Notes")
                        });
                    }
                }
            }

            return items;
        }

        public IReadOnlyList<DirectoryLookupItem> LoadFrpdReferences()
        {
            return ReferenceDirectorySqlHelpers.LoadLookupItems(@"
SELECT
    fr.IDFRPD AS Id,
    fr.NMFRPD AS Name
FROM dbo.FRPD fr
WHERE NULLIF(LTRIM(RTRIM(fr.NMFRPD)), '') IS NOT NULL
ORDER BY fr.NMFRPD, fr.IDFRPD;");
        }

        public IReadOnlyList<DirectoryLookupItem> LoadTypeSizeReferences()
        {
            return ReferenceDirectorySqlHelpers.LoadLookupItems(@"
SELECT
    tprz.IDTPRZ AS Id,
    LTRIM(RTRIM(
        COALESCE(NULLIF(areas.NMOI, N'') + N' / ', N'')
        + COALESCE(NULLIF(names.NMTP, N'') + N' / ', N'')
        + COALESCE(NULLIF(tips.TP, N''), N'')
        + CASE WHEN NULLIF(LTRIM(RTRIM(tprz.DPZN)), N'') IS NULL THEN N'' ELSE N' / ' + tprz.DPZN END
        + CASE
            WHEN NULLIF(LTRIM(RTRIM(CONVERT(nvarchar(50), tprz.NNGSRS))), N'') IS NULL THEN N''
            ELSE N' / № ГР ' + CONVERT(nvarchar(50), tprz.NNGSRS)
          END
    )) AS Name
FROM dbo.TPRZ tprz
JOIN dbo.TIPS tips ON tips.IDTIPS = tprz.IDTIPS
JOIN dbo.SPNMTP names ON names.IDSPNMTP = tips.IDSPNMTP
JOIN dbo.SPOI areas ON areas.IDSPOI = tips.IDSPOI
ORDER BY areas.NMOI, names.NMTP, tips.TP, tprz.DPZN, tprz.IDTPRZ;");
        }

        public void UpdateEkzItem(EkzDirectoryItem item)
        {
            var normalizedItem = NormalizeEkzItem(item);
            if (normalizedItem.Id <= 0)
            {
                throw new InvalidOperationException("Не выбрана запись EKZ для изменения.");
            }

            const string sql = @"
UPDATE dbo.EKZ
SET IDTPRZ = @TypeSizeId,
    IDFRPDV = @OwnerOrganizationId,
    NNZV = @SerialNumber,
    NNIN = @InventoryNumber,
    DSEKZ = @Notes
WHERE IDEKZ = @Id;

SELECT @@ROWCOUNT;";

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                EnsureEkzIsUnique(connection, normalizedItem.TypeSizeId, normalizedItem.OwnerOrganizationId, normalizedItem.SerialNumber, normalizedItem.Id);

                command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
                command.Parameters.Add("@Id", SqlDbType.Int).Value = normalizedItem.Id;
                command.Parameters.Add("@TypeSizeId", SqlDbType.Int).Value = normalizedItem.TypeSizeId;
                command.Parameters.Add("@OwnerOrganizationId", SqlDbType.Int).Value = normalizedItem.OwnerOrganizationId;
                command.Parameters.Add("@SerialNumber", SqlDbType.VarChar, EkzDirectoryRules.SerialNumberMaxLength).Value = normalizedItem.SerialNumber;
                ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@InventoryNumber", SqlDbType.VarChar, EkzDirectoryRules.InventoryNumberMaxLength, normalizedItem.InventoryNumber);
                ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@Notes", SqlDbType.VarChar, EkzDirectoryRules.NotesMaxLength, normalizedItem.Notes);

                if (Convert.ToInt32(command.ExecuteScalar()) == 0)
                {
                    throw new InvalidOperationException("Запись EKZ для изменения не найдена.");
                }
            }
        }

        private static EkzDeletePreview BuildEkzDeletePreview(SqlConnection connection, SqlTransaction transaction, int id)
        {
            if (!EkzExists(connection, transaction, id))
            {
                return new EkzDeletePreview
                {
                    CanDelete = false,
                    ImpactItems = Array.Empty<EkzDeleteImpactItem>(),
                    WarningMessage = "Запись EKZ для удаления не найдена."
                };
            }

            var cardIds = LoadEkzMkCardIds(connection, transaction, id);
            var blockers = new List<DeleteBlockerInfo>();
            blockers.AddRange(ReferenceDirectorySqlHelpers.LoadDeleteBlockersFromForeignKeys(connection, transaction, "EKZ", id, CascadingEkzChildTables));
            blockers.AddRange(LoadUnhandledDeleteBlockers(connection, transaction, "EKZMK", cardIds, CascadingEkzMkChildTables));

            var impactItems = BuildImpactItems(connection, transaction, id, cardIds);
            var mergedBlockers = MergeBlockers(blockers);
            if (mergedBlockers.Count > 0)
            {
                return new EkzDeletePreview
                {
                    CanDelete = false,
                    ImpactItems = impactItems,
                    WarningMessage = CreateEkzCascadeBlockedMessage(mergedBlockers)
                };
            }

            return new EkzDeletePreview
            {
                CanDelete = true,
                ImpactItems = impactItems,
                ConfirmationMessage = CreateEkzDeleteConfirmationMessage(impactItems)
            };
        }

        private static IReadOnlyList<EkzDeleteImpactItem> BuildImpactItems(SqlConnection connection, SqlTransaction transaction, int instrumentId, IReadOnlyCollection<int> cardIds)
        {
            var impactItems = new List<EkzDeleteImpactItem>();
            AddImpactItem(impactItems, "EKZ", 1);
            AddImpactItem(impactItems, "EKZMK", cardIds == null ? 0 : cardIds.Count);
            AddImpactItem(impactItems, "DMS", CountEkzDms(connection, transaction, instrumentId));
            AddImpactItem(impactItems, "EKZMKFCTVL", CountEkzMkFctvl(connection, transaction, instrumentId));
            AddImpactItem(impactItems, "EKZMKDH", CountEkzMkDh(connection, transaction, instrumentId));
            AddImpactItem(impactItems, "EKZMKEKZK", CountEkzMkEkzk(connection, transaction, instrumentId));
            AddImpactItem(impactItems, "EKZMKND", CountEkzMkNd(connection, transaction, instrumentId));
            AddImpactItem(impactItems, "KSPELEKZMK", CountKspelEkzMk(connection, transaction, instrumentId));
            AddImpactItem(impactItems, "EKZMCP", CountEkzMcp(connection, transaction, instrumentId));
            return OrderImpactItems(impactItems);
        }

        private static IReadOnlyList<EkzDeleteImpactItem> OrderImpactItems(IEnumerable<EkzDeleteImpactItem> impactItems)
        {
            var order = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
            {
                { "EKZ", 1 },
                { "EKZMK", 2 },
                { "DMS", 3 },
                { "EKZMKFCTVL", 4 },
                { "EKZMKDH", 5 },
                { "EKZMKEKZK", 6 },
                { "EKZMKND", 7 },
                { "KSPELEKZMK", 8 },
                { "EKZMCP", 9 }
            };

            return (impactItems ?? Enumerable.Empty<EkzDeleteImpactItem>())
                .Where(delegate(EkzDeleteImpactItem item) { return item != null && item.RowCount > 0; })
                .OrderBy(delegate(EkzDeleteImpactItem item)
                {
                    int value;
                    return order.TryGetValue(item.TableName ?? string.Empty, out value) ? value : int.MaxValue;
                })
                .ThenBy(delegate(EkzDeleteImpactItem item) { return item.TableName; }, StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        private static void AddImpactItem(ICollection<EkzDeleteImpactItem> impactItems, string tableName, int rowCount)
        {
            if (impactItems == null || string.IsNullOrWhiteSpace(tableName) || rowCount <= 0)
            {
                return;
            }

            impactItems.Add(new EkzDeleteImpactItem
            {
                TableName = tableName,
                RowCount = rowCount
            });
        }

        private static string CreateEkzDeleteConfirmationMessage(IEnumerable<EkzDeleteImpactItem> impactItems)
        {
            var items = OrderImpactItems(impactItems).ToList();
            var lines = new List<string>();
            if (items.Count == 0)
            {
                lines.Add("Будет физически удалена только запись EKZ.");
            }
            else
            {
                lines.Add("Будут физически удалены записи:");
                foreach (var item in items)
                {
                    lines.Add(string.Format("{0}: {1}", item.TableName, item.RowCount));
                }
            }

            lines.Add(string.Empty);
            lines.Add("Продолжить?");
            return string.Join(Environment.NewLine, lines.ToArray());
        }

        private static string CreateEkzCascadeBlockedMessage(IEnumerable<DeleteBlockerInfo> blockers)
        {
            return string.Format(
                "Экземпляр не может быть удалён автоматически. Есть связанные записи в таблицах, которые не входят в каскад удаления: {0}.",
                FormatBlockerDetails(blockers));
        }

        private static string CreateEkzDeleteFailedMessage(SqlException ex)
        {
            var suffix = ex == null || string.IsNullOrWhiteSpace(ex.Message)
                ? string.Empty
                : " " + ex.Message.Trim();

            return "Экземпляр не может быть удалён из-за ограничений ссылочной целостности БД." + suffix;
        }

        private static string FormatBlockerDetails(IEnumerable<DeleteBlockerInfo> blockers)
        {
            var details = string.Join(", ", (blockers ?? Enumerable.Empty<DeleteBlockerInfo>())
                .Where(delegate(DeleteBlockerInfo blocker) { return blocker != null && blocker.RowCount > 0; })
                .OrderBy(delegate(DeleteBlockerInfo blocker) { return blocker.TableName; }, StringComparer.OrdinalIgnoreCase)
                .Select(delegate(DeleteBlockerInfo blocker) { return string.Format("{0}: {1}", blocker.TableName, blocker.RowCount); }));

            return string.IsNullOrWhiteSpace(details) ? "связанные данные" : details;
        }

        private static List<DeleteBlockerInfo> MergeBlockers(IEnumerable<DeleteBlockerInfo> blockers)
        {
            return (blockers ?? Enumerable.Empty<DeleteBlockerInfo>())
                .Where(delegate(DeleteBlockerInfo blocker) { return blocker != null && blocker.RowCount > 0 && !string.IsNullOrWhiteSpace(blocker.TableName); })
                .GroupBy(delegate(DeleteBlockerInfo blocker) { return blocker.TableName; }, StringComparer.OrdinalIgnoreCase)
                .Select(delegate(IGrouping<string, DeleteBlockerInfo> group)
                {
                    return new DeleteBlockerInfo
                    {
                        TableName = group.Key,
                        RowCount = group.Sum(delegate(DeleteBlockerInfo blocker) { return blocker.RowCount; })
                    };
                })
                .OrderBy(delegate(DeleteBlockerInfo blocker) { return blocker.TableName; }, StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        private static List<DeleteBlockerInfo> LoadUnhandledDeleteBlockers(SqlConnection connection, SqlTransaction transaction, string parentTableName, IEnumerable<int> ids, IEnumerable<string> excludedChildTables)
        {
            var blockers = new List<DeleteBlockerInfo>();
            foreach (var id in (ids ?? Enumerable.Empty<int>()).Where(delegate(int value) { return value > 0; }).Distinct())
            {
                blockers.AddRange(ReferenceDirectorySqlHelpers.LoadDeleteBlockersFromForeignKeys(connection, transaction, parentTableName, id, excludedChildTables));
            }

            return MergeBlockers(blockers);
        }

        private static bool EkzExists(SqlConnection connection, SqlTransaction transaction, int id)
        {
            const string sql = @"
SELECT COUNT(1)
FROM dbo.EKZ
WHERE IDEKZ = @Id;";

            return ExecuteScalarForId(connection, transaction, sql, id) > 0;
        }

        private static List<int> LoadEkzMkCardIds(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            const string sql = @"
SELECT IDEKZMK
FROM dbo.EKZMK
WHERE IDEKZ = @InstrumentId
ORDER BY IDEKZMK;";

            return ExecuteIdList(connection, transaction, sql, "@InstrumentId", instrumentId);
        }

        private static int CountEkzMcp(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
SELECT COUNT(*)
FROM dbo.EKZMCP
WHERE IDEKZ = @InstrumentId;", instrumentId);
        }

        private static int CountEkzMkFctvl(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
SELECT COUNT(*)
FROM dbo.EKZMKFCTVL child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;", instrumentId);
        }

        private static int CountEkzMkDh(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
SELECT COUNT(*)
FROM dbo.EKZMKDH child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;", instrumentId);
        }

        private static int CountEkzMkEkzk(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
SELECT COUNT(*)
FROM dbo.EKZMKEKZK child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;", instrumentId);
        }

        private static int CountEkzMkNd(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
SELECT COUNT(*)
FROM dbo.EKZMKND child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;", instrumentId);
        }

        private static int CountKspelEkzMk(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
SELECT COUNT(*)
FROM dbo.KSPELEKZMK child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;", instrumentId);
        }

        private static int CountEkzDms(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
SELECT COUNT(*)
FROM dbo.DMS d
JOIN dbo.VDODVDD vdd ON vdd.IDVDODVDD = d.IDVDODVDD
JOIN dbo.EKZMK m ON m.IDEKZMK = d.IDOD
WHERE m.IDEKZ = @InstrumentId
  AND vdd.IDSPVDOD = 2;", instrumentId);
        }

        private static int DeleteEkzMcp(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
DELETE FROM dbo.EKZMCP
WHERE IDEKZ = @InstrumentId;

SELECT @@ROWCOUNT;", instrumentId);
        }

        private static int DeleteEkzMkFctvl(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
DELETE child
FROM dbo.EKZMKFCTVL child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;

SELECT @@ROWCOUNT;", instrumentId);
        }

        private static int DeleteEkzMkDh(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
DELETE child
FROM dbo.EKZMKDH child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;

SELECT @@ROWCOUNT;", instrumentId);
        }

        private static int DeleteEkzMkEkzk(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
DELETE child
FROM dbo.EKZMKEKZK child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;

SELECT @@ROWCOUNT;", instrumentId);
        }

        private static int DeleteEkzMkNd(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
DELETE child
FROM dbo.EKZMKND child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;

SELECT @@ROWCOUNT;", instrumentId);
        }

        private static int DeleteKspelEkzMk(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
DELETE child
FROM dbo.KSPELEKZMK child
JOIN dbo.EKZMK parent ON parent.IDEKZMK = child.IDEKZMK
WHERE parent.IDEKZ = @InstrumentId;

SELECT @@ROWCOUNT;", instrumentId);
        }

        private static int DeleteEkzDms(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
DELETE d
FROM dbo.DMS d
JOIN dbo.VDODVDD vdd ON vdd.IDVDODVDD = d.IDVDODVDD
JOIN dbo.EKZMK m ON m.IDEKZMK = d.IDOD
WHERE m.IDEKZ = @InstrumentId
  AND vdd.IDSPVDOD = 2;

SELECT @@ROWCOUNT;", instrumentId);
        }

        private static int DeleteEkzMk(SqlConnection connection, SqlTransaction transaction, int instrumentId)
        {
            return ExecuteScalarForInstrument(connection, transaction, @"
DELETE FROM dbo.EKZMK
WHERE IDEKZ = @InstrumentId;

SELECT @@ROWCOUNT;", instrumentId);
        }

        private static int DeleteEkz(SqlConnection connection, SqlTransaction transaction, int id)
        {
            return ExecuteScalarForId(connection, transaction, @"
DELETE FROM dbo.EKZ
WHERE IDEKZ = @Id;

SELECT @@ROWCOUNT;", id);
        }

        private static int ExecuteScalarForInstrument(SqlConnection connection, SqlTransaction transaction, string sql, int instrumentId)
        {
            using (var command = new SqlCommand(sql, connection, transaction))
            {
                command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
                command.Parameters.Add("@InstrumentId", SqlDbType.Int).Value = instrumentId;
                return Convert.ToInt32(command.ExecuteScalar());
            }
        }

        private static int ExecuteScalarForId(SqlConnection connection, SqlTransaction transaction, string sql, int id)
        {
            using (var command = new SqlCommand(sql, connection, transaction))
            {
                command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
                command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
                return Convert.ToInt32(command.ExecuteScalar());
            }
        }

        private static List<int> ExecuteIdList(SqlConnection connection, SqlTransaction transaction, string sql, string parameterName, int parameterValue)
        {
            var result = new List<int>();
            using (var command = new SqlCommand(sql, connection, transaction))
            {
                command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
                command.Parameters.Add(parameterName, SqlDbType.Int).Value = parameterValue;

                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        result.Add(reader.GetInt32(0));
                    }
                }
            }

            return result;
        }

        private static string NormalizeOptional(string value)
        {
            return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
        }

        private static EkzDirectoryItem NormalizeEkzItem(EkzDirectoryItem item)
        {
            if (item == null)
            {
                throw new InvalidOperationException("Не переданы данные записи EKZ.");
            }

            var normalizedItem = new EkzDirectoryItem
            {
                Id = item.Id,
                TypeSizeId = item.TypeSizeId,
                OwnerOrganizationId = item.OwnerOrganizationId,
                SerialNumber = NormalizeOptional(item.SerialNumber),
                InventoryNumber = NormalizeOptional(item.InventoryNumber),
                Notes = NormalizeOptional(item.Notes)
            };

            if (normalizedItem.TypeSizeId <= 0)
            {
                throw new InvalidOperationException("Не указан типоразмер СИ.");
            }

            if (normalizedItem.OwnerOrganizationId <= 0)
            {
                throw new InvalidOperationException("Не указана организация-владелец.");
            }

            if (string.IsNullOrWhiteSpace(normalizedItem.SerialNumber))
            {
                throw new InvalidOperationException("Не указан заводской номер.");
            }

            if (normalizedItem.SerialNumber.Length > EkzDirectoryRules.SerialNumberMaxLength)
            {
                throw new InvalidOperationException(string.Format("Заводской номер не должен превышать {0} символов.", EkzDirectoryRules.SerialNumberMaxLength));
            }

            if (!string.IsNullOrWhiteSpace(normalizedItem.InventoryNumber) && normalizedItem.InventoryNumber.Length > EkzDirectoryRules.InventoryNumberMaxLength)
            {
                throw new InvalidOperationException(string.Format("Инвентарный номер не должен превышать {0} символов.", EkzDirectoryRules.InventoryNumberMaxLength));
            }

            if (!string.IsNullOrWhiteSpace(normalizedItem.Notes) && normalizedItem.Notes.Length > EkzDirectoryRules.NotesMaxLength)
            {
                throw new InvalidOperationException(string.Format("Примечание не должно превышать {0} символов.", EkzDirectoryRules.NotesMaxLength));
            }

            return normalizedItem;
        }

        private static void EnsureEkzIsUnique(SqlConnection connection, int typeSizeId, int ownerOrganizationId, string serialNumber, int? excludeId)
        {
            const string sql = @"
SELECT TOP (1) z.IDEKZ
FROM dbo.EKZ z
WHERE z.IDTPRZ = @TypeSizeId
  AND z.IDFRPDV = @OwnerOrganizationId
  AND z.NNZV = @SerialNumber
  AND ISNULL(z.IsDeleted, 0) = 0
  AND (@ExcludeId IS NULL OR z.IDEKZ <> @ExcludeId)
ORDER BY z.IDEKZ DESC;";

            using (var command = new SqlCommand(sql, connection))
            {
                command.CommandTimeout = ReferenceDirectorySqlHelpers.GetCommandTimeoutSeconds();
                command.Parameters.Add("@TypeSizeId", SqlDbType.Int).Value = typeSizeId;
                command.Parameters.Add("@OwnerOrganizationId", SqlDbType.Int).Value = ownerOrganizationId;
                command.Parameters.Add("@SerialNumber", SqlDbType.VarChar, EkzDirectoryRules.SerialNumberMaxLength).Value = serialNumber;
                ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ExcludeId", excludeId);

                if (command.ExecuteScalar() != null)
                {
                    throw new InvalidOperationException("Экземпляр с таким типоразмером, владельцем и заводским номером уже существует.");
                }
            }
        }
    }
}
