using System.ComponentModel;
using Evos.Api.CollaborationRepository.Exporting;
using Evos.Api.Common.Families;
using Evos.Api.Model;
using Evos.Api.Model.PredefinedFamilies;
using Evos.Api.Model.Serialization;
using Evos.Api.Repository;
using Evos.Api.Samples.Infrastructure;
using Evos.CommonValueObjects;
using Evos.Core.Container.ProjectScope;
using Evos.ModelsRepository;
using Evos.ModelsRepository.BasicData;
using Evos.ModelsRepository.Connection;
using Evos.ModelsRepository.DataProvider.TransactionDescriptions;
using Evos.ModelsRepository.Products;
using Evos.ModelsRepository.SpaceModel;
using Nanosoft.Common.InternationalSystemOfUnits.Access;
using Nanosoft.Common.ValueObjects.Common;

namespace Evos.Api.Samples
{
    /// Публикация обменной модели в локальный репозиторий Evos — то место, где сходятся оба API.
    ///
    /// Репозиторий открывает низкоуровневое API (ApiConcurrentRepositoryFactory), и этот же
    /// IConcurrentRepository высокоуровневое принимает как есть: экспортёру нужен именно он.
    /// Никакого «репозитория обменной модели» не существует — модель публикуется в обычный
    /// репозиторий Evos, открытый обычной фабрикой.
    [Localizable(false)]
    public static class PublishToRepositorySample
    {
        private static readonly Color Grey = Color.FromSRgb(180, 180, 180, 255, false);
        private static readonly Color Red = Color.FromSRgb(220, 40, 40, 255, false);

        public static void Run()
        {
            var directory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());

            try
            {
                var location = new ProjectLocation(directory, Path.GetRandomFileName());

                // Шаг 1. Открываем репозиторий низкоуровневым API — как в RepositoryLifecycleSample.
                using var repository = new ApiConcurrentRepositoryFactory().OpenNewRepository(location);

                // Шаг 2. Наполняем его низкоуровневым API: базовый продукт «Компонент» Evos заводит
                // в каждом проекте, поэтому он есть и здесь — и попадает в подсчёты.
                repository.TryExecuteTransaction(
                    writeAccess => writeAccess.NewNamedProduct("Компонент", ProductKind.Designed),
                    new TransactionInfo(TransactionDescription.AddingProduct, TransactionMode.Default));

                // Шаг 3. В тот же самый репозиторий публикуем обменную модель высокоуровневым API.
                PublishExchangeModel(repository);

                // Шаг 4. Линейный продукт и графика экземпляром: у них своя механика публикации.
                PublishLinearProductAndGraphic(repository);
            }
            finally
            {
                Directory.Delete(directory, true);
            }
        }

        private static void PublishExchangeModel(IConcurrentRepository repository)
        {
            var model = BuildExchangeModelSample.CreateModelWithInstances(SampleIdentifiers.ProjectId);
            Export(repository, model);

            ShowProducts("после первого экспорта", ReadProductNames(repository));

            // Экспорт идемпотентен по ключевым свойствам: та же модель не создаёт дублей,
            // а обновляет уже существующие продукты.
            Export(repository, model);

            var productsAfterSecondExport = ReadProductNames(repository);
            ShowProducts("после повторного экспорта", productsAfterSecondExport);

            SampleCheck.That(productsAfterSecondExport.Count == 3, "повторный экспорт не должен плодить дубли продуктов");
            CheckAssemblyHasThreeParts(repository, "у сборки должно быть три экземпляра детали");

            ExportRebuiltModel(repository);
            ExportSerializedModel(repository, model);
        }

        /// Настоящая проверка идемпотентности: модель собирают заново — новые объекты в памяти,
        /// но те же идентификаторы проекта и объектов. Именно так выглядит повторный импорт из вашей
        /// системы, и именно здесь Guid.NewGuid() дал бы вторую тройку экземпляров вместо обновления.
        private static void ExportRebuiltModel(IConcurrentRepository repository)
        {
            Export(repository, BuildExchangeModelSample.CreateModelWithInstances(SampleIdentifiers.ProjectId));

            var products = ReadProductNames(repository);
            ShowProducts("после экспорта заново собранной модели", products);

            SampleCheck.That(products.Count == 3, "стабильные идентификаторы: продукты не должны задвоиться");
            CheckAssemblyHasThreeParts(repository, "и экземпляры детали тоже");
        }

        /// Модель, пришедшая файлом или потоком, публикуется без промежуточной десериализации:
        /// ExportFromFile и ExportFromStream делают это сами. Отдаём ту же модель, что уже уехала
        /// в репозиторий выше: ключевые свойства у неё те же, поэтому дублей она не создаёт —
        /// ни продуктов, ни экземпляров.
        private static void ExportSerializedModel(IConcurrentRepository repository, IReadOnlyExchangeModel model)
        {
            var exporter = ExchangeModelToEvosExporterFactory.Create();

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

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

                _ = exporter.ExportFromStream(repository, stream).ValueOrThrow();
            }

            var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.evosexchange");
            try
            {
                ExchangeModelSerializerFactory.CreateSerializer().Serialize(model, filePath);

                _ = exporter.ExportFromFile(repository, filePath).ValueOrThrow();
            }
            finally
            {
                File.Delete(filePath);
            }

            var products = ReadProductNames(repository);
            ShowProducts("после экспорта из потока и файла", products);

            SampleCheck.That(products.Count == 3, "сериализованная модель — та же модель: дублей продуктов быть не должно");
            CheckAssemblyHasThreeParts(repository, "и дублей экземпляров тоже");
        }

        /// Линейный продукт (воздуховод, труба, лоток) в репозитории превращается в повторитель —
        /// тот самый, что низкоуровневым API строит RepeaterSample. Графика, добавленная экземпляром,
        /// уезжает отдельным графическим продуктом и вхождением в изделие.
        private static void PublishLinearProductAndGraphic(IConcurrentRepository repository)
        {
            var productsBefore = ReadProductNames(repository).Count;

            var model = CreateLinearAndGraphicModel();

            Export(repository, model);

            var productsAfterFirstExport = ReadProductNames(repository);
            ShowProducts("после публикации линейного продукта и графики", productsAfterFirstExport);

            SampleCheck.That(productsAfterFirstExport.Count > productsBefore, "новые продукты должны появиться");
            SampleCheck.That(HasRepeater(repository), "линейный продукт публикуется повторителем");
            SampleCheck.That(HasNestedInstances(repository, "Насос"), "графика добавляется вхождением в изделие");

            // Повторитель создаётся заново на каждый несопоставленный линейный продукт, поэтому важно,
            // чтобы у линейного продукта были собственные ключевые свойства: по ним он и находится.
            Export(repository, CreateLinearAndGraphicModel());

            var productsAfterSecondExport = ReadProductNames(repository);
            SampleCheck.That(
                productsAfterSecondExport.Count == productsAfterFirstExport.Count,
                "повторная публикация не должна плодить ни повторителей, ни графических продуктов");
        }

        private static IReadOnlyExchangeModel CreateLinearAndGraphicModel()
        {
            var builder = ExchangeModelBuilderFactory.Create(SampleIdentifiers.ProjectId);

            // Сегмент — обычный продукт со своими ключами, которому положили меш. Обратите внимание:
            // SetProductMesh кладёт продукту ещё и ключевое свойство — хэш геометрии.
            var segment = builder.NewProduct();
            segment.KeyProperties.SetValue(CommonFamilies.КаталогПроизводитель.Производитель, "Evos");
            segment.KeyProperties.SetValue(CommonFamilies.КаталогПроизводитель.АртикулПроизводителя, "DUCT-SEGMENT-100");
            segment.Properties.SetValue(EvosFamilies.ProductName.Name, "Сегмент воздуховода");
            builder.GraphicsBuilder.SetProductMesh(SampleGeometry.Triangle(Grey), segment);

            // Линейный продукт: тот же сегмент, растиражированный по длине.
            var duct = builder.NewLinearProduct(segment, CreateLength(3.5));
            duct.KeyProperties.SetValue(CommonFamilies.КаталогПроизводитель.Производитель, "Evos");
            duct.KeyProperties.SetValue(CommonFamilies.КаталогПроизводитель.АртикулПроизводителя, "DUCT-100");
            duct.Properties.SetValue(EvosFamilies.ProductName.Name, "Воздуховод 100×100");

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

            var _ = builder.GraphicsBuilder.AddGraphicToProductAsInstance(pump, SampleGeometry.Triangle(Red));

            return builder.Build().ValueOrThrow();
        }

        private static bool HasRepeater(IConcurrentRepository repository)
        {
            using var connection = repository.ReadConnection();

            return connection.GetProducts().Any(product => product.IsRepeater());
        }

        private static bool HasNestedInstances(IConcurrentRepository repository, string productName)
        {
            using var connection = repository.ReadConnection();

            return FindByName(connection, productName).NestedInstances.Any();
        }

        private static Length CreateLength(double meters)
        {
            return Length.CreateOrThrow(ValueObjectsAccess.Meter.FromMeter(meters));
        }

        private static List<string> ReadProductNames(IConcurrentRepository repository)
        {
            using var connection = repository.ReadConnection();

            return connection.GetProducts()
                .Select(ReadProductName)
                .ToList();
        }

        private static void CheckAssemblyHasThreeParts(IConcurrentRepository repository, string message)
        {
            using var connection = repository.ReadConnection();

            var assembly = FindByName(connection, "Сборка");
            SampleCheck.That(assembly.NestedInstances.Count() == 3, message);
        }

        private static void ShowProducts(string stage, IReadOnlyCollection<string> productNames)
        {
            Console.WriteLine($"В репозитории продуктов {stage}: {productNames.Count}");

            foreach (var productName in productNames)
            {
                Console.WriteLine($"  {productName}");
            }
        }

        /// Экспорт идемпотентен по ключевым свойствам: повторный экспорт той же модели
        /// не создаёт дублей, а обновляет уже существующие продукты.
        /// Модель из памяти уходит напрямую — сериализация нужна только для обмена файлом или потоком.
        private static void Export(IConcurrentRepository repository, IReadOnlyExchangeModel model)
        {
            var exporter = ExchangeModelToEvosExporterFactory.Create();
            _ = exporter.Export(repository, model).ValueOrThrow();
        }

        private static IReadOnlyProduct FindByName(IRepositoryReadAccess connection, string name)
        {
            var product = connection.GetProducts()
                .FirstOrDefault(candidate => ReadProductName(candidate) == name);

            SampleCheck.That(product is not null, $"продукт '{name}' должен существовать в репозитории");
            return product!;
        }

        private static string ReadProductName(IReadOnlyProduct product)
        {
            return product.GetPredefinedFamilyOrThrow<IProductName>().Facade().Name;
        }
    }
}
