using Valg.EVA.Skanning.Contracts.CommonEnums;
using Valg.EVA.Skanning.Commands;
using Valg.EVA.Skanning.Commands.SaveBallotPaperVerifications;
using Valg.EVA.Skanning.Commands.SaveCombinedScanImage;
using Valg.EVA.Skanning.Commands.UpdateBallotPaperSeenStatus;
using Valg.EVA.Skanning.Repositories.BallotPaper;
using Valg.EVA.Skanning.Repositories.Box;
using Valg.EVA.Skanning.Repositories.Counting;
using Valg.EVA.Skanning.ServicesModel.Internal.Verification;
using Valg.EVA.Skanning.ServicesModel.ServiceContracts.Verification.Read;
using Valg.EVA.Skanning.ServicesModel.ServiceContracts.Verification.Write;
using Valg.EVA.Skanning.Tools;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Valg.EVA.Skanning.Contracts;
using Valg.EVA.Skanning.Diagnostics;
using Valg.EVA.Skanning.Entity.ReadModel;
using Valg.EVA.Skanning.Services.SystemSettings;
using Valg.EVA.Skanning.ServicesModel.ElectionConfig.DataProvider;
using Valg.EVA.Skanning.ServicesModel.Internal;
using Valg.EVA.Skanning.ServicesModel.Internal.SystemSettings;
using BallotPaper = Valg.EVA.Skanning.ServicesModel.ServiceContracts.Verification.Read.BallotPaper;
using BallotPaperPositionInBox = Valg.EVA.Skanning.ServicesModel.ServiceContracts.Verification.Read.BallotPaperPositionInBox;

namespace Valg.EVA.Skanning.Services.Verification
{
    public class VerificationService : IVerificationService
    {
        private readonly IBallotPaperMapper ballotPaperMapper;
        private readonly IBallotPaperRepository ballotPaperRepository;
        private readonly IBallotPaperToVerifyResolver ballotPaperToVerifyResolver;
        private readonly IBoxRepository boxRepository;
        private readonly ICommandDispatcher commandDispatcher;
        private readonly ICountingRepository countingRepository;
        private readonly ITiffConverter tiffConverter;
        private readonly ICandidateListDataProvider candidateListDataProvider;
        private readonly ITimingProbe timingProbe;
        private readonly IDatabaseConfiguration databaseConfiguration;
        private readonly ISystemSettingsService systemSettingsService;

        public VerificationService(ICommandDispatcher commandDispatcher, IBallotPaperToVerifyResolver ballotPaperToVerifyResolver, IBoxRepository boxRepository,
            IBallotPaperRepository ballotPaperRepository, IBallotPaperMapper ballotPaperMapper, ICountingRepository countingRepository,
            ITiffConverter tiffConverter, ICandidateListDataProvider candidateListDataProvider, 
            ITimingProbe timingProbe, IDatabaseConfiguration databaseConfiguration,
            ISystemSettingsService systemSettingsService)
        {
            this.commandDispatcher = commandDispatcher;
            this.ballotPaperToVerifyResolver = ballotPaperToVerifyResolver;
            this.boxRepository = boxRepository;
            this.ballotPaperRepository = ballotPaperRepository;
            this.ballotPaperMapper = ballotPaperMapper;
            this.countingRepository = countingRepository;
            this.tiffConverter = tiffConverter;
            this.candidateListDataProvider = candidateListDataProvider;
            this.timingProbe = timingProbe;
            this.databaseConfiguration = databaseConfiguration;
            this.systemSettingsService = systemSettingsService;
        }

        public void CreateAndSaveCombinedScanImage(CreateAndSaveCombinedScanImageParameters parameters)
        {
            var images = ballotPaperRepository.GetBallotPaperImages(parameters.BallotPaperId);

            byte[] combinedImage = tiffConverter.GetCombinedImage(images.ImageFront, images.ImageBack);

            var command = new SaveCombinedScanImageCommand(parameters.BallotPaperId, combinedImage);

            commandDispatcher.DispatchCommand(command);
        }

