using System.Security.Cryptography;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
using ZB.MOM.WW.ScadaBridge.CentralUI.Auth;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.ExternalSystems;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories;
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Transport;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
using ZB.MOM.WW.ScadaBridge.Transport;
using ZB.MOM.WW.ScadaBridge.Transport.Export;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Design;

/// <summary>
/// Code-behind for the TransportExport wizard (Transport feature).
///
/// Four-step state machine:
///   <list type="number">
///     <item><see cref="ExportWizardStep.Select"/> — pick templates + flat artifact lists.</item>
///     <item><see cref="ExportWizardStep.Review"/> — show resolved closure (seed + auto-included).</item>
///     <item><see cref="ExportWizardStep.Encrypt"/> — passphrase + secret-count warning, or explicit unencrypted opt-out.</item>
///     <item><see cref="ExportWizardStep.Download"/> — call <see cref="IBundleExporter"/>, render the file via JS interop.</item>
///   </list>
///
/// The wizard transitions are linear; "Back" returns to the previous step
/// without clearing selection state. "Done" on Step 4 resets to Step 1 fresh.
///
/// <c>SourceEnvironment</c> is sourced from <see cref="TransportOptions.SourceEnvironment"/>
/// (bound from <c>ScadaBridge:Transport:SourceEnvironment</c>) so a multi-cluster
/// deployment can label its bundles distinctly. Defaults to <c>"scadabridge"</c>.
/// </summary>
public partial class TransportExport : ComponentBase
{
    public enum ExportWizardStep
    {
        Select = 1,
        Review = 2,
        Encrypt = 3,
        Download = 4,
    }

    // ---- Injected services ----
    [Inject] private IBundleExporter BundleExporter { get; set; } = default!;
    [Inject] private ITemplateEngineRepository TemplateRepo { get; set; } = default!;
    [Inject] private IExternalSystemRepository ExternalRepo { get; set; } = default!;
    [Inject] private INotificationRepository NotificationRepo { get; set; } = default!;
    [Inject] private IInboundApiRepository InboundApiRepo { get; set; } = default!;
    [Inject] private ISiteRepository SiteRepo { get; set; } = default!;
    [Inject] private DependencyResolver DepResolver { get; set; } = default!;
    [Inject] private IJSRuntime JS { get; set; } = default!;
    [Inject] private AuthenticationStateProvider Auth { get; set; } = default!;
    [Inject] private IOptions<TransportOptions> TransportOptions { get; set; } = default!;

    // ---- Wizard state ----
    private ExportWizardStep _step = ExportWizardStep.Select;
    private bool _loading = true;
    private string? _errorMessage;

    // ---- Step 1: source data ----
    private List<Template> _templates = new();
    private List<TemplateFolder> _folders = new();
    private List<SharedScript> _sharedScripts = new();
    private List<ExternalSystemDefinition> _externalSystems = new();
    private List<DatabaseConnectionDefinition> _dbConnections = new();
    private List<NotificationList> _notificationLists = new();
    private List<SmtpConfiguration> _smtpConfigs = new();
    // SMS provider configs, mirroring _smtpConfigs. Keyed by AccountSid in the
    // bundle; the wizard selects by entity id (same as SMTP) and never surfaces AuthToken.
    private List<SmsConfiguration> _smsConfigs = new();
    // Inbound API keys are not transported between environments; only methods.
    private List<ApiMethod> _apiMethods = new();
    // Site/instance-scoped export. Sites are listed flat; each site's
    // instances hang off it (loaded eagerly via ISiteRepository.GetInstancesBySiteIdAsync)
    // so the operator can pick a whole site or drill into individual instances.
    private List<Site> _sites = new();
    private readonly Dictionary<int, List<Instance>> _instancesBySiteId = new();

