using System.Collections.Concurrent;
using Hc900Crawler.Core.Domain.Entities;
using Hc900Crawler.Infrastructure.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

namespace Hc900Crawler.Infrastructure.Kb;

public sealed record KbLoginResult(bool Success, string? Token, DateTime? ExpiresAt, string? Error);

public interface IKbAuthService
{
    Task EnsureCredentialAsync(CancellationToken ct = default);
    Task<KbLoginResult> LoginAsync(string password, string? clientIp, CancellationToken ct = default);
    Task<bool> ValidateAsync(string? token, CancellationToken ct = default);
    Task LogoutAsync(string token, CancellationToken ct = default);
    Task<bool> ChangePasswordAsync(string oldPassword, string newPassword, CancellationToken ct = default);
}

public sealed class KbAuthService : IKbAuthService
{
    private readonly Hc900DbContext _db;
    private readonly IConfiguration _config;
    private readonly ILogger<KbAuthService> _logger;

    // 인스턴스별이 아닌 프로세스 전역 throttle 상태 — KbAuthService는 Scoped로 등록되므로 static 필요
    private static readonly ConcurrentDictionary<string, LoginAttemptRecord> _attempts = new();

    private sealed class LoginAttemptRecord
    {
        public int FailCount;
        public DateTime FirstFailUtc;
        public DateTime? LockedUntilUtc;
    }

    public KbAuthService(Hc900DbContext db, IConfiguration config, ILogger<KbAuthService> logger)
    {
        _db = db;
        _config = config;
        _logger = logger;
    }

    private int SessionMinutes =>
        int.TryParse(_config["Kb:AdminSessionMinutes"], out var m) && m > 0 ? m : 60;

    private int MaxFailAttempts =>
        int.TryParse(_config["Kb:Throttle:MaxFailAttempts"], out var m) && m > 0 ? m : 5;
    private int FailWindowMinutes =>
        int.TryParse(_config["Kb:Throttle:FailWindowMinutes"], out var m) && m > 0 ? m : 5;
    private int LockoutMinutes =>
        int.TryParse(_config["Kb:Throttle:LockoutMinutes"], out var m) && m > 0 ? m : 15;

    public async Task EnsureCredentialAsync(CancellationToken ct = default)
    {
        var existing = await _db.KbAdminCredentials.FirstOrDefaultAsync(ct);
        if (existing != null) return;

        var initial = _config["Kb:AdminInitialPassword"]
            ?? Environment.GetEnvironmentVariable("KB_ADMIN_INITIAL_PASSWORD");

        bool generated = false;
        if (string.IsNullOrWhiteSpace(initial))
        {
            initial = Guid.NewGuid().ToString("N")[..16];
            generated = true;
        }

        var (hash, salt) = PasswordHasher.Hash(initial);
        _db.KbAdminCredentials.Add(new KbAdminCredential
        {
            Id = 1,
            PasswordHash = hash,
            Salt = salt,
            Algorithm = "argon2id",
            UpdatedAt = DateTime.UtcNow
        });
        await _db.SaveChangesAsync(ct);

        if (generated)
        {
            // 로그 파일에는 마스킹된 값만 — 평문은 콘솔로만 1회 출력 (파일 logger 미포함)
            var masked = initial.Length > 4
                ? initial.Substring(0, 4) + new string('*', initial.Length - 4)
                : new string('*', initial.Length);
            _logger.LogWarning("[Kb] 관리자 초기 비밀번호 자동 생성됨 (콘솔 참고, 마스킹: {Mask}) ← 즉시 변경하세요", masked);
            Console.Out.WriteLine();
            Console.Out.WriteLine("════════════════════════════════════════════════════════════");
            Console.Out.WriteLine($"  [Kb] 관리자 초기 비밀번호: {initial}");
            Console.Out.WriteLine("  ← 14번 RAG 관리 탭에서 즉시 변경하세요. 본 출력은 1회뿐입니다.");
            Console.Out.WriteLine("════════════════════════════════════════════════════════════");
            Console.Out.WriteLine();
        }
        else
        {
            _logger.LogInformation("[Kb] 관리자 초기 비밀번호 설정 (환경변수 사용)");
        }
    }

    public async Task<KbLoginResult> LoginAsync(string password, string? clientIp, CancellationToken ct = default)
    {
        // 1) Throttle 체크 — IP 단위 lockout
        var key = string.IsNullOrWhiteSpace(clientIp) ? "<unknown>" : clientIp!;
        var now = DateTime.UtcNow;
        if (_attempts.TryGetValue(key, out var rec))
        {
            if (rec.LockedUntilUtc is { } until && until > now)
            {
                var remain = (int)Math.Ceiling((until - now).TotalSeconds);
                _logger.LogWarning("[Kb] 로그인 차단 (lockout) IP={Ip} 남은시간={Sec}s", key, remain);
                return new(false, null, null, $"too many failed attempts. retry in {remain}s");
            }
            // 윈도우 만료 → 카운터 초기화
            if (rec.FirstFailUtc < now.AddMinutes(-FailWindowMinutes))
            {
                rec.FailCount = 0;
                rec.LockedUntilUtc = null;
            }
        }

        var cred = await _db.KbAdminCredentials.FirstOrDefaultAsync(ct);
        if (cred == null) return new(false, null, null, "credential not initialized");

        if (!PasswordHasher.Verify(password, cred.PasswordHash, cred.Salt))
        {
            // 실패 카운트 누적
            var r = _attempts.GetOrAdd(key, _ => new LoginAttemptRecord { FirstFailUtc = now });
            lock (r)
            {
                if (r.FailCount == 0) r.FirstFailUtc = now;
                r.FailCount++;
                if (r.FailCount >= MaxFailAttempts)
                {
                    r.LockedUntilUtc = now.AddMinutes(LockoutMinutes);
                    _logger.LogWarning("[Kb] 로그인 실패 누적 → 차단 적용 IP={Ip} 실패={N} 차단={Min}분",
                        key, r.FailCount, LockoutMinutes);
                }
            }
            return new(false, null, null, "invalid password");
        }

        // 2) 성공 시 throttle 카운터 초기화
        _attempts.TryRemove(key, out _);

        var token = PasswordHasher.NewSessionToken();
        var expires = DateTime.UtcNow.AddMinutes(SessionMinutes);
        _db.KbAdminSessions.Add(new KbAdminSession
        {
            Token = token,
            IssuedAt = DateTime.UtcNow,
            ExpiresAt = expires,
            ClientIp = clientIp
        });
        await _db.SaveChangesAsync(ct);

        await PurgeExpiredAsync(ct);
        return new(true, token, expires, null);
    }

    public async Task<bool> ValidateAsync(string? token, CancellationToken ct = default)
    {
        if (string.IsNullOrWhiteSpace(token)) return false;
        var s = await _db.KbAdminSessions.FirstOrDefaultAsync(x => x.Token == token, ct);
        if (s == null) return false;
        if (s.ExpiresAt < DateTime.UtcNow)
        {
            _db.KbAdminSessions.Remove(s);
            await _db.SaveChangesAsync(ct);
            return false;
        }
        return true;
    }

    public async Task LogoutAsync(string token, CancellationToken ct = default)
    {
        var s = await _db.KbAdminSessions.FirstOrDefaultAsync(x => x.Token == token, ct);
        if (s != null)
        {
            _db.KbAdminSessions.Remove(s);
            await _db.SaveChangesAsync(ct);
        }
    }

    public async Task<bool> ChangePasswordAsync(string oldPassword, string newPassword, CancellationToken ct = default)
    {
        var cred = await _db.KbAdminCredentials.FirstOrDefaultAsync(ct);
        if (cred == null) return false;
        if (!PasswordHasher.Verify(oldPassword, cred.PasswordHash, cred.Salt)) return false;
        if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6) return false;

        var (hash, salt) = PasswordHasher.Hash(newPassword);
        cred.PasswordHash = hash;
        cred.Salt = salt;
        cred.Algorithm = "argon2id";
        cred.UpdatedAt = DateTime.UtcNow;

        var now = DateTime.UtcNow;
        var sessions = await _db.KbAdminSessions.ToListAsync(ct);
        _db.KbAdminSessions.RemoveRange(sessions);
        await _db.SaveChangesAsync(ct);
        return true;
    }

    private async Task PurgeExpiredAsync(CancellationToken ct)
    {
        var now = DateTime.UtcNow;
        var expired = await _db.KbAdminSessions.Where(s => s.ExpiresAt < now).ToListAsync(ct);
        if (expired.Count > 0)
        {
            _db.KbAdminSessions.RemoveRange(expired);
            await _db.SaveChangesAsync(ct);
        }
    }
}
