using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
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>
    /// Picks a Case + optional subfolder (filesystem-style, via Marcus-Law's
    /// NetworkStorageIntegration) for saving raw attachment files.
    /// </summary>
    public sealed partial class SaveAttachmentsViewModel : ObservableObject
    {
        // Default subfolders the integration creates under each Case folder.
        // Mirrors Resources/metadata/app/networkStorage.json on the server.
        private static readonly string[] DefaultCaseSubfolders =
        {
            "מסמכים",
            "אסמכתאות",
            "התכתבויות",
            "כללי"
        };

        private const string RootSentinel = "(שורש התיק)";

        private readonly IEspoCrmClient _client;
        private readonly ILogger _logger;
        private CancellationTokenSource? _searchCts;

        [ObservableProperty]
        private string searchText = string.Empty;

        [ObservableProperty]
        private CaseEntity? selectedCase;

        [ObservableProperty]
        private string? selectedSubfolder;

        [ObservableProperty]
        private string? statusMessage;

        [ObservableProperty]
        private bool isLoadingSubfolders;

        /// <summary>
        /// Resolved server-side case folder path for the currently
        /// selected case (e.g. "47-אריאל מונאס"). Populated by
        /// <see cref="RefreshSubfoldersForSelectedCaseAsync"/> after the
        /// case + primary contact have been fetched. Empty until then.
        /// </summary>
        public string? ResolvedCaseFolderPath { get; private set; }

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

        public ObservableCollection<string> Subfolders { get; } = new ObservableCollection<string>();

        public ObservableCollection<AttachmentRowViewModel> Attachments { get; } = new ObservableCollection<AttachmentRowViewModel>();

        public bool? DialogResult { get; private set; }

        public IReadOnlyList<EspoAttachment> SelectedAttachments
        {
            get
            {
                var list = new List<EspoAttachment>();
                foreach (var row in Attachments)
                {
                    if (row.IsSelected) list.Add(row.Data);
                }
                return list;
            }
        }

        /// <summary>
        /// Subfolder string the user picked, or null to save to the case
        /// folder root.
        /// </summary>
        public string? ChosenSubfolderName =>
            string.IsNullOrEmpty(SelectedSubfolder) ||
            string.Equals(SelectedSubfolder, RootSentinel, StringComparison.Ordinal)
                ? null
                : SelectedSubfolder;

        public CaseEntity? PickedCase => SelectedCase;

        public event EventHandler? RequestClose;

        public SaveAttachmentsViewModel(
            IEspoCrmClient client,
            ILogger logger,
            IEnumerable<EspoAttachment> attachments)
        {
            _client = client ?? throw new ArgumentNullException(nameof(client));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));

            ResetSubfolders();

            foreach (var att in attachments)
            {
                Attachments.Add(new AttachmentRowViewModel(att));
            }
        }

        private void ResetSubfolders()
        {
            Subfolders.Clear();
            Subfolders.Add(RootSentinel);
            foreach (var f in DefaultCaseSubfolders) Subfolders.Add(f);
            SelectedSubfolder = Subfolders[0];
        }

        partial void OnSearchTextChanged(string value)
        {
            _searchCts?.Cancel();
            _searchCts = new CancellationTokenSource();
            _ = SearchAsync(value, _searchCts.Token);
        }

        partial void OnSelectedCaseChanged(CaseEntity? value)
        {
            SubmitCommand.NotifyCanExecuteChanged();
            _ = RefreshSubfoldersForSelectedCaseAsync();
        }

        /// <summary>
        /// Refreshes the subfolder dropdown to merge static defaults with
        /// the actual folders that already exist on the network share for
        /// the chosen case. Also resolves <see cref="ResolvedCaseFolderPath"/>
        /// using the same logic as the server (primary contact's name, not
        /// the case's lawsuit title).
        /// </summary>
        public async Task RefreshSubfoldersForSelectedCaseAsync()
        {
            ResetSubfolders();
            ResolvedCaseFolderPath = null;

            var c = SelectedCase;
            if (c == null || string.IsNullOrEmpty(c.Id)) return;

            IsLoadingSubfolders = true;
            try
            {
                await RefreshSubfoldersCoreAsync(c).ConfigureAwait(true);
            }
            finally
            {
                IsLoadingSubfolders = false;
            }
        }

        private async Task RefreshSubfoldersCoreAsync(CaseEntity c)
        {
            try { await _client.EnsureCaseSubfoldersAsync(c.Id).ConfigureAwait(true); }
            catch (Exception ex) { _logger.Warning(ex, "EnsureCaseSubfolders failed; subfolders may be missing on disk"); }

            string? primaryContactName = null;
            try
            {
                var detail = await _client.GetCaseAsync(
                    c.Id, "id,name,number,contactsIds,networkStorageFolderPath", default).ConfigureAwait(true);
                c.Number = detail.Number;
                c.Name = detail.Name;
                c.NetworkStorageFolderPath = detail.NetworkStorageFolderPath;
                c.ContactsIds = detail.ContactsIds ?? new List<string>();

                if (c.ContactsIds.Count > 0)
                {
                    try
                    {
                        var contact = await _client.GetContactAsync(c.ContactsIds[0], "id,name", default).ConfigureAwait(true);
                        primaryContactName = contact?.Name;
                    }
                    catch (Exception ex) { _logger.Warning(ex, "Primary contact fetch failed for case {CaseId}", c.Id); }
                }
            }
            catch (Exception ex) { _logger.Warning(ex, "GetCase for subfolder list failed"); }

            var folderPath = ResolveCaseFolderPath(c, primaryContactName);
            ResolvedCaseFolderPath = folderPath;
            _logger.Information(
                "Resolved case folder for {CaseId} (number={Number}, contact={Contact}, stored={Stored}) → {Path}",
                c.Id, c.Number ?? "(none)", primaryContactName ?? "(none)", c.NetworkStorageFolderPath ?? "(none)", folderPath);
            if (string.IsNullOrEmpty(folderPath)) return;

            try
            {
                var items = await _client.ListNetworkStorageFolderAsync(folderPath).ConfigureAwait(true);
                foreach (var item in items)
                {
                    if (!item.IsFolder) continue;
                    if (Subfolders.Contains(item.Name)) continue;
                    if (string.Equals(item.Name, RootSentinel, StringComparison.Ordinal)) continue;
                    Subfolders.Add(item.Name);
                }
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "Listing subfolders for case {CaseId} failed", c.Id);
            }
        }

        /// <summary>
        /// Mirrors NetworkDocumentService::buildEntityFolderName for Case
        /// on the server side. Fallback order:
        ///   1) Case.networkStorageFolderPath (if the integration's hook
        ///      has set it — most authoritative)
        ///   2) "&lt;number&gt;-&lt;primary contact name&gt;"   ←  server's recipe
        ///   3) "&lt;number&gt;-&lt;case lawsuit name&gt;"
        ///   4) contact / case name on its own
        /// </summary>
        public static string ResolveCaseFolderPath(CaseEntity c, string? primaryContactName = null)
        {
            if (c == null) return string.Empty;
            if (!string.IsNullOrWhiteSpace(c.NetworkStorageFolderPath))
            {
                return c.NetworkStorageFolderPath!.TrimEnd('/');
            }

            var nameForFolder = !string.IsNullOrWhiteSpace(primaryContactName)
                ? primaryContactName!
                : (c.Name ?? string.Empty);

            var raw = !string.IsNullOrWhiteSpace(c.Number)
                ? $"{c.Number}-{nameForFolder}"
                : nameForFolder;
            return SanitizeFolderSegment(raw);
        }

        /// <summary>
        /// Mirrors LocalFilesystemClient::sanitizeFileName on the server side
        /// so client-computed paths match what the integration writes.
        /// </summary>
        public static string SanitizeFolderSegment(string name)
        {
            if (string.IsNullOrWhiteSpace(name)) return string.Empty;
            foreach (var c in new[] { '׳', '״', '\'', '"', '‘', '’', '“', '”' })
            {
                name = name.Replace(c.ToString(), string.Empty);
            }
            foreach (var c in new[] { '/', '\\', ':', '*', '?', '<', '>', '|' })
            {
                name = name.Replace(c, '-');
            }
            while (name.Contains("--")) name = name.Replace("--", "-");
            while (name.Contains("  ")) name = name.Replace("  ", " ");
            name = name.Replace("-.", ".");
            return name.Trim(' ', '-');
        }

        private async Task<CaseEntity?> LoadCaseRowAsync(EspoEntityRef hit, CancellationToken ct)
        {
            var row = new CaseEntity { Id = hit.Id, Name = hit.Name };
            if (string.IsNullOrEmpty(hit.Id)) return row;
            try
            {
                var detail = await _client.GetCaseAsync(
                    hit.Id, "id,name,number,status,cCourtCaseNumber", ct).ConfigureAwait(false);
                row.Number = detail.Number;
                row.Status = detail.Status;
                row.CourtCaseNumber = detail.CourtCaseNumber;
                if (!string.IsNullOrEmpty(detail.Name)) row.Name = detail.Name;
            }
            catch (OperationCanceledException) { throw; }
            catch (Exception ex)
            {
                _logger.Warning(ex, "Hydrating case {CaseId} for picker failed", hit.Id);
            }
            return row;
        }

        private async Task SearchAsync(string query, CancellationToken ct)
        {
            try
            {
                await Task.Delay(250, ct).ConfigureAwait(true);

                var trimmed = (query ?? string.Empty).Trim();
                if (trimmed.Length < 2)
                {
                    Cases.Clear();
                    StatusMessage = null;
                    return;
                }

                var search = await _client.GlobalSearchAsync(trimmed, ct).ConfigureAwait(true);
                ct.ThrowIfCancellationRequested();

                Cases.Clear();
                var caseHits = new List<EspoEntityRef>();
                foreach (var hit in search.List)
                {
                    if (string.Equals(hit.EntityType, "Case", StringComparison.OrdinalIgnoreCase))
                        caseHits.Add(hit);
                }

                if (caseHits.Count == 0)
                {
                    StatusMessage = "אין תוצאות מסוג תיק (Case)";
                    return;
                }

                var detailTasks = new List<Task<CaseEntity?>>(caseHits.Count);
                foreach (var hit in caseHits)
                {
                    detailTasks.Add(LoadCaseRowAsync(hit, ct));
                }
                var details = await Task.WhenAll(detailTasks).ConfigureAwait(true);
                ct.ThrowIfCancellationRequested();

                foreach (var row in details)
                {
                    if (row != null) Cases.Add(row);
                }
                StatusMessage = $"נמצאו {Cases.Count} תיקים";
            }
            catch (OperationCanceledException) { /* superseded */ }
            catch (EspoCrmAuthorizationException)
            {
                StatusMessage = "מפתח ה-API נדחה — יש לעדכן בהגדרות";
            }
            catch (Exception ex)
            {
                _logger.Warning(ex, "SaveAttachments search failed");
                StatusMessage = "שגיאת חיפוש: " + ex.Message;
            }
        }

        [RelayCommand(CanExecute = nameof(CanSubmit))]
        private void Submit()
        {
            DialogResult = true;
            RequestClose?.Invoke(this, EventArgs.Empty);
        }

        private bool CanSubmit()
        {
            if (SelectedCase == null || string.IsNullOrWhiteSpace(SelectedCase.Id)) return false;
            foreach (var row in Attachments)
            {
                if (row.IsSelected) return true;
            }
            return false;
        }

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