@namespace ZB.MOM.WW.Secrets.Ui.Components
@implements IDisposable
@inject ISecretCipher Cipher
@inject ISecretStore Store
@inject IAuditWriter Audit
@inject IServiceProvider Services
@* Components/SecretEditor.razor — add (ExistingName == null) or rotate (ExistingName set) a secret.
   The value is entered through a masked input and never rendered back or placed in any audit field. *@

<TechCard Title="@(IsRotate ? "Rotate secret" : "Add secret")">
    <TechField Label="Name">
        @if (IsRotate)
        {
            <input class="form-control mono" type="text" readonly
                   value="@_name" data-testid="secret-name" />
        }
        else
        {
            <input class="form-control mono" type="text"
                   @bind="_name" data-testid="secret-name" />
        }
    </TechField>

    <TechField Label="Description">
        <input class="form-control" type="text" @bind="_description" data-testid="secret-description" />
    </TechField>

    <TechField Label="Content type">
        <select class="form-select" @bind="_contentType" data-testid="secret-content-type">
            @foreach (SecretContentType ct in Enum.GetValues<SecretContentType>())
            {
                <option value="@ct">@ct</option>
            }
        </select>
    </TechField>

    <TechField Label="Value">
        <input class="form-control mono" type="password" autocomplete="new-password"
               @bind="_value" data-testid="secret-value" />
    </TechField>

    @if (_error is not null)
    {
        <p class="field-error s-bad" data-testid="editor-error">@_error</p>
    }

    <div class="btn-row">
        <TechButton Variant="ButtonVariant.Primary" Busy="_busy"
                    data-testid="editor-submit" @onclick="SubmitAsync">
            @(IsRotate ? "Rotate" : "Add")
        </TechButton>
        <TechButton Variant="ButtonVariant.Secondary"
                    data-testid="editor-cancel" @onclick="CancelAsync">
            Cancel
        </TechButton>
    </div>
</TechCard>

@code {
    private readonly CancellationTokenSource _cts = new();

    private string _name = string.Empty;
    private string _description = string.Empty;
    private SecretContentType _contentType = SecretContentType.Text;
    private string _value = string.Empty;
    private string? _error;
    private bool _busy;

    /// <summary>
    /// The secret being rotated. When <c>null</c> the editor is in <em>add</em> mode (name is
    /// editable); when set, the editor is in <em>rotate</em> mode (name is read-only and fixed).
    /// </summary>
    [Parameter] public SecretName? ExistingName { get; set; }

    /// <summary>Raised after a successful add or rotate, so a parent can refresh its listing.</summary>
    [Parameter] public EventCallback OnSaved { get; set; }

    /// <summary>Raised when the user cancels the editor without saving.</summary>
    [Parameter] public EventCallback OnCancelled { get; set; }

    /// <summary>The cascading authentication state, used as the fallback source of the audit actor.</summary>
    [CascadingParameter] private Task<AuthenticationState>? AuthState { get; set; }

    private bool IsRotate => ExistingName is not null;
    private bool _metadataLoaded;

    /// <inheritdoc />
    protected override async Task OnParametersSetAsync()
    {
        if (IsRotate)
        {
            _name = ExistingName!.Value.Value;

            // Rotate must preserve the existing row's metadata: the store's upsert overwrites the
            // description and content type from the incoming row, so pre-fill both from the current
            // row (metadata only — no plaintext is read or decrypted). Load once so a re-render does
            // not clobber operator edits. The operator still supplies the NEW value.
            if (!_metadataLoaded)
            {
                _metadataLoaded = true;
                StoredSecret? row = await Store.GetAsync(ExistingName.Value, _cts.Token);
                _description = row?.Description ?? string.Empty;
                _contentType = row?.ContentType ?? SecretContentType.Text;
            }
        }
    }

    private async Task SubmitAsync()
    {
        _error = null;

        SecretName name;
        try
        {
            name = ExistingName ?? new SecretName(_name);
        }
        catch (ArgumentException ex)
        {
            _error = ex.Message;
            return;
        }

        _busy = true;
        bool saved = false;
        try
        {
            string actor = await SecretActorResolver.ResolveAsync(Services, AuthState);

            try
            {
                StoredSecret row = Cipher.Encrypt(name, _value, _contentType);
                row = row with
                {
                    Description = string.IsNullOrWhiteSpace(_description) ? null : _description,
                    CreatedBy = actor,
                    UpdatedBy = actor,
                };
                await Store.UpsertAsync(row, _cts.Token);

                await Audit.WriteAsync(
                    new AuditEvent
                    {
                        EventId = Guid.NewGuid(),
                        OccurredAtUtc = DateTimeOffset.UtcNow,
                        Actor = actor,
                        Action = IsRotate ? "secret.rotate" : "secret.add",
                        Outcome = AuditOutcome.Success,
                        Category = "Secrets",
                        Target = name.Value,
                    },
                    _cts.Token);

                saved = true;
            }
            catch (Exception)
            {
                // An unexpected store/cipher fault (e.g. master key unavailable, timeout) must still
                // be audited as a Failure and surfaced to the operator — never the value — instead of
                // tearing down the circuit. The detail carries no plaintext.
                _error = IsRotate ? "Unable to rotate the secret." : "Unable to add the secret.";
                await WriteFailureAuditAsync(
                    actor, IsRotate ? "secret.rotate" : "secret.add", name.Value);
            }
        }
        finally
        {
            _busy = false;
        }

        if (saved)
        {
            // Evict any cached plaintext so the ${secret:} / ISecretResolver path does not serve a
            // stale value after this rotate/add for the remainder of its TTL. Optional seam — a no-op
            // when the host has not registered an invalidator.
            Services.GetService<ISecretCacheInvalidator>()?.Invalidate(name);

            await OnSaved.InvokeAsync();
        }
    }

    private async Task WriteFailureAuditAsync(string actor, string action, string target)
    {
        try
        {
            await Audit.WriteAsync(
                new AuditEvent
                {
                    EventId = Guid.NewGuid(),
                    OccurredAtUtc = DateTimeOffset.UtcNow,
                    Actor = actor,
                    Action = action,
                    Outcome = AuditOutcome.Failure,
                    Category = "Secrets",
                    Target = target,
                    DetailsJson = "{\"reason\":\"error\"}",
                },
                _cts.Token);
        }
        catch
        {
            // Best-effort audit of a failure; never let audit itself surface into the event pipeline.
        }
    }

    private Task CancelAsync() => OnCancelled.InvokeAsync();

    /// <inheritdoc />
    public void Dispose()
    {
        _cts.Cancel();
        _cts.Dispose();
    }
}
