using AsbCloudApp.Data;
using AsbCloudApp.Services;
using AsbCloudDb.Model;
using ClosedXML.Excel;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace AsbCloudInfrastructure.Services.WellOperationService
{

    /*
     * password for WellOperationImportTemplate.xlsx is ASB2020!
     */

    public class WellOperationImportService : IWellOperationImportService
    {
        private const string sheetNamePlan = "План";
        private const string sheetNameFact = "Факт";

        private const int headerRowsCount = 1;
        private const int columnSection = 1;
        private const int columnCategory = 2;
        private const int columnCategoryInfo = 3;
        private const int columnDepthStart = 4;
        private const int columnDepthEnd = 5;
        private const int columnDate = 6;
        private const int columnDuration = 7;
        private const int columnComment = 8;

        private static readonly DateTime dateLimitMin = new DateTime(2001, 1, 1, 0, 0, 0);
        private static readonly DateTime dateLimitMax = new DateTime(2099, 1, 1, 0, 0, 0);
        private static readonly TimeSpan drillingDurationLimitMax = TimeSpan.FromDays(366);

        private readonly IAsbCloudDbContext db;
        private readonly IWellService wellService;
        private List<WellOperationCategory> categories = null!;
        public List<WellOperationCategory> Categories
        {
            get
            {
                if (categories is null)
                {
                    categories = db.WellOperationCategories
                        .Where(c => c.Id >= 5000)
                        .AsNoTracking()
                        .ToList();
                }

                return categories;
            }
        }

        private List<WellSectionType> sections = null!;
        public List<WellSectionType> Sections
        {
            get
            {
                if (sections is null)
                    sections = db.WellSectionTypes
                        .AsNoTracking()
                        .ToList();
                return sections;
            }
        }

        // TODO: use WellOperationRepository instead of DB
        public WellOperationImportService(IAsbCloudDbContext db, IWellService wellService)
        {
            this.db = db;
            this.wellService = wellService;
        }

        public void Import(int idWell, Stream stream, int idUser, bool deleteWellOperationsBeforeImport = false)
        {
            using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled);
            var operations = ParseFileStream(stream);
            foreach (var operation in operations)
            {
                operation.IdWell = idWell;
                operation.IdUser = idUser;
            } 

            SaveOperations(idWell, operations, deleteWellOperationsBeforeImport);
        }

        public Stream Export(int idWell)
        {
            var operations = db.WellOperations
                .Include(o => o.WellSectionType)
                .Include(o => o.OperationCategory)
                .Where(o => o.IdWell == idWell)
                .OrderBy(o => o.DateStart)
                .AsNoTracking()
                .ToList();

            var timezone = wellService.GetTimezone(idWell);

            return MakeExelFileStream(operations, timezone.Hours);
        }

        public Stream GetExcelTemplateStream()
        {
            var resourceName = System.Reflection.Assembly.GetExecutingAssembly()
                .GetManifestResourceNames()
                .FirstOrDefault(n => n.EndsWith("WellOperationImportTemplate.xlsx"))!;

            var stream = System.Reflection.Assembly.GetExecutingAssembly()
                .GetManifestResourceStream(resourceName)!;
            return stream;
        }

        private Stream MakeExelFileStream(IEnumerable<WellOperation> operations, double timezoneOffset)
        {
            using Stream ecxelTemplateStream = GetExcelTemplateStream();

            using var workbook = new XLWorkbook(ecxelTemplateStream, XLEventTracking.Disabled);
            AddOperationsToWorkbook(workbook, operations, timezoneOffset);

            MemoryStream memoryStream = new MemoryStream();
            workbook.SaveAs(memoryStream, new SaveOptions { });
            memoryStream.Seek(0, SeekOrigin.Begin);
            return memoryStream;
        }

        private static void AddOperationsToWorkbook(XLWorkbook workbook, IEnumerable<WellOperation> operations, double timezoneOffset)
        {
            var planOperations = operations.Where(o => o.IdType == 0);
            if (planOperations.Any())
            {
                var sheetPlan = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan);
                if (sheetPlan is not null)
                    AddOperationsToSheet(sheetPlan, planOperations, timezoneOffset);
            }

            var factOperations = operations.Where(o => o.IdType == 1);
            if (factOperations.Any())
            {
                var sheetFact = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNameFact);
                if (sheetFact is not null)
                    AddOperationsToSheet(sheetFact, factOperations, timezoneOffset);
            }
        }

        private static void AddOperationsToSheet(IXLWorksheet sheet, IEnumerable<WellOperation> operations, double timezoneOffset)
        {
            var operationsList = operations.ToList();
            for (int i = 0; i < operationsList.Count; i++)
            {
                var row = sheet.Row(1 + i + headerRowsCount);
                AddOperationToRow(row, operationsList[i], timezoneOffset);
            }
        }

        private static void AddOperationToRow(IXLRow row, WellOperation operation, double timezoneOffset)
        {
            row.Cell(columnSection).Value = operation.WellSectionType?.Caption;
            row.Cell(columnCategory).Value = operation.OperationCategory?.Name;
            row.Cell(columnCategoryInfo).Value = operation.CategoryInfo;
            row.Cell(columnDepthStart).Value = operation.DepthStart;
            row.Cell(columnDepthEnd).Value = operation.DepthEnd;
            row.Cell(columnDate).Value = operation.DateStart.ToRemoteDateTime(timezoneOffset);
            row.Cell(columnDuration).Value = operation.DurationHours;
            row.Cell(columnComment).Value = operation.Comment;
        }

        private void SaveOperations(int idWell, IEnumerable<WellOperationDto> operations, bool deleteWellOperationsBeforeImport = false)
        {
            var timezone = wellService.GetTimezone(idWell);

            var transaction = db.Database.BeginTransaction();
            try
            {
                if (deleteWellOperationsBeforeImport)
                    db.WellOperations.RemoveRange(db.WellOperations.Where(o => o.IdWell == idWell));
                var entities = operations.Select(o =>
                {
                    var entity = o.Adapt<WellOperation>();
                    entity.IdWell = idWell;
                    entity.DateStart = o.DateStart.ToUtcDateTimeOffset(timezone.Hours);
                    entity.LastUpdateDate = DateTimeOffset.UtcNow;
                    return entity;
                });
                db.WellOperations.AddRange(entities);
                db.SaveChanges();
                transaction.Commit();
            }
            catch
            {
                transaction.Rollback();
                throw;
            }
        }

        private IEnumerable<WellOperationDto> ParseFileStream(Stream stream)
        {
            using var workbook = new XLWorkbook(stream, XLEventTracking.Disabled);
            return ParseWorkbook(workbook);
        }

        private IEnumerable<WellOperationDto> ParseWorkbook(IXLWorkbook workbook)
        {
            var sheetPlan = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan);
            if (sheetPlan is null)
                throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}.");

            var sheetFact = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNameFact);
            if (sheetFact is null)
                throw new FileFormatException($"Книга excel не содержит листа {sheetNameFact}.");

            //sheetPlan.RangeUsed().RangeAddress.LastAddress.ColumnNumber
            var wellOperations = new List<WellOperationDto>();

            var wellOperationsPlan = ParseSheet(sheetPlan, 0);
            wellOperations.AddRange(wellOperationsPlan);

            var wellOperationsFact = ParseSheet(sheetFact, 1);
            wellOperations.AddRange(wellOperationsFact);

            return wellOperations;
        }

        private IEnumerable<WellOperationDto> ParseSheet(IXLWorksheet sheet, int idType)
        {

            if (sheet.RangeUsed().RangeAddress.LastAddress.ColumnNumber < 7)
                throw new FileFormatException($"Лист {sheet.Name} содержит меньшее количество столбцов.");

            var count = sheet.RowsUsed().Count() - headerRowsCount;

            if (count > 1024)
                throw new FileFormatException($"Лист {sheet.Name} содержит слишком большое количество операций.");

            if (count <= 0)
                return new List<WellOperationDto>();

            var operations = new List<WellOperationDto>(count);
            var parseErrors = new List<string>();
            DateTime lastOperationDateStart = new DateTime();
            for (int i = 0; i < count; i++)
            {
                var row = sheet.Row(1 + i + headerRowsCount);
                try
                {
                    var operation = ParseRow(row, idType);
                    operations.Add(operation);

                    if (lastOperationDateStart > operation.DateStart)
                        parseErrors.Add($"Лист {sheet.Name} строка {row.RowNumber()} дата позднее даты предыдущей операции.");

                    lastOperationDateStart = operation.DateStart;
                }
                catch (FileFormatException ex)
                {
                    parseErrors.Add(ex.Message);
                }
            };

            if (parseErrors.Any())
                throw new FileFormatException(string.Join("\r\n", parseErrors));
            else
            {
                if (operations.Any())
                    if (operations.Min(o => o.DateStart) - operations.Max(o => o.DateStart) > drillingDurationLimitMax)
                        parseErrors.Add($"Лист {sheet.Name} содержит диапазон дат больше {drillingDurationLimitMax}");
            }

            return operations;
        }

        private WellOperationDto ParseRow(IXLRow row, int idType)
        {
            var vSection = row.Cell(columnSection).Value;
            var vCategory = row.Cell(columnCategory).Value;
            var vCategoryInfo = row.Cell(columnCategoryInfo).Value;
            var vDepthStart = row.Cell(columnDepthStart).Value;
            var vDepthEnd = row.Cell(columnDepthEnd).Value;
            var vDate = row.Cell(columnDate).Value;
            var vDuration = row.Cell(columnDuration).Value;
            var vComment = row.Cell(columnComment).Value;

            var operation = new WellOperationDto { IdType = idType };

            if (vSection is string sectionName)
            {
                var section = Sections.Find(c => c.Caption.ToLower() == sectionName.ToLower());
                if (section is null)
                    throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} указана некорректная секция");

                operation.IdWellSectionType = section.Id;
                operation.WellSectionTypeName = section.Caption;
            }
            else
                throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана секция");

            if (vCategory is string categoryName)
            {
                var category = Categories.Find(c => c.Name.ToLower() == categoryName.ToLower());
                if (category is null)
                    throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} указана некорректная операция ({categoryName})");

                operation.IdCategory = category.Id;
                operation.CategoryName = category.Name;
            }
            else
                throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана операция");

            if (vCategoryInfo is not null)
                operation.CategoryInfo = vCategoryInfo.ToString();

            if (vDepthStart is double depthStart && depthStart >= 0d && depthStart <= 20_000d)
                operation.DepthStart = depthStart;
            else
                throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана глубина на начало операции");

            if (vDepthEnd is double depthEnd && depthEnd >= 0d && depthEnd <= 20_000d)
                operation.DepthEnd = depthEnd;
            else
                throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана глубина при завершении операции");

            if (vDate is DateTime date && date > dateLimitMin && date < dateLimitMax)
                operation.DateStart = date;
            else
                throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} неправильно указана дата/время начала операции");

            if (vDuration is double duration && duration >= 0d && duration <= 240d)
                operation.DurationHours = duration;
            else
                throw new FileFormatException($"Лист {row.Worksheet.Name}. Строка {row.RowNumber()} не указана длительность операции");

            if (vComment is not null)
                operation.Comment = vComment.ToString();

            return operation;
        }
    }

}