using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MarcusLaw.OutlookAddin.Core.Models;
using Microsoft.Extensions.Caching.Memory;
using Serilog;

namespace MarcusLaw.OutlookAddin.Core.Services
{
    public sealed class MatchingService : IMatchingService
    {
        private static readonly TimeSpan CacheExpiry = TimeSpan.FromMinutes(10);
        private const string ContactSelect = "id,name,emailAddress,accountId,accountName";

        private readonly IEspoCrmClient _client;
        private readonly IMemoryCache _cache;
        private readonly ILogger _logger;

        public MatchingService(IEspoCrmClient client, IMemoryCache cache, ILogger logger)
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
            _cache = cache ?? throw new ArgumentNullException(nameof(cache));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public async Task<MatchResult?> LookupAsync(string senderEmail, CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(senderEmail)) return null;
            if (!senderEmail.Contains("@")) return null;

            var key = CacheKey(senderEmail);
            if (_cache.TryGetValue<MatchResult?>(key, out var cached))
            {
                return cached;
            }

            try
            {
                var matches = await _client.EmailAddressSearchAsync(senderEmail, "Contact", cancellationToken).ConfigureAwait(false);

                if (matches.Count == 0)
                {
                    _cache.Set(key, (MatchResult?)null, CacheExpiry);
                    return null;
                }

                // EspoCRM's /EmailAddress/search can return the same Contact
                // more than once when the contact has the searched-for
                // address registered under multiple emailAddressData rows.
                // Dedupe by Id so the sidebar shows one row per unique person.
                var distinctIds = matches
                    .Where(m => !string.IsNullOrWhiteSpace(m.Id))
                    .Select(m => m.Id)
                    .Distinct(StringComparer.Ordinal)
                    .Take(20)
                    .ToList();

                if (distinctIds.Count == 0)
                {
                    _cache.Set(key, (MatchResult?)null, CacheExpiry);
                    return null;
                }

                if (distinctIds.Count == 1)
                {
                    var single = await LoadContactBundleAsync(distinctIds[0], senderEmail, cancellationToken).ConfigureAwait(false);
                    _cache.Set<MatchResult?>(key, single, CacheExpiry);
                    return single;
                }

                // Two or more genuinely distinct contacts. Fetch each one's
                // full record (in parallel) so the picker can show real
                // name + account, not just the bare email payload.
                var fetched = await Task.WhenAll(
                    distinctIds.Select(id => SafeGetContactAsync(id, cancellationToken))
                ).ConfigureAwait(false);

                var candidates = fetched
                    .Where(c => c != null && !string.IsNullOrWhiteSpace(c!.Id))
                    .Select(c => c!)
                    .ToList();

                var ambiguous = new MatchResult
                {
                    SenderEmail = senderEmail,
                    IsAmbiguous = true,
                    CandidateCount = candidates.Count,
                    Candidates = candidates
                };
                _cache.Set<MatchResult?>(key, ambiguous, CacheExpiry);
                return ambiguous;
            }
            catch (EspoCrmAuthorizationException)
            {
                throw;
            }
            catch (OperationCanceledException)
            {
                throw;
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "MatchingService lookup failed for {Email}", senderEmail);
                return null;
            }
        }

        public async Task<MatchResult?> LookupContactAsync(string contactId, string? senderEmail = null, CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(contactId)) return null;

            try
            {
                return await LoadContactBundleAsync(contactId, senderEmail ?? string.Empty, cancellationToken).ConfigureAwait(false);
            }
            catch (EspoCrmAuthorizationException)
            {
                throw;
            }
            catch (OperationCanceledException)
            {
                throw;
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "MatchingService: LookupContactAsync failed for {ContactId}", contactId);
                return null;
            }
        }

        private async Task<MatchResult> LoadContactBundleAsync(string contactId, string senderEmail, CancellationToken cancellationToken)
        {
            var contact = await _client.GetContactAsync(contactId, ContactSelect, cancellationToken).ConfigureAwait(false);

            AccountEntity? account = null;
            if (!string.IsNullOrWhiteSpace(contact.AccountId))
            {
                try
                {
                    account = await _client.GetAccountAsync(contact.AccountId!, "id,name,emailAddress", cancellationToken).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    _logger.Warning(ex, "MatchingService: failed to load account {AccountId} for contact {ContactId}", contact.AccountId, contactId);
                }
            }

            var cases = new System.Collections.Generic.List<CaseEntity>();
            try
            {
                // Pull up to 50 cases per contact so the sidebar can show the
                // whole list, not just the 5 most recent. 50 is well below the
                // EspoCRM default page limit and large enough for any realistic
                // attorney/contact relationship.
                var list = await _client.ListCasesForContactAsync(contact.Id, 50, cancellationToken).ConfigureAwait(false);
                cases.AddRange(list.List);
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "MatchingService: failed to list cases for {ContactId}", contact.Id);
            }

            return new MatchResult
            {
                SenderEmail = senderEmail,
                Contact = contact,
                Account = account,
                RecentCases = cases,
                CandidateCount = 1
            };
        }

        private async Task<ContactEntity?> SafeGetContactAsync(string contactId, CancellationToken cancellationToken)
        {
            try
            {
                return await _client.GetContactAsync(contactId, ContactSelect, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException) { throw; }
            catch (EspoCrmAuthorizationException) { throw; }
            catch (Exception ex)
            {
                _logger.Warning(ex, "MatchingService: candidate contact fetch failed for {Id}", contactId);
                return null;
            }
        }

        public void Invalidate(string senderEmail)
        {
            if (string.IsNullOrWhiteSpace(senderEmail)) return;
            _cache.Remove(CacheKey(senderEmail));
        }

        private static string CacheKey(string senderEmail)
            => "match:" + senderEmail.Trim().ToLowerInvariant();
    }
}
