using System.Security.Cryptography;
using System.Text;
using Forge.Core.Models;
using Forge.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace Forge.Web.Auth;

public partial class ConfiguredAuthService : IAuthService
{
    private readonly AuthOptions _options;
    private readonly ForgeDbContext _db;

    public ConfiguredAuthService(IOptions<AuthOptions> options, ForgeDbContext db)
    {
        _options = options.Value;
        _db = db;
    }

    public async Task<UserAccount?> ValidateCredentialsAsync(string? username, string? password)
    {
        if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
        {
            return null;
        }

        var normalizedUsername = NormalizeUsername(username);
        if (normalizedUsername is null)
        {
            return null;
        }

        var user = await _db.UserAccounts.SingleOrDefaultAsync(u => u.Username == normalizedUsername);
        return user is not null && PasswordHasher.Verify(password, user.PasswordHash)
            ? user
            : null;
    }

    public async Task<UserAccount?> GetUserAsync(string username)
    {
        var normalizedUsername = NormalizeUsername(username);
        return normalizedUsername is null
            ? null
            : await _db.UserAccounts.SingleOrDefaultAsync(u => u.Username == normalizedUsername);
    }

    public async Task<IReadOnlyList<UserAccount>> GetUsersAsync() =>
        await _db.UserAccounts
            .OrderBy(u => u.Username)
            .ToListAsync();

    public async Task<UserAccount> CreateUserAsync(string username, string password, string role)
    {
        var normalizedUsername = NormalizeUsername(username)
            ?? throw new InvalidOperationException("Username is required.");
        var normalizedRole = NormalizeRole(role);

        if (!UsernamePattern().IsMatch(normalizedUsername))
        {
            throw new InvalidOperationException("Username may only contain letters, numbers, dots, dashes, and underscores.");
        }

        ValidatePassword(password);

        if (await _db.UserAccounts.AnyAsync(u => u.Username == normalizedUsername))
        {
            throw new InvalidOperationException("A user with that username already exists.");
        }

        var user = new UserAccount
        {
            Id = Guid.NewGuid(),
            Username = normalizedUsername,
            PasswordHash = PasswordHasher.Hash(password),
            Role = normalizedRole,
            CreatedAt = DateTime.UtcNow
        };

        _db.UserAccounts.Add(user);
        await _db.SaveChangesAsync();
        return user;
    }

    public async Task UpdatePasswordAsync(string username, string newPassword)
    {
        var normalizedUsername = NormalizeUsername(username)
            ?? throw new InvalidOperationException("Username is required.");

        ValidatePassword(newPassword);

        var user = await _db.UserAccounts.SingleOrDefaultAsync(u => u.Username == normalizedUsername)
            ?? throw new InvalidOperationException("User not found.");

        user.PasswordHash = PasswordHasher.Hash(newPassword);
        await _db.SaveChangesAsync();

        if (string.Equals(normalizedUsername, _options.Username, StringComparison.Ordinal)
            && !string.IsNullOrWhiteSpace(_options.PasswordFile))
        {
            File.WriteAllText(_options.PasswordFile, newPassword);
        }
    }

    public async Task EnsureConfiguredAdminAsync()
    {
        var username = NormalizeUsername(_options.Username);
        var configuredPassword = GetConfiguredPassword();

        if (username is null || string.IsNullOrWhiteSpace(configuredPassword))
        {
            return;
        }

        var existing = await _db.UserAccounts.SingleOrDefaultAsync(u => u.Username == username);
        var passwordHash = PasswordHasher.Hash(configuredPassword);

        if (existing is null)
        {
            _db.UserAccounts.Add(new UserAccount
            {
                Id = Guid.NewGuid(),
                Username = username,
                PasswordHash = passwordHash,
                Role = UserRoles.Admin,
                CreatedAt = DateTime.UtcNow
            });
        }
        else
        {
            existing.Role = UserRoles.Admin;
            existing.PasswordHash = passwordHash;
        }

        await _db.SaveChangesAsync();
    }

    private static string? NormalizeUsername(string? username)
    {
        var normalized = username?.Trim();
        return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
    }

    private static string NormalizeRole(string? role)
    {
        var normalized = role?.Trim();
        if (string.IsNullOrWhiteSpace(normalized))
        {
            return UserRoles.User;
        }

        var matchedRole = UserRoles.All.FirstOrDefault(available =>
            string.Equals(available, normalized, StringComparison.OrdinalIgnoreCase));

        return matchedRole
            ?? throw new InvalidOperationException($"Unsupported role '{normalized}'.");
    }

    private static void ValidatePassword(string password)
    {
        if (password.Length < 6)
        {
            throw new InvalidOperationException("Password must be at least 6 characters.");
        }
    }

    private string GetConfiguredPassword()
    {
        if (!string.IsNullOrWhiteSpace(_options.Password))
        {
            return _options.Password;
        }

        if (string.IsNullOrWhiteSpace(_options.PasswordFile))
        {
            return string.Empty;
        }

        try
        {
            return File.Exists(_options.PasswordFile)
                ? File.ReadAllText(_options.PasswordFile).Trim()
                : string.Empty;
        }
        catch
        {
            return string.Empty;
        }
    }

    [System.Text.RegularExpressions.GeneratedRegex("^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$")]
    private static partial System.Text.RegularExpressions.Regex UsernamePattern();
}

internal static class PasswordHasher
{
    private const int SaltSize = 16;
    private const int KeySize = 32;
    private const int Iterations = 100_000;

    public static string Hash(string password)
    {
        var salt = RandomNumberGenerator.GetBytes(SaltSize);
        var hash = Rfc2898DeriveBytes.Pbkdf2(
            Encoding.UTF8.GetBytes(password),
            salt,
            Iterations,
            HashAlgorithmName.SHA256,
            KeySize);

        return $"pbkdf2-sha256${Iterations}${Convert.ToBase64String(salt)}${Convert.ToBase64String(hash)}";
    }

    public static bool Verify(string password, string storedHash)
    {
        var parts = storedHash.Split('$', StringSplitOptions.RemoveEmptyEntries);
        if (parts.Length != 4 || !string.Equals(parts[0], "pbkdf2-sha256", StringComparison.Ordinal))
        {
            return false;
        }

        if (!int.TryParse(parts[1], out var iterations))
        {
            return false;
        }

        try
        {
            var salt = Convert.FromBase64String(parts[2]);
            var expected = Convert.FromBase64String(parts[3]);
            var actual = Rfc2898DeriveBytes.Pbkdf2(
                Encoding.UTF8.GetBytes(password),
                salt,
                iterations,
                HashAlgorithmName.SHA256,
                expected.Length);

            return CryptographicOperations.FixedTimeEquals(actual, expected);
        }
        catch
        {
            return false;
        }
    }
}
An unhandled error has occurred. Reload 🗙