using MemberCenter.Application.Abstractions;
using MemberCenter.Application.Models.Admin;
using MemberCenter.Domain.Entities;
using MemberCenter.Infrastructure.Persistence;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using System.Net;
using System.Net.Mail;
using System.Security.Cryptography;

namespace MemberCenter.Infrastructure.Services;

public sealed class SecuritySettingsService : ISecuritySettingsService
{
    private const string AccessTokenKey = "token_access_minutes";
    private const string RefreshTokenKey = "token_refresh_days";
    private const string PublicBaseUrlKey = "public_base_url";
    private const string SmtpRelayHostKey = "smtp_relay_host";
    private const string SmtpRelayPortKey = "smtp_relay_port";
    private const string SmtpUseTlsKey = "smtp_use_tls";
    private const string SmtpUseSslKey = "smtp_use_ssl";
    private const string SmtpTimeoutSecondsKey = "smtp_timeout_seconds";
    private const string SmtpUsernameKey = "smtp_username";
    private const string SmtpPasswordKey = "smtp_password";
    private const string ProtectedValuePrefix = "protected:v1:";
    private const string SmtpPasswordProtectorPurpose = "MemberCenter.SecuritySettings.SmtpPassword.v1";
    private const string SenderNameKey = "smtp_sender_name";
    private const string SenderEmailKey = "smtp_sender_email";

    private readonly MemberCenterDbContext _dbContext;
    private readonly IAuditLogWriter _auditLogWriter;
    private readonly IDataProtector _smtpPasswordProtector;

    public SecuritySettingsService(
        MemberCenterDbContext dbContext,
        IAuditLogWriter auditLogWriter,
        IDataProtectionProvider dataProtectionProvider)
    {
        _dbContext = dbContext;
        _auditLogWriter = auditLogWriter;
        _smtpPasswordProtector = dataProtectionProvider.CreateProtector(SmtpPasswordProtectorPurpose);
    }

    public async Task<SecuritySettingsDto> GetAsync()
    {
        var tokenLifetime = await GetTokenLifetimeSettingsAsync();
        var smtp = await GetSmtpSettingsAsync();
        return new SecuritySettingsDto(
            tokenLifetime.AccessTokenMinutes,
            tokenLifetime.RefreshTokenDays,
            smtp.PublicBaseUrl,
            smtp.RelayHost,
            smtp.RelayPort,
            smtp.UseTls,
            smtp.UseSsl,
            smtp.TimeoutSeconds,
            smtp.Username,
            string.Empty,
            smtp.HasPassword,
            smtp.SenderName,
            smtp.SenderEmail,
            string.Empty);
    }

    public async Task<TokenLifetimeSettingsDto> GetTokenLifetimeSettingsAsync()
    {
        var access = await GetFlagAsync(AccessTokenKey, 60);
        var refresh = await GetFlagAsync(RefreshTokenKey, 30);
        return new TokenLifetimeSettingsDto(access, refresh);
    }

    public async Task<SmtpSettingsDto> GetSmtpSettingsAsync()
    {
        var relayHost = await GetFlagAsync(SmtpRelayHostKey, string.Empty);
        var publicBaseUrl = await GetFlagAsync(PublicBaseUrlKey, string.Empty);
        var relayPort = await GetFlagAsync(SmtpRelayPortKey, 587);
        var useTls = await GetFlagAsync(SmtpUseTlsKey, true);
        var useSsl = await GetFlagAsync(SmtpUseSslKey, false);
        var timeoutSeconds = await GetFlagAsync(SmtpTimeoutSecondsKey, 15);
        var username = await GetFlagAsync(SmtpUsernameKey, string.Empty);
        var password = await GetSmtpPasswordAsync();
        var senderName = await GetFlagAsync(SenderNameKey, "Member Center");
        var senderEmail = await GetFlagAsync(SenderEmailKey, string.Empty);
        return new SmtpSettingsDto(
            publicBaseUrl,
            relayHost,
            relayPort,
            useTls,
            useSsl,
            timeoutSeconds,
            username,
            password,
            !string.IsNullOrWhiteSpace(password),
            senderName,
            senderEmail);
    }

    public async Task SaveAsync(SecuritySettingsDto settings, Guid? actorUserId = null)
    {
        if (settings.AccessTokenMinutes is < 5 or > 1440)
        {
            throw new InvalidOperationException("Access token lifetime must be between 5 and 1440 minutes.");
        }

        if (settings.RefreshTokenDays is < 1 or > 365)
        {
            throw new InvalidOperationException("Refresh token lifetime must be between 1 and 365 days.");
        }

        var publicBaseUrl = Normalize(settings.PublicBaseUrl);
        var relayHost = Normalize(settings.SmtpRelayHost);
        var username = Normalize(settings.SmtpUsername);
        var senderName = Normalize(settings.SenderName);
        var senderEmail = Normalize(settings.SenderEmail);

        if (settings.SmtpUseTls && settings.SmtpUseSsl)
        {
            throw new InvalidOperationException("SMTP TLS and SSL cannot both be enabled.");
        }

        await SetFlagAsync(AccessTokenKey, settings.AccessTokenMinutes.ToString());
        await SetFlagAsync(RefreshTokenKey, settings.RefreshTokenDays.ToString());
        await SetFlagAsync(PublicBaseUrlKey, publicBaseUrl);
        await SetFlagAsync(SmtpRelayHostKey, relayHost);
        await SetFlagAsync(SmtpRelayPortKey, settings.SmtpRelayPort.ToString());
        await SetFlagAsync(SmtpUseTlsKey, settings.SmtpUseTls.ToString());
        await SetFlagAsync(SmtpUseSslKey, settings.SmtpUseSsl.ToString());
        await SetFlagAsync(SmtpTimeoutSecondsKey, settings.SmtpTimeoutSeconds.ToString());
        await SetFlagAsync(SmtpUsernameKey, username);
        if (!string.IsNullOrWhiteSpace(settings.SmtpPassword))
        {
            await SetFlagAsync(SmtpPasswordKey, ProtectSmtpPassword(settings.SmtpPassword));
        }
        await SetFlagAsync(SenderNameKey, senderName);
        await SetFlagAsync(SenderEmailKey, senderEmail);

        await _auditLogWriter.WriteAsync("user", actorUserId, "system.security_settings_updated", new
        {
            settings.AccessTokenMinutes,
            settings.RefreshTokenDays,
            PublicBaseUrl = publicBaseUrl,
            SmtpRelayHost = relayHost,
            settings.SmtpRelayPort,
            settings.SmtpUseTls,
            settings.SmtpUseSsl,
            settings.SmtpTimeoutSeconds,
            SmtpUsername = username,
            SenderName = senderName,
            SenderEmail = senderEmail
        });

        await _dbContext.SaveChangesAsync();
    }

