using System;
using Valg.EVA.Skanning.Commands.DetectDuplicatesInCounting;
using Valg.EVA.Skanning.Commands.SaveInterpreterModifications;
using Valg.EVA.Skanning.Contracts;
using Valg.EVA.Skanning.Commands;
using Valg.EVA.Skanning.Commands.SaveScannedBallotPaper;
using Valg.EVA.Skanning.Commands.SetBoxCompletelyScanned;
using Valg.EVA.Skanning.Commands.StartScanning;
using Valg.EVA.Skanning.Entity.SerializedEntities.FieldValueModel;
using Valg.EVA.Skanning.Repositories.Counting;
using Valg.EVA.Skanning.ServicesModel.Internal.Interpreter;
using Valg.EVA.Skanning.ServicesModel.Internal.Serializers;
using System.Collections.Generic;
using System.Linq;
using Valg.EVA.Skanning.Contracts.CommonEnums;
using Valg.EVA.Skanning.Commands.RegisterBox;
using Valg.EVA.Skanning.ServicesModel.ElectionConfig.DataProvider;
using Valg.EVA.Skanning.ServicesModel.Internal;
using Valg.EVA.Skanning.ServicesModel.ServiceContracts.Box.Read;
using Valg.EVA.Skanning.ServicesModel.ServiceContracts.Count.Read;
using Valg.EVA.Skanning.ServicesModel.ServiceContracts.Count.Write;
using Valg.EVA.Skanning.ServicesModel.ServiceContracts.Scan.Write;

using Valg.EVA.Skanning.Diagnostics;

namespace Valg.EVA.Skanning.Services.Scan
{
    public class ScanService : IScanService
    {
        private readonly ICommandDispatcher commandDispatcher;
        private readonly IBoxContentsInterpreter boxContentsInterpreter;
        private readonly IWriteInFieldGenerator writeInFieldGenerator;
        private readonly ICountingRepository countingRepository;
        private readonly BallotIdValidator ballotIdValidator;
        private readonly IElectionConfigDataProvider electionConfigDataProvider;
        private readonly ICandidateListDataProvider candidateListDataProvider;
        private readonly IMunicipalityDataProvider municipalityDataProvider;
        private readonly CountingUniqueIdCreator countingUniqueIdCreator;
        private readonly ITimingProbe timingProbe;
        private readonly IDatabaseConfiguration databaseConfiguration;

        public ScanService(ICommandDispatcher commandDispatcher, IBoxContentsInterpreter boxContentsInterpreter,
            IWriteInFieldGenerator writeInFieldGenerator, ICountingRepository countingRepository,
            BallotIdValidator ballotIdValidator, IElectionConfigDataProvider electionConfigDataProvider,
            ICandidateListDataProvider candidateListDataProvider, IMunicipalityDataProvider municipalityDataProvider,
            CountingUniqueIdCreator countingUniqueIdCreator, ITimingProbe timingProbe, IDatabaseConfiguration databaseConfiguration)
        {
            this.commandDispatcher = commandDispatcher;
            this.boxContentsInterpreter = boxContentsInterpreter;
            this.writeInFieldGenerator = writeInFieldGenerator;
            this.countingRepository = countingRepository;
            this.ballotIdValidator = ballotIdValidator;
            this.electionConfigDataProvider = electionConfigDataProvider;
            this.candidateListDataProvider = candidateListDataProvider;
            this.municipalityDataProvider = municipalityDataProvider;
            this.countingUniqueIdCreator = countingUniqueIdCreator;
            this.timingProbe = timingProbe;
            this.databaseConfiguration = databaseConfiguration;
        }

        
        public RegisterBoxForCountingResult RegisterBoxForCounting(RegisterBoxForCountingParameters parameters)
        {
            var command = new RegisterBoxForCountingCommand(parameters.BarcodeLong, parameters.BarcodeShort, parameters.CountingEmlAreaContextId, parameters.ChangedBy, parameters.IsInEmergencyMode);
            var commandResult = commandDispatcher.DispatchCommand<RegisterBoxForCountingCommand, RegisterBoxForCountingCommandResult>(command);

            var result = new RegisterBoxForCountingResult
            {
                CountingId = commandResult.CountingId,
                BoxIsReadyForScan = commandResult.BoxIsReadyForScan
            };

            return result;
        }

        
        public ScannableCounting[] GetScannableCountings(string barcodeLong, string barcodeShort)
        {
            var uniqueId = countingUniqueIdCreator.Create(barcodeLong, barcodeShort);

            var countings = MapScannableCountings(uniqueId);
            var electionEventId = barcodeLong.GetElectionEventIdFromBarcodeLong();
            
            foreach (var c in countings)
            {
                c.Name = municipalityDataProvider.GetAreaName(electionEventId, c.CountingEmlAreaContextId);
            }
            return countings;
        }

        public BoxCounting GetBoxCountingById(int boxCountingId)
        {
            var boxCounting = countingRepository.GetBoxCountingById(boxCountingId);
            return new BoxCounting
            {
                BarcodeLong = boxCounting.BarcodeLong,
                BarcodeShort = boxCounting.BarcodeShort,
                BoxCountingState = boxCounting.State,
                Id = boxCounting.Id,
                CountingType = boxCounting.CountingType,
                ConcurrencyToken = boxCounting.Modified,
                CountNumber = boxCounting.CountNumber
            };
        }

        public SaveScannedBallotPaperResult SaveScannedBallotPaper(SaveScannedBallotPaperParameters parameters)
        {
            SaveScannedBallotPaperResult result = null;
            timingProbe.Measure(() =>
            {
                result = SaveScannedBallotPaperInternal(parameters);
            }, () => GetSaveScannedBallotPaperInstrumentationContext(parameters.CountingEmlAreaContextId));

            return result;
        }

        private SaveScannedBallotPaperResult SaveScannedBallotPaperInternal(SaveScannedBallotPaperParameters parameters)
        {
            var missingWriteInsResult = CreateMissingWriteIns(parameters);

            if (missingWriteInsResult.HasDatabaseErrors)
            {
                return SaveScannedBallotPaperResult.DatabaseErrorsResult(missingWriteInsResult.HasDatabaseErrors, missingWriteInsResult.DatabaseError);
            }
            else
            {
                var validBallotId = GetValidBallotId(parameters.ScannedFields.SingleOrDefault(f => f.IsBallotIdField()));

                var newFieldCollection = parameters.ScannedFields
                    .Where(f => !f.IsBallotIdField())
                    .Union(missingWriteInsResult.NewWriteIns).ToArray();

                var command = new SaveScannedBallotPaperCommand(
                    parameters.BallotPaperType,
                    parameters.ScannedImages,
                    newFieldCollection,
                    parameters.CreatedBy,
                    parameters.BarcodeLong,
                    parameters.BarcodeShort,
                    parameters.CountingIdBoxWasScannedFor,
                    parameters.BoxCountingId,
                    parameters.BoxCountingConcurrencyToken,
                    parameters.CountingEmlAreaContextId,
                    validBallotId,
                    parameters.ScannedOrder);

                var commandResult = commandDispatcher.DispatchCommand<SaveScannedBallotPaperCommand, SaveScannedBallotPaperCommandResult>(command);

                var result = new SaveScannedBallotPaperResult(commandResult.BoxCountingIsNotAcceptingScans,
                    commandResult.BoxCountingDoesNotExist,
                    commandResult.HasDatabaseErrors,
                    commandResult.DatabaseError,
                    commandResult.BallotPaperId);

                return result;
            }
        }

        private IMeasurementEventData GetSaveScannedBallotPaperInstrumentationContext(string geo)
        {
            var evt = ScanInstrumentedEvents.SaveScannedBallotPaper;
            var context = new DefaultMeasurementEventData(geo, (int)evt, evt.ToString(), databaseConfiguration.DatabaseServerName);
            return context;
        }

        private int? GetValidBallotId(ScannedField ballotIdField)
        {
            if (ballotIdField != null)
            {
                var isValid = ballotIdValidator.IsValid(ballotIdField.Value);
                if (isValid)
                {
                    return Convert.ToInt32(ballotIdField.Value);
                }
            }

            return null;
        }

        public BoxCountingWithElectionData GetBoxCountingWithElectionData(string barcodeLong, string barcodeShort, int countingId)
        {
            var boxCountingFromRepo = countingRepository.GetBoxCounting(barcodeLong, barcodeShort, countingId);
            var boxCounting = new BoxCounting
            {
                Id = boxCountingFromRepo.Id,
                CountingType = boxCountingFromRepo.CountingType,
                ConcurrencyToken = boxCountingFromRepo.Modified,
                BarcodeLong = boxCountingFromRepo.BarcodeLong,
                BarcodeShort = boxCountingFromRepo.BarcodeShort,
                BoxCountingState = boxCountingFromRepo.State,
                CountNumber = boxCountingFromRepo.CountNumber
            };
            var boxCountingWithElectionData = new BoxCountingWithElectionData(boxCounting);

            MapElectionData(boxCountingWithElectionData);

            return boxCountingWithElectionData;
        }

        private void MapElectionData(BoxCountingWithElectionData boxCountingWithElectionData)
        {
            var barcodeLong = boxCountingWithElectionData.BoxCounting.BarcodeLong;
            var barcodeShort = boxCountingWithElectionData.BoxCounting.BarcodeShort;

            var electionEventId = barcodeLong.GetElectionEventIdFromBarcodeLong();

            var election = electionConfigDataProvider.GetElection(electionEventId, barcodeLong.GetElectionIdFromBarcodeLong());
            var municipality = municipalityDataProvider.GetMunicipality(electionEventId, barcodeShort.GetMunicipalityCodeFromBarcodeShort());
            var votingCategory = electionConfigDataProvider.GetVotingCategory(electionEventId, barcodeShort, barcodeLong.GetVotingCategoryTypeFromBarcodeLong());
            var pollingDistrict = candidateListDataProvider.GetPollingDistrict(electionEventId, barcodeShort);

            boxCountingWithElectionData.Election = election.Name;
            boxCountingWithElectionData.Municipality = municipality.Name;
            boxCountingWithElectionData.County = municipality.CountyName;
            boxCountingWithElectionData.PollingDistrict = pollingDistrict.GetPollingDistrictName();
            boxCountingWithElectionData.BoxNumber = barcodeLong.GetBoxNumberFromBarcodeLong();
            boxCountingWithElectionData.TotalBoxes = barcodeLong.GetTotalBoxCountFromBarcodeLong();
            boxCountingWithElectionData.Category = votingCategory.Name;
        }

        private CreateEmptyWriteInFieldPlaceholdersResult CreateMissingWriteIns(SaveScannedBallotPaperParameters parameters)
        {
            var originalScannedWriteIns = parameters.ScannedFields.Where(f => f.IsWriteInField()).ToArray();
            ConvertValueToJsonSerializedObject(originalScannedWriteIns);
            return writeInFieldGenerator.CreateEmptyWriteInFieldPlaceholders(originalScannedWriteIns, parameters.BarcodeLong);
        }

        private static void ConvertValueToJsonSerializedObject(IReadOnlyCollection<ScannedField> fields)
        {
            foreach (var field in fields)
            {
                var writeInData = new WriteInData
                {
                    ScannerValue = field.Value,
                    NeededVerification = field.NeedsVerification,
                    NeedsVerification = field.NeedsVerification,
                    IgnoreWriteIn = false
                };
                field.SerializeWriteInDataToJson(writeInData);
            }
        }

        public StartScanningCommandResult StartScanning(StartScanningParameters parameters)
        {
            var command = new StartScanningCommand(
                parameters.BarcodeLong,
                parameters.BarcodeShort,
                parameters.CountingId,
                parameters.BoxCountingConcurrencyToken,
                parameters.ChangedBy);

            var commandResult = commandDispatcher.DispatchCommand<StartScanningCommand, StartScanningCommandResult>(command);

            return new StartScanningCommandResult
            {
                CountingIsNotActive = commandResult.CountingIsNotActive,
                BoxIsNotReadyForScan = commandResult.BoxIsNotReadyForScan
            };
        }

        public BoxIsCompletelyScannedResult BoxIsCompletelyScanned(BoxIsCompletelyScannedParameters parameters)
        {
            BoxIsCompletelyScannedResult result = null;
            timingProbe.Measure(() =>
            {
                result = CompleteScanOfBoxInternal(parameters);
            }, () => GetBoxIsCompletelyScannedInstrumentationContext(parameters.CountingEmlAreaContextId));

            return result;
        }

        private BoxIsCompletelyScannedResult CompleteScanOfBoxInternal(BoxIsCompletelyScannedParameters parameters)
        {
            var boxCounting = countingRepository.GetBoxCounting(parameters.BarcodeLong, parameters.BarcodeShort, parameters.CountingId);

            var performCompletionStep = boxCounting == null 
                                        || (boxCounting != null && boxCounting.State == BoxCountingState.Scanning);
            var performInterpretStep = (boxCounting != null && boxCounting.State == BoxCountingState.ScanComplete);
            var performDetectDuplicateStep = (boxCounting != null && boxCounting.State == BoxCountingState.ReadyForVerify);

            if (performCompletionStep)
            {
                var command = new SetBoxCompletelyScannedCommand(
                    parameters.BarcodeLong,
                    parameters.BarcodeShort,
                    parameters.CountingId,
                    parameters.BoxCountingConcurrencyToken,
                    parameters.CompletedByUser,
                    parameters.IsInEmergencyMode);

                var setBoxCompletelyScannedResult = commandDispatcher.DispatchCommand<SetBoxCompletelyScannedCommand, SetBoxCompletelyScannedResult>(command);

                if (!setBoxCompletelyScannedResult.Success)
                {
                    return BoxIsCompletelyScannedResult.BoxDeleted(null);
                }

                performInterpretStep = true;
            }

            if (performInterpretStep)
            {
                var tryInterpretResult = TryInterpret(parameters);
                if (!tryInterpretResult.Success)
                {
                    return BoxIsCompletelyScannedResult.InterpretFailed(tryInterpretResult.PossibleException);
                }

                performDetectDuplicateStep = true;
            }

            if (performDetectDuplicateStep)
            {
                var detectDuplicatesResult = TryDetectDuplicates(parameters, boxCounting.Id);
                if (!detectDuplicatesResult.Success)
                {
                    return BoxIsCompletelyScannedResult.DuplicateDetectionFailed(detectDuplicatesResult.PossibleException);
                }
            }

            return BoxIsCompletelyScannedResult.Success();
        }

        private IMeasurementEventData GetBoxIsCompletelyScannedInstrumentationContext(string eventGeoOrigin)
        {
            var ev = ScanInstrumentedEvents.BoxIsCompletelyScanned;
            return new DefaultMeasurementEventData(eventGeoOrigin, (int)ev, ev.ToString(), databaseConfiguration.DatabaseServerName);
        }

        protected virtual TryDuplicatesResult TryDetectDuplicates(BoxIsCompletelyScannedParameters parameters, int boxCountingId)
        {
            Exception possibleException = null;
            var success = true;
            try
            {
                var boxCounting = countingRepository.GetBoxCountingById(boxCountingId);
                commandDispatcher.DispatchCommand(new DetectDuplicatesInCountingCommand(boxCounting.Id, parameters.CountingId, parameters.CompletedByUser, boxCounting.Modified));
            }
            catch (Exception ex)
            {
                possibleException = ex;
                success = false;
                EvaSkanningEventSource.Log.DuplicateCheckStepFailed(ex.ToString());
            }
            return new TryDuplicatesResult(success, possibleException);
        }

        protected class TryDuplicatesResult
        {
            public Exception PossibleException { get; }
            public bool Success { get; }

            public TryDuplicatesResult(bool success, Exception possibleException)
            {
                Success = success;
                PossibleException = possibleException;
            }
        }

        protected virtual TryInterpretResult TryInterpret(BoxIsCompletelyScannedParameters parameters)
        {
            int? boxCountingId = null;
            bool success = true;
            Exception possibleException = null;
            try
            {
                var modificationsByInterpreter = boxContentsInterpreter.InterpretBoxContents(
                    parameters.BarcodeLong,
                    parameters.BarcodeShort,
                    parameters.CountingId,
                    parameters.CountingEmlAreaContextId);

                var command = new SaveInterpreterModificationsCommand
                {
                    BoxCountingBarcodeLong = modificationsByInterpreter.ActiveBoxCounting.BarcodeLong,
                    BoxCountingBarcodeShort = modificationsByInterpreter.ActiveBoxCounting.BarcodeShort,
                    CountingId = modificationsByInterpreter.ActiveBoxCounting.CountingId,
                    BoxCountingLastModified = modificationsByInterpreter.ActiveBoxCounting.Modified,
                    ModifiedFields = modificationsByInterpreter.GetModifiedFields(),
                    BallotPaperValuesToUpdate = modificationsByInterpreter.GetBallotPapersToUpdate(),
                    ChangedBy = parameters.CompletedByUser
                };

                commandDispatcher.DispatchCommand(command);
                boxCountingId = modificationsByInterpreter.ActiveBoxCounting.Id;
            }
            catch (Exception e)
            {
                possibleException = e;
                success = false;
                EvaSkanningEventSource.Log.BoxInterpretationStepFailed(e.ToString());
            }

            return new TryInterpretResult(boxCountingId, success, possibleException);
        }

        protected class TryInterpretResult
        {
            public int? BoxCountingId { get; }
            public bool Success { get; }
            public Exception PossibleException { get; }

            public TryInterpretResult(int? boxCountingId, bool success, Exception possibleException)
            {
                BoxCountingId = boxCountingId;
                Success = success;
                PossibleException = possibleException;
            }
        }

        private ScannableCounting[] MapScannableCountings(string uniqueId)
        {
            var scannableCountings = countingRepository.GetScannableCountings(uniqueId);
            return scannableCountings.Select(from => new ScannableCounting
            {
                CountingEmlAreaContextId = from.CountingEmlAreaContextId,
                Id = from.Id,
                UniqueId = from.UniqueId
            }).ToArray();
        }
    }
}
