using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MarcusLaw.OutlookAddin.Core.Models;
using MarcusLaw.OutlookAddin.Core.Services;
using Serilog;

namespace MarcusLaw.OutlookAddin.UI.ViewModels
{
    /// <summary>
    /// Possible states of the sidebar. Each maps to a Visibility binding
    /// in <c>ReadingPaneView.xaml</c>.
    /// </summary>
    public enum ReadingPaneState
    {
        Empty,
        Loading,
        Match,
        NoMatch,
        Ambiguous,
        Error
    }

    public sealed partial class ReadingPaneViewModel : ObservableObject
    {
        private readonly IMatchingService _matchingService;
        private readonly ILogger _logger;
        private CancellationTokenSource? _activeLookup;
        // Stashes the ambiguous MatchResult while the user is drilling into
        // one of its candidates, so the "→ חזור" button can restore it.
        private MatchResult? _ambiguousReturnTarget;

        [ObservableProperty]
        private ReadingPaneState state = ReadingPaneState.Empty;

        [ObservableProperty]
        private string senderEmail = string.Empty;

        [ObservableProperty]
        private MatchResult? match;

        [ObservableProperty]
        private string? errorMessage;

        [ObservableProperty]
        private string? ambiguousMessage;

        [ObservableProperty]
        private string? espoCrmBaseUrl;

        public ObservableCollection<CaseEntity> RecentCases { get; } = new ObservableCollection<CaseEntity>();

        public ObservableCollection<ContactEntity> Candidates { get; } = new ObservableCollection<ContactEntity>();

        public Visibility EmptyVisibility => Vis(State == ReadingPaneState.Empty);
        public Visibility LoadingVisibility => Vis(State == ReadingPaneState.Loading);
        public Visibility MatchVisibility => Vis(State == ReadingPaneState.Match);
        public Visibility NoMatchVisibility => Vis(State == ReadingPaneState.NoMatch);
        public Visibility AmbiguousVisibility => Vis(State == ReadingPaneState.Ambiguous);
        public Visibility ErrorVisibility => Vis(State == ReadingPaneState.Error);
        public Visibility AccountVisibility => Vis(Match?.Account != null);
        public Visibility NoCasesVisibility => Vis(State == ReadingPaneState.Match && RecentCases.Count == 0);
        public Visibility ActionsVisibility => Vis(State == ReadingPaneState.Match);

        /// <summary>
        /// True when the matched contact has 2+ linked cases. The sidebar
        /// uses this to hide the bottom "תייק לתיק הזה" button (which is
        /// ambiguous in that scenario) and to surface the per-row "תייק"
        /// buttons that let the user pick which case to file to.
        /// </summary>
        public Visibility MultiCaseHintVisibility => Vis(State == ReadingPaneState.Match && RecentCases.Count >= 2);
        public Visibility SingleCaseFileVisibility => Vis(State == ReadingPaneState.Match && RecentCases.Count == 1);

        /// <summary>
        /// "→ חזור לרשימת אנשי קשר" is only shown when the user drilled
        /// into a candidate from the ambiguous picker; not in a normal
        /// single-match lookup.
        /// </summary>
        public Visibility BackToCandidatesVisibility => Vis(State == ReadingPaneState.Match && _ambiguousReturnTarget != null);

        /// <summary>
        /// Raised when the user clicks "File to this case" (carries the chosen
        /// MatchResult.Contact / RecentCases pick) or "File elsewhere" (null).
        /// Host wires this to <c>AddInHost.LaunchFileToCaseAsync</c>.
        /// </summary>
        public event EventHandler<FileFromSidebarEventArgs>? FileRequested;

        /// <summary>
        /// Raised when the user clicks "Open in EspoCRM". The host opens the URL.
        /// </summary>
        public event EventHandler<string>? OpenInBrowserRequested;

