using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using AsbCloudApp.Data;
using AsbCloudApp.Data.ProcessMaps;
using AsbCloudApp.Exceptions;
using AsbCloudApp.Repositories;
using AsbCloudApp.Services;
using AsbCloudApp.Services.ProcessMaps;
using ClosedXML.Excel;

namespace AsbCloudInfrastructure.Services.ProcessMaps.WellDrilling;

/*
 * password for ProcessMapImportTemplate.xlsx is ASB2020!
 */
public class ProcessMapPlanImportWellDrillingService : IProcessMapPlanImportService
{
    private readonly IProcessMapPlanRepository<ProcessMapPlanWellDrillingDto> processMapPlanWellDrillingRepository;
    private readonly ICrudRepository<WellSectionTypeDto> wellSectionTypeRepository;
    private readonly IWellService wellService;

    private const string sheetNamePlan = "План";

    private const int headerRowsCount = 2;

    private const int columnWellSectionType = 1;
    private const int columnMode = 2;
    private const int columnDepthStart = 3;
    private const int columnDepthEnd = 4;
    private const int columnPressurePlan = 5;
    private const int columnPressureLimitMax = 6;
    private const int columnAxialLoadPlan = 7;
    private const int columnAxialLoadLimitMax = 8;
    private const int columnTopDriveTorquePlan = 9;
    private const int columnTopDriveTorqueLimitMax = 10;
    private const int columnTopDriveSpeedPlan = 11;
    private const int columnTopDriveSpeedLimitMax = 12;
    private const int columnFlowPlan = 13;
    private const int columnFlowLimitMax = 14;
    private const int columnRopPlan = 15;
    private const int columnUsageSaub = 16;
    private const int columnUsageSpin = 17;
    private const int columnComment = 18;

    private WellSectionTypeDto[] sections = null!;

    public ProcessMapPlanImportWellDrillingService(IProcessMapPlanRepository<ProcessMapPlanWellDrillingDto> processMapPlanWellDrillingRepository,
        ICrudRepository<WellSectionTypeDto> wellSectionTypeRepository,
        IWellService wellService)
    {
        this.processMapPlanWellDrillingRepository = processMapPlanWellDrillingRepository;
        this.wellSectionTypeRepository = wellSectionTypeRepository;
        this.wellService = wellService;
    }

    public async Task ImportAsync(int idWell, int idUser, bool deleteProcessMapPlansBeforeImport, Stream stream,
        CancellationToken cancellationToken)
    {
        sections = (await wellSectionTypeRepository.GetAllAsync(cancellationToken)).ToArray();

        using var workBook = new XLWorkbook(stream);

        var wellDrillingProcessMaps = ParseWorkBook(workBook);

        if (deleteProcessMapPlansBeforeImport)
            await processMapPlanWellDrillingRepository.RemoveByWellAsync(idWell);

        foreach (var wellDrillingProcessMap in wellDrillingProcessMaps)
        {
            wellDrillingProcessMap.IdWell = idWell;
            wellDrillingProcessMap.IdUser = idUser;
        }

        await processMapPlanWellDrillingRepository.InsertRangeAsync(wellDrillingProcessMaps, cancellationToken);
    }

    public async Task<(string Name, Stream File)> ExportAsync(int idWell, CancellationToken cancellationToken)
    {
        var well = await wellService.GetOrDefaultAsync(idWell, cancellationToken)
                   ?? throw new ArgumentInvalidException(nameof(idWell), $"Скважины с {idWell} не существует");

        sections = (await wellSectionTypeRepository.GetAllAsync(cancellationToken)).ToArray();

        var processMapPlans = (await processMapPlanWellDrillingRepository.GetByIdWellAsync(idWell,
            cancellationToken)).ToArray();

        var file = await GenerateExcelFileStreamAsync(processMapPlans, cancellationToken);

        var fileName = $"РТК-план бурение по скважине {well.Caption} куст {well.Cluster}.xlsx";

        return (fileName, file);
    }

    public async Task<(string Name, Stream File)> GetExcelTemplateStreamAsync(CancellationToken cancellationToken)
    {
        var resourceName = Assembly.GetExecutingAssembly()
            .GetManifestResourceNames()
            .FirstOrDefault(n => n.EndsWith("ProcessMapPlanImportWellDrillingTemplate.xlsx"))!;

        using var stream = Assembly.GetExecutingAssembly()
            .GetManifestResourceStream(resourceName)!;

        var memoryStream = new MemoryStream();
        await stream.CopyToAsync(memoryStream, cancellationToken);
        memoryStream.Position = 0;

        var name = "ЕЦП_шаблон_файла_РТК_бурение.xlsx";

        return (name, memoryStream);
    }

