using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MarcusLaw.OutlookAddin.Core.Configuration;
using MarcusLaw.OutlookAddin.Core.Models;
using MarcusLaw.OutlookAddin.Core.Security;
using MarcusLaw.OutlookAddin.Core.Services;
using Serilog;

namespace MarcusLaw.OutlookAddin.UI.ViewModels
{
    public sealed partial class SettingsViewModel : ObservableObject
    {
        private static readonly Brush NeutralBrush = Brushes.Gray;
        private static readonly Brush ErrorBrush = Brushes.Firebrick;
        private static readonly Brush SuccessBrush = Brushes.SeaGreen;

        private readonly AppSettings _settings;
        private readonly SettingsManager _settingsManager;
        private readonly DpapiCredentialStore _credentialStore;
        private readonly ILogger _logger;

        [ObservableProperty]
        private string espoCrmUrl = string.Empty;

        [ObservableProperty]
        private string username = string.Empty;

        [ObservableProperty]
        private string locale = "he-IL";

        [ObservableProperty]
        private string? statusMessage;

        [ObservableProperty]
        private Brush statusBrush = NeutralBrush;

        [ObservableProperty]
        private string? updateStatus;

        [ObservableProperty]
        private Brush updateStatusBrush = NeutralBrush;

        [ObservableProperty]
        private bool isCheckingForUpdate;

        public string InitialApiKey { get; }

        public string VersionLine { get; }

        public Func<string>? ApiKeyReader { get; set; }

        public bool? DialogResult { get; private set; }

        public event EventHandler? RequestClose;

        public ObservableCollection<FolderRowViewModel> Folders { get; } = new ObservableCollection<FolderRowViewModel>();

        public SettingsViewModel(
            AppSettings settings,
            SettingsManager settingsManager,
            DpapiCredentialStore credentialStore,
            ILogger logger)
        {
            _settings = settings ?? throw new ArgumentNullException(nameof(settings));
            _settingsManager = settingsManager ?? throw new ArgumentNullException(nameof(settingsManager));
            _credentialStore = credentialStore ?? throw new ArgumentNullException(nameof(credentialStore));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));

            EspoCrmUrl = settings.EspoCrmUrl;
            Locale = string.IsNullOrWhiteSpace(settings.Locale) ? "he-IL" : settings.Locale;

            var creds = _credentialStore.Load();
            Username = creds?.Username ?? string.Empty;
            InitialApiKey = creds?.ApiKey ?? string.Empty;

            VersionLine = $"גרסה {ResolveDisplayVersion()}";
        }

        /// <summary>
        /// Picks the most accurate version to surface in the About tab:
        /// the ClickOnce activation version when the add-in is installed
        /// over the wire (auto-updated to the real deployment version),
        /// else the assembly's AssemblyVersion (which is set per-source
        /// and may lag behind the deployed ClickOnce manifest).
        /// </summary>
        private static string ResolveDisplayVersion()
        {
            Version? v = null;
            try
            {
                if (System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed)
                {
                    v = System.Deployment.Application.ApplicationDeployment
                        .CurrentDeployment.CurrentVersion;
                }
            }
            catch
            {
                // Not running under ClickOnce (dev / F5), or System.Deployment
                // is not available — fall through to the AssemblyVersion.
            }
            // The host VSTO assembly is where versioning policy lives
            // (csproj + AssemblyInfo). The UI assembly is SDK-style with
            // no <Version>, so it always reports 1.0.0.0 — don't fall back
            // to it. Walk loaded assemblies for the host by name suffix.
            if (v == null)
            {
                foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
                {
                    var name = asm.GetName().Name;
                    if (name != null && name.EndsWith(".OutlookAddin", StringComparison.Ordinal))
                    {
                        v = asm.GetName().Version;
                        break;
                    }
                }
            }
            v ??= typeof(SettingsViewModel).Assembly.GetName().Version;
            if (v == null) return "1.0.0";

            // ClickOnce stores 4 components; project policy is MAJOR.MINOR.PATCH —
            // hide the trailing revision when it's 0.
            return v.Revision == 0
                ? $"{v.Major}.{v.Minor}.{v.Build}"
                : v.ToString();
        }

        /// <summary>
        /// Populates the Folders tab. The host project owns the Outlook
        /// session and must build the flat tree on the STA thread before
        /// handing it here.
        /// </summary>
        public void LoadFolderTree(IReadOnlyList<FolderTreeNode> tree)
        {
            Folders.Clear();
            if (tree == null) return;

            var watchedLookup = new Dictionary<string, WatchedFolder>(StringComparer.Ordinal);
            foreach (var w in _settings.WatchedFolders)
            {
                watchedLookup[WatchedKey(w.StoreId, w.FolderPath)] = w;
            }

            foreach (var node in tree)
            {
                var row = new FolderRowViewModel(node);
                if (watchedLookup.TryGetValue(WatchedKey(node.StoreId, node.FolderPath), out var existing))
                {
                    row.IsWatched = true;
                    row.Mode = existing.Mode;
                    row.ConfidenceThreshold = existing.ConfidenceThreshold;
                }
                Folders.Add(row);
            }
        }

        private static string WatchedKey(string storeId, string path) => storeId + "|" + path;

        [RelayCommand(CanExecute = nameof(CanCheckForUpdate))]
        private async Task CheckForUpdateAsync()
        {
            IsCheckingForUpdate = true;
            UpdateStatusBrush = NeutralBrush;
            UpdateStatus = "בודק עדכונים…";
            try
            {
                if (!System.Deployment.Application.ApplicationDeployment.IsNetworkDeployed)
                {
                    UpdateStatusBrush = ErrorBrush;
                    UpdateStatus = "התוסף לא מותקן דרך ClickOnce — אין כאן עדכון אוטומטי.";
                    return;
                }

                var deployment = System.Deployment.Application.ApplicationDeployment.CurrentDeployment;
                var info = await Task.Run(() => deployment.CheckForDetailedUpdate(false)).ConfigureAwait(true);
                if (!info.UpdateAvailable)
                {
                    UpdateStatusBrush = SuccessBrush;
                    UpdateStatus = "אתה על הגרסה האחרונה (" + deployment.CurrentVersion + ").";
                    return;
                }

                UpdateStatus = $"זמינה גרסה {info.AvailableVersion} — מוריד…";
                var result = await Task.Run(() => deployment.Update()).ConfigureAwait(true);
                UpdateStatusBrush = SuccessBrush;
                UpdateStatus = $"הותקנה גרסה {info.AvailableVersion}. סגור ופתח את Outlook כדי לטעון אותה.";
            }
            catch (System.Deployment.Application.DeploymentDownloadException ex)
            {
                _logger.Warning(ex, "CheckForUpdate: download failed");
                UpdateStatusBrush = ErrorBrush;
                UpdateStatus = "הורדת העדכון נכשלה: " + ex.Message;
            }
            catch (System.Deployment.Application.InvalidDeploymentException ex)
            {
                _logger.Warning(ex, "CheckForUpdate: invalid deployment manifest");
                UpdateStatusBrush = ErrorBrush;
                UpdateStatus = "ה-deployment manifest על השרת לא תקין: " + ex.Message;
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "CheckForUpdate failed");
                UpdateStatusBrush = ErrorBrush;
                UpdateStatus = "שגיאה בבדיקת עדכונים: " + ex.Message;
            }
            finally
            {
                IsCheckingForUpdate = false;
            }
        }

        private bool CanCheckForUpdate() => !IsCheckingForUpdate;

        partial void OnIsCheckingForUpdateChanged(bool value)
        {
            CheckForUpdateCommand.NotifyCanExecuteChanged();
        }

        [RelayCommand]
        private async Task TestConnectionAsync()
        {
            StatusBrush = NeutralBrush;
            StatusMessage = "בודק חיבור…";

            if (!Uri.TryCreate(EspoCrmUrl, UriKind.Absolute, out var baseUrl))
            {
                Fail("כתובת שרת לא תקינה");
                return;
            }

            var apiKey = ReadApiKey();
            if (string.IsNullOrWhiteSpace(apiKey))
            {
                Fail("יש להזין מפתח API");
                return;
            }

            HttpClient? http = null;
            try
            {
                http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
                var probe = new EspoCrmClient(http, baseUrl.ToString(), Username ?? string.Empty, apiKey, _logger);
                var user = await probe.TestConnectionAsync().ConfigureAwait(true);
                Succeed($"התחברות תקינה — משתמש: {user.UserName ?? user.Id}");
            }
            catch (EspoCrmAuthorizationException)
            {
                Fail("מפתח ה-API או שם המשתמש שגויים");
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "TestConnection failed for {Url}", baseUrl);
                Fail("שגיאת חיבור: " + FormatExceptionForUser(ex, baseUrl));
            }
            finally
            {
                http?.Dispose();
            }
        }

        /// <summary>
        /// Walks the InnerException chain and pulls out the underlying socket
        /// error code so the dialog actually tells the user something useful
        /// (DNS miss, connection refused, TLS failure, etc.) instead of just
        /// "One or more errors occurred".
        /// </summary>
        private static string FormatExceptionForUser(Exception ex, Uri target)
        {
            var current = ex;
            while (current.InnerException != null)
            {
                current = current.InnerException;
            }

            if (current is System.Net.WebException web)
            {
                switch (web.Status)
                {
                    case System.Net.WebExceptionStatus.SecureChannelFailure:
                        return "כשל TLS — לא הצלחתי ליצור ערוץ מאובטח. ייתכן שהשרת דורש TLS 1.2/1.3 שאינו מופעל ב-.NET Framework. נסה לסגור ולפתוח Outlook מחדש; אם הבעיה חוזרת, פנה לאדמין.";
                    case System.Net.WebExceptionStatus.TrustFailure:
                        return "אישור ה-TLS של השרת לא נסמך. ייתכן שצריך להתקין את ה-CA של השרת ב-Trusted Root.";
                    case System.Net.WebExceptionStatus.NameResolutionFailure:
                        return $"השרת {target.Host} לא נמצא ב-DNS. ודא שהכתובת נכונה.";
                    case System.Net.WebExceptionStatus.ConnectFailure:
                        return $"החיבור ל-{target.Host}:{target.Port} נכשל. ודא שהשרת רץ ושאין חסימה ברשת.";
                    case System.Net.WebExceptionStatus.Timeout:
                        return "החיבור פג זמן. השרת אולי לא מגיב.";
                }
            }
            if (current is System.Net.Sockets.SocketException sock)
            {
                switch (sock.SocketErrorCode)
                {
                    case System.Net.Sockets.SocketError.HostNotFound:
                        return $"השרת {target.Host} לא נמצא ב-DNS. ודא שהכתובת נכונה ושיש חיבור לאינטרנט.";
                    case System.Net.Sockets.SocketError.ConnectionRefused:
                        return $"החיבור ל-{target.Host}:{target.Port} נדחה. ודא שהשרת רץ ומאזין.";
                    case System.Net.Sockets.SocketError.TimedOut:
                        return $"חיבור ל-{target.Host} פג זמן. ייתכן שהשרת לא זמין או שיש חסימה ברשת.";
                    case System.Net.Sockets.SocketError.NetworkUnreachable:
                    case System.Net.Sockets.SocketError.HostUnreachable:
                        return "לא הצלחתי להגיע לרשת של השרת. בדוק את ה-VPN/חיבור הרשת.";
                    default:
                        return $"שגיאת רשת ({sock.SocketErrorCode}): {sock.Message}";
                }
            }
            if (current is TaskCanceledException)
            {
                return "החיבור לא הגיב תוך 15 שניות. השרת אולי לא זמין או שהוא איטי.";
            }
            if (current is System.Security.Authentication.AuthenticationException)
            {
                return "אישור ה-TLS של השרת לא תקין. בדוק את הקישור (https vs http) או פנה לאדמין.";
            }

            // Fallback: outer + inner with newline so the user sees the chain.
            var outer = ex.GetType().Name + ": " + ex.Message;
            if (current != ex)
            {
                outer += " — " + current.GetType().Name + ": " + current.Message;
            }
            return outer;
        }

        [RelayCommand]
        private void Save()
        {
            if (!Uri.TryCreate(EspoCrmUrl, UriKind.Absolute, out _))
            {
                Fail("כתובת שרת לא תקינה");
                return;
            }
            var apiKey = ReadApiKey();
            if (string.IsNullOrWhiteSpace(apiKey))
            {
                Fail("יש להזין מפתח API");
                return;
            }

            try
            {
                _settings.EspoCrmUrl = EspoCrmUrl.TrimEnd('/');
                _settings.Locale = string.IsNullOrWhiteSpace(Locale) ? "he-IL" : Locale;

                _settings.WatchedFolders.Clear();
                foreach (var row in Folders)
                {
                    if (row.IsWatched) _settings.WatchedFolders.Add(row.ToWatchedFolder());
                }

                _settingsManager.Save(_settings);

                _credentialStore.Save(string.IsNullOrWhiteSpace(Username) ? null : Username.Trim(), apiKey);
                _logger.Information("Settings saved (user={User}, url={Url}, watched={WatchedCount})",
                    Username, _settings.EspoCrmUrl, _settings.WatchedFolders.Count);

                DialogResult = true;
                RequestClose?.Invoke(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                _logger.Error(ex, "Settings save failed");
                Fail("שמירת ההגדרות נכשלה: " + ex.Message);
            }
        }

        [RelayCommand]
        private void Cancel()
        {
            DialogResult = false;
            RequestClose?.Invoke(this, EventArgs.Empty);
        }

        private string ReadApiKey() => ApiKeyReader?.Invoke() ?? string.Empty;

        private void Fail(string message)
        {
            StatusBrush = ErrorBrush;
            StatusMessage = message;
        }

        private void Succeed(string message)
        {
            StatusBrush = SuccessBrush;
            StatusMessage = message;
        }
    }
}