    public async Task<int> SendTestEmailAsync(string toEmail, Guid? actorUserId = null)
    {
        var smtp = await GetSmtpSettingsAsync();
        ValidateSmtpSettings(smtp);

        using var message = new MailMessage
        {
            Subject = "[SMTP Test] Member Center SMTP 設定測試",
            Body = "這是一封測試信，代表 SMTP 設定可正常寄送。",
            IsBodyHtml = false,
            From = new MailAddress(smtp.SenderEmail, smtp.SenderName)
        };
        message.To.Add(new MailAddress(toEmail));
        message.AlternateViews.Add(AlternateView.CreateAlternateViewFromString("<p>這是一封測試信，代表 SMTP 設定可正常寄送。</p>", null, "text/html"));

        using var client = new SmtpClient(smtp.RelayHost, smtp.RelayPort)
        {
            EnableSsl = smtp.UseSsl || smtp.UseTls,
            DeliveryMethod = SmtpDeliveryMethod.Network,
            Timeout = Math.Max(1000, smtp.TimeoutSeconds * 1000)
        };

        if (!string.IsNullOrWhiteSpace(smtp.Username))
        {
            client.Credentials = new NetworkCredential(smtp.Username, smtp.Password);
        }

        await client.SendMailAsync(message);
        await _auditLogWriter.WriteAsync("user", actorUserId, "system.security_test_email_sent", new
        {
            to_email = toEmail,
            sender_email = smtp.SenderEmail
        });
        return 1;
    }

    private static void ValidateSmtpSettings(SmtpSettingsDto smtp)
    {
        if (string.IsNullOrWhiteSpace(smtp.RelayHost))
        {
            throw new InvalidOperationException("SMTP relay host is empty. Please save SMTP settings first.");
        }

        if (smtp.UseTls && smtp.UseSsl)
        {
            throw new InvalidOperationException("SMTP TLS and SSL cannot both be enabled.");
        }

        if (string.IsNullOrWhiteSpace(smtp.SenderEmail))
        {
            throw new InvalidOperationException("Sender email is empty. Please save sender settings first.");
        }
    }

    private async Task<int> GetFlagAsync(string key, int defaultValue)
    {
        var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == key);
        if (flag is null)
        {
            return defaultValue;
        }

        return int.TryParse(flag.Value, out var value) ? value : defaultValue;
    }

    private async Task<bool> GetFlagAsync(string key, bool defaultValue)
    {
        var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == key);
        if (flag is null)
        {
            return defaultValue;
        }

        return bool.TryParse(flag.Value, out var value) ? value : defaultValue;
    }

    private async Task<string> GetFlagAsync(string key, string defaultValue)
    {
        var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == key);
        return flag?.Value ?? defaultValue;
    }

    private async Task SetFlagAsync(string key, string value)
    {
        var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == key);
        if (flag is null)
        {
            _dbContext.SystemFlags.Add(new SystemFlag
            {
                Id = Guid.NewGuid(),
                Key = key,
                Value = value,
                UpdatedAt = DateTimeOffset.UtcNow
            });
        }
        else
        {
            flag.Value = value;
            flag.UpdatedAt = DateTimeOffset.UtcNow;
        }
    }

    private async Task<string> GetSmtpPasswordAsync()
    {
        var flag = await _dbContext.SystemFlags.FirstOrDefaultAsync(f => f.Key == SmtpPasswordKey);
        if (flag is null || string.IsNullOrEmpty(flag.Value))
        {
            return string.Empty;
        }

        if (!flag.Value.StartsWith(ProtectedValuePrefix, StringComparison.Ordinal))
        {
            var plaintext = flag.Value;
            flag.Value = ProtectSmtpPassword(plaintext);
            flag.UpdatedAt = DateTimeOffset.UtcNow;
            await _dbContext.SaveChangesAsync();
            return plaintext;
        }

        try
        {
            return _smtpPasswordProtector.Unprotect(flag.Value[ProtectedValuePrefix.Length..]);
        }
        catch (CryptographicException ex)
        {
            throw new InvalidOperationException("SMTP password cannot be decrypted with the current Data Protection key ring.", ex);
        }
    }

    private string ProtectSmtpPassword(string plaintext) =>
        ProtectedValuePrefix + _smtpPasswordProtector.Protect(plaintext);

    private static string Normalize(string? value) => value?.Trim() ?? string.Empty;

}