    private void AddToWorkbook(XLWorkbook workbook, IEnumerable<ProcessMapPlanWellDrillingDto> processMapPlanWellDrillings)
    {
        var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan)
                    ?? throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}.");

        AddToSheet(sheet, processMapPlanWellDrillings.ToArray());
    }

    private void AddToSheet(IXLWorksheet sheet, IList<ProcessMapPlanWellDrillingDto> processMapPlanWellDrillings)
    {
        if (!processMapPlanWellDrillings.Any())
            return;

        for (int i = 0; i < processMapPlanWellDrillings.Count; i++)
        {
            var row = sheet.Row(1 + i + headerRowsCount);
            AddToRow(row, processMapPlanWellDrillings[i]);
        }
    }

    private void AddToRow(IXLRow row, ProcessMapPlanWellDrillingDto processMapPlanWellDrillings)
    {
        row.Cell(columnWellSectionType).Value = sections.First(x => x.Id == processMapPlanWellDrillings.IdWellSectionType).Caption;
        row.Cell(columnMode).Value = GetModeCaption(processMapPlanWellDrillings.IdMode);
        row.Cell(columnDepthStart).Value = processMapPlanWellDrillings.DepthStart;
        row.Cell(columnDepthEnd).Value = processMapPlanWellDrillings.DepthEnd;
        row.Cell(columnPressurePlan).Value = processMapPlanWellDrillings.Pressure.Plan;
        row.Cell(columnPressureLimitMax).Value = processMapPlanWellDrillings.Pressure.LimitMax;
        row.Cell(columnAxialLoadPlan).Value = processMapPlanWellDrillings.AxialLoad.Plan;
        row.Cell(columnAxialLoadLimitMax).Value = processMapPlanWellDrillings.AxialLoad.LimitMax;
        row.Cell(columnTopDriveTorquePlan).Value = processMapPlanWellDrillings.TopDriveTorque.Plan;
        row.Cell(columnTopDriveTorqueLimitMax).Value = processMapPlanWellDrillings.TopDriveTorque.LimitMax;
        row.Cell(columnTopDriveSpeedPlan).Value = processMapPlanWellDrillings.TopDriveSpeed.Plan;
        row.Cell(columnTopDriveSpeedLimitMax).Value = processMapPlanWellDrillings.TopDriveSpeed.LimitMax;
        row.Cell(columnFlowPlan).Value = processMapPlanWellDrillings.Flow.Plan;
        row.Cell(columnFlowLimitMax).Value = processMapPlanWellDrillings.Flow.LimitMax;
        row.Cell(columnRopPlan).Value = processMapPlanWellDrillings.RopPlan;
        row.Cell(columnUsageSaub).Value = processMapPlanWellDrillings.UsageSaub;
        row.Cell(columnUsageSpin).Value = processMapPlanWellDrillings.UsageSpin;
        row.Cell(columnComment).Value = processMapPlanWellDrillings.Comment;
    }

    private IEnumerable<ProcessMapPlanWellDrillingDto> ParseWorkBook(IXLWorkbook workbook)
    {
        var sheet = workbook.Worksheets.FirstOrDefault(ws => ws.Name == sheetNamePlan)
                    ?? throw new FileFormatException($"Книга excel не содержит листа {sheetNamePlan}.");

        return ParseSheet(sheet);
    }

    private IEnumerable<ProcessMapPlanWellDrillingDto> ParseSheet(IXLWorksheet sheet)
    {
        const int columnsCount = 17;

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

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

        if (rowsCount <= 0)
            return Array.Empty<ProcessMapPlanWellDrillingDto>();

        var processMapPlans = new ProcessMapPlanWellDrillingDto[rowsCount];

        var parseErrors = new List<string>();

        for (int i = 0; i < processMapPlans.Length; i++)
        {
            var row = sheet.Row(1 + i + headerRowsCount);

            try
            {
                processMapPlans[i] = ParseRow(row);
            }
            catch (FileFormatException ex)
            {
                parseErrors.Add(ex.Message);
            }
        }

        if (parseErrors.Any())
            throw new FileFormatException(string.Join("\r\n", parseErrors));

        return processMapPlans;
    }

    private ProcessMapPlanWellDrillingDto ParseRow(IXLRow row)
    {
        var wellSectionTypeCaption = row.Cell(columnWellSectionType).GetCellValue<string>()?.Trim().ToLower();
        var modeName = row.Cell(columnMode).GetCellValue<string>()?.Trim().ToLower();
        var depthStart = row.Cell(columnDepthStart).GetCellValue<double>();
        var depthEnd = row.Cell(columnDepthEnd).GetCellValue<double>();
        var pressurePlan = row.Cell(columnPressurePlan).GetCellValue<double>();
        var pressureLimitMax = row.Cell(columnPressureLimitMax).GetCellValue<double>();
        var axialLoadPlan = row.Cell(columnAxialLoadPlan).GetCellValue<double>();
        var axialLoadLimitMax = row.Cell(columnAxialLoadLimitMax).GetCellValue<double>();
        var topDriveTorquePlan = row.Cell(columnTopDriveTorquePlan).GetCellValue<double>();
        var topDriveTorqueLimitMax = row.Cell(columnTopDriveTorqueLimitMax).GetCellValue<double>();
        var topDriveSpeedPlan = row.Cell(columnTopDriveSpeedPlan).GetCellValue<double>();
        var topDriveSpeedLimitMax = row.Cell(columnTopDriveSpeedLimitMax).GetCellValue<double>();
        var flowPlan = row.Cell(columnFlowPlan).GetCellValue<double>();
        var flowLimitMax = row.Cell(columnFlowLimitMax).GetCellValue<double>();
        var ropPlan = row.Cell(columnRopPlan).GetCellValue<double>();
        var usageSaub = row.Cell(columnUsageSaub).GetCellValue<double>();
        var usageSpin = row.Cell(columnUsageSpin).GetCellValue<double>();
        var comment = row.Cell(columnComment).GetCellValue<string?>();

        var wellSection = sections.FirstOrDefault(s => s.Caption.Trim().ToLower() == wellSectionTypeCaption)
                          ?? throw new FileFormatException(
                              $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная секция");

        if(string.IsNullOrEmpty(modeName))
            throw new FileFormatException(
                         $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} не указан режим");

        var idMode = GetIdMode(modeName)
                     ?? throw new FileFormatException(
                         $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректный режим");

        if (depthStart is < 0 or > 99999.9)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная стартовая глубина");

        if (depthEnd is < 0 or > 99999.9)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указана некорректная конечная глубина");

        if (pressurePlan is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение перепада давления");

        if (pressureLimitMax is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение перепада давления");

        if (axialLoadPlan is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение нагрузки");

        if (axialLoadLimitMax is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение нагрузки");

        if (topDriveTorquePlan is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение момента на ВСП");

        if (topDriveTorqueLimitMax is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение момента на ВСП");

        if (topDriveSpeedPlan is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение оборотов на ВСП");

        if (topDriveSpeedLimitMax is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничения оборота на ВСП");

        if (flowPlan is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение расхода");

        if (flowLimitMax is < 0 or > 50000)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное ограничение расхода");

        if (ropPlan is < 0 or > 99999.9)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указано некорректное плановое значение механической скорости");

        if (usageSaub is < 0 or > 100)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректный плановый процент использования АКБ");

        if (usageSpin is < 0 or > 100)
            throw new FileFormatException(
                $"Лист {row.Worksheet.Name}. В строке {row.RowNumber()} указан некорректные плановый процент использования spin master");

        return new()
        {
            IdWellSectionType = wellSection.Id,
            IdMode = idMode,
            DepthStart = depthStart,
            LastUpdate = DateTime.UtcNow,
            DepthEnd = depthEnd,
            Pressure = new()
            {
                Plan = pressurePlan,
                LimitMax = pressureLimitMax
            },
            AxialLoad = new()
            {
                Plan = axialLoadPlan,
                LimitMax = axialLoadLimitMax
            },
            TopDriveTorque = new()
            {
                Plan = topDriveTorquePlan,
                LimitMax = topDriveTorqueLimitMax
            },
            TopDriveSpeed = new()
            {
                Plan = topDriveSpeedPlan,
                LimitMax = topDriveSpeedLimitMax
            },
            Flow = new()
            {
                Plan = flowPlan,
                LimitMax = flowLimitMax
            },
            RopPlan = ropPlan,
            UsageSaub = usageSaub,
            UsageSpin = usageSpin,
            Comment = comment
        };
    }

    private async Task<Stream> GenerateExcelFileStreamAsync(ProcessMapPlanWellDrillingDto[] processMapPlanWellDrillings,
        CancellationToken cancellationToken)
    {
        using var excelTemplateStream = (await GetExcelTemplateStreamAsync(cancellationToken)).File;

        using var workbook = new XLWorkbook(excelTemplateStream, XLEventTracking.Disabled);

        AddToWorkbook(workbook, processMapPlanWellDrillings);

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

    private static int? GetIdMode(string modeName) =>
        modeName switch
        {
            "ручной" => 0,
            "ротор" => 1,
            "слайд" => 2,
            _ => null
        };

    private static string GetModeCaption(int idMode)
        => idMode switch
        {
            1 => "Ротор",
            2 => "Слайд",
            _ => "Ручной",
        };
}