using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Cryptography.Audit;

namespace StellaOps.Authority.Plugins.Abstractions;

/// <summary>
/// Describes feature support advertised by an identity provider plugin.
/// </summary>
public sealed record AuthorityIdentityProviderCapabilities(
    bool SupportsPassword,
    bool SupportsMfa,
    bool SupportsClientProvisioning)
{
    /// <summary>
    /// Builds capabilities metadata from a list of capability identifiers.
    /// </summary>
    public static AuthorityIdentityProviderCapabilities FromCapabilities(IEnumerable<string> capabilities)
    {
        if (capabilities is null)
        {
            return new AuthorityIdentityProviderCapabilities(false, false, false);
        }

        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var entry in capabilities)
        {
            if (string.IsNullOrWhiteSpace(entry))
            {
                continue;
            }

            seen.Add(entry.Trim());
        }

        return new AuthorityIdentityProviderCapabilities(
            SupportsPassword: seen.Contains(AuthorityPluginCapabilities.Password),
            SupportsMfa: seen.Contains(AuthorityPluginCapabilities.Mfa),
            SupportsClientProvisioning: seen.Contains(AuthorityPluginCapabilities.ClientProvisioning));
    }
}

/// <summary>
/// Represents a loaded Authority identity provider plugin instance.
/// </summary>
public interface IIdentityProviderPlugin
{
    /// <summary>
    /// Gets the logical name of the plugin instance (matches the manifest key).
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Gets the plugin type identifier (e.g. <c>standard</c>, <c>ldap</c>).
    /// </summary>
    string Type { get; }

    /// <summary>
    /// Gets the plugin context comprising the manifest and bound configuration.
    /// </summary>
    AuthorityPluginContext Context { get; }

    /// <summary>
    /// Gets the credential store responsible for authenticator validation and user provisioning.
    /// </summary>
    IUserCredentialStore Credentials { get; }

    /// <summary>
    /// Gets the claims enricher applied to issued principals.
    /// </summary>
    IClaimsEnricher ClaimsEnricher { get; }

    /// <summary>
    /// Gets the optional client provisioning store exposed by the plugin.
    /// </summary>
    IClientProvisioningStore? ClientProvisioning { get; }

    /// <summary>
    /// Gets the capability metadata advertised by the plugin.
    /// </summary>
    AuthorityIdentityProviderCapabilities Capabilities { get; }

    /// <summary>
    /// Evaluates the health of the plugin and backing data stores.
    /// </summary>
    /// <param name="cancellationToken">Token used to cancel the operation.</param>
    /// <returns>Health result describing the plugin status.</returns>
    ValueTask<AuthorityPluginHealthResult> CheckHealthAsync(CancellationToken cancellationToken);
}

/// <summary>
/// Supplies operations for validating credentials and managing user records.
/// </summary>
public interface IUserCredentialStore
{
    /// <summary>
    /// Verifies the supplied username/password combination.
    /// </summary>
    ValueTask<AuthorityCredentialVerificationResult> VerifyPasswordAsync(
        string username,
        string password,
        CancellationToken cancellationToken);

    /// <summary>
    /// Creates or updates a user record based on the supplied registration data.
    /// </summary>
    ValueTask<AuthorityPluginOperationResult<AuthorityUserDescriptor>> UpsertUserAsync(
        AuthorityUserRegistration registration,
        CancellationToken cancellationToken);

    /// <summary>
    /// Attempts to resolve a user descriptor by its canonical subject identifier.
    /// </summary>
    ValueTask<AuthorityUserDescriptor?> FindBySubjectAsync(
        string subjectId,
        CancellationToken cancellationToken);
}

/// <summary>
/// Enriches issued principals with additional claims based on plugin-specific rules.
/// </summary>
public interface IClaimsEnricher
{
    /// <summary>
    /// Adds or adjusts claims on the provided identity.
    /// </summary>
    ValueTask EnrichAsync(
        ClaimsIdentity identity,
        AuthorityClaimsEnrichmentContext context,
        CancellationToken cancellationToken);
}

/// <summary>
/// Manages client (machine-to-machine) provisioning for Authority.
/// </summary>
public interface IClientProvisioningStore
{
    /// <summary>
    /// Creates or updates a client registration.
    /// </summary>
    ValueTask<AuthorityPluginOperationResult<AuthorityClientDescriptor>> CreateOrUpdateAsync(
        AuthorityClientRegistration registration,
        CancellationToken cancellationToken);

    /// <summary>
    /// Attempts to resolve a client descriptor by its identifier.
    /// </summary>
    ValueTask<AuthorityClientDescriptor?> FindByClientIdAsync(
        string clientId,
        CancellationToken cancellationToken);

    /// <summary>
    /// Removes a client registration.
    /// </summary>
    ValueTask<AuthorityPluginOperationResult> DeleteAsync(
        string clientId,
        CancellationToken cancellationToken);
}

/// <summary>
/// Represents the health state of a plugin or backing store.
/// </summary>
public enum AuthorityPluginHealthStatus
{
    /// <summary>
    /// Plugin is healthy and operational.
    /// </summary>
    Healthy,

    /// <summary>
    /// Plugin is degraded but still usable (e.g. transient connectivity issues).
    /// </summary>
    Degraded,

    /// <summary>
    /// Plugin is unavailable and cannot service requests.
    /// </summary>
    Unavailable
}

/// <summary>
/// Result of a plugin health probe.
/// </summary>
public sealed record AuthorityPluginHealthResult
{
    private AuthorityPluginHealthResult(
        AuthorityPluginHealthStatus status,
        string? message,
        IReadOnlyDictionary<string, string?> details)
    {
        Status = status;
        Message = message;
        Details = details;
    }

    /// <summary>
    /// Gets the overall status of the plugin.
    /// </summary>
    public AuthorityPluginHealthStatus Status { get; }

    /// <summary>
    /// Gets an optional human-readable status description.
    /// </summary>
    public string? Message { get; }

    /// <summary>
    /// Gets optional structured details for diagnostics.
    /// </summary>
    public IReadOnlyDictionary<string, string?> Details { get; }

    /// <summary>
    /// Creates a healthy result.
    /// </summary>
    public static AuthorityPluginHealthResult Healthy(
        string? message = null,
        IReadOnlyDictionary<string, string?>? details = null)
        => new(AuthorityPluginHealthStatus.Healthy, message, details ?? EmptyDetails);

    /// <summary>
    /// Creates a degraded result.
    /// </summary>
    public static AuthorityPluginHealthResult Degraded(
        string? message = null,
        IReadOnlyDictionary<string, string?>? details = null)
        => new(AuthorityPluginHealthStatus.Degraded, message, details ?? EmptyDetails);

    /// <summary>
    /// Creates an unavailable result.
    /// </summary>
    public static AuthorityPluginHealthResult Unavailable(
        string? message = null,
        IReadOnlyDictionary<string, string?>? details = null)
        => new(AuthorityPluginHealthStatus.Unavailable, message, details ?? EmptyDetails);

    private static readonly IReadOnlyDictionary<string, string?> EmptyDetails =
        new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
}

/// <summary>
/// Describes a canonical Authority user surfaced by a plugin.
/// </summary>
public sealed record AuthorityUserDescriptor
{
    /// <summary>
    /// Initialises a new user descriptor.
    /// </summary>
    public AuthorityUserDescriptor(
        string subjectId,
        string username,
        string? displayName,
        bool requiresPasswordReset,
        IReadOnlyCollection<string>? roles = null,
        IReadOnlyDictionary<string, string?>? attributes = null)
    {
        SubjectId = ValidateRequired(subjectId, nameof(subjectId));
        Username = ValidateRequired(username, nameof(username));
        DisplayName = displayName;
        RequiresPasswordReset = requiresPasswordReset;
        Roles = roles is null ? Array.Empty<string>() : roles.ToArray();
        Attributes = attributes is null
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
            : new Dictionary<string, string?>(attributes, StringComparer.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Stable subject identifier for token issuance.
    /// </summary>
    public string SubjectId { get; }

    /// <summary>
    /// Canonical username (case-normalised).
    /// </summary>
    public string Username { get; }

    /// <summary>
    /// Optional human-friendly display name.
    /// </summary>
    public string? DisplayName { get; }

    /// <summary>
    /// Indicates whether the user must reset their password.
    /// </summary>
    public bool RequiresPasswordReset { get; }

    /// <summary>
    /// Collection of role identifiers associated with the user.
    /// </summary>
    public IReadOnlyCollection<string> Roles { get; }

    /// <summary>
    /// Arbitrary plugin-defined attributes (used by claims enricher).
    /// </summary>
    public IReadOnlyDictionary<string, string?> Attributes { get; }

    private static string ValidateRequired(string value, string paramName)
        => string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
            : value;
}

/// <summary>
/// Outcome of a credential verification attempt.
/// </summary>
public sealed record AuthorityCredentialVerificationResult
{
    private AuthorityCredentialVerificationResult(
        bool succeeded,
        AuthorityUserDescriptor? user,
        AuthorityCredentialFailureCode? failureCode,
        string? message,
        TimeSpan? retryAfter,
        IReadOnlyList<AuthEventProperty> auditProperties)
    {
        Succeeded = succeeded;
        User = user;
        FailureCode = failureCode;
        Message = message;
        RetryAfter = retryAfter;
        AuditProperties = auditProperties ?? Array.Empty<AuthEventProperty>();
    }

    /// <summary>
    /// Indicates whether the verification succeeded.
    /// </summary>
    public bool Succeeded { get; }

    /// <summary>
    /// Resolved user descriptor when successful.
    /// </summary>
    public AuthorityUserDescriptor? User { get; }

    /// <summary>
    /// Failure classification when unsuccessful.
    /// </summary>
    public AuthorityCredentialFailureCode? FailureCode { get; }

    /// <summary>
    /// Optional message describing the outcome.
    /// </summary>
    public string? Message { get; }

    /// <summary>
    /// Optional suggested retry interval (e.g. for lockouts).
    /// </summary>
    public TimeSpan? RetryAfter { get; }

    /// <summary>
    /// Additional audit properties emitted by the credential store.
    /// </summary>
    public IReadOnlyList<AuthEventProperty> AuditProperties { get; }

    /// <summary>
    /// Builds a successful verification result.
    /// </summary>
    public static AuthorityCredentialVerificationResult Success(
        AuthorityUserDescriptor user,
        string? message = null,
        IReadOnlyList<AuthEventProperty>? auditProperties = null)
        => new(true, user ?? throw new ArgumentNullException(nameof(user)), null, message, null, auditProperties ?? Array.Empty<AuthEventProperty>());

    /// <summary>
    /// Builds a failed verification result.
    /// </summary>
    public static AuthorityCredentialVerificationResult Failure(
        AuthorityCredentialFailureCode failureCode,
        string? message = null,
        TimeSpan? retryAfter = null,
        IReadOnlyList<AuthEventProperty>? auditProperties = null)
        => new(false, null, failureCode, message, retryAfter, auditProperties ?? Array.Empty<AuthEventProperty>());
}

/// <summary>
/// Classifies credential verification failures.
/// </summary>
public enum AuthorityCredentialFailureCode
{
    /// <summary>
    /// Username/password combination is invalid.
    /// </summary>
    InvalidCredentials,

    /// <summary>
    /// Account is locked out (retry after a specified duration).
    /// </summary>
    LockedOut,

    /// <summary>
    /// Password has expired and must be reset.
    /// </summary>
    PasswordExpired,

    /// <summary>
    /// User must reset password before proceeding.
    /// </summary>
    RequiresPasswordReset,

    /// <summary>
    /// Additional multi-factor authentication is required.
    /// </summary>
    RequiresMfa,

    /// <summary>
    /// Unexpected failure occurred (see message for details).
    /// </summary>
    UnknownError
}

/// <summary>
/// Represents a user provisioning request.
/// </summary>
public sealed record AuthorityUserRegistration
{
    /// <summary>
    /// Initialises a new registration.
    /// </summary>
    public AuthorityUserRegistration(
        string username,
        string? password,
        string? displayName,
        string? email,
        bool requirePasswordReset,
        IReadOnlyCollection<string>? roles = null,
        IReadOnlyDictionary<string, string?>? attributes = null)
    {
        Username = ValidateRequired(username, nameof(username));
        Password = password;
        DisplayName = displayName;
        Email = email;
        RequirePasswordReset = requirePasswordReset;
        Roles = roles is null ? Array.Empty<string>() : roles.ToArray();
        Attributes = attributes is null
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
            : new Dictionary<string, string?>(attributes, StringComparer.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Canonical username (unique).
    /// </summary>
    public string Username { get; }

    /// <summary>
    /// Optional raw password (hashed by plugin).
    /// </summary>
    public string? Password { get; init; }

    /// <summary>
    /// Optional human-friendly display name.
    /// </summary>
    public string? DisplayName { get; }

    /// <summary>
    /// Optional contact email.
    /// </summary>
    public string? Email { get; }

    /// <summary>
    /// Indicates whether the user must reset their password at next login.
    /// </summary>
    public bool RequirePasswordReset { get; }

    /// <summary>
    /// Associated roles.
    /// </summary>
    public IReadOnlyCollection<string> Roles { get; }

    /// <summary>
    /// Plugin-defined attributes.
    /// </summary>
    public IReadOnlyDictionary<string, string?> Attributes { get; }

    /// <summary>
    /// Creates a copy with the provided password while preserving other fields.
    /// </summary>
    public AuthorityUserRegistration WithPassword(string? password)
        => new(Username, password, DisplayName, Email, RequirePasswordReset, Roles, Attributes);

    private static string ValidateRequired(string value, string paramName)
        => string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
            : value;
}

/// <summary>
/// Generic operation result utilised by plugins.
/// </summary>
public sealed record AuthorityPluginOperationResult
{
    private AuthorityPluginOperationResult(bool succeeded, string? errorCode, string? message)
    {
        Succeeded = succeeded;
        ErrorCode = errorCode;
        Message = message;
    }

    /// <summary>
    /// Indicates whether the operation succeeded.
    /// </summary>
    public bool Succeeded { get; }

    /// <summary>
    /// Machine-readable error code (populated on failure).
    /// </summary>
    public string? ErrorCode { get; }

    /// <summary>
    /// Optional human-readable message.
    /// </summary>
    public string? Message { get; }

    /// <summary>
    /// Returns a successful result.
    /// </summary>
    public static AuthorityPluginOperationResult Success(string? message = null)
        => new(true, null, message);

    /// <summary>
    /// Returns a failed result with the supplied error code.
    /// </summary>
    public static AuthorityPluginOperationResult Failure(string errorCode, string? message = null)
        => new(false, ValidateErrorCode(errorCode), message);

    internal static string ValidateErrorCode(string errorCode)
        => string.IsNullOrWhiteSpace(errorCode)
            ? throw new ArgumentException("Error code is required for failures.", nameof(errorCode))
            : errorCode;
}

/// <summary>
/// Generic operation result that returns a value.
/// </summary>
public sealed record AuthorityPluginOperationResult<TValue>
{
    private AuthorityPluginOperationResult(
        bool succeeded,
        TValue? value,
        string? errorCode,
        string? message)
    {
        Succeeded = succeeded;
        Value = value;
        ErrorCode = errorCode;
        Message = message;
    }

    /// <summary>
    /// Indicates whether the operation succeeded.
    /// </summary>
    public bool Succeeded { get; }

    /// <summary>
    /// Returned value when successful.
    /// </summary>
    public TValue? Value { get; }

    /// <summary>
    /// Machine-readable error code (on failure).
    /// </summary>
    public string? ErrorCode { get; }

    /// <summary>
    /// Optional human-readable message.
    /// </summary>
    public string? Message { get; }

    /// <summary>
    /// Returns a successful result with the provided value.
    /// </summary>
    public static AuthorityPluginOperationResult<TValue> Success(TValue value, string? message = null)
        => new(true, value, null, message);

    /// <summary>
    /// Returns a successful result without a value (defaults to <c>default</c>).
    /// </summary>
    public static AuthorityPluginOperationResult<TValue> Success(string? message = null)
        => new(true, default, null, message);

    /// <summary>
    /// Returns a failed result with the supplied error code.
    /// </summary>
    public static AuthorityPluginOperationResult<TValue> Failure(string errorCode, string? message = null)
        => new(false, default, AuthorityPluginOperationResult.ValidateErrorCode(errorCode), message);
}

/// <summary>
/// Context supplied to claims enrichment routines.
/// </summary>
public sealed class AuthorityClaimsEnrichmentContext
{
    private readonly Dictionary<string, object?> items;

    /// <summary>
    /// Initialises a new context instance.
    /// </summary>
    public AuthorityClaimsEnrichmentContext(
        AuthorityPluginContext plugin,
        AuthorityUserDescriptor? user,
        AuthorityClientDescriptor? client)
    {
        Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
        User = user;
        Client = client;
        items = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Gets the plugin context associated with the principal.
    /// </summary>
    public AuthorityPluginContext Plugin { get; }

    /// <summary>
    /// Gets the user descriptor when available.
    /// </summary>
    public AuthorityUserDescriptor? User { get; }

    /// <summary>
    /// Gets the client descriptor when available.
    /// </summary>
    public AuthorityClientDescriptor? Client { get; }

    /// <summary>
    /// Extensible bag for plugin-specific data passed between enrichment stages.
    /// </summary>
    public IDictionary<string, object?> Items => items;
}

/// <summary>
/// Represents a registered OAuth/OpenID client.
/// </summary>
public sealed record AuthorityClientDescriptor
{
    /// <summary>
    /// Initialises a new client descriptor.
    /// </summary>
    public AuthorityClientDescriptor(
        string clientId,
        string? displayName,
        bool confidential,
        IReadOnlyCollection<string>? allowedGrantTypes = null,
        IReadOnlyCollection<string>? allowedScopes = null,
        IReadOnlyCollection<Uri>? redirectUris = null,
        IReadOnlyCollection<Uri>? postLogoutRedirectUris = null,
        IReadOnlyDictionary<string, string?>? properties = null)
    {
        ClientId = ValidateRequired(clientId, nameof(clientId));
        DisplayName = displayName;
        Confidential = confidential;
        AllowedGrantTypes = allowedGrantTypes is null ? Array.Empty<string>() : allowedGrantTypes.ToArray();
        AllowedScopes = allowedScopes is null ? Array.Empty<string>() : allowedScopes.ToArray();
        RedirectUris = redirectUris is null ? Array.Empty<Uri>() : redirectUris.ToArray();
        PostLogoutRedirectUris = postLogoutRedirectUris is null ? Array.Empty<Uri>() : postLogoutRedirectUris.ToArray();
        Properties = properties is null
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
            : new Dictionary<string, string?>(properties, StringComparer.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Unique client identifier.
    /// </summary>
    public string ClientId { get; }

    /// <summary>
    /// Optional display name.
    /// </summary>
    public string? DisplayName { get; }

    /// <summary>
    /// Indicates whether the client is confidential (requires secret).
    /// </summary>
    public bool Confidential { get; }

    /// <summary>
    /// Permitted OAuth grant types.
    /// </summary>
    public IReadOnlyCollection<string> AllowedGrantTypes { get; }

    /// <summary>
    /// Permitted scopes.
    /// </summary>
    public IReadOnlyCollection<string> AllowedScopes { get; }

    /// <summary>
    /// Registered redirect URIs.
    /// </summary>
    public IReadOnlyCollection<Uri> RedirectUris { get; }

    /// <summary>
    /// Registered post-logout redirect URIs.
    /// </summary>
    public IReadOnlyCollection<Uri> PostLogoutRedirectUris { get; }

    /// <summary>
    /// Additional plugin-defined metadata.
    /// </summary>
    public IReadOnlyDictionary<string, string?> Properties { get; }

    private static string ValidateRequired(string value, string paramName)
        => string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
            : value;
}

/// <summary>
/// Client registration payload used when provisioning clients through plugins.
/// </summary>
public sealed record AuthorityClientRegistration
{
    /// <summary>
    /// Initialises a new registration.
    /// </summary>
    public AuthorityClientRegistration(
        string clientId,
        bool confidential,
        string? displayName,
        string? clientSecret,
        IReadOnlyCollection<string>? allowedGrantTypes = null,
        IReadOnlyCollection<string>? allowedScopes = null,
        IReadOnlyCollection<Uri>? redirectUris = null,
        IReadOnlyCollection<Uri>? postLogoutRedirectUris = null,
        IReadOnlyDictionary<string, string?>? properties = null)
    {
        ClientId = ValidateRequired(clientId, nameof(clientId));
        Confidential = confidential;
        DisplayName = displayName;
        ClientSecret = confidential
            ? ValidateRequired(clientSecret ?? string.Empty, nameof(clientSecret))
            : clientSecret;
        AllowedGrantTypes = allowedGrantTypes is null ? Array.Empty<string>() : allowedGrantTypes.ToArray();
        AllowedScopes = allowedScopes is null ? Array.Empty<string>() : allowedScopes.ToArray();
        RedirectUris = redirectUris is null ? Array.Empty<Uri>() : redirectUris.ToArray();
        PostLogoutRedirectUris = postLogoutRedirectUris is null ? Array.Empty<Uri>() : postLogoutRedirectUris.ToArray();
        Properties = properties is null
            ? new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
            : new Dictionary<string, string?>(properties, StringComparer.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Unique client identifier.
    /// </summary>
    public string ClientId { get; }

    /// <summary>
    /// Indicates whether the client is confidential (requires secret handling).
    /// </summary>
    public bool Confidential { get; }

    /// <summary>
    /// Optional display name.
    /// </summary>
    public string? DisplayName { get; }

    /// <summary>
    /// Optional raw client secret (hashed by the plugin for storage).
    /// </summary>
    public string? ClientSecret { get; init; }

    /// <summary>
    /// Grant types to enable.
    /// </summary>
    public IReadOnlyCollection<string> AllowedGrantTypes { get; }

    /// <summary>
    /// Scopes assigned to the client.
    /// </summary>
    public IReadOnlyCollection<string> AllowedScopes { get; }

    /// <summary>
    /// Redirect URIs permitted for the client.
    /// </summary>
    public IReadOnlyCollection<Uri> RedirectUris { get; }

    /// <summary>
    /// Post-logout redirect URIs.
    /// </summary>
    public IReadOnlyCollection<Uri> PostLogoutRedirectUris { get; }

    /// <summary>
    /// Additional metadata for the plugin.
    /// </summary>
    public IReadOnlyDictionary<string, string?> Properties { get; }

    /// <summary>
    /// Creates a copy of the registration with the provided client secret.
    /// </summary>
    public AuthorityClientRegistration WithClientSecret(string? clientSecret)
        => new(ClientId, Confidential, DisplayName, clientSecret, AllowedGrantTypes, AllowedScopes, RedirectUris, PostLogoutRedirectUris, Properties);

    private static string ValidateRequired(string value, string paramName)
        => string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Value cannot be null or whitespace.", paramName)
            : value;
}
