using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows.Input;
using ytLive.Helpers;
using ytLive.Models;
using ytLive.Services;

namespace ytLive.ViewModels;

/// <summary>
/// A validated social entry in the dialog's working copy (non-YouTube slots).
/// </summary>
public sealed record DialogEntry(SocialService Service, string Handle, string ProfileUrl, string? FediverseSoftware = null);

/// <summary>
/// One of the six fixed dialog rows. Row 0 is always YouTube (signed-in account
/// or a sign-in prompt); rows 1-5 hold validated entries, an empty slot, or — on
/// the free tier — a locked "Premium" slot. Rows are a positional projection of
/// the dialog's working list, so deleting an entry makes the ones above advance up.
/// </summary>
public sealed class SocialSlotViewModel : ViewModelBase
{
    public int Index { get; }
    public bool IsYoutube => Index == 0;

    private SocialService _service = SocialService.Link;
    public SocialService Service
    {
        get => _service;
        set { if (SetProperty(ref _service, value)) OnPropertyChanged(nameof(LogoData)); }
    }

    public string LogoData => Service == SocialService.Fediverse
        ? SocialServiceIcons.LogoDataForFediverse(FediverseSoftware)
        : SocialServiceIcons.LogoDataFor(Service);
    public string LockedIconData => SocialServiceIcons.LockedIconData;
    public string DoNotIconData => SocialServiceIcons.DoNotIconData;

    private bool _isLocked;
    public bool IsLocked
    {
        get => _isLocked;
        set
        {
            if (SetProperty(ref _isLocked, value))
            {
                OnPropertyChanged(nameof(ShowAddButton));
                OnPropertyChanged(nameof(ShowDoNot));
                OnPropertyChanged(nameof(DisplayText));
            }
        }
    }

    private bool _isFilled;
    public bool IsFilled
    {
        get => _isFilled;
        set
        {
            if (SetProperty(ref _isFilled, value))
            {
                OnPropertyChanged(nameof(ShowEditButton));
                OnPropertyChanged(nameof(ShowDeleteButton));
                OnPropertyChanged(nameof(ShowAddButton));
                OnPropertyChanged(nameof(ShowDoNot));
                OnPropertyChanged(nameof(DisplayText));
            }
        }
    }

    private bool _isSignIn;
    public bool IsSignIn
    {
        get => _isSignIn;
        set
        {
            if (SetProperty(ref _isSignIn, value))
            {
                OnPropertyChanged(nameof(ShowSignInButton));
                OnPropertyChanged(nameof(ShowDeleteButton));
                OnPropertyChanged(nameof(ShowLogo));
                OnPropertyChanged(nameof(DisplayText));
            }
        }
    }

    private bool _isEditing;
    public bool IsEditing
    {
        get => _isEditing;
        set
        {
            if (SetProperty(ref _isEditing, value))
            {
                OnPropertyChanged(nameof(ShowEditBox));
                OnPropertyChanged(nameof(ShowDisplay));
                OnPropertyChanged(nameof(ShowEditButton));
                OnPropertyChanged(nameof(ShowDeleteButton));
                OnPropertyChanged(nameof(ShowAddButton));
                OnPropertyChanged(nameof(ShowDoNot));
            }
        }
    }

    private bool _isValidating;
    public bool IsValidating
    {
        get => _isValidating;
        set
        {
            if (SetProperty(ref _isValidating, value))
            {
                OnPropertyChanged(nameof(ShowBusy));
                OnPropertyChanged(nameof(ShowDoNot));
            }
        }
    }

    private string _handle = string.Empty;
    public string Handle
    {
        get => _handle;
        set
        {
            if (SetProperty(ref _handle, value))
                OnPropertyChanged(nameof(DisplayText));
        }
    }

    private string? _fediverseSoftware;
    public string? FediverseSoftware
    {
        get => _fediverseSoftware;
        set
        {
            if (SetProperty(ref _fediverseSoftware, value))
                OnPropertyChanged(nameof(LogoData));
        }
    }

    public string ProfileUrl { get; set; } = string.Empty;

    private string? _error;
    public string? Error
    {
        get => _error;
        set
        {
            if (SetProperty(ref _error, value))
            {
                OnPropertyChanged(nameof(HasError));
                OnPropertyChanged(nameof(ShowError));
                OnPropertyChanged(nameof(ShowDoNot));
            }
        }
    }

    private string _editText = string.Empty;
    public string EditText
    {
        get => _editText;
        set => SetProperty(ref _editText, value);
    }

    /// <summary>The last text submitted for validation (used to skip re-validating
    /// identical input — e.g. a LostFocus firing as the user clicks Cancel).</summary>
    public string? LastValidatedInput { get; set; }

    public string DisplayText =>
        IsYoutube
            ? (IsSignIn ? "Sign in to YouTube" : Handle)
            : IsLocked ? "Unlock with Premium"
            : IsFilled ? Handle
            : "No social yet";

    public bool HasError => !string.IsNullOrEmpty(Error);
    public bool ShowError => HasError;
    public bool ShowLogo => IsYoutube ? !IsSignIn : IsFilled;
    public bool ShowBusy => IsValidating;
    public bool ShowEditBox => IsEditing;
    public bool ShowDisplay => !IsEditing && !IsValidating;
    public bool ShowSignInButton => IsYoutube && IsSignIn && !IsValidating;
    public bool ShowAddButton => !IsYoutube && !IsLocked && !IsFilled && !IsEditing && !IsValidating;
    public bool ShowEditButton => !IsYoutube && IsFilled && !IsEditing && !IsValidating;
    public bool ShowDeleteButton => (IsFilled || IsYoutube) && !IsEditing && !IsValidating;
    public bool ShowDoNot => !IsYoutube && !IsLocked && (!IsFilled || HasError || IsValidating);

    public SocialSlotViewModel(int index) => Index = index;
}

/// <summary>
/// View model for the Social Media Site Promotion dialog. Owns the working copy
/// of the social bar config: the six fixed slots, the sign-in gate, per-row
/// validation, the freemium lock, and the YouTube sign-out. Deliberately WPF-free
/// (no MessageBox, no Window) so the whole flow is unit-testable with fakes.
/// The window surfaces the confirmations (delete, sign-in gate) as dialogs and
/// calls back into the VM.
/// </summary>
public sealed class SocialsDialogViewModel : ViewModelBase
{
    public const int MaxSlots = 6;

    private readonly ISocialValidator _validator;
    private readonly Func<Task<YouTubeChannel?>> _signInProvider;
    private readonly Func<Task> _signOutAction;
    private readonly bool _isPremium;
    private readonly List<DialogEntry> _working = new();
    private readonly CancellationTokenSource _cts = new();
    private YouTubeChannel? _account;

    private bool _isSignedIn;
    private bool _isBusy;
    private bool _barEnabled = true;

    public SocialsDialogViewModel(
        ISocialValidator validator,
        Func<Task<YouTubeChannel?>> signInProvider,
        Func<Task> signOutAction,
        YouTubeChannel? account,
        bool isPremium,
        SocialsConfig? current = null)
    {
        _validator = validator;
        _signInProvider = signInProvider;
        _signOutAction = signOutAction;
        _isPremium = isPremium;
        _barEnabled = current?.BarEnabled ?? true;
        _account = account;
        _isSignedIn = account != null;

        foreach (var entry in current?.Entries ?? [])
            if (entry.Service != SocialService.YouTube)
                _working.Add(new DialogEntry(entry.Service, entry.Handle, entry.ProfileUrl));

        SignInCommand = new RelayCommand(_ => _ = SignInAsync(), _ => CanSignIn);
        SaveCommand = new RelayCommand(_ => Save(), _ => CanSave);
        CancelCommand = new RelayCommand(_ => Cancel());

        RebuildSlots();
    }

    public ObservableCollection<SocialSlotViewModel> Slots { get; } = new();

    public bool BarEnabled
    {
        get => _barEnabled;
        set => SetProperty(ref _barEnabled, value);
    }

    public bool IsSignedIn
    {
        get => _isSignedIn;
        private set
        {
            if (SetProperty(ref _isSignedIn, value))
                OnPropertyChanged(nameof(ShowSignInBanner));
        }
    }

    public bool IsBusy
    {
        get => _isBusy;
        private set
        {
            if (SetProperty(ref _isBusy, value))
            {
                OnPropertyChanged(nameof(ShowSignInBanner));
                CommandManager.InvalidateRequerySuggested();
            }
        }
    }

    public bool ShowSignInBanner => !IsSignedIn && !IsBusy;
    public bool CanSignIn => !IsBusy;

    /// <summary>
    /// Save is blocked while any slot has un-validated input, is mid-validation,
    /// or failed validation — an empty slot or a confirmed entry never blocks.
    /// </summary>
    public bool CanSave =>
        !IsBusy &&
        Slots.All(s => !s.IsEditing || string.IsNullOrWhiteSpace(s.EditText)) &&
        Slots.All(s => !s.IsValidating && !s.HasError);

    public ICommand SignInCommand { get; }
    public ICommand SaveCommand { get; }
    public ICommand CancelCommand { get; }

    public event Action? SaveRequested;
    public event Action? CancelRequested;

    /// <summary>Entries to persist after a successful save (YouTube first when signed in).</summary>
    public IReadOnlyList<SocialEntry>? CommittedEntries { get; private set; }

    /// <summary>
    /// Dismissal is a hard stop: abort every in-flight validation and get out.
    /// The canceled continuations never touch slot state.
    /// </summary>
    public void Cancel()
    {
        _cts.Cancel();
        CancelRequested?.Invoke();
    }

    /// <summary>Aborts in-flight work without raising <see cref="CancelRequested"/>
    /// (used by the window's Closing handler so a Save close isn't clobbered).</summary>
    public void AbortPending() => _cts.Cancel();

    private async Task SignInAsync()
    {
        if (IsBusy) return;
        IsBusy = true;
        try
        {
            var channel = await _signInProvider();
            if (channel != null)
            {
                _account = channel;
                IsSignedIn = true;
                RebuildSlots();
            }
        }
        finally
        {
            IsBusy = false;
        }
    }

    /// <summary>Starts editing the slot's input box (add or edit).</summary>
    public void StartEdit(int slotIndex)
    {
        if (slotIndex <= 0 || slotIndex >= Slots.Count) return;
        var slot = Slots[slotIndex];
        if (slot.IsLocked || slot.IsYoutube || slot.IsValidating) return;
        slot.EditText = slot.IsFilled ? slot.Handle : string.Empty;
        slot.Error = null;
        slot.IsEditing = true;
    }

    /// <summary>Commits the slot's input — empty cancels, otherwise validates.
    /// Re-submitting text that was already handled is a no-op, so a focus shift
    /// (clicking Cancel, tabbing away) never re-fires a lookup.</summary>
    public void ConfirmEdit(int slotIndex)
    {
        if (slotIndex <= 0 || slotIndex >= Slots.Count) return;
        var slot = Slots[slotIndex];
        if (!slot.IsEditing || slot.IsValidating) return;
        if (string.IsNullOrWhiteSpace(slot.EditText))
        {
            slot.IsEditing = false;
            return;
        }
        var text = slot.EditText.Trim();
        if (slot.IsFilled && text == slot.Handle)
        {
            slot.IsEditing = false;
            return;
        }
        if (slot.LastValidatedInput == text)
        {
            if (slot.IsFilled) slot.IsEditing = false;
            return;
        }
        slot.LastValidatedInput = text;
        _ = ValidateAsync(slot);
    }

    /// <summary>Runs the lookup; on success snaps the row back to its logo + handle.
    /// A canceled lookup (dismissal) leaves the slot untouched.</summary>
    private async Task ValidateAsync(SocialSlotViewModel slot)
    {
        var ct = _cts.Token;
        slot.IsValidating = true;
        slot.Error = null;
        try
        {
            var text = slot.EditText.Trim();
            var (service, handle, _) = SocialServiceIcons.DetectService(text);
            var result = await _validator.LookupAsync(service, handle, ct);
            if (ct.IsCancellationRequested || result.Canceled) return;

            if (result.Success)
                ApplyValidated(slot, new DialogEntry(service, result.Handle, result.ProfileUrl, result.FediverseSoftware));
            else
                slot.Error = result.Error;
        }
        finally
        {
            slot.IsValidating = false;
        }
    }

    private void ApplyValidated(SocialSlotViewModel slot, DialogEntry entry)
    {
        var index = slot.Index - 1;
        if (slot.IsFilled && index >= 0 && index < _working.Count)
            _working[index] = entry;
        else
            _working.Add(entry);
        slot.Service = entry.Service;
        slot.Handle = entry.Handle;
        slot.ProfileUrl = entry.ProfileUrl;
        slot.FediverseSoftware = entry.FediverseSoftware;
        slot.IsFilled = true;
        slot.IsEditing = false;
        RebuildSlots();
    }

    /// <summary>The window confirms the delete, then calls this. Slot 0 signs out of YouTube.</summary>
    public async Task DeleteSlotAsync(int slotIndex)
    {
        if (slotIndex == 0)
        {
            if (!IsSignedIn) return;
            await _signOutAction();
            _account = null;
            IsSignedIn = false;
            RebuildSlots();
            return;
        }

        var entryIndex = slotIndex - 1;
        if (entryIndex < 0 || entryIndex >= _working.Count) return;
        _working.RemoveAt(entryIndex);
        RebuildSlots();
    }

    private void Save()
    {
        if (!CanSave) return;
        var entries = new List<SocialEntry>();
        if (IsSignedIn && _account != null)
        {
            var handle = _account.DisplayName.Trim().TrimStart('@');
            entries.Add(new SocialEntry
            {
                Service = SocialService.YouTube,
                Handle = handle,
                ProfileUrl = string.IsNullOrWhiteSpace(_account.ChannelId)
                    ? SocialServiceIcons.CanonicalUrlFor(SocialService.YouTube, handle)
                    : $"https://www.youtube.com/channel/{_account.ChannelId}",
            });
        }
        foreach (var entry in _working)
            entries.Add(new SocialEntry
            {
                Service = entry.Service,
                Handle = entry.Handle,
                ProfileUrl = entry.ProfileUrl,
                FediverseSoftware = entry.FediverseSoftware,
            });
        CommittedEntries = entries;
        SaveRequested?.Invoke();
    }

    /// <summary>Builds the config the main window commits on save, or null when empty.</summary>
    public SocialsConfig? BuildSocialsConfig()
    {
        var entries = CommittedEntries ?? throw new InvalidOperationException("Save was not completed.");
        if (entries.Count == 0) return null;
        var config = new SocialsConfig { BarEnabled = BarEnabled };
        foreach (var entry in entries)
            config.Entries.Add(entry);
        return config;
    }

    private void RebuildSlots()
    {
        foreach (var slot in Slots)
            slot.PropertyChanged -= OnSlotPropertyChanged;
        Slots.Clear();

        for (var i = 0; i < MaxSlots; i++)
        {
            var slot = new SocialSlotViewModel(i);
            if (i == 0)
            {
                slot.Service = SocialService.YouTube;
                slot.IsSignIn = !IsSignedIn;
                slot.IsFilled = IsSignedIn;
                if (IsSignedIn && _account != null)
                    slot.Handle = _account.DisplayName;
            }
            else
            {
                var entryIndex = i - 1;
                if (entryIndex < _working.Count)
                {
                    var entry = _working[entryIndex];
                    slot.Service = entry.Service;
                    slot.IsFilled = true;
                    slot.Handle = entry.Handle;
                    slot.ProfileUrl = entry.ProfileUrl;
                    slot.FediverseSoftware = entry.FediverseSoftware;
                }
                else if (!_isPremium && i >= 2)
                {
                    slot.IsLocked = true;
                }
            }
            slot.PropertyChanged += OnSlotPropertyChanged;
            Slots.Add(slot);
        }
    }

    private void OnSlotPropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName is nameof(SocialSlotViewModel.IsEditing)
            or nameof(SocialSlotViewModel.IsValidating)
            or nameof(SocialSlotViewModel.HasError)
            or nameof(SocialSlotViewModel.EditText))
        {
            OnPropertyChanged(nameof(CanSave));
            CommandManager.InvalidateRequerySuggested();
        }
    }
}
