using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using OmsorgCore.Api.Contracts;
using OmsorgCore.Application.Abstractions;
using OmsorgCore.Application.Services;
using OmsorgCore.Engine.Events;
using OmsorgCore.Infrastructure.Security;

namespace OmsorgCore.Api.Controllers;

[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    private const string RefreshTokenCookieName = "refreshToken";

    private readonly IAuthService _authService;
    private readonly ICurrentUserService _currentUserService;
    private readonly IPasswordResetService _passwordResetService;
    private readonly IUserService _userService;
    private readonly IDomainEventDispatcher _dispatcher;
    private readonly PasswordPolicyOptions _passwordPolicyOptions;
    private readonly RefreshTokenOptions _refreshTokenOptions;
    private readonly IWebHostEnvironment _environment;
    private readonly ILogger<AuthController> _logger;

    public AuthController(
        IAuthService authService,
        ICurrentUserService currentUserService,
        IPasswordResetService passwordResetService,
        IUserService userService,
        IDomainEventDispatcher dispatcher,
        IOptions<PasswordPolicyOptions> passwordPolicyOptions,
        IOptions<RefreshTokenOptions> refreshTokenOptions,
        IWebHostEnvironment environment,
        ILogger<AuthController> logger)
    {
        _authService = authService;
        _currentUserService = currentUserService;
        _passwordResetService = passwordResetService;
        _userService = userService;
        _dispatcher = dispatcher;
        _passwordPolicyOptions = passwordPolicyOptions.Value;
        _refreshTokenOptions = refreshTokenOptions.Value;
        _environment = environment;
        _logger = logger;
    }

    // HttpOnly, damit ein Browser-Frontend den Refresh-Token nie per JS lesen kann (XSS-Schutz) -
    // Path auf /api/auth eingeschränkt, da nur login/refresh/logout ihn brauchen. Secure nur außerhalb
    // von Development, weil der lokale Dev-Server per launchSettings.json standardmäßig nur über
    // http:// läuft (kein https-Profil default) - ein Secure-Cookie würde der Browser dort nie setzen.
    private void SetRefreshTokenCookie(string refreshToken)
    {
        Response.Cookies.Append(RefreshTokenCookieName, refreshToken, new CookieOptions
        {
            HttpOnly = true,
            Secure = !_environment.IsDevelopment(),
            SameSite = SameSiteMode.Lax,
            Expires = DateTimeOffset.UtcNow.AddDays(_refreshTokenOptions.ExpiryDays),
            Path = "/api/auth"
        });
    }

    [HttpPost("login")]
    public async Task<ActionResult<LoginResponse>> Login(LoginRequest request, CancellationToken cancellationToken)
    {
        var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        var result = await _authService.LoginAsync(request.Username, request.Password, ipAddress, cancellationToken);

        if (result.IsLockedOut)
        {
            return StatusCode(StatusCodes.Status429TooManyRequests, new { error = "too_many_attempts" });
        }

        if (!result.Success || result.Token is null || result.RefreshToken is null || result.ExpiresAt is null)
        {
            await _dispatcher.DispatchAsync(new AuditEvent(null, request.Username, ipAddress, "LoginFailed"), cancellationToken);
            return Unauthorized();
        }

        await _dispatcher.DispatchAsync(new AuditEvent(result.UserId, result.Username, ipAddress, "Login"), cancellationToken);
        SetRefreshTokenCookie(result.RefreshToken);
        return Ok(new LoginResponse(result.Token, result.RefreshToken, result.ExpiresAt.Value, result.MustChangePassword));
    }

    // Zwei Transportwege für den vorgelegten Refresh-Token, in dieser Reihenfolge geprüft:
    // 1. HttpOnly-Cookie (Browser, omsorgapp) - schickt bewusst keinen Body.
    // 2. Body (server-seitige API-Clients ohne Cookie-Jar, z. B. omsorgWeb per PHP-cURL/Guzzle).
    private string? ResolveRefreshToken(string? bodyRefreshToken)
    {
        if (Request.Cookies.TryGetValue(RefreshTokenCookieName, out var cookieToken) && !string.IsNullOrEmpty(cookieToken))
        {
            return cookieToken;
        }
        return string.IsNullOrEmpty(bodyRefreshToken) ? null : bodyRefreshToken;
    }

    [HttpPost("refresh")]
    public async Task<ActionResult<LoginResponse>> Refresh(RefreshRequest? request, CancellationToken cancellationToken)
    {
        var refreshToken = ResolveRefreshToken(request?.RefreshToken);
        if (refreshToken is null)
        {
            return Unauthorized();
        }

        var result = await _authService.RefreshAsync(refreshToken, cancellationToken);
        if (!result.Success || result.Token is null || result.RefreshToken is null || result.ExpiresAt is null)
        {
            return Unauthorized();
        }

        // Cookie nur erneuern, wenn der Aufrufer auch eine Cookie vorgelegt hat - ein reiner
        // API-Client (Body-basiert) soll keine Cookie untergeschoben bekommen, die er nie abfragt.
        if (Request.Cookies.ContainsKey(RefreshTokenCookieName))
        {
            SetRefreshTokenCookie(result.RefreshToken);
        }
        return Ok(new LoginResponse(result.Token, result.RefreshToken, result.ExpiresAt.Value, result.MustChangePassword));
    }

    [HttpPost("logout")]
    public async Task<IActionResult> Logout(LogoutRequest? request, CancellationToken cancellationToken)
    {
        var refreshToken = ResolveRefreshToken(request?.RefreshToken);
        if (refreshToken is not null)
        {
            await _authService.RevokeAsync(refreshToken, cancellationToken);
        }

        if (Request.Cookies.ContainsKey(RefreshTokenCookieName))
        {
            Response.Cookies.Delete(RefreshTokenCookieName, new CookieOptions { Path = "/api/auth" });
        }
        await _dispatcher.DispatchAsync(
            new AuditEvent(_currentUserService.UserId, _currentUserService.Username, _currentUserService.IpAddress, "Logout"),
            cancellationToken);
        return NoContent();
    }

    [HttpGet("me")]
    [Authorize]
    public async Task<ActionResult<MeResponse>> Me(CancellationToken cancellationToken)
    {
        if (_currentUserService.UserId is not { } userId)
        {
            return Unauthorized();
        }

        var profile = await _authService.GetProfileAsync(userId, cancellationToken);
        if (profile is null)
        {
            return Unauthorized();
        }

        var permissions = profile.Permissions
            .Select(p => new PermissionDto(p.Module, p.Action, p.Scope))
            .ToList();

        return Ok(new MeResponse(
            profile.UserId,
            profile.Username,
            profile.RoleName,
            permissions,
            profile.EmployeeId,
            profile.FirstName,
            profile.LastName,
            profile.Email,
            profile.PhoneNumber,
            profile.AvatarFileName));
    }

    [HttpPost("change-password")]
    [Authorize]
    public async Task<IActionResult> ChangePassword(ChangePasswordRequest request, CancellationToken cancellationToken)
    {
        if (_currentUserService.UserId is not { } userId)
        {
            return Unauthorized();
        }

        var result = await _userService.ChangeOwnPasswordAsync(userId, request.CurrentPassword, request.NewPassword, cancellationToken);
        return result switch
        {
            ChangeOwnPasswordResult.Success => NoContent(),
            ChangeOwnPasswordResult.PasswordTooShort => BadRequest($"NewPassword muss mindestens {_passwordPolicyOptions.MinLength} Zeichen lang sein."),
            _ => Unauthorized()
        };
    }

    [HttpGet("password-policy")]
    public ActionResult<PasswordPolicyResponse> PasswordPolicy()
    {
        return Ok(new PasswordPolicyResponse(_passwordPolicyOptions.MinLength));
    }

    [HttpPost("forgot-password/request")]
    public async Task<ActionResult<ForgotPasswordRequestResponse>> ForgotPasswordRequest(
        ForgotPasswordRequestRequest request, CancellationToken cancellationToken)
    {
        var result = await _passwordResetService.RequestResetAsync(request.Username, cancellationToken);
        if (result.Status == PasswordResetRequestStatus.Sent && result.Email is not null && result.RawPin is not null)
        {
            try
            {
                await _dispatcher.DispatchAsync(
                    new PasswordResetRequestedEvent(result.Email, result.RawPin), cancellationToken);
            }
            catch (Exception ex)
            {
                // Der Reset-Code wurde bereits in der DB angelegt (PasswordResetService.RequestResetAsync) -
                // nur der E-Mail-Versand ist fehlgeschlagen (z.B. SMTP nicht erreichbar/falsch konfiguriert).
                // Client bekommt "email_unavailable" statt "sent", damit die UI ehrlich anzeigt, dass gerade
                // kein Code angekommen ist, statt den Nutzer auf einen leeren PIN-Eingabe-Schritt zu schicken.
                // Kein zusätzliches Enumeration-Risiko ggü. heute: "sent" vs. "cannot_reset" unterscheidet
                // bereits, ob der Username existiert (siehe ForgotPasswordRequestResponse-Doku).
                _logger.LogError(ex, "Passwort-Reset-E-Mail konnte nicht versendet werden für UserId {UserId}", result.UserId);
                return Ok(new ForgotPasswordRequestResponse("email_unavailable"));
            }
        }

        var status = result.Status == PasswordResetRequestStatus.Sent ? "sent" : "cannot_reset";
        return Ok(new ForgotPasswordRequestResponse(status));
    }

    [HttpPost("forgot-password/verify")]
    public async Task<ActionResult<ForgotPasswordVerifyResponse>> ForgotPasswordVerify(
        ForgotPasswordVerifyRequest request, CancellationToken cancellationToken)
    {
        var result = await _passwordResetService.VerifyCodeAsync(request.Username, request.Pin, cancellationToken);
        if (!result.Success || result.ResetToken is null)
        {
            return Unauthorized(new { error = result.FailureReason ?? "invalid_or_expired" });
        }

        return Ok(new ForgotPasswordVerifyResponse(result.ResetToken));
    }

    [HttpPost("forgot-password/reset")]
    public async Task<IActionResult> ForgotPasswordReset(
        ForgotPasswordResetRequest request, CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(request.NewPassword))
        {
            return Unauthorized();
        }

        var result = await _passwordResetService.ResetPasswordAsync(
            request.ResetToken, request.NewPassword, cancellationToken);
        return result switch
        {
            ResetPasswordResult.Success => NoContent(),
            ResetPasswordResult.PasswordTooShort => BadRequest($"NewPassword muss mindestens {_passwordPolicyOptions.MinLength} Zeichen lang sein."),
            _ => Unauthorized()
        };
    }
}
