package org.raddatz.familienarchiv.auth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.raddatz.familienarchiv.user.AppUser;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.web.bind.annotation.*;

// @RequirePermission is intentionally absent: login is unauthenticated by design;
// logout requires an authenticated session (enforced by SecurityConfig), not a specific permission.
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthSessionController {

    private final AuthService authService;
    private final SessionAuthenticationStrategy sessionAuthenticationStrategy;

    @PostMapping("/login")
    public ResponseEntity<AppUser> login(
            @RequestBody LoginRequest request,
            HttpServletRequest httpRequest,
            HttpServletResponse httpResponse) {

        String ip = resolveClientIp(httpRequest);
        String ua = resolveUserAgent(httpRequest);

        AuthService.LoginResult result = authService.login(request.email(), request.password(), ip, ua);

        // Session-fixation defense (CWE-384): rotate the session ID at the authentication
        // boundary. ChangeSessionIdAuthenticationStrategy invalidates any pre-auth session ID
        // an attacker may have planted and mints a fresh one before we attach the SecurityContext.
        httpRequest.getSession(true);
        sessionAuthenticationStrategy.onAuthentication(result.authentication(), httpRequest, httpResponse);

        // Spring Session JDBC intercepts setAttribute() and persists the record under the
        // (now rotated) opaque ID; the Set-Cookie: fa_session=<opaque-id> is added automatically.
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(result.authentication());
        SecurityContextHolder.setContext(context);
        httpRequest.getSession()
                .setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context);

        return ResponseEntity.ok(result.user());
    }

    @PostMapping("/logout")
    public ResponseEntity<Void> logout(Authentication authentication, HttpServletRequest httpRequest) {
        String email = authentication.getName();
        String ip = resolveClientIp(httpRequest);
        String ua = resolveUserAgent(httpRequest);

        // CWE-613 defense: invalidate the session first — that is the contract the user
        // is relying on when they click "Log out." Audit is best-effort and must not
        // bubble up: if the user record was deleted while the session was live, the
        // audit lookup throws, but the session row in spring_session must still die.
        HttpSession session = httpRequest.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        SecurityContextHolder.clearContext();

        try {
            authService.logout(email, ip, ua);
        } catch (Exception ex) {
            log.warn("Audit logout failed for {}; session was already invalidated", email, ex);
        }

        return ResponseEntity.noContent().build();
    }

    /**
     * Resolves the client IP for audit-log purposes.
     *
     * <p>Trust model: the leftmost {@code X-Forwarded-For} value is taken at face value.
     * This is correct <em>only</em> if the ingress (Caddy in production) strips any
     * client-supplied XFF before forwarding — otherwise an attacker can pin audit-log
     * IPs to whatever they want. Verify the reverse-proxy config before exposing this
     * service behind a different ingress.
     */
    private static String resolveClientIp(HttpServletRequest request) {
        String forwarded = request.getHeader("X-Forwarded-For");
        if (forwarded != null && !forwarded.isBlank()) {
            return forwarded.split(",")[0].trim();
        }
        return request.getRemoteAddr();
    }

    private static String resolveUserAgent(HttpServletRequest request) {
        String ua = request.getHeader("User-Agent");
        return ua != null ? ua : "";
    }
}
