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

namespace XLAB2
{
    internal sealed class DeleteBlockerInfo
    {
        public int RowCount { get; set; }

        public string TableName { get; set; }
    }

    internal static class ReferenceDirectorySqlHelpers
    {
        public static SqlConnection CreateConnection()
        {
            return SqlServerConnectionFactory.Current.CreateConnection();
        }

        public static int GetCommandTimeoutSeconds()
        {
            return SqlServerConnectionFactory.Current.Options.CommandTimeoutSeconds;
        }

        public static IReadOnlyList<DirectoryLookupItem> LoadLookupItems(string sql)
        {
            var items = new List<DirectoryLookupItem>();

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

                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        items.Add(new DirectoryLookupItem
                        {
                            Id = GetInt32(reader, "Id"),
                            Name = GetString(reader, "Name")
                        });
                    }
                }
            }

            return items;
        }

        public static List<DeleteBlockerInfo> LoadDeleteBlockersFromForeignKeys(SqlConnection connection, string parentTableName, int id, IEnumerable<string> excludedChildTables = null)
        {
            return LoadDeleteBlockersFromForeignKeys(connection, null, parentTableName, id, excludedChildTables);
        }

        public static List<DeleteBlockerInfo> LoadDeleteBlockersFromForeignKeys(SqlConnection connection, SqlTransaction transaction, string parentTableName, int id, IEnumerable<string> excludedChildTables = null)
        {
            var excluded = new HashSet<string>(excludedChildTables ?? Enumerable.Empty<string>(), StringComparer.OrdinalIgnoreCase);
            var metadata = LoadForeignKeyMetadata(connection, transaction, parentTableName)
                .Where(delegate(ForeignKeyMetadata item) { return !excluded.Contains(item.TableName); })
                .GroupBy(delegate(ForeignKeyMetadata item) { return item.SchemaName + "." + item.TableName; })
                .Select(delegate(IGrouping<string, ForeignKeyMetadata> group)
                {
                    var first = group.First();
                    return new
                    {
                        first.SchemaName,
                        first.TableName,
                        Columns = group.Select(delegate(ForeignKeyMetadata item) { return item.ColumnName; }).Distinct(StringComparer.OrdinalIgnoreCase).ToList()
                    };
                })
                .ToList();

            var blockers = new List<DeleteBlockerInfo>();
            foreach (var item in metadata)
            {
                var whereClause = string.Join(" OR ", item.Columns.Select(delegate(string columnName)
                {
                    return string.Format("[{0}] = @Id", EscapeIdentifier(columnName));
                }));
                var sql = string.Format(
                    "SELECT COUNT(*) FROM [{0}].[{1}] WHERE {2};",
                    EscapeIdentifier(item.SchemaName),
                    EscapeIdentifier(item.TableName),
                    whereClause);

                using (var command = new SqlCommand(sql, connection))
                {
                    command.Transaction = transaction;
                    command.CommandTimeout = GetCommandTimeoutSeconds();
                    command.Parameters.Add("@Id", SqlDbType.Int).Value = id;
                    var rowCount = Convert.ToInt32(command.ExecuteScalar());
                    if (rowCount > 0)
                    {
                        blockers.Add(new DeleteBlockerInfo
                        {
                            TableName = item.TableName,
                            RowCount = rowCount
                        });
                    }
                }
            }

            return blockers.OrderBy(delegate(DeleteBlockerInfo blocker) { return blocker.TableName; }).ToList();
        }

        public static string CreateDeleteBlockedMessage(string tableName, IEnumerable<DeleteBlockerInfo> blockers)
        {
            var details = string.Join(", ", (blockers ?? Enumerable.Empty<DeleteBlockerInfo>())
                .OrderBy(delegate(DeleteBlockerInfo blocker) { return blocker.TableName; })
                .Select(delegate(DeleteBlockerInfo blocker) { return string.Format("{0}: {1}", blocker.TableName, blocker.RowCount); }));

            return string.Format(
                "Р—Р°РїРёСЃСЊ {0} РЅРµ РјРѕР¶РµС‚ Р±С‹С‚СЊ СѓРґР°Р»РµРЅР°, РїРѕС‚РѕРјСѓ С‡С‚Рѕ РЅР° РЅРµС‘ РµСЃС‚СЊ СЃСЃС‹Р»РєРё РІ С‚Р°Р±Р»РёС†Р°С…: {1}.",
                tableName,
                string.IsNullOrWhiteSpace(details) ? "СЃРІСЏР·Р°РЅРЅС‹Рµ РґР°РЅРЅС‹Рµ" : details);
        }

        public static string CreateDeleteBlockedMessage(string tableName, SqlException ex)
        {
            var suffix = ex == null || string.IsNullOrWhiteSpace(ex.Message)
                ? string.Empty
                : " " + ex.Message.Trim();

            return string.Format(
                "Р—Р°РїРёСЃСЊ {0} РЅРµ РјРѕР¶РµС‚ Р±С‹С‚СЊ СѓРґР°Р»РµРЅР° РёР·-Р·Р° РѕРіСЂР°РЅРёС‡РµРЅРёР№ СЃСЃС‹Р»РѕС‡РЅРѕР№ С†РµР»РѕСЃС‚РЅРѕСЃС‚Рё РІ Р‘Р”.{1}",
                tableName,
                suffix);
        }

        public static bool IsDuplicateViolation(SqlException ex, string indexName)
        {
            return ex != null
                   && (ex.Number == 2601 || ex.Number == 2627)
                   && ex.Message.IndexOf(indexName, StringComparison.OrdinalIgnoreCase) >= 0;
        }

        public static int GetInt32(SqlDataReader reader, string columnName)
        {
            return reader.GetInt32(reader.GetOrdinal(columnName));
        }

        public static int? GetNullableInt32(SqlDataReader reader, string columnName)
        {
            var ordinal = reader.GetOrdinal(columnName);
            return reader.IsDBNull(ordinal) ? (int?)null : reader.GetInt32(ordinal);
        }

        public static string GetString(SqlDataReader reader, string columnName)
        {
            var ordinal = reader.GetOrdinal(columnName);
            return reader.IsDBNull(ordinal) ? string.Empty : Convert.ToString(reader.GetValue(ordinal));
        }

        public static DateTime? GetNullableDateTime(SqlDataReader reader, string columnName)
        {
            var ordinal = reader.GetOrdinal(columnName);
            return reader.IsDBNull(ordinal) ? (DateTime?)null : reader.GetDateTime(ordinal);
        }

        public static bool? GetNullableBoolean(SqlDataReader reader, string columnName)
        {
            var ordinal = reader.GetOrdinal(columnName);
            return reader.IsDBNull(ordinal) ? (bool?)null : reader.GetBoolean(ordinal);
        }

        public static void AddNullableIntParameter(SqlCommand command, string name, int? value)
        {
            command.Parameters.Add(name, SqlDbType.Int).Value = (object)value ?? DBNull.Value;
        }

        public static void AddNullableStringParameter(SqlCommand command, string name, SqlDbType dbType, int size, string value)
        {
            command.Parameters.Add(name, dbType, size).Value = (object)value ?? DBNull.Value;
        }

        public static void AddNullableDateTimeParameter(SqlCommand command, string name, DateTime? value)
        {
            command.Parameters.Add(name, SqlDbType.DateTime).Value = (object)value ?? DBNull.Value;
        }

        public static void AddNullableTextParameter(SqlCommand command, string name, string value)
        {
            command.Parameters.Add(name, SqlDbType.VarChar, -1).Value = (object)value ?? DBNull.Value;
        }

        private static IReadOnlyList<ForeignKeyMetadata> LoadForeignKeyMetadata(SqlConnection connection, SqlTransaction transaction, string parentTableName)
        {
            const string sql = @"
SELECT
    OBJECT_SCHEMA_NAME(fk.parent_object_id) AS SchemaName,
    OBJECT_NAME(fk.parent_object_id) AS TableName,
    childColumn.name AS ColumnName
FROM sys.foreign_keys fk
JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
JOIN sys.columns childColumn ON childColumn.object_id = fkc.parent_object_id AND childColumn.column_id = fkc.parent_column_id
WHERE OBJECT_NAME(fk.referenced_object_id) = @ParentTableName
ORDER BY TableName, ColumnName;";

            var result = new List<ForeignKeyMetadata>();

            using (var command = new SqlCommand(sql, connection))
            {
                command.Transaction = transaction;
                command.CommandTimeout = GetCommandTimeoutSeconds();
                command.Parameters.Add("@ParentTableName", SqlDbType.VarChar, 128).Value = parentTableName;

                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        result.Add(new ForeignKeyMetadata
                        {
                            SchemaName = GetString(reader, "SchemaName"),
                            TableName = GetString(reader, "TableName"),
                            ColumnName = GetString(reader, "ColumnName")
                        });
                    }
                }
            }

            return result;
        }

        private static string EscapeIdentifier(string identifier)
        {
            return (identifier ?? string.Empty).Replace("]", "]]");
        }

        private sealed class ForeignKeyMetadata
        {
            public string ColumnName { get; set; }

            public string SchemaName { get; set; }

            public string TableName { get; set; }
        }
    }
}

