using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Valg.EVA.Skanning.Contracts;
using Valg.EVA.Skanning.Contracts.CommonEnums;
using Valg.EVA.Skanning.Entity.ReadModel;
using Valg.EVA.Skanning.Entity.SerializedEntities.CountResultModel;
using Valg.EVA.Skanning.Entity.SerializedEntities.FieldValueModel;
using Valg.EVA.Skanning.Repositories.BallotPaper;
using Newtonsoft.Json;
using Valg.EVA.Skanning.ServicesModel.ElectionConfig.DataProvider;
using Valg.EVA.Skanning.ServicesModel.ElectionConfig.Model;
using Valg.EVA.Skanning.ServicesModel.Internal.BallotNumber;
using Valg.EVA.Skanning.ServicesModel.Internal.SystemSettings;
using Valg.EVA.Skanning.ServicesModel.Internal.Validation;

namespace Valg.EVA.Skanning.ServicesModel.Internal.CountResult
{
    public class CastVoteMapper
    {
        private readonly ICandidateListDataProvider candidateListDataProvider;
        private readonly IElectionConfigDataProvider electionConfigDataProvider;
        private readonly ISystemSettingsProvider systemSettingsProvider;
        private readonly IBallotPaperRepository ballotPaperRepository;
        private readonly FillPercentageRangeCheckerFactory fillPercentageRangeCheckerFactory;

        public CastVoteMapper(ICandidateListDataProvider candidateListDataProvider,
            IElectionConfigDataProvider electionConfigDataProvider,
            ISystemSettingsProvider systemSettingsProvider,
            IBallotPaperRepository ballotPaperRepository,
            FillPercentageRangeCheckerFactory fillPercentageRangeCheckerFactory)
        {
            this.candidateListDataProvider = candidateListDataProvider;
            this.electionConfigDataProvider = electionConfigDataProvider;
            this.systemSettingsProvider = systemSettingsProvider;
            this.ballotPaperRepository = ballotPaperRepository;
            this.fillPercentageRangeCheckerFactory = fillPercentageRangeCheckerFactory;
        }

        public ReadOnlyCollection<CastVote> MapToCastVotes(Counting counting, string reportingUnitIdentifier)
        {
            Guard.AssertAreaContextFormat(counting.EmlAreaContextId);

            var castVotes = new List<CastVote>();

            var ballotPaperIds = ballotPaperRepository.GetBallotPapersInCounting(counting.Id);
            var ballotPapers = ballotPaperIds.Select(id => ballotPaperRepository.GetBallotPaper(id));

            foreach (var ballotPaperDb in ballotPapers)
            {
                var castVote = new CastVote();
                castVotes.Add(castVote);

                castVote.ReportingUnitIdentifier = reportingUnitIdentifier;
                SetCountingSpecificData(counting, castVote);

                SetBallotPaperSpecificData(counting, ballotPaperDb, castVote);
            }

            return castVotes.AsReadOnly();
        }

        private static Candidate LookupAndAssertCandidate(Affiliation affiliation, int ballotPaperId, string votingFeature, int? candidateIndexToAssert)
        {
            var candidate = affiliation.Candidates.SingleOrDefault(p => p.RankInAffiliation == candidateIndexToAssert);
            if (candidate == null)
            {
                throw new InvalidOperationException(string.Format("Could not find the candidate the {4} was given for. " +
                                                                  "RankInAffiliation does not match given Affiliation. RankInAffiliation: {0}, Affiliation: ({1}, {2}), BallotPaperId = {3}",
                    candidateIndexToAssert, affiliation.AffiliationShortCode, affiliation.AffiliationId, ballotPaperId, votingFeature)
                    );
            }
            return candidate;
        }

        private void SetCountingSpecificData(Counting counting, CastVote castVote)
        {
            castVote.CountingType = counting.CountingType;
            castVote.VotingCategory = counting.VotingCategory;
            castVote.ElectionEventId = counting.ElectionEventId;
            castVote.ContestId = counting.ContestId;
            castVote.AreaContextId = counting.EmlAreaContextId;
            castVote.ElectionGroup = counting.ElectionGroup;
            castVote.ElectionIdentifier = counting.ElectionIdentifier;
        }

        private void SetBallotPaperSpecificData(Counting counting, BallotPaper ballotPaperDb, CastVote castVote)
        {
            castVote.BallotDbId = ballotPaperDb.Id;

            if (ballotPaperDb.IsValid)
            {
                if (ballotPaperDb.RejectionReasonType.GetValueOrDefault() == RejectionReasonType.NotRejected)
                {
                    var currentFieldValues = ballotPaperRepository.GetCurrentFieldValues(ballotPaperDb.Id);
                    var affiliation = LookupAndAssertAffiliation(currentFieldValues.SingleOrDefault(a => a.IsBallotNoField()),
                        ballotPaperDb.Id,
                        counting);

                    castVote.AffiliationIdentifier = GetAffilationIdentifier(affiliation);
                    castVote.ProposedRejection = CastVote.RejectionReasonCode.NotRejected;

                    if (!AreAllPersonalVotingFeaturesToBeIgnoredDueToCountingAuthority(counting)
                        && counting.IsFinalCounting
                        && !ballotPaperDb.IgnoreCandidates
                        && !affiliation.IsBlank)
                    {
                        var countMetrics = electionConfigDataProvider.GetCountMetricsForContest(counting.ElectionEventId, counting.ContestId);

                        MapPersonalVotingFeatures(castVote, countMetrics, currentFieldValues, affiliation, ballotPaperDb.Id, counting);
                    }
                }
                else
                {
                    castVote.ProposedRejection = ProposedRejectionHelper.GetProposedRejection(ballotPaperDb.RejectionReasonType.GetValueOrDefault(), counting.VotingCategory);
                }
            }
            else
            {
                if (!counting.IsFinalCounting)
                {
                    castVote.InDoubt = true;
                }
                else
                {
                    throw new InvalidOperationException(string.Format("Valid-state = false for ballotPaper with id = {0} is illegal at this point (FinalCounting)", ballotPaperDb.Id));
                }
            }
        }

        private void MapPersonalVotingFeatures(CastVote castVote, CountMetricsForContest countMetrics,
            ReadOnlyCollection<BallotPaperFieldValue> currentFieldValues, Affiliation affiliation, int ballotPaperId, Counting counting)
        {
            if (countMetrics.HasWriteIns.GetValueOrDefault())
                castVote.WriteIns = GetWriteIns(currentFieldValues, counting, ballotPaperId);

            if (countMetrics.HasPersonalVotes.GetValueOrDefault())
                castVote.PersonalVotes = GetPersonalVotes(currentFieldValues, affiliation, ballotPaperId);

            if (countMetrics.HasRenumbering.GetValueOrDefault())
                castVote.Renumberings = GetRenumberings(currentFieldValues, affiliation, ballotPaperId);

            if (countMetrics.HasStrikeouts.GetValueOrDefault())
                castVote.Strikeouts = GetStrikeouts(currentFieldValues, affiliation, ballotPaperId);
        }

        private List<Strikeout> GetStrikeouts(IEnumerable<BallotPaperFieldValue> currentFieldValues, Affiliation affiliation, int ballotPaperId)
        {
            var rangeChecker = fillPercentageRangeCheckerFactory.GetStrikeoutRangeChecker();
            var filter = new FilterForStrikeoutInclusion(rangeChecker, affiliation);

            return currentFieldValues.Where(filter.IsIncluded)
                .Select(value => MapStrikeout(value, affiliation, ballotPaperId))
                .Where(v => v != null)
                .ToList();
        }

        private List<Renumber> GetRenumberings(IEnumerable<BallotPaperFieldValue> currentFieldValues, Affiliation affiliation, int ballotPaperId)
        {
            var filter = new FilterForRenumberInclusion(affiliation);

            return currentFieldValues.Where(filter.IsIncluded)
                .Select(value => MapRenumber(value, affiliation, ballotPaperId))
                .Where(v => v != null)
                .ToList();
        }

        private List<PersonalVote> GetPersonalVotes(IEnumerable<BallotPaperFieldValue> currentFieldValues, Affiliation affiliation, int ballotPaperId)
        {
            var rangeChecker = fillPercentageRangeCheckerFactory.GetPersonVoteRangeChecker();
            var filter = new FilterForPersonVoteInclusion(rangeChecker, affiliation);

            var toReturn = new List<PersonalVote>();

            foreach (var personVote in currentFieldValues.Where(filter.IsIncluded))
            {
                var candidate = LookupAndAssertCandidate(affiliation, ballotPaperId, "personalVote", personVote.FieldIndex);
                if (candidate != null)
                {
                    toReturn.Add(new PersonalVote
                    {
                        RankInAffiliation = candidate.RankInAffiliation.ToString()
                    });
                }
            }

            return toReturn;
        }

        private List<WriteIn> GetWriteIns(IEnumerable<BallotPaperFieldValue> currentFieldValues, Counting counting, int ballotPaperId)
        {
            var maxWriteIns = electionConfigDataProvider.GetMaxWriteInForContest(counting.ElectionEventId, counting.ContestId).GetValueOrDefault();

            var writeIns = new List<WriteIn>();

            foreach (var writeInField in currentFieldValues.Where(a => a.IsWriteInField()))
            {
                var writeInData = JsonConvert.DeserializeObject<WriteInData>(writeInField.Value);
                if (!writeInData.IgnoreWriteIn && (maxWriteIns == 0 || writeInField.FieldIndex <= maxWriteIns))
                {
                    var affiliation = AssertAffiliation(ballotPaperId, counting, writeInData.AffiliationShortCode);

                    var writeIn = new WriteIn
                    {
                        AffiliationIdentifier = GetAffilationIdentifier(affiliation),
                        RankInAffiliation = writeInData.RankInAffiliation.ToString()
                    };

                    writeIns.Add(writeIn);
                }
            }

            return writeIns;
        }

        private Strikeout MapStrikeout(BallotPaperFieldValue fieldValue, Affiliation affiliation, int ballotPaperId)
        {
            var candidate = LookupAndAssertCandidate(affiliation, ballotPaperId, "strikeout", fieldValue.FieldIndex);
            if (candidate != null)
            {
                return new Strikeout { RankInAffiliation = candidate.RankInAffiliation.ToString() };
            }

            return null;
        }

        private Renumber MapRenumber(BallotPaperFieldValue fieldValue, Affiliation affiliation, int ballotPaperId)
        {
            var candidate = LookupAndAssertCandidate(affiliation, ballotPaperId, "renumber", fieldValue.FieldIndex);

            int renumberedPosition;
            var parseSuccess = int.TryParse(fieldValue.Value, out renumberedPosition);

            if (parseSuccess && renumberedPosition > 0)
            {
                LookupAndAssertCandidate(affiliation, ballotPaperId, "renumberedPosition", renumberedPosition);
                
                return new Renumber
                {
                    RankInAffiliation = candidate.RankInAffiliation.ToString(),
                    RenumberedPosition = renumberedPosition.ToString()
                };
            }
            
            
            throw new FormatException(string.Format("The renumbering given for candidate index {0}, ballotPaperId = {1} was not a non-zero integer. Attempted renumber value was '{2}'.",
                    fieldValue.FieldIndex, ballotPaperId, fieldValue.Value));
        }

        private Affiliation LookupAndAssertAffiliation(BallotPaperFieldValue ballotNoField, int ballotPaperId, Counting counting)
        {
            Guard.AssertBallotNoFieldPresent(ballotPaperId, ballotNoField);

            var affiliationShortCode = BallotNumberSplitter.GetAffiliationShortCode(ballotNoField.Value);
            return AssertAffiliation(ballotPaperId, counting, affiliationShortCode);
        }

        private Affiliation AssertAffiliation(int ballotPaperId, Counting counting, string affiliationShortCode)
        {
            var affiliation = candidateListDataProvider.GetAffiliation(counting.ElectionEventId, counting.ContestId, affiliationShortCode);
            if (affiliation == null)
            {
                throw new InvalidOperationException(string.Format("Could not find matching affiliation for ballotPaperId = {0}, attempted affilationShortCode was {1}", ballotPaperId,
                    affiliationShortCode));
            }

            return affiliation;
        }

        private bool AreAllPersonalVotingFeaturesToBeIgnoredDueToCountingAuthority(Counting counting)
        {
            var additionalElectionConfig = systemSettingsProvider.GetAdditionalElectionConfiguration(counting.ElectionEventId, counting.ElectionIdentifier, counting.ElectionGroup, counting.ContestId);

            var areAllPersonalVotingFeaturesToBeIgnordeDueToCountingAuthority = additionalElectionConfig.AlsoCountedOnLowerLevels
                                                                                && !counting.IsCurrentlyCountedByTopLevelCountingAuthority();
            return areAllPersonalVotingFeaturesToBeIgnordeDueToCountingAuthority;
        }

        private AffiliationIdentifier GetAffilationIdentifier(Affiliation affiliation)
        {
            return new AffiliationIdentifier { AffiliationId = affiliation.AffiliationId, AffiliationShortCode = affiliation.AffiliationShortCode };
        }
    }
}
