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

namespace XLAB
{
    internal sealed class FrpdDirectoryService
    {
        public int AddFrpdItem(FrpdDirectoryItem item)
        {
            var normalizedItem = NormalizeFrpdItem(item);
            var guidForInsert = string.IsNullOrWhiteSpace(normalizedItem.Guid)
                ? Guid.NewGuid().ToString().ToUpperInvariant()
                : normalizedItem.Guid;

            const string sql = @"
INSERT INTO dbo.FRPD
(
    IDFRPDR,
    NMFRPD,
    KDFRPDLC,
    FRPDGUID,
    DTSZFRPD,
    DTLKFRPD
)
VALUES
(
    @ParentId,
    @Name,
    @LocalCode,
    @Guid,
    @CreatedOn,
    @LiquidatedOn
);

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

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                EnsureFrpdGuidIsUnique(connection, guidForInsert, null);

                command.CommandTimeout = 60;
                ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ParentId", normalizedItem.ParentId);
                command.Parameters.Add("@Name", SqlDbType.VarChar, FrpdDirectoryRules.NameMaxLength).Value = normalizedItem.Name;
                ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@LocalCode", SqlDbType.VarChar, FrpdDirectoryRules.LocalCodeMaxLength, normalizedItem.LocalCode);
                ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength, guidForInsert);
                ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@CreatedOn", normalizedItem.CreatedOn);
                ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@LiquidatedOn", normalizedItem.LiquidatedOn);

                try
                {
                    return Convert.ToInt32(command.ExecuteScalar());
                }
                catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "IX_FRPD_FRPDGUID"))
                {
                    throw CreateFrpdDuplicateGuidException(guidForInsert);
                }
            }
        }

        public int AddFrpdvdItem(FrpdvdDirectoryItem item)
        {
            var normalizedItem = NormalizeFrpdvdItem(item);

            const string sql = @"
INSERT INTO dbo.FRPDVD
(
    IDFRPD,
    IDSPVDDO
)
VALUES
(
    @FrpdId,
    @ActivityId
);

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

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                EnsureFrpdvdIsUnique(connection, normalizedItem.FrpdId, normalizedItem.ActivityId, null);

                command.CommandTimeout = 60;
                command.Parameters.Add("@FrpdId", SqlDbType.Int).Value = normalizedItem.FrpdId;
                command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = normalizedItem.ActivityId;

                try
                {
                    return Convert.ToInt32(command.ExecuteScalar());
                }
                catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "XAK1FRPDVD"))
                {
                    throw CreateFrpdvdDuplicateException();
                }
            }
        }

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

            const string sql = @"
DELETE FROM dbo.FRPD
WHERE IDFRPD = @Id;

SELECT @@ROWCOUNT;";

            try
            {
                using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
                {
                    connection.Open();
                    var blockers = ReferenceDirectorySqlHelpers.LoadDeleteBlockersFromForeignKeys(connection, "FRPD", id);
                    if (blockers.Count > 0)
                    {
                        return new DirectoryDeleteResult
                        {
                            IsDeleted = false,
                            WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPD", blockers)
                        };
                    }

                    using (var command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60;
                        command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
                        if (Convert.ToInt32(command.ExecuteScalar()) == 0)
                        {
                            throw new InvalidOperationException("Запись FRPD для удаления не найдена.");
                        }
                    }

                    return new DirectoryDeleteResult
                    {
                        IsDeleted = true
                    };
                }
            }
            catch (SqlException ex) when (ex.Number == 547)
            {
                return new DirectoryDeleteResult
                {
                    IsDeleted = false,
                    WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPD", ex)
                };
            }
        }

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

            const string sql = @"
DELETE FROM dbo.FRPDVD
WHERE IDFRPDVD = @Id;

