using System.Collections.ObjectModel;
using System.IO;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
using SharepointToolbox.Services;
using AppGraphClientFactory = SharepointToolbox.Infrastructure.Auth.GraphClientFactory;

namespace SharepointToolbox.ViewModels;

public partial class ProfileManagementViewModel : ObservableObject
{
    private readonly ProfileService _profileService;
    private readonly IBrandingService _brandingService;
    private readonly AppGraphClientFactory _graphClientFactory;
    private readonly ILogger<ProfileManagementViewModel> _logger;
    private readonly IAppRegistrationService _appRegistrationService;

    // Well-known public client (Microsoft Graph Command Line Tools) used as a bootstrap
    // when a profile has no ClientId yet, so the user can sign in as admin and have the
    // app registration created for them.
    private const string BootstrapClientId = "14d82eec-204b-4c2f-b7e8-296a70dab67e";

    [ObservableProperty]
    private TenantProfile? _selectedProfile;

    [ObservableProperty]
    private string _newName = string.Empty;

    [ObservableProperty]
    private string _newTenantUrl = string.Empty;

    [ObservableProperty]
    private string _newClientId = string.Empty;

    [ObservableProperty]
    private string _validationMessage = string.Empty;

    [ObservableProperty]
    private bool _isRegistering;

    [ObservableProperty]
    private bool _showFallbackInstructions;

    [ObservableProperty]
    private string _registrationStatus = string.Empty;

    public bool HasRegisteredApp => SelectedProfile?.AppId != null;

    private string? _clientLogoPreview;
    public string? ClientLogoPreview
    {
        get => _clientLogoPreview;
        private set { _clientLogoPreview = value; OnPropertyChanged(); }
    }

    public ObservableCollection<TenantProfile> Profiles { get; } = new();

    public IAsyncRelayCommand AddCommand { get; }
    public IAsyncRelayCommand SaveCommand { get; }
    public IAsyncRelayCommand DeleteCommand { get; }
    public IAsyncRelayCommand BrowseClientLogoCommand { get; }
    public IAsyncRelayCommand ClearClientLogoCommand { get; }
    public IAsyncRelayCommand AutoPullClientLogoCommand { get; }
    public IAsyncRelayCommand RegisterAppCommand { get; }
    public IAsyncRelayCommand RemoveAppCommand { get; }

    public ProfileManagementViewModel(
        ProfileService profileService,
        IBrandingService brandingService,
        AppGraphClientFactory graphClientFactory,
        ILogger<ProfileManagementViewModel> logger,
        IAppRegistrationService appRegistrationService)
    {
        _profileService = profileService;
        _brandingService = brandingService;
        _graphClientFactory = graphClientFactory;
        _logger = logger;
        _appRegistrationService = appRegistrationService;

        AddCommand = new AsyncRelayCommand(AddAsync, CanAdd);
        SaveCommand = new AsyncRelayCommand(SaveAsync, CanSave);
        DeleteCommand = new AsyncRelayCommand(DeleteAsync, () => SelectedProfile != null);
        BrowseClientLogoCommand = new AsyncRelayCommand(BrowseClientLogoAsync, () => SelectedProfile != null);
        ClearClientLogoCommand = new AsyncRelayCommand(ClearClientLogoAsync, () => SelectedProfile != null);
        AutoPullClientLogoCommand = new AsyncRelayCommand(AutoPullClientLogoAsync, () => SelectedProfile != null);
        RegisterAppCommand = new AsyncRelayCommand(RegisterAppAsync, CanRegisterApp);
        RemoveAppCommand = new AsyncRelayCommand(RemoveAppAsync, CanRemoveApp);
    }

    public async Task LoadAsync()
    {
        try
        {
            var profiles = await _profileService.GetProfilesAsync();
            Profiles.Clear();
            foreach (var p in profiles)
                Profiles.Add(p);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load profiles.");
        }
    }

    partial void OnNewNameChanged(string value) => NotifyCommandsCanExecuteChanged();
    partial void OnNewTenantUrlChanged(string value) => NotifyCommandsCanExecuteChanged();
    partial void OnNewClientIdChanged(string value) => NotifyCommandsCanExecuteChanged();

    partial void OnSelectedProfileChanged(TenantProfile? value)
    {
        if (value != null)
        {
            NewName = value.Name;
            NewTenantUrl = value.TenantUrl;
            NewClientId = value.ClientId ?? string.Empty;
        }
        else
        {
            NewName = string.Empty;
            NewTenantUrl = string.Empty;
            NewClientId = string.Empty;
        }
        ValidationMessage = string.Empty;
        ClientLogoPreview = FormatLogoPreview(value?.ClientLogo);
        BrowseClientLogoCommand.NotifyCanExecuteChanged();
        ClearClientLogoCommand.NotifyCanExecuteChanged();
        AutoPullClientLogoCommand.NotifyCanExecuteChanged();
        SaveCommand.NotifyCanExecuteChanged();
        DeleteCommand.NotifyCanExecuteChanged();
        OnPropertyChanged(nameof(HasRegisteredApp));
        RegisterAppCommand.NotifyCanExecuteChanged();
        RemoveAppCommand.NotifyCanExecuteChanged();
    }

    partial void OnIsRegisteringChanged(bool value)
    {
        RegisterAppCommand.NotifyCanExecuteChanged();
        RemoveAppCommand.NotifyCanExecuteChanged();
    }

    private static string? FormatLogoPreview(LogoData? logo)
        => logo is not null ? $"data:{logo.MimeType};base64,{logo.Base64}" : null;

    private void NotifyCommandsCanExecuteChanged()
    {
        AddCommand.NotifyCanExecuteChanged();
        SaveCommand.NotifyCanExecuteChanged();
    }

    private bool CanAdd()
    {
        if (string.IsNullOrWhiteSpace(NewName)) return false;
        if (!Uri.TryCreate(NewTenantUrl, UriKind.Absolute, out _)) return false;
        // Fields mirror the selected profile after selection; block Add so the user doesn't
        // create a duplicate — they should use Save to update, or change the name to fork.
        if (SelectedProfile != null &&
            string.Equals(NewName.Trim(), SelectedProfile.Name, StringComparison.Ordinal))
            return false;
        // ClientId is optional — leaving it blank lets the user register the app from within the tool.
        return true;
    }

    private bool CanSave()
    {
        if (SelectedProfile == null) return false;
        if (string.IsNullOrWhiteSpace(NewName)) return false;
        if (!Uri.TryCreate(NewTenantUrl, UriKind.Absolute, out _)) return false;
        return true;
    }

    private async Task AddAsync()
    {
        if (!CanAdd()) return;
        try
        {
            var profile = new TenantProfile
            {
                Name = NewName.Trim(),
                TenantUrl = NewTenantUrl.Trim(),
                ClientId = NewClientId?.Trim() ?? string.Empty
            };
            await _profileService.AddProfileAsync(profile);
            Profiles.Add(profile);
            NewName = string.Empty;
            NewTenantUrl = string.Empty;
            NewClientId = string.Empty;
            ValidationMessage = string.Empty;
        }
        catch (Exception ex)
        {
            ValidationMessage = ex.Message;
            _logger.LogError(ex, "Failed to add profile.");
        }
    }

    private async Task SaveAsync()
    {
        if (!CanSave()) return;
        var target = SelectedProfile!;
        try
        {
            var newName = NewName.Trim();
            var newUrl = NewTenantUrl.Trim();
            var newClientId = NewClientId?.Trim() ?? string.Empty;
            var oldName = target.Name;

            if (!string.Equals(oldName, newName, StringComparison.Ordinal))
            {
                await _profileService.RenameProfileAsync(oldName, newName);
                target.Name = newName;
            }

            target.TenantUrl = newUrl;
            target.ClientId = newClientId;
            await _profileService.UpdateProfileAsync(target);

            // Force ListBox to pick up the renamed entry (TenantProfile is a plain POCO,
            // so mutating Name does not raise PropertyChanged).
            var idx = Profiles.IndexOf(target);
            if (idx >= 0)
            {
                Profiles.RemoveAt(idx);
                Profiles.Insert(idx, target);
                SelectedProfile = target;
            }

            ValidationMessage = string.Empty;
        }
        catch (Exception ex)
        {
            ValidationMessage = ex.Message;
            _logger.LogError(ex, "Failed to save profile.");
        }
    }

    private async Task DeleteAsync()
    {
        if (SelectedProfile == null) return;
        try
        {
            await _profileService.DeleteProfileAsync(SelectedProfile.Name);
            Profiles.Remove(SelectedProfile);
            SelectedProfile = null;
        }
        catch (Exception ex)
        {
            ValidationMessage = ex.Message;
            _logger.LogError(ex, "Failed to delete profile.");
        }
    }

    private async Task BrowseClientLogoAsync()
    {
        if (SelectedProfile == null) return;
        var dialog = new OpenFileDialog
        {
            Title = "Select client logo",
            Filter = "Image files (*.png;*.jpg;*.jpeg)|*.png;*.jpg;*.jpeg",
        };
        if (dialog.ShowDialog() != true) return;
        try
        {
            var logo = await _brandingService.ImportLogoAsync(dialog.FileName);
            SelectedProfile.ClientLogo = logo;
            ClientLogoPreview = FormatLogoPreview(logo);
            await _profileService.UpdateProfileAsync(SelectedProfile);
            ValidationMessage = string.Empty;
        }
        catch (Exception ex)
        {
            ValidationMessage = ex.Message;
            _logger.LogError(ex, "Failed to import client logo.");
        }
    }

    private async Task ClearClientLogoAsync()
    {
        if (SelectedProfile == null) return;
        try
        {
            SelectedProfile.ClientLogo = null;
            ClientLogoPreview = null;
            await _profileService.UpdateProfileAsync(SelectedProfile);
            ValidationMessage = string.Empty;
        }
        catch (Exception ex)
        {
            ValidationMessage = ex.Message;
            _logger.LogError(ex, "Failed to clear client logo.");
        }
    }

    private async Task AutoPullClientLogoAsync()
    {
        if (SelectedProfile == null) return;
        try
        {
            var graphClient = await _graphClientFactory.CreateClientAsync(
                SelectedProfile.ClientId, CancellationToken.None);

            var orgs = await graphClient.Organization.GetAsync();
            var orgId = orgs?.Value?.FirstOrDefault()?.Id;
            if (orgId is null)
            {
                ValidationMessage = "Could not determine organization ID.";
                return;
            }

            var stream = await graphClient.Organization[orgId]
                .Branding.Localizations["default"].SquareLogo.GetAsync();

            if (stream is null || stream.Length == 0)
            {
                ValidationMessage = "No branding logo found for this tenant.";
                return;
            }

            using var ms = new MemoryStream();
            await stream.CopyToAsync(ms);
            var bytes = ms.ToArray();

            var logo = await _brandingService.ImportLogoFromBytesAsync(bytes);
            SelectedProfile.ClientLogo = logo;
            ClientLogoPreview = FormatLogoPreview(logo);
            await _profileService.UpdateProfileAsync(SelectedProfile);
            ValidationMessage = "Client logo pulled from Entra branding.";
        }
        catch (Microsoft.Graph.Models.ODataErrors.ODataError ex) when (ex.ResponseStatusCode == 404)
        {
            ValidationMessage = "No Entra branding configured for this tenant.";
        }
        catch (Exception ex)
        {
            ValidationMessage = $"Failed to pull logo: {ex.Message}";
            _logger.LogWarning(ex, "Auto-pull client logo failed.");
        }
    }

    private bool CanRegisterApp()
        => SelectedProfile != null && SelectedProfile.AppId == null && !IsRegistering;

    private bool CanRemoveApp()
        => SelectedProfile != null && SelectedProfile.AppId != null && !IsRegistering;

    /// <summary>
    /// Set by the view to display the pre-registration warning. Returns true if the
    /// user accepts and registration should proceed.
    /// </summary>
    public Func<string, bool>? ConfirmRegisterApp { get; set; }

    private async Task RegisterAppAsync(CancellationToken ct)
    {
        if (SelectedProfile == null) return;

        // Auth caching reduces this to one prompt in the common case, but a fresh
        // tenant or different admin account may still trigger up to two — warn so
        // the user knows another window is expected after they sign in.
        if (ConfirmRegisterApp != null)
        {
            var msg = string.Format(TranslationSource.Instance["profile.register.warning"], 2);
            if (!ConfirmRegisterApp(msg)) return;
        }

        IsRegistering = true;
        ShowFallbackInstructions = false;
        RegistrationStatus = TranslationSource.Instance["profile.register.checking"];
        try
        {
            // Use the profile's own ClientId if it has one; otherwise bootstrap with the
            // Microsoft Graph Command Line Tools public client so a first-time profile
            // (name + URL only) can still perform registration.
            var authClientId = string.IsNullOrWhiteSpace(SelectedProfile.ClientId)
                ? BootstrapClientId
                : SelectedProfile.ClientId;

            // No preflight admin check: it used Global Admin as the criterion and
            // rejected Application Admins / Cloud Application Admins who can also
            // create apps. Let Entra enforce authorization via the POST itself —
            // any 401/403 returns FallbackRequired and triggers the tutorial.
            RegistrationStatus = TranslationSource.Instance["profile.register.registering"];
            var result = await _appRegistrationService.RegisterAsync(authClientId, SelectedProfile.TenantUrl, SelectedProfile.Name, ct);

            if (result.IsSuccess)
            {
                SelectedProfile.AppId = result.AppId;
                // If the profile had no ClientId, adopt the freshly registered app's id
                // so subsequent sign-ins use the profile's own app registration.
                if (string.IsNullOrWhiteSpace(SelectedProfile.ClientId))
                    SelectedProfile.ClientId = result.AppId!;
                await _profileService.UpdateProfileAsync(SelectedProfile);
                // Reflect adopted ClientId in the bound TextBox. Without this the
                // field stays blank and the next Save would overwrite the stored
                // ClientId with an empty string.
                NewClientId = SelectedProfile.ClientId;
                RegistrationStatus = TranslationSource.Instance["profile.register.success"];
                OnPropertyChanged(nameof(HasRegisteredApp));
            }
            else if (result.IsFallback)
            {
                ShowFallbackInstructions = true;
                RegistrationStatus = TranslationSource.Instance["profile.register.noperm"];
            }
            else
            {
                RegistrationStatus = result.ErrorMessage ?? TranslationSource.Instance["profile.register.failed"];
            }
        }
        catch (Exception ex)
        {
            RegistrationStatus = ex.Message;
            _logger.LogError(ex, "Failed to register application.");
        }
        finally
        {
            IsRegistering = false;
        }
    }

    private async Task RemoveAppAsync(CancellationToken ct)
    {
        if (SelectedProfile == null) return;
        IsRegistering = true;
        RegistrationStatus = TranslationSource.Instance["profile.remove.removing"];
        try
        {
            await _appRegistrationService.RemoveAsync(SelectedProfile.ClientId, SelectedProfile.TenantUrl, SelectedProfile.AppId!, ct);
            await _appRegistrationService.ClearMsalSessionAsync(SelectedProfile.ClientId, SelectedProfile.TenantUrl);
            SelectedProfile.AppId = null;
            await _profileService.UpdateProfileAsync(SelectedProfile);
            RegistrationStatus = TranslationSource.Instance["profile.remove.success"];
            OnPropertyChanged(nameof(HasRegisteredApp));
        }
        catch (Exception ex)
        {
            RegistrationStatus = ex.Message;
            _logger.LogError(ex, "Failed to remove application.");
        }
        finally
        {
            IsRegistering = false;
        }
    }
}