        public ReadingPaneViewModel(IMatchingService matchingService, ILogger logger)
        {
            _matchingService = matchingService ?? throw new ArgumentNullException(nameof(matchingService));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        partial void OnStateChanged(ReadingPaneState value)
        {
            OnPropertyChanged(nameof(EmptyVisibility));
            OnPropertyChanged(nameof(LoadingVisibility));
            OnPropertyChanged(nameof(MatchVisibility));
            OnPropertyChanged(nameof(NoMatchVisibility));
            OnPropertyChanged(nameof(AmbiguousVisibility));
            OnPropertyChanged(nameof(ErrorVisibility));
            OnPropertyChanged(nameof(ActionsVisibility));
            OnPropertyChanged(nameof(NoCasesVisibility));
            OnPropertyChanged(nameof(MultiCaseHintVisibility));
            OnPropertyChanged(nameof(SingleCaseFileVisibility));
            OnPropertyChanged(nameof(BackToCandidatesVisibility));
            FileToThisCommand.NotifyCanExecuteChanged();
            FileElsewhereCommand.NotifyCanExecuteChanged();
            OpenInEspoCrmCommand.NotifyCanExecuteChanged();
        }

        partial void OnMatchChanged(MatchResult? value)
        {
            RecentCases.Clear();
            if (value?.RecentCases != null)
            {
                foreach (var c in value.RecentCases) RecentCases.Add(c);
            }
            Candidates.Clear();
            if (value?.Candidates != null)
            {
                foreach (var c in value.Candidates) Candidates.Add(c);
            }
            OnPropertyChanged(nameof(AccountVisibility));
            OnPropertyChanged(nameof(NoCasesVisibility));
            OnPropertyChanged(nameof(MultiCaseHintVisibility));
            OnPropertyChanged(nameof(SingleCaseFileVisibility));
            OnPropertyChanged(nameof(BackToCandidatesVisibility));
        }

        public void Reset()
        {
            CancelInFlight();
            SenderEmail = string.Empty;
            Match = null;
            ErrorMessage = null;
            AmbiguousMessage = null;
            _ambiguousReturnTarget = null;
            State = ReadingPaneState.Empty;
        }

        public async Task LoadAsync(string senderEmail)
        {
            CancelInFlight();
            if (string.IsNullOrWhiteSpace(senderEmail))
            {
                Reset();
                return;
            }

            SenderEmail = senderEmail;
            // New email selection — drop any stashed candidate-list state.
            _ambiguousReturnTarget = null;
            State = ReadingPaneState.Loading;

            var cts = new CancellationTokenSource();
            _activeLookup = cts;

            try
            {
                var result = await _matchingService.LookupAsync(senderEmail, cts.Token).ConfigureAwait(true);
                if (cts.IsCancellationRequested) return;

                if (result == null)
                {
                    Match = null;
                    State = ReadingPaneState.NoMatch;
                    return;
                }

                if (result.IsAmbiguous)
                {
                    Match = result;
                    AmbiguousMessage = $"נמצאו {result.CandidateCount} אנשי קשר עם הכתובת הזו — נדרשת בחירה ידנית.";
                    State = ReadingPaneState.Ambiguous;
                    return;
                }

                Match = result;
                State = ReadingPaneState.Match;
            }
            catch (OperationCanceledException)
            {
                // Selection changed before lookup finished — ignore.
            }
            catch (EspoCrmAuthorizationException)
            {
                ErrorMessage = "מפתח ה-API נדחה. עדכן בהגדרות.";
                State = ReadingPaneState.Error;
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "ReadingPaneViewModel.LoadAsync failed for {Email}", senderEmail);
                ErrorMessage = "שגיאת התחברות לשרת.";
                State = ReadingPaneState.Error;
            }
        }

        private void CancelInFlight()
        {
            try { _activeLookup?.Cancel(); } catch { /* ignore */ }
            _activeLookup = null;
        }

        [RelayCommand(CanExecute = nameof(CanFileToThis))]
        private void FileToThis()
        {
            FileRequested?.Invoke(this, new FileFromSidebarEventArgs(Match!));
        }

        // Bottom "תייק לתיק הזה" only makes sense with exactly one linked
        // case. 0 cases → nothing to file to; 2+ cases → ambiguous, the
        // per-row "תייק" buttons handle the pick.
        private bool CanFileToThis() => State == ReadingPaneState.Match && Match?.Contact != null && RecentCases.Count == 1;

        [RelayCommand]
        private void FileToCase(CaseEntity? caseEntity)
        {
            if (caseEntity == null || string.IsNullOrWhiteSpace(caseEntity.Id) || Match == null) return;
            // Build a synthetic MatchResult so the host's
            // TryResolveSidebarAutoTarget path sees exactly one case and
            // auto-files without re-opening the search dialog.
            var synthetic = new MatchResult
            {
                SenderEmail = Match.SenderEmail,
                Contact = Match.Contact,
                Account = Match.Account,
                RecentCases = new System.Collections.Generic.List<CaseEntity> { caseEntity },
                CandidateCount = 1
            };
            FileRequested?.Invoke(this, new FileFromSidebarEventArgs(synthetic));
        }

        [RelayCommand(CanExecute = nameof(CanFileElsewhere))]
        private void FileElsewhere()
        {
            FileRequested?.Invoke(this, new FileFromSidebarEventArgs(null));
        }

        private bool CanFileElsewhere() => true;

        [RelayCommand(CanExecute = nameof(CanOpenInCrm))]
        private void OpenInEspoCrm()
        {
            if (string.IsNullOrWhiteSpace(EspoCrmBaseUrl) || Match?.Contact == null) return;
            var url = EspoCrmBaseUrl!.TrimEnd('/') + "/#Contact/view/" + Match.Contact.Id;
            OpenInBrowserRequested?.Invoke(this, url);
        }

        private bool CanOpenInCrm() => State == ReadingPaneState.Match && Match?.Contact != null;

        [RelayCommand]
        private void OpenCaseInCrm(CaseEntity? caseEntity)
        {
            if (string.IsNullOrWhiteSpace(EspoCrmBaseUrl) || caseEntity == null || string.IsNullOrWhiteSpace(caseEntity.Id)) return;
            var url = EspoCrmBaseUrl!.TrimEnd('/') + "/#Case/view/" + caseEntity.Id;
            OpenInBrowserRequested?.Invoke(this, url);
        }

        [RelayCommand]
        private void OpenManualSearch()
        {
            FileRequested?.Invoke(this, new FileFromSidebarEventArgs(null));
        }

        [RelayCommand]
        private void BackToCandidates()
        {
            if (_ambiguousReturnTarget == null) return;
            var stash = _ambiguousReturnTarget;
            // Clear stash before we re-enter Ambiguous so visibility refresh
            // (triggered by OnMatchChanged) sees the cleared state and hides
            // the back button until the user drills in again.
            _ambiguousReturnTarget = null;
            Match = stash;
            AmbiguousMessage = $"נמצאו {stash.CandidateCount} אנשי קשר עם הכתובת הזו.";
            State = ReadingPaneState.Ambiguous;
        }

        /// <summary>
        /// User picked one of the ambiguous candidates from the sidebar.
        /// Load that contact's full bundle (account + cases) and transition
        /// to the Match state so the user can pick a case or file.
        /// </summary>
        [RelayCommand]
        private async Task SelectCandidateAsync(ContactEntity? candidate)
        {
            if (candidate == null || string.IsNullOrWhiteSpace(candidate.Id)) return;

            CancelInFlight();
            var cts = new CancellationTokenSource();
            _activeLookup = cts;

            var rememberedEmail = SenderEmail;
            // Stash the ambiguous result so "→ חזור" can restore it.
            if (Match?.IsAmbiguous == true)
            {
                _ambiguousReturnTarget = Match;
            }
            State = ReadingPaneState.Loading;

            try
            {
                var result = await _matchingService.LookupContactAsync(candidate.Id, rememberedEmail, cts.Token).ConfigureAwait(true);
                if (cts.IsCancellationRequested) return;

                if (result == null || result.Contact == null)
                {
                    ErrorMessage = "טעינת איש הקשר נכשלה.";
                    State = ReadingPaneState.Error;
                    return;
                }

                Match = result;
                State = ReadingPaneState.Match;
            }
            catch (OperationCanceledException)
            {
                // Selection changed before lookup finished — ignore.
            }
            catch (EspoCrmAuthorizationException)
            {
                ErrorMessage = "מפתח ה-API נדחה. עדכן בהגדרות.";
                State = ReadingPaneState.Error;
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "ReadingPaneViewModel.SelectCandidateAsync failed for {ContactId}", candidate.Id);
                ErrorMessage = "שגיאת התחברות לשרת.";
                State = ReadingPaneState.Error;
            }
        }

        private static Visibility Vis(bool show) => show ? Visibility.Visible : Visibility.Collapsed;
    }

    public sealed class FileFromSidebarEventArgs : EventArgs
    {
        public MatchResult? PreselectedMatch { get; }

        public FileFromSidebarEventArgs(MatchResult? preselected)
        {
            PreselectedMatch = preselected;
        }
    }
}