SELECT @@ROWCOUNT;";

            try
            {
                using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
                {
                    connection.Open();
                    var blockers = ReferenceDirectorySqlHelpers.LoadDeleteBlockersFromForeignKeys(connection, "FRPDVD", id);
                    if (blockers.Count > 0)
                    {
                        return new DirectoryDeleteResult
                        {
                            IsDeleted = false,
                            WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPDVD", blockers)
                        };
                    }

                    using (var command = new SqlCommand(sql, connection))
                    {
                        command.CommandTimeout = 60;
                        command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
                        if (Convert.ToInt32(command.ExecuteScalar()) == 0)
                        {
                            throw new InvalidOperationException("Запись FRPDVD для удаления не найдена.");
                        }
                    }

                    return new DirectoryDeleteResult
                    {
                        IsDeleted = true
                    };
                }
            }
            catch (SqlException ex) when (ex.Number == 547)
            {
                return new DirectoryDeleteResult
                {
                    IsDeleted = false,
                    WarningMessage = ReferenceDirectorySqlHelpers.CreateDeleteBlockedMessage("FRPDVD", ex)
                };
            }
        }

        public IReadOnlyList<FrpdDirectoryItem> LoadFrpdItems()
        {
            const string sql = @"
SELECT
    fr.IDFRPD AS Id,
    fr.IDFRPDR AS ParentId,
    parent.NMFRPD AS ParentName,
    fr.NMFRPD AS Name,
    fr.KDFRPDLC AS LocalCode,
    fr.FRPDGUID AS Guid,
    fr.DTSZFRPD AS CreatedOn,
    fr.DTLKFRPD AS LiquidatedOn,
    ISNULL(
        STUFF((
            SELECT ', ' + activity.ActivityName
            FROM
            (
                SELECT DISTINCT sp.NMVDDO AS ActivityName
                FROM dbo.FRPDVD link
                JOIN dbo.SPVDDO sp ON sp.IDSPVDDO = link.IDSPVDDO
                WHERE link.IDFRPD = fr.IDFRPD
            ) activity
            ORDER BY activity.ActivityName
            FOR XML PATH(''), TYPE
        ).value('.', 'nvarchar(max)'), 1, 2, ''),
        ''
    ) AS ActivityNames
FROM dbo.FRPD fr
LEFT JOIN dbo.FRPD parent ON parent.IDFRPD = fr.IDFRPDR
ORDER BY fr.NMFRPD, fr.IDFRPD;";

            var items = new List<FrpdDirectoryItem>();

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

                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        items.Add(new FrpdDirectoryItem
                        {
                            ActivityNames = ReferenceDirectorySqlHelpers.GetString(reader, "ActivityNames"),
                            CreatedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "CreatedOn"),
                            Guid = ReferenceDirectorySqlHelpers.GetString(reader, "Guid"),
                            Id = ReferenceDirectorySqlHelpers.GetInt32(reader, "Id"),
                            LiquidatedOn = ReferenceDirectorySqlHelpers.GetNullableDateTime(reader, "LiquidatedOn"),
                            LocalCode = ReferenceDirectorySqlHelpers.GetString(reader, "LocalCode"),
                            Name = ReferenceDirectorySqlHelpers.GetString(reader, "Name"),
                            ParentId = ReferenceDirectorySqlHelpers.GetNullableInt32(reader, "ParentId"),
                            ParentName = ReferenceDirectorySqlHelpers.GetString(reader, "ParentName")
                        });
                    }
                }
            }

            return items;
        }

        public IReadOnlyList<DirectoryLookupItem> LoadFrpdReferences()
        {
            return ReferenceDirectorySqlHelpers.LoadLookupItems(@"
SELECT
    fr.IDFRPD AS Id,
    fr.NMFRPD AS Name
FROM dbo.FRPD fr
ORDER BY fr.NMFRPD, fr.IDFRPD;");
        }

        public IReadOnlyList<FrpdvdDirectoryItem> LoadFrpdvdItems(int frpdId)
        {
            const string sql = @"
SELECT
    link.IDFRPDVD AS Id,
    link.IDFRPD AS FrpdId,
    link.IDSPVDDO AS ActivityId,
    sp.NMVDDO AS ActivityName
FROM dbo.FRPDVD link
JOIN dbo.SPVDDO sp ON sp.IDSPVDDO = link.IDSPVDDO
WHERE link.IDFRPD = @FrpdId
ORDER BY sp.NMVDDO, link.IDFRPDVD;";

            var items = new List<FrpdvdDirectoryItem>();

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

                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        items.Add(new FrpdvdDirectoryItem
                        {
                            ActivityId = ReferenceDirectorySqlHelpers.GetInt32(reader, "ActivityId"),
                            ActivityName = ReferenceDirectorySqlHelpers.GetString(reader, "ActivityName"),
                            FrpdId = ReferenceDirectorySqlHelpers.GetInt32(reader, "FrpdId"),
                            Id = ReferenceDirectorySqlHelpers.GetInt32(reader, "Id")
                        });
                    }
                }
            }

            return items;
        }

        public IReadOnlyList<DirectoryLookupItem> LoadSpvddoReferences()
        {
            return ReferenceDirectorySqlHelpers.LoadLookupItems(@"
SELECT
    sp.IDSPVDDO AS Id,
    sp.NMVDDO AS Name
FROM dbo.SPVDDO sp
ORDER BY sp.NMVDDO, sp.IDSPVDDO;");
        }

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

            const string sql = @"
UPDATE dbo.FRPD
SET IDFRPDR = @ParentId,
    NMFRPD = @Name,
    KDFRPDLC = @LocalCode,
    FRPDGUID = @Guid,
    DTSZFRPD = @CreatedOn,
    DTLKFRPD = @LiquidatedOn
WHERE IDFRPD = @Id;

SELECT @@ROWCOUNT;";

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                EnsureFrpdGuidIsUnique(connection, normalizedItem.Guid, normalizedItem.Id);

                command.CommandTimeout = 60;
                command.Parameters.Add("@Id", SqlDbType.Int).Value = normalizedItem.Id;
                ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ParentId", normalizedItem.ParentId);
                command.Parameters.Add("@Name", SqlDbType.VarChar, FrpdDirectoryRules.NameMaxLength).Value = normalizedItem.Name;
                ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@LocalCode", SqlDbType.VarChar, FrpdDirectoryRules.LocalCodeMaxLength, normalizedItem.LocalCode);
                ReferenceDirectorySqlHelpers.AddNullableStringParameter(command, "@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength, normalizedItem.Guid);
                ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@CreatedOn", normalizedItem.CreatedOn);
                ReferenceDirectorySqlHelpers.AddNullableDateTimeParameter(command, "@LiquidatedOn", normalizedItem.LiquidatedOn);

                try
                {
                    if (Convert.ToInt32(command.ExecuteScalar()) == 0)
                    {
                        throw new InvalidOperationException("Запись FRPD для изменения не найдена.");
                    }
                }
                catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "IX_FRPD_FRPDGUID"))
                {
                    throw CreateFrpdDuplicateGuidException(normalizedItem.Guid);
                }
            }
        }

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

            const string sql = @"
UPDATE dbo.FRPDVD
SET IDSPVDDO = @ActivityId
WHERE IDFRPDVD = @Id;

SELECT @@ROWCOUNT;";

            using (var connection = ReferenceDirectorySqlHelpers.CreateConnection())
            using (var command = new SqlCommand(sql, connection))
            {
                connection.Open();
                EnsureFrpdvdIsUnique(connection, normalizedItem.FrpdId, normalizedItem.ActivityId, normalizedItem.Id);

                command.CommandTimeout = 60;
                command.Parameters.Add("@Id", SqlDbType.Int).Value = normalizedItem.Id;
                command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = normalizedItem.ActivityId;

                try
                {
                    if (Convert.ToInt32(command.ExecuteScalar()) == 0)
                    {
                        throw new InvalidOperationException("Запись FRPDVD для изменения не найдена.");
                    }
                }
                catch (SqlException ex) when (ReferenceDirectorySqlHelpers.IsDuplicateViolation(ex, "XAK1FRPDVD"))
                {
                    throw CreateFrpdvdDuplicateException();
                }
            }
        }

        private static InvalidOperationException CreateFrpdDuplicateGuidException(string guid)
        {
            return new InvalidOperationException(string.Format("GUID подразделения \"{0}\" уже существует в справочнике.", guid));
        }

        private static InvalidOperationException CreateFrpdvdDuplicateException()
        {
            return new InvalidOperationException("Выбранный вид деятельности уже существует у этой организации/подразделения.");
        }

        private static void EnsureFrpdGuidIsUnique(SqlConnection connection, string guid, int? excludeId)
        {
            if (string.IsNullOrWhiteSpace(guid))
            {
                return;
            }

            const string sql = @"
SELECT COUNT(1)
FROM dbo.FRPD
WHERE FRPDGUID = @Guid
  AND (@ExcludeId IS NULL OR IDFRPD <> @ExcludeId);";

            using (var command = new SqlCommand(sql, connection))
            {
                command.CommandTimeout = 60;
                command.Parameters.Add("@Guid", SqlDbType.VarChar, FrpdDirectoryRules.GuidMaxLength).Value = guid;
                ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ExcludeId", excludeId);
                if (Convert.ToInt32(command.ExecuteScalar()) > 0)
                {
                    throw CreateFrpdDuplicateGuidException(guid);
                }
            }
        }

        private static void EnsureFrpdvdIsUnique(SqlConnection connection, int frpdId, int activityId, int? excludeId)
        {
            const string sql = @"
SELECT COUNT(1)
FROM dbo.FRPDVD
WHERE IDFRPD = @FrpdId
  AND IDSPVDDO = @ActivityId
  AND (@ExcludeId IS NULL OR IDFRPDVD <> @ExcludeId);";

            using (var command = new SqlCommand(sql, connection))
            {
                command.CommandTimeout = 60;
                command.Parameters.Add("@FrpdId", SqlDbType.Int).Value = frpdId;
                command.Parameters.Add("@ActivityId", SqlDbType.Int).Value = activityId;
                ReferenceDirectorySqlHelpers.AddNullableIntParameter(command, "@ExcludeId", excludeId);
                if (Convert.ToInt32(command.ExecuteScalar()) > 0)
                {
                    throw CreateFrpdvdDuplicateException();
                }
            }
        }

        private static string NormalizeRequired(string value, int maxLength, string fieldDisplayName)
        {
            var normalizedValue = string.IsNullOrWhiteSpace(value) ? string.Empty : value.Trim();
            if (normalizedValue.Length == 0)
            {
                throw new InvalidOperationException(string.Format("Укажите {0}.", fieldDisplayName));
            }

            if (normalizedValue.Length > maxLength)
            {
                throw new InvalidOperationException(string.Format("{0} не должно превышать {1} символов.", fieldDisplayName, maxLength));
            }

            return normalizedValue;
        }

        private static string NormalizeNullable(string value, int maxLength, string fieldDisplayName)
        {
            var normalizedValue = string.IsNullOrWhiteSpace(value) ? null : value.Trim();
            if (normalizedValue != null && normalizedValue.Length > maxLength)
            {
                throw new InvalidOperationException(string.Format("{0} не должно превышать {1} символов.", fieldDisplayName, maxLength));
            }

            return normalizedValue;
        }

        private static FrpdDirectoryItem NormalizeFrpdItem(FrpdDirectoryItem item)
        {
            if (item == null)
            {
                throw new InvalidOperationException("Не передана запись FRPD.");
            }

            var normalizedItem = new FrpdDirectoryItem
            {
                CreatedOn = item.CreatedOn,
                Guid = NormalizeNullable(item.Guid, FrpdDirectoryRules.GuidMaxLength, "GUID подразделения"),
                Id = item.Id,
                LiquidatedOn = item.LiquidatedOn,
                LocalCode = NormalizeNullable(item.LocalCode, FrpdDirectoryRules.LocalCodeMaxLength, "Локальный код организации/подразделения"),
                Name = NormalizeRequired(item.Name, FrpdDirectoryRules.NameMaxLength, "организацию/подразделение"),
                ParentId = item.ParentId.HasValue && item.ParentId.Value > 0 ? item.ParentId : null
            };

            if (normalizedItem.Id > 0
                && normalizedItem.ParentId.HasValue
                && normalizedItem.ParentId.Value == normalizedItem.Id)
            {
                throw new InvalidOperationException("Организация/подразделение не может ссылаться на себя как на родительскую запись.");
            }

            return normalizedItem;
        }

        private static FrpdvdDirectoryItem NormalizeFrpdvdItem(FrpdvdDirectoryItem item)
        {
            if (item == null)
            {
                throw new InvalidOperationException("Не передана запись FRPDVD.");
            }

            if (item.FrpdId <= 0)
            {
                throw new InvalidOperationException("Не выбрана организация/подразделение для вида деятельности.");
            }

            if (item.ActivityId <= 0)
            {
                throw new InvalidOperationException("Укажите вид деятельности организации.");
            }

            return new FrpdvdDirectoryItem
            {
                ActivityId = item.ActivityId,
                FrpdId = item.FrpdId,
                Id = item.Id
            };
        }
    }
}
