using System.ComponentModel;
using System.Text.RegularExpressions;
using Evos.Api.CollaborationRepository;
using Evos.Api.CollaborationRepository.Extensibility;
using Evos.Api.CollaborationRepository.Logging;
using Evos.Api.CollaborationRepository.Reading;
using Evos.Api.Common.Families;
using Evos.Api.Model;
using Evos.Api.Model.Graphic;
using Evos.Api.Model.PredefinedFamilies;
using Evos.ModelsRepository.BasicData;
using Guard;

namespace Evos.Api.Samples
{
    /// Высокоуровневое API: работа с проектом на Collaboration-сервере.
    /// В отличие от остальных сэмплов, этому нужен живой сервер Evos: адрес, проект и ветка
    /// приходят снаружи, а проект и ветка должны существовать заранее — создавать их API не умеет.
    [Localizable(false)]
    public static class CollaborationSample
    {
        public static void Run(string serverUrl, string projectName, string branchName)
        {
            ThrowIf.Argument.IsNullOrEmpty(serverUrl, nameof(serverUrl));
            ThrowIf.Argument.IsNullOrEmpty(projectName, nameof(projectName));
            ThrowIf.Argument.IsNullOrEmpty(branchName, nameof(branchName));

            var location = new RepositoryLocation(serverUrl, projectName, branchName);

            // Collaboration-репозиторий собирается билдером. Логгер и провайдер путей
            // необязательны: без них берутся значения по умолчанию.
            var collaborationRepository = new CollaborationRepositoryBuilder()
                .UseLogger(new ConsoleCollaborationLogger())
                .UseRepositoryCachePathProvider(new TempRepositoryCachePathProvider())
                .Build();

            PublishModel(collaborationRepository, location);
            ReadStructure(collaborationRepository, location);
            PublishOntoLevel(collaborationRepository, location);
            UpdateProductById(collaborationRepository, location);
        }

        /// ImportAndPush делает всё разом: подтягивает проект с сервера, вливает в него
        /// обменную модель и отправляет результат обратно. Модель можно отдать как объект
        /// или как поток — в поток её кладёт SerializeExchangeModelSample.
        private static void PublishModel(ICollaborationRepositoryWriter writer, RepositoryLocation location)
        {
            var model = BuildExchangeModelSample.CreateModelWithInstances(Guid.NewGuid());

            _ = writer.ImportAndPush(location, model).ValueOrThrow();
            Console.WriteLine("Модель опубликована на сервере.");
        }

        /// Чтение структуры проекта с сервера: уровни и дерево «здания — уровни — помещения».
        /// Каждый вызов сам синхронизирует локальный кэш репозитория с сервером.
        private static void ReadStructure(ICollaborationRepositoryReader reader, RepositoryLocation location)
        {
            // Любая операция высокоуровневого API возвращает OperationResult: ValueOrThrow()
            // отдаёт значение либо падает, выписав в исключение диагностики и ошибку сервера.
            var levels = reader.GetLevels(location).ValueOrThrow();
            Console.WriteLine($"Уровней в проекте: {levels.Count}");
            foreach (var level in levels)
            {
                Console.WriteLine($"  {level.Path}");
            }

            var structure = reader.GetProductStructure(location, "Сборка").ValueOrThrow();
            Console.WriteLine($"Структура продукта {structure.ProductName}: зданий — {structure.Buildings.Count}");

            // Если готовых читателей не хватает, открывают сессию и читают репозиторий
            // своим IEvosReader — он получает доступ к низкоуровневой read-грани.
            using var session = reader.OpenReadSession(location).ValueOrThrow();
            var productNames = session.Read(new ProductNamesReader());

            Console.WriteLine($"Продуктов в репозитории: {productNames.Count}");
        }

        /// Привязка оборудования к уровню проекта. Level — это не номер и не имя, а ссылка на
        /// уровень, который в проекте уже есть: её берут из GetLevels и кладут в экземпляр как есть.
        ///
        /// Именно поэтому такой сэмпл живёт здесь, а не в локальных: EvosInstanceRef выдаёт только
        /// чтение проекта, собрать его самому из локального репозитория публичным API нечем.
        private static void PublishOntoLevel(ICollaborationRepository collaboration, RepositoryLocation location)
        {
            var levels = collaboration.GetLevels(location).ValueOrThrow();

            if (levels.Count == 0)
            {
                Console.WriteLine("В проекте нет уровней — привязывать оборудование не к чему.");
                return;
            }

            var level = levels[0];

            var builder = ExchangeModelBuilderFactory.Create(Guid.NewGuid());

            var floor = builder.NewProduct();
            floor.KeyProperties.SetValue(CommonFamilies.КаталогПроизводитель.Производитель, "Evos");
            floor.KeyProperties.SetValue(CommonFamilies.КаталогПроизводитель.АртикулПроизводителя, "FLOOR-1");
            floor.Properties.SetValue(EvosFamilies.ProductName.Name, "Оборудование этажа");

            var pump = builder.NewProduct();
            pump.KeyProperties.SetValue(CommonFamilies.КаталогПроизводитель.Производитель, "Grundfos");
            pump.KeyProperties.SetValue(CommonFamilies.КаталогПроизводитель.АртикулПроизводителя, "UPS-25");
            pump.Properties.SetValue(EvosFamilies.ProductName.Name, "Насос циркуляционный");

            var pumpInstance = floor.NewProductInstance(pump);
            pumpInstance.KeyProperties.SetValue(GraphicFamilies.ModelObjectId.ObjectId, Guid.NewGuid());
            pumpInstance.Level = level.LevelInstanceId;

            _ = collaboration.ImportAndPush(location, builder.Build().ValueOrThrow()).ValueOrThrow();

            Console.WriteLine($"Насос поставлен на уровень «{level.Path}».");
        }

        /// Адресное обновление. Обычно продукт находят по ключевым свойствам, но если нужно
        /// поправить конкретный продукт, уже лежащий в проекте, — ему проставляют ModelId,
        /// полученный чтением проекта. Ключевых свойств у такого продукта быть не должно:
        /// адрес у него уже есть, и тождество по ключам для него не считается.
        private static void UpdateProductById(ICollaborationRepository collaboration, RepositoryLocation location)
        {
            var structure = collaboration.GetProductStructure(location, "Сборка").ValueOrThrow();

            if (structure.ProductId is null)
            {
                Console.WriteLine("Продукт «Сборка» не найден — обновлять нечего.");
                return;
            }

            var builder = ExchangeModelBuilderFactory.Create(Guid.NewGuid());

            var assembly = builder.NewProduct();
            assembly.ModelId = structure.ProductId;
            assembly.Properties.SetValue(EvosFamilies.ProductName.Name, "Сборка (обновлена)");

            _ = collaboration.ImportAndPush(location, builder.Build().ValueOrThrow()).ValueOrThrow();

            Console.WriteLine($"Продукт {structure.ProductName} обновлён по ModelId.");
        }

        /// Свой читатель: получает read-доступ к репозиторию и возвращает всё, что нужно.
        private sealed class ProductNamesReader : IEvosReader<List<string>>
        {
            public List<string> Read(IEvosReadContext context)
            {
                ThrowIf.Argument.IsNull(context, nameof(context));

                return context.ReadAccess.GetProducts()
                    .Select(product => product.GetPredefinedFamilyOrThrow<IProductName>().Facade().Name)
                    .ToList();
            }
        }

        /// Куда класть локальный кэш репозитория, скачанного с сервера.
        private sealed class TempRepositoryCachePathProvider : IRepositoryCachePathProvider
        {
            private static readonly string RootDirectory =
                Path.Combine(Path.GetTempPath(), "Evos.Api.Samples", "Collaboration");

            public string GetRepositoryDirectory(string projectName)
            {
                ThrowIf.Argument.IsNullOrEmpty(projectName, nameof(projectName));

                return Path.Combine(RootDirectory, projectName);
            }

            public string GetRepositoryFilePath(string projectName)
            {
                ThrowIf.Argument.IsNullOrEmpty(projectName, nameof(projectName));

                return Path.Combine(GetRepositoryDirectory(projectName), $"{projectName}.repository");
            }
        }

        /// Логи Collaboration (синхронизация, публикация, ошибки сервера) приходят сюда.
        private sealed class ConsoleCollaborationLogger : ICollaborationLogger
        {
            /// Места в шаблоне названы, а не пронумерованы: {Message}, {Path}, {ProductName}.
            private static readonly Regex TemplatePlaceholder = new(@"\{\w+\}", RegexOptions.Compiled);

            public bool IsEnabled(CollaborationLogLevel level)
            {
                return level >= CollaborationLogLevel.Information;
            }

            public void Log(CollaborationLogLevel level, string message)
            {
                Write(level, message);
            }

            public void Log(CollaborationLogLevel level, string format, params object[] args)
            {
                Write(level, Format(format, args));
            }

            public void Log(CollaborationLogLevel level, Exception exception, string message)
            {
                Write(level, $"{message}: {exception.Message}");
            }

            public void Log<T>(CollaborationLogLevel level, string format, T arg)
            {
                Write(level, Format(format, arg));
            }

            public void Log<T1, T2>(CollaborationLogLevel level, string format, T1 arg1, T2 arg2)
            {
                Write(level, Format(format, arg1, arg2));
            }

            public void Log<T1, T2, T3>(CollaborationLogLevel level, string format, T1 arg1, T2 arg2, T3 arg3)
            {
                Write(level, Format(format, arg1, arg2, arg3));
            }

            public void Log<T>(CollaborationLogLevel level, Exception exception, string format, T arg)
            {
                Write(level, $"{Format(format, arg)}: {exception.Message}");
            }

            /// Evos логирует шаблонами вида "Deleting repository cache at {Path}", а не "{0}".
            /// string.Format такой шаблон не понимает и бросает FormatException, поэтому
            /// подставляем аргументы в именованные места по порядку — так же, как это делают
            /// Serilog и Microsoft.Extensions.Logging.
            private static string Format(string messageTemplate, params object?[] arguments)
            {
                var argumentIndex = 0;

                return TemplatePlaceholder.Replace(
                    messageTemplate,
                    _ => argumentIndex < arguments.Length
                        ? arguments[argumentIndex++]?.ToString() ?? string.Empty
                        : string.Empty);
            }

            private void Write(CollaborationLogLevel level, string message)
            {
                if (IsEnabled(level))
                {
                    Console.WriteLine($"  [{level}] {message}");
                }
            }
        }
    }
}