        public GetBallotPaperResult GetBallotPaper(GetBallotPaperParameters parameters)
        {
            GetBallotPaperResult result = null;

            timingProbe.Measure(() =>
            {
                var ballotPaper = ballotPaperMapper.GetBallotPaper(parameters.BallotPaperId);
                var counting = CountingMapper.MapCounting(countingRepository.GetCountingByBoxCountingId(ballotPaper.BoxCountingId));
                var totalToVerification = ballotPaperRepository.GetTotalBallotPapersForVerificationCount(ballotPaper.BoxCountingId);
                result = new GetBallotPaperResult(ballotPaper, counting, totalToVerification);
            }, () => new DefaultMeasurementEventData(parameters.CountingEmlAreaContextId, (int) ServicesInstrumentedEvents.GetBallotPaper, 
            ServicesInstrumentedEvents.GetBallotPaper.ToString(), databaseConfiguration.DatabaseServerName));

            return result;
        }

        public GetBallotPaperIdsResult GetBallotPaperIdsVerified(GetBallotPaperIdsVerifiedParameters parameters)
        {
            var result = new GetBallotPaperIdsResult();
            var ballotPaperIds = boxRepository.GetBallotPapersVerifiedByUserAndInBox(parameters.BoxCountingId, parameters.FilterByUser);
            result.BallotPaperIds = ballotPaperIds;
            return result;
        }

        public GetBallotPaperImagesResult GetBallotPaperImages(GetBallotPaperImagesParameters parameters)
        {
            BallotPaperImages images = null;
            BallotPaperLayoutDescription layoutDescription = null;

            timingProbe.Measure(() =>
            {
                images = ballotPaperRepository.GetBallotPaperImages(parameters.BallotPaperId);

                if(parameters.IncludeLayoutDescription)
                {
                    layoutDescription = TemplateBasedLayoutDescriptionProvider.GetBallotPaperLayoutDescription(parameters.BallotPaperType);
                }
            }, () => new DefaultMeasurementEventData(parameters.CountingEmlAreaContextId,
                (int) ServicesInstrumentedEvents.LoadBallotPaperImages,
                ServicesInstrumentedEvents.LoadBallotPaperImages.ToString(),
                databaseConfiguration.DatabaseServerName));

            return new GetBallotPaperImagesResult(images.ImageFront, images.ImageBack, images.RotationFront, images.RotationBack, layoutDescription);
        }

        public GetBoxAggregatedStatusForVerifyResult GetBoxAggregatedStatusForVerify(GetBoxAggregatedStatusForVerifyParameters parameters)
        {
            var result = new GetBoxAggregatedStatusForVerifyResult();

            var boxesNotCompletelyVerified = boxRepository
                .GetBoxesWithAggregatedToVerifyStatus(parameters.ContestId, parameters.AreaContextFilter, parameters.ElectionEventId)
                .Where(box => box.EmlAreaContextId.StartsWith(parameters.EmlAreaContextId));
            result.BoxesNotCompletelyVerified = boxesNotCompletelyVerified.Select(MapSingleBoxAggregatedStatusForVerify).ToArray();

            var boxesPartiallyOrCompletelyVerifiedInTotal = boxRepository
                .GetBoxesWithAggregatedPartiallyOrCompletelyVerifiedStatus(true, string.Empty, parameters.ContestId, parameters.AreaContextFilter, parameters.ElectionEventId)
                .Where(box => box.EmlAreaContextId.StartsWith(parameters.EmlAreaContextId));

            result.BoxesPartiallyOrCompletelyVerified = MapBoxesPartiallyOrCompletelyVerified(boxesPartiallyOrCompletelyVerifiedInTotal);

            UpdateNumberOfBallotPapersVerifiedByCurrentUser(parameters, result);

            RemoveBoxesPartiallyOrCompletelyVerifiedNotVerifiedByCurrentUser(result);

            return result;
        }

        private List<BoxAggregatedStatusForVerifiedBoxes> MapBoxesPartiallyOrCompletelyVerified(IEnumerable<BoxWithAggregatedPartiallyOrCompletelyVerifiedStatus> fromCollection)
        {
            return fromCollection.Select(from => new BoxAggregatedStatusForVerifiedBoxes
            {
                BoxNumber = from.BarcodeLong.GetBoxNumberFromBarcodeLong(),
                OriginatingLocation = GetPollingDistrictName(from.BarcodeLong, from.BarcodeShort),
                VotingCategoryType = from.BarcodeLong.GetVotingCategoryTypeFromBarcodeLong(),
                NumberOfBallotPapersVerifiedInTotal = from.NumberOfBallotPapers,
                BoxCountingId = from.BoxCountingId,
                ElectionEventId = from.ElectionEventId
            }).ToList();
        }

        public BoxAggregatedStatusForVerify GetFirstBoxAggregatedStatusForVerify(string contestId, string areaContextFilter, string emlAreaContextId, string electionEventId)
        {
            var newBoxToVerify = new BoxAggregatedStatusForVerify();
            var totalBoxesLeftToVerify = boxRepository.GetBoxesWithAggregatedToVerifyStatus(contestId, areaContextFilter, electionEventId)
                .Where(box => box.EmlAreaContextId.StartsWith(emlAreaContextId)).ToArray();

            if (totalBoxesLeftToVerify.Length > 0)
            {
                var first = totalBoxesLeftToVerify.First();
                newBoxToVerify = MapSingleBoxAggregatedStatusForVerify(first);
            }
            return newBoxToVerify;
        }

        private BoxAggregatedStatusForVerify MapSingleBoxAggregatedStatusForVerify(BoxWithAggregatedToVerifyStatus from)
        {
            return new BoxAggregatedStatusForVerify
            {
                BoxCountingId = from.BoxCountingId,
                BoxNumber = from.BarcodeLong.GetBoxNumberFromBarcodeLong(),
                OriginatingLocation = GetPollingDistrictName(from.BarcodeLong, from.BarcodeShort),
                VotingCategoryType = from.BarcodeLong.GetVotingCategoryTypeFromBarcodeLong(),
                NumberOfBallotPapers = from.NumberOfBallotPapersToVerify,
                ElectionEventId = from.ElectionEventId
            };
        }

        private string GetPollingDistrictName(string barcodeLong, string barcodeShort)
        {
            var electionEventId = barcodeLong.GetElectionEventIdFromBarcodeLong();
            var district = candidateListDataProvider.GetPollingDistrict(electionEventId, barcodeShort);
            return district.GetPollingDistrictName();
        }

        public GetNextBallotPaperToVerifyResult GetNextBallotPaperToVerify(GetNextBallotPaperToVerifyParameters parameters)
        {
            BallotPaper ballotPaperFromDatabase = null;
            bool shouldRetry = true;

            Exception possibleException = null;
            while (shouldRetry)
            {
                try
                {
                    shouldRetry = false;

                    ballotPaperFromDatabase = ballotPaperToVerifyResolver.GetNextBallotPaperToVerify(parameters.BoxCountingId, parameters.UserRequestingTheBallotPaper);
                }
                catch (DBConcurrencyException dbe)
                {
                    EvaSkanningEventSource.Log.ConcurrencyError(GetType().ToString(), dbe.ToString());
                    
                    shouldRetry = true;
                }
                catch (Exception ex)
                {
                    possibleException = ex;
                    EvaSkanningEventSource.Log.GetNextBallotToVerifyFailed(ex.ToString());
                    shouldRetry = false;
                }
            }

            if (possibleException != null)
            {
                return new GetNextBallotPaperToVerifyResult(possibleException);
            }
            if (ballotPaperFromDatabase != null)
            {
                var counting = CountingMapper.MapCounting(countingRepository.GetCountingByBoxCountingId(ballotPaperFromDatabase.BoxCountingId));
                var totalToVerification = ballotPaperRepository.GetTotalBallotPapersForVerificationCount(ballotPaperFromDatabase.BoxCountingId);
                return new GetNextBallotPaperToVerifyResult(ballotPaperFromDatabase, counting, totalToVerification);
            }

            return GetNextBallotPaperToVerifyResult.NoBallotPaperWasFoundResult;
        }

        public bool IsBoxStillOpenForVerification(int boxCountingId)
        {
            var boxCounting = countingRepository.GetBoxCountingById(boxCountingId);
            return boxCounting?.State == BoxCountingState.ReadyForVerify;
        }

        public RevokeBallotPaperSeenStatusResult RevokeBallotPaperSeenStatus(RevokeBallotPaperSeenStatusParameters parameters)
        {
            var command = new UpdateBallotPaperSeenStatusCommand { BallotPaperId = parameters.BallotPaperId, RevokeSeenStatus = true, BallotPaperConcurrencyToken = parameters.BallotPaperConcurrrencyToken, ChangedBy = parameters.RevokedBy };
            var result = commandDispatcher.DispatchCommand<UpdateBallotPaperSeenStatusCommand, UpdateBallotPaperSeenStatusCommandResult>(command);
            return new RevokeBallotPaperSeenStatusResult(result.BallotWasRevoked, result.BallotWasAlreadyValidAndNotRevoked);
        }

        public SaveBallotPaperVerificationsResult SaveBallotPaperVerifications(SaveBallotPaperVerificationsParameters parameters)
        {
            var command = new SaveBallotPaperVerificationsCommand(parameters.BallotNumberVerification, parameters.PersonVoteVerifications, parameters.BallotPaperId,
                parameters.BallotPaperConcurrencyToken, parameters.RejectionReasonVerification, parameters.StampVerification, parameters.ChangedBy, parameters.WriteInVerifications, parameters.IsInEmergencyMode, parameters.RenumberStrikeoutVerifications, parameters.IsAffiliationHeadersValid);

            SaveBallotPaperVerificationsResult result = null;
            timingProbe.Measure(() =>
            {
                result = commandDispatcher.DispatchCommand<SaveBallotPaperVerificationsCommand, SaveBallotPaperVerificationsResult>(command);

                if (ShouldCreateAndSaveCombinedImage(parameters.RejectionReasonVerification, result))
                {
                    CreateAndSaveCombinedScanImage(new CreateAndSaveCombinedScanImageParameters(parameters.BallotPaperId));
                }
            }, () => new DefaultMeasurementEventData(parameters.CountingEmlAreaContextId, 
                (int) ServicesInstrumentedEvents.SaveBallotPaperVerifications, 
                ServicesInstrumentedEvents.SaveBallotPaperVerifications.ToString(),
                databaseConfiguration.DatabaseServerName));
            
            

            return result;
        }

        public BallotPaperPositionInBox GetBallotPaperPositionInBox(int ballotPaperId, int boxCountingId)
        {
            var from = ballotPaperRepository.GetBallotPaperPositionInBox(ballotPaperId, boxCountingId);
            var ballotPaperPositionInBox = new BallotPaperPositionInBox
            {
                PlacementIndex = from.PlacementIndex,
                TotalNumberOfBallotPapers = from.TotalNumberOfBallotPapers
            };
            return ballotPaperPositionInBox;
        }

        private void RemoveBoxesPartiallyOrCompletelyVerifiedNotVerifiedByCurrentUser(GetBoxAggregatedStatusForVerifyResult result)
        {
            foreach (var box in result.BoxesPartiallyOrCompletelyVerified.ToArray())
            {
                if (box.NumberOfBallotPapersVerifiedByCurrentUser == 0)
                {
                    result.BoxesPartiallyOrCompletelyVerified.Remove(box);
                }
            }
        }

        private bool ShouldCreateAndSaveCombinedImage(RejectionReasonVerification rejectionReasonVerification, SaveBallotPaperVerificationsResult result)
        {
            return
                rejectionReasonVerification != null &&
                rejectionReasonVerification.NewValue != RejectionReasonType.NotRejected &&
                result.Success &&
                result.SaveBallotPaperVerificationsOperationResult == SaveBallotPaperVerificationsOperationResult.DataUpdated;
        }

        private void UpdateNumberOfBallotPapersVerifiedByCurrentUser(GetBoxAggregatedStatusForVerifyParameters parameters, GetBoxAggregatedStatusForVerifyResult result)
        {
            var boxesPartiallyOrCompletelyVerifiedByUser = boxRepository.GetBoxesWithAggregatedPartiallyOrCompletelyVerifiedStatus(false, parameters.CurrentUserName, parameters.ContestId, parameters.AreaContextFilter, parameters.ElectionEventId)
                .Where(box => box.EmlAreaContextId.StartsWith(parameters.EmlAreaContextId));
            foreach (var verifiedByUser in boxesPartiallyOrCompletelyVerifiedByUser)
            {
                var boxWithTotalInfo = result.BoxesPartiallyOrCompletelyVerified.First(b => b.BoxCountingId == verifiedByUser.BoxCountingId);
                boxWithTotalInfo.NumberOfBallotPapersVerifiedByCurrentUser = verifiedByUser.NumberOfBallotPapers;
            }
        }
    }
}