    // ---- Step 1: selection state ----
    // TemplateFolderTree uses string keys ("t:{id}", "f:{id}") via TemplateTreeNode.Key.
    // Templates are selected via the tree; the other artifacts use flat checkbox lists
    // backed by integer-id HashSets so wiring is uniform across categories.
    private readonly HashSet<object> _selectedTemplateKeys = new();
    private readonly HashSet<int> _selectedSharedScripts = new();
    private readonly HashSet<int> _selectedExternalSystems = new();
    private readonly HashSet<int> _selectedDbConnections = new();
    private readonly HashSet<int> _selectedNotificationLists = new();
    private readonly HashSet<int> _selectedSmtpConfigs = new();
    // SMS provider config selection, mirroring _selectedSmtpConfigs.
    private readonly HashSet<int> _selectedSmsConfigs = new();
    // No _selectedApiKeys: inbound API keys are not transported.
    private readonly HashSet<int> _selectedApiMethods = new();
    // Site/instance selection backed by entity primary keys, matching the
    // DependencyResolver's SiteIds/InstanceIds (NOT names — the resolver fetches by id).
    private readonly HashSet<int> _selectedSites = new();
    private readonly HashSet<int> _selectedInstances = new();
    // Which site rows are expanded to reveal their instance checkboxes (UI-only).
    private readonly HashSet<int> _expandedSites = new();
    private string _filter = string.Empty;
    private bool _includeDependencies = true;

    // ---- Step 2: dependency-resolved closure ----
    private ResolvedExport? _resolved;
    private bool _resolving;

    // ---- Step 3: encryption ----
    private string _passphrase = string.Empty;
    private string _passphraseConfirm = string.Empty;
    private bool _showUnencryptedConfirm;
    private bool _exportUnencrypted;
    private int _secretCount;

