using System.Diagnostics;
using System.Linq.Expressions;
using e_suite.API.Common;
using e_suite.API.Common.exceptions;
using e_suite.API.Common.models;
using e_suite.API.Common.repository;
using e_suite.Database.Audit;
using e_suite.Utilities.Pagination;
using eSuite.Core.Clock;
using eSuite.Core.Miscellaneous;

namespace e_suite.Modules.SequenceManager;

public class SequenceManager : ISequenceManager
{
    private readonly ISequenceManagerRepository _sequenceManagerRepository;
    private readonly IClock _clock;

    public SequenceManager(ISequenceManagerRepository sequenceManagerRepository, IClock clock)
    {
        _sequenceManagerRepository = sequenceManagerRepository;
        _clock = clock;
    }

    public async Task<Sequence> GetSequence(IGeneralIdRef generalIdRef, CancellationToken cancellationToken)
    {
        var sequence = await _sequenceManagerRepository.GetSequenceById(generalIdRef, cancellationToken);

        if (sequence == null)
            throw new NotFoundException();

        if (sequence.Deleted)
            throw new NotFoundException();

        var sequenceModel = new Sequence
        {
            GeneralIdRef = new GeneralIdRef
            {
                Guid = sequence.Guid,
                Id = sequence.Id
            },
            Name = sequence.Name,
            Pattern = sequence.Pattern,
            Increment = sequence.Increment,
            Seed = sequence.Seed,
            RolloverType = sequence.Rollover
        };

        return sequenceModel;
    }

    public async Task<IPaginatedData<ReadSequence>> GetSequences(Paging paging, CancellationToken cancellationToken)
    {
        var sequences = _sequenceManagerRepository.GetSequenceList();

        var paginatedData = await PaginatedData.Paginate(sequences, paging,
            KeySelector, cancellationToken);

        var paginatedResult = new PaginatedData<ReadSequence>()
        {
            Count = paginatedData.Count,
            Page = paginatedData.Page,
            PageSize = paginatedData.PageSize,
            Data = paginatedData.Data.Select(x => new ReadSequence
            {
                Id = x.Id,
                Guid = x.Guid,
                Name = x.Name
            })

        };
        
        return paginatedResult;
    }

    private Expression<Func<Database.Core.Tables.Sequences.Sequence, object>> KeySelector(string? sortKey)
    {
        return sortKey?.ToLowerInvariant() switch
        {
            "id" => x => x.Id,
            "guid" => x => x.Guid,
            _ => x => x.Name
        };
    }

    public async Task CreateSequence(AuditUserDetails auditUserDetails, NewSequence sequence, CancellationToken cancellationToken)
    {
        await _sequenceManagerRepository.TransactionAsync(async () =>
        {
            var existingSequence = sequence.Guid == null
                    ? null
                    : await _sequenceManagerRepository.GetSequenceById(new GeneralIdRef
                    {
                        Guid = sequence.Guid
                    }, cancellationToken);

            if (existingSequence != null && !existingSequence.Deleted)
                throw new ExistsException($"Sequence with guid '{sequence.Guid}' already exists");


            if (existingSequence == null)
            {
                existingSequence = await _sequenceManagerRepository.GetSequenceByName(sequence.Name, cancellationToken);
                if (existingSequence != null && !existingSequence.Deleted)
                    throw new ExistsException($"Sequence with name '{sequence.Name}' already exists");
                
            }

            existingSequence ??= new Database.Core.Tables.Sequences.Sequence
            {
                Guid = sequence.Guid ?? Guid.NewGuid()
            };

            existingSequence.Name = sequence.Name;
            existingSequence.Seed = sequence.Seed;
            existingSequence.Increment = sequence.Increment;
            existingSequence.Pattern = sequence.Pattern;
            existingSequence.Rollover = sequence.RolloverType;

            if (existingSequence.Deleted)
            {
                existingSequence.Deleted = false;
                await _sequenceManagerRepository.EditSequence(auditUserDetails, existingSequence, cancellationToken);
            }
            else
            {
                await _sequenceManagerRepository.AddSequence(auditUserDetails, existingSequence, cancellationToken);
            }
        });
    }
    
    public async Task EditSequence(AuditUserDetails auditUserDetails, Sequence editSequence, CancellationToken cancellationToken)
    {
        await _sequenceManagerRepository.TransactionAsync(async () =>
        {
            var existingSequence =
                await _sequenceManagerRepository.GetSequenceById(editSequence.GeneralIdRef!, cancellationToken);

            if (existingSequence == null || existingSequence.Deleted)
                throw new NotFoundException("A sequence with this Id doesn't exist");

            var identicalNameSequence =
                await _sequenceManagerRepository.GetSequenceByName(editSequence.Name, cancellationToken);
            if (identicalNameSequence != null && (identicalNameSequence.Id != editSequence.GeneralIdRef!.Id &&
                                                  identicalNameSequence.Guid != editSequence.GeneralIdRef.Guid))
                throw new ExistsException($"Sequence with name '{editSequence.Name}' already exists");

            existingSequence.Name = editSequence.Name;
            existingSequence.Increment = editSequence.Increment;
            existingSequence.Seed = editSequence.Seed;
            existingSequence.Pattern = editSequence.Pattern;
            existingSequence.Rollover = editSequence.RolloverType;

            await _sequenceManagerRepository.EditSequence(auditUserDetails, existingSequence, cancellationToken);
        });
    }
        
    public async Task DeleteSequence(AuditUserDetails auditUserDetails, IGeneralIdRef generalIdRef, CancellationToken cancellationToken)
    {
        await _sequenceManagerRepository.TransactionAsync(async () =>
        {
            var existingSequence = await _sequenceManagerRepository.GetSequenceById(generalIdRef, cancellationToken);
            if (existingSequence == null || existingSequence.Deleted)
                throw new NotFoundException("A sequence with this Id doesn't exist");

            existingSequence.Deleted = true;

            await _sequenceManagerRepository.EditSequence(auditUserDetails, existingSequence, cancellationToken);
        });
    }

    public async Task<IEnumerable<string>> NextValue(AuditUserDetails auditUserDetails, IGeneralIdRef generalIdRef,
        CancellationToken cancellationToken)
    {
        return await NextValue( auditUserDetails, generalIdRef, 1, cancellationToken).ConfigureAwait(false);
    }

    public async Task<IEnumerable<string>> NextValue(AuditUserDetails auditUserDetails, IGeneralIdRef generalIdRef, int number, CancellationToken cancellationToken)
    {
        return await _sequenceManagerRepository.TransactionAsync(async () =>
        {
            var existingSequence = await _sequenceManagerRepository.GetSequenceById(generalIdRef, cancellationToken);
            if (existingSequence == null || existingSequence.Deleted)
                throw new NotFoundException("A sequence with this Id doesn't exist");

            var result = new List<string>();

            var stopwatch = Stopwatch.StartNew();
            while (result.Count < number)
            {
                if (stopwatch.ElapsedMilliseconds > 2000)
                    throw new TimeoutException($"Took more than 2 seconds to create count list. Created {result.Count} values in the available time");
                cancellationToken.ThrowIfCancellationRequested();
                result.Add(SequenceNumberBuilder.GetNextSequenceNumber(existingSequence, _clock));
            }

            await _sequenceManagerRepository.EditSequence(auditUserDetails, existingSequence, cancellationToken);

            return result;
        });
    }
}