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.Output;
using Evos.Api.Model.PredefinedFamilies;
using Evos.Api.Model.Serialization;
using Evos.Api.Samples.Infrastructure;
using Evos.ModelsRepository;
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())
                .AddUncommittedChangesProcessor(new UncommittedChangesReporter())
                .Build();

            PublishModel(collaborationRepository, location);
            PublishModelAsStream(collaborationRepository, location);
            ReadStructure(collaborationRepository, location);
            PublishOntoExistingLevel(collaborationRepository, location);
        }

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

            var result = writer.ImportAndPush(location, model);

            // Отчёт обработчика незакоммиченных изменений приходит сюда же, полезной нагрузкой
            // в диагностике публикации.
            ShowUncommittedChanges(result);

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

        /// Что именно ImportAndPush записал в проект — видно до коммита, из отчёта обработчика.
        private static void ShowUncommittedChanges(OperationResult<Unit> result)
        {
            foreach (var entry in result.Diagnostics)
            {
                if (entry.Payload is not UncommittedChangesReportDto report)
                {
                    continue;
                }

                Console.WriteLine($"До коммита накопилось операций: {report.TotalCount}");

                foreach (var transaction in report.Transactions)
                {
                    Console.WriteLine($"  {transaction.Description}: операций — {transaction.Operations.Count}");
                }
            }
        }

        /// Ту же модель можно отдать потоком — так публикуют, когда модель приходит по сети
        /// или собирается в другом процессе. Идентификаторы те же, поэтому дублей не будет:
        /// это повторная публикация того же самого, а не новые объекты.
        private static void PublishModelAsStream(ICollaborationRepositoryWriter writer, RepositoryLocation location)
        {
            var model = BuildExchangeModelSample.CreateModelWithInstances(SampleIdentifiers.ProjectId);

            using var stream = new MemoryStream();
            ExchangeModelSerializerFactory.CreateSerializer().Serialize(model, stream);

            // Сериализатор оставляет поток в конце — перед чтением его перематывают на начало.
            stream.Position = 0;

            _ = writer.ImportAndPush(location, stream).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}");
        }

        /// Основной боевой сценарий: поставить оборудование на уровень, который в проекте уже есть.
        ///
        /// Чтение проекта отдаёт по уровню два идентификатора, и оба кладут в модель как есть:
        ///   ContainerId      — адрес продукта-контейнера, внутри которого лежит уровень;
        ///   LevelInstanceId  — ссылка на сам уровень.
        ///
        /// Контейнер адресуют ModelId — так оборудование попадает внутрь существующего продукта
        /// проекта, а не в новый, заведённый рядом. Ключевых свойств у такого продукта быть не должно:
        /// адрес у него уже есть, и тождество по ключам для него не считается.
        ///
        /// Именно поэтому сценарий живёт здесь, а не в локальных сэмплах: оба идентификатора выдаёт
        /// только чтение проекта — собрать их самому из локального репозитория публичным API нечем.
        private static void PublishOntoExistingLevel(ICollaborationRepository collaboration, RepositoryLocation location)
        {
            var levels = collaboration.GetLevels(location).ValueOrThrow();

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

            var level = levels[0];

            if (level.ContainerId is null || level.LevelInstanceId is null)
            {
                Console.WriteLine($"У уровня «{level.Path}» нет идентификаторов — пропускаем.");
                return;
            }

            var builder = ExchangeModelBuilderFactory.Create(SampleIdentifiers.ProjectId);

            // Продукт-контейнер уровня: не создаём новый, а подключаемся к существующему.
            var floor = builder.NewProduct();
            floor.ModelId = level.ContainerId;

            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, SampleIdentifiers.ObjectId(100));
            pumpInstance.Level = level.LevelInstanceId;

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

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

        /// Точка расширения: обработчик вызывают перед коммитом — модель уже влита в локальный кэш,
        /// но на сервер ещё ничего не ушло. Удобное место, чтобы посмотреть или залогировать,
        /// что именно публикация собирается отправить.
        ///
        /// Диагностика, которую вернёт обработчик, попадает в OperationResult самой публикации:
        /// отчёт отдаём полезной нагрузкой (UncommittedChangesReportDto), а не текстом, — так его
        /// можно разобрать программно.
        private sealed class UncommittedChangesReporter : IUncommittedChangesProcessor
        {
            public OperationResult<Unit> Process(IConcurrentRepository repository)
            {
                ThrowIf.Argument.IsNull(repository, nameof(repository));

                using var vcsRead = repository.VCSReadConnection();

                var report = new UncommittedChangesReportDto();

                foreach (var transaction in vcsRead.LocalUncommittedChanges())
                {
                    var transactionReport = new TransactionDto
                    {
                        Id = transaction.Id.ToString(),
                        Description = transaction.Description.ToString()
                    };

                    foreach (var operation in transaction.Operations)
                    {
                        transactionReport.Operations.Add(new OperationDto
                        {
                            OperationType = operation.GetType().Name,
                            Summary = operation.ToString() ?? string.Empty
                        });

                        report.TotalCount++;
                    }

                    report.Transactions.Add(transactionReport);
                }

                var diagnostics = new List<OperationOutputEntry>();
                diagnostics.Report(report, OutputLevel.Info);

                return diagnostics.ToOperationResult();
            }
        }

        /// Свой читатель: получает 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}");
                }
            }
        }
    }
}