    // ---- Step 4: download result ----
    private string? _downloadFilename;
    private long _downloadSize;
    private string? _downloadSha256;
    private bool _downloadInProgress;
    private string? _downloadError;

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        await LoadAllAsync();
    }

    private async Task LoadAllAsync()
    {
        _loading = true;
        _errorMessage = null;
        try
        {
            _templates = (await TemplateRepo.GetAllTemplatesAsync()).ToList();
            _folders = (await TemplateRepo.GetAllFoldersAsync()).ToList();
            _sharedScripts = (await TemplateRepo.GetAllSharedScriptsAsync()).ToList();
            _externalSystems = (await ExternalRepo.GetAllExternalSystemsAsync()).ToList();
            _dbConnections = (await ExternalRepo.GetAllDatabaseConnectionsAsync()).ToList();
            _notificationLists = (await NotificationRepo.GetAllNotificationListsAsync()).ToList();
            _smtpConfigs = (await NotificationRepo.GetAllSmtpConfigurationsAsync()).ToList();
            // SMS provider configs, mirroring the SMTP load path above.
            _smsConfigs = (await NotificationRepo.GetAllSmsConfigurationsAsync()).ToList();
            // Inbound API keys are not transported — only methods are loaded.
            _apiMethods = (await InboundApiRepo.GetAllApiMethodsAsync()).ToList();

            // Sites + their instances for site/instance-scoped export. Each
            // site's instances are loaded eagerly so the expandable picker has them
            // without a per-click round-trip; sites are ordered by identifier to match
            // the bundle's deterministic site ordering.
            _sites = (await SiteRepo.GetAllSitesAsync())
                .OrderBy(s => s.SiteIdentifier, StringComparer.OrdinalIgnoreCase)
                .ToList();
            _instancesBySiteId.Clear();
            foreach (var site in _sites)
            {
                var instances = (await SiteRepo.GetInstancesBySiteIdAsync(site.Id))
                    .OrderBy(i => i.UniqueName, StringComparer.OrdinalIgnoreCase)
                    .ToList();
                _instancesBySiteId[site.Id] = instances;
            }
        }
        catch (Exception ex)
        {
            _errorMessage = $"Failed to load export source data: {ex.Message}";
        }
        finally
        {
            _loading = false;
        }
    }

    // ---- Selection helpers ----

    /// <summary>
    /// Project the tree's checkbox-keys back to template ids. Template keys are
    /// the strings produced by <c>TemplateTreeNode.Key</c> — folder ids are
    /// excluded (folders aren't directly exportable; their templates are).
    /// </summary>
    private IReadOnlyList<int> SelectedTemplateIds()
    {
        var ids = new List<int>();
        foreach (var key in _selectedTemplateKeys)
        {
            if (key is string s && s.StartsWith("t:", StringComparison.Ordinal)
                && int.TryParse(s.AsSpan(2), out var id))
            {
                ids.Add(id);
            }
        }
        return ids;
    }

    /// <summary>
    /// True when the user has ticked at least one item in any category. Drives
    /// the "Next" button on Step 1.
    /// </summary>
    internal bool HasAnySelection =>
        SelectedTemplateIds().Count > 0
        || _selectedSharedScripts.Count > 0
        || _selectedExternalSystems.Count > 0
        || _selectedDbConnections.Count > 0
        || _selectedNotificationLists.Count > 0
        || _selectedSmtpConfigs.Count > 0
        || _selectedSmsConfigs.Count > 0
        || _selectedApiMethods.Count > 0
        || _selectedSites.Count > 0
        || _selectedInstances.Count > 0;

    private bool PassphraseValid =>
        !string.IsNullOrEmpty(_passphrase)
        && _passphrase.Length >= 8
        && _passphrase == _passphraseConfirm;

    /// <summary>
    /// Coarse strength score 0-4 based on length + character-class diversity. Used
    /// to colour an inline strength meter; never used to gate the export — the
    /// importer enforces its own strength + lockout policies.
    /// </summary>
    /// <param name="s">The passphrase string to score.</param>
    /// <returns>An integer from 0 (blank) to 4 (long, mixed case, digits, and symbols).</returns>
    internal static int PassphraseStrength(string s)
    {
        if (string.IsNullOrEmpty(s)) return 0;
        var score = 0;
        if (s.Length >= 8) score++;
        if (s.Length >= 16) score++;
        if (s.Any(char.IsUpper) && s.Any(char.IsLower)) score++;
        if (s.Any(char.IsDigit) && s.Any(ch => !char.IsLetterOrDigit(ch))) score++;
        return Math.Min(4, score);
    }

    // ---- Wizard transitions ----

    private ExportSelection BuildSelection()
    {
        return new ExportSelection(
            TemplateIds: SelectedTemplateIds(),
            SharedScriptIds: _selectedSharedScripts.ToList(),
            ExternalSystemIds: _selectedExternalSystems.ToList(),
            DatabaseConnectionIds: _selectedDbConnections.ToList(),
            NotificationListIds: _selectedNotificationLists.ToList(),
            SmtpConfigurationIds: _selectedSmtpConfigs.ToList(),
            // Inbound API keys are not transported — methods only.
            ApiMethodIds: _selectedApiMethods.ToList(),
            IncludeDependencies: _includeDependencies)
        {
            // Site/instance ids feed the resolver's SiteIds/InstanceIds.
            // Set via init-only properties so the positional ctor stays the documented
            // additive shape; the resolver dedups a selected instance against its
            // already-selected owning site.
            SiteIds = _selectedSites.ToList(),
            InstanceIds = _selectedInstances.ToList(),
            // SMS provider config ids feed the resolver's SmsConfigurationIds,
            // exactly as SmtpConfigurationIds above (init-only, trailing additive shape).
            SmsConfigurationIds = _selectedSmsConfigs.ToList(),
        };
    }

    private async Task GoToReviewAsync()
    {
        if (!HasAnySelection) return;
        _resolving = true;
        _errorMessage = null;
        try
        {
            var selection = BuildSelection();
            _resolved = await DepResolver.ResolveAsync(selection, CancellationToken.None);
            _step = ExportWizardStep.Review;
        }
        catch (Exception ex)
        {
            _errorMessage = $"Failed to resolve dependencies: {ex.Message}";
        }
        finally
        {
            _resolving = false;
        }
    }

    private async Task ReresolveAsync()
    {
        // Re-run resolution from Step 2 when the user toggles IncludeDependencies.
        _resolving = true;
        try
        {
            _resolved = await DepResolver.ResolveAsync(BuildSelection(), CancellationToken.None);
        }
        finally
        {
            _resolving = false;
        }
    }

    private void GoToEncrypt()
    {
        // Recompute the secret-field count from the resolved closure so the
        // warning banner stays honest if the user backed up and re-resolved.
        if (_resolved is not null)
        {
            _secretCount = CountSecrets(_resolved);
        }
        _step = ExportWizardStep.Encrypt;
    }

    /// <summary>
    /// Count the secret fields that <see cref="BundleSecretEncryptor"/> will
    /// envelope-encrypt. Surfaces in the Step 3 warning banner so the user
    /// knows exactly what an unencrypted export would leak.
    /// </summary>
    /// <param name="resolved">The resolved export closure whose secret fields are counted.</param>
    /// <returns>The total number of non-empty secret fields across all external systems, SMTP configs, and database connections.</returns>
    internal static int CountSecrets(ResolvedExport resolved)
    {
        var count = 0;
        foreach (var es in resolved.ExternalSystems)
        {
            if (!string.IsNullOrEmpty(es.AuthConfiguration)) count++;
        }
        foreach (var smtp in resolved.SmtpConfigs)
        {
            if (!string.IsNullOrEmpty(smtp.Credentials)) count++;
        }
        // SMS provider AuthToken is a secret, mirroring SMTP Credentials above.
        foreach (var sms in resolved.SmsConfigs)
        {
            if (!string.IsNullOrEmpty(sms.AuthToken)) count++;
        }
        foreach (var db in resolved.DatabaseConnections)
        {
            if (!string.IsNullOrEmpty(db.ConnectionString)) count++;
        }
        foreach (var dc in resolved.DataConnections)
        {
            if (!string.IsNullOrEmpty(dc.PrimaryConfiguration)) count++;
            if (!string.IsNullOrEmpty(dc.BackupConfiguration)) count++;
        }
        return count;
    }

    private void BackToSelect() => _step = ExportWizardStep.Select;
    private void BackToReview() => _step = ExportWizardStep.Review;

    private void OpenUnencryptedConfirm()
    {
        _showUnencryptedConfirm = true;
    }

    private async Task ConfirmUnencryptedExport()
    {
        _showUnencryptedConfirm = false;
        _exportUnencrypted = true;
        await StartExportAsync(passphrase: null);
    }

    private void CancelUnencryptedConfirm()
    {
        _showUnencryptedConfirm = false;
    }

    private async Task StartEncryptedExportAsync()
    {
        if (!PassphraseValid) return;
        _exportUnencrypted = false;
        await StartExportAsync(_passphrase);
    }

    /// <summary>
    /// Final export step: invokes <see cref="IBundleExporter.ExportAsync"/>,
    /// captures the bundle bytes, computes a display-side SHA-256 (matching
    /// the manifest's content hash naming), and pushes the file to the browser
    /// via JS interop. Errors surface inline; the page never throws to the user.
    /// </summary>
    private async Task StartExportAsync(string? passphrase)
    {
        _step = ExportWizardStep.Download;
        _downloadInProgress = true;
        _downloadError = null;
        try
        {
            var user = await Auth.GetCurrentUsernameAsync();
            var sourceEnv = TransportOptions.Value.SourceEnvironment;
            if (string.IsNullOrWhiteSpace(sourceEnv))
            {
                sourceEnv = "scadabridge";
            }

            var selection = BuildSelection();
            await using var stream = await BundleExporter.ExportAsync(
                selection, user, sourceEnv, passphrase, CancellationToken.None);

            byte[] bytes;
            if (stream is MemoryStream ms)
            {
                bytes = ms.ToArray();
            }
            else
            {
                using var copy = new MemoryStream();
                await stream.CopyToAsync(copy);
                bytes = copy.ToArray();
            }

            _downloadSize = bytes.LongLength;
            _downloadSha256 = "sha256:" + Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
            _downloadFilename = BuildFilename(sourceEnv);

            var base64 = Convert.ToBase64String(bytes);
            await JS.InvokeVoidAsync("scadabridgeTransport.downloadBundle", _downloadFilename, base64);
        }
        catch (Exception ex)
        {
            _downloadError = ex.Message;
        }
        finally
        {
            _downloadInProgress = false;
        }
    }

    /// <summary>
    /// Filename pattern <c>scadabundle-{sourceEnv}-{yyyy-MM-dd-HHmmss}.scadabundle</c>.
    /// Source environment characters are sanitised to a filename-safe alphabet so
    /// odd chars in <c>TransportOptions.SourceEnvironment</c> don't produce
    /// browser-rejected filenames.
    /// </summary>
    /// <param name="sourceEnvironment">The environment label to embed in the filename (sanitised to filename-safe characters).</param>
    /// <param name="nowUtc">Timestamp to use for the datetime segment; defaults to <see cref="DateTimeOffset.UtcNow"/> when null.</param>
    /// <returns>A filename of the form <c>scadabundle-{env}-{yyyy-MM-dd-HHmmss}.scadabundle</c>.</returns>
    internal static string BuildFilename(string sourceEnvironment, DateTimeOffset? nowUtc = null)
    {
        var safe = SanitizeForFilename(sourceEnvironment);
        var ts = (nowUtc ?? DateTimeOffset.UtcNow).ToString("yyyy-MM-dd-HHmmss");
        return $"scadabundle-{safe}-{ts}.scadabundle";
    }

    private static string SanitizeForFilename(string input)
    {
        if (string.IsNullOrWhiteSpace(input)) return "scadabridge";
        var chars = input.Select(c => char.IsLetterOrDigit(c) || c is '-' or '_' ? c : '-').ToArray();
        return new string(chars);
    }

    private void Done()
    {
        // Reset every wizard piece so the operator can immediately start a fresh
        // export without page-refresh-induced data reload.
        _step = ExportWizardStep.Select;
        _selectedTemplateKeys.Clear();
        _selectedSharedScripts.Clear();
        _selectedExternalSystems.Clear();
        _selectedDbConnections.Clear();
        _selectedNotificationLists.Clear();
        _selectedSmtpConfigs.Clear();
        _selectedSmsConfigs.Clear();
        _selectedApiMethods.Clear();
        _selectedSites.Clear();
        _selectedInstances.Clear();
        _expandedSites.Clear();
        _filter = string.Empty;
        _includeDependencies = true;
        _resolved = null;
        _passphrase = string.Empty;
        _passphraseConfirm = string.Empty;
        _exportUnencrypted = false;
        _showUnencryptedConfirm = false;
        _downloadFilename = null;
        _downloadSize = 0;
        _downloadSha256 = null;
        _downloadError = null;
    }

    // ---- Flat-list filter helpers (search box reuses TemplateFolderTree.Filter for the tree) ----
    private bool MatchesFilter(string name) =>
        string.IsNullOrWhiteSpace(_filter)
        || name.Contains(_filter, StringComparison.OrdinalIgnoreCase);

    private static void Toggle(HashSet<int> set, int id, bool value)
    {
        if (value) set.Add(id);
        else set.Remove(id);
    }

    // ---- Step 1 site/instance helpers ----

    /// <summary>Instances loaded for a site, or an empty list when the site has none.</summary>
    private IReadOnlyList<Instance> InstancesFor(int siteId) =>
        _instancesBySiteId.TryGetValue(siteId, out var list) ? list : Array.Empty<Instance>();

    /// <summary>Expand/collapse a site row's nested instance list (UI-only state).</summary>
    private void ToggleSiteExpanded(int siteId)
    {
        if (!_expandedSites.Remove(siteId))
        {
            _expandedSites.Add(siteId);
        }
    }

    /// <summary>
    /// Toggle a whole site. Selecting a site lets the resolver pull its instances,
    /// so on select we clear any redundant per-instance ticks for that site; on
    /// deselect we leave individual instances untouched (the operator may still
    /// want a subset). Matches the resolver's dedup semantics.
    /// </summary>
    private void ToggleSite(int siteId, bool value)
    {
        if (value)
        {
            _selectedSites.Add(siteId);
            foreach (var instance in InstancesFor(siteId))
            {
                _selectedInstances.Remove(instance.Id);
            }
        }
        else
        {
            _selectedSites.Remove(siteId);
        }
    }

    // ---- Step 2 grouping helpers ----

    /// <summary>
    /// Items that are in <paramref name="all"/> but NOT in <paramref name="seed"/> —
    /// the auto-included dependencies the resolver pulled in for the user.
    /// </summary>
    /// <typeparam name="T">The element type of the artifact list.</typeparam>
    /// <param name="all">The full resolved list including both seed and auto-included items.</param>
    /// <param name="seed">The set of explicitly selected item ids.</param>
    /// <param name="idOf">Function that extracts the integer id from an item.</param>
    /// <returns>Items from <paramref name="all"/> whose ids are not in <paramref name="seed"/> (auto-included dependencies).</returns>
    internal static IReadOnlyList<T> AutoIncluded<T>(IReadOnlyList<T> all, IReadOnlyCollection<int> seed, Func<T, int> idOf)
    {
        return all.Where(x => !seed.Contains(idOf(x))).ToList();
    }
}
