package cn.nianxx.thhotel.platform.identity.service.impl;

import cn.nianxx.thhotel.platform.access.service.AccessControlService;
import cn.nianxx.thhotel.platform.common.time.UtcTimeFormatter;
import cn.nianxx.thhotel.platform.hotel.service.HotelAccessService;
import cn.nianxx.thhotel.platform.identity.common.dto.AuthSessionSnapshot;
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserSessionStatus;
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
import cn.nianxx.thhotel.platform.identity.common.request.AuthLoginRequest;
import cn.nianxx.thhotel.platform.identity.common.result.AuthHotelResult;
import cn.nianxx.thhotel.platform.identity.common.result.AuthLoginResult;
import cn.nianxx.thhotel.platform.identity.common.result.AuthLogoutResult;
import cn.nianxx.thhotel.platform.identity.common.result.AuthMeResult;
import cn.nianxx.thhotel.platform.identity.common.result.AuthMenuResult;
import cn.nianxx.thhotel.platform.identity.common.result.AuthUserResult;
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserSessionEntity;
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
import cn.nianxx.thhotel.platform.identity.service.AuthService;
import cn.nianxx.thhotel.platform.navigation.service.NavigationService;
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
import jakarta.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * 登录认证服务实现。第一版使用数据库 session token，不使用 JWT。
 */
@Service
public class AuthServiceImpl implements AuthService {

    private static final String TOKEN_TYPE = "Bearer";
    private static final String INVALID_CREDENTIALS_MESSAGE = "用户名或密码错误。";

    private final PlatformIdentityRepository identityRepository;
    private final AccessControlService accessControlService;
    private final HotelAccessService hotelAccessService;
    private final NavigationService navigationService;
    private final AuthPasswordService passwordService;
    private final AuthTokenService tokenService;
    private final AuthProperties authProperties;

    /**
     * 注入登录所需仓储、上下文服务和 token / 密码工具。
     */
    public AuthServiceImpl(
            PlatformIdentityRepository identityRepository,
            AccessControlService accessControlService,
            HotelAccessService hotelAccessService,
            NavigationService navigationService,
            AuthPasswordService passwordService,
            AuthTokenService tokenService,
            AuthProperties authProperties) {
        this.identityRepository = identityRepository;
        this.accessControlService = accessControlService;
        this.hotelAccessService = hotelAccessService;
        this.navigationService = navigationService;
        this.passwordService = passwordService;
        this.tokenService = tokenService;
        this.authProperties = authProperties;
    }

    /**
     * 用户名密码登录。失败时统一返回通用错误，避免泄露账号是否存在或是否被禁用。
     */
    @Override
    @Transactional
    public AuthLoginResult login(AuthLoginRequest request, HttpServletRequest servletRequest) {
        String username = request.username() == null ? "" : request.username().trim();
        PlatformUserEntity user = identityRepository.findUserByUsername(username)
                .filter(this::isActiveUser)
                .filter(existingUser -> passwordService.matches(request.password(), existingUser.getPasswordHash()))
                .orElseThrow(() -> new AuthException(
                        HttpStatus.UNAUTHORIZED,
                        "AUTH_INVALID_CREDENTIALS",
                        INVALID_CREDENTIALS_MESSAGE));

        LocalDateTime now = nowUtc();
        LocalDateTime expiresAt = now.plusMinutes(Math.max(1L, authProperties.getSession().getTtlMinutes()));
        String accessToken = tokenService.generateToken();
        PlatformUserSessionEntity session = new PlatformUserSessionEntity();
        session.setUserId(user.getId());
        session.setTokenHash(tokenService.hashToken(accessToken));
        session.setSessionStatus(PlatformUserSessionStatus.ACTIVE.name());
        session.setIssuedAt(now);
        session.setExpiresAt(expiresAt);
        session.setLastSeenAt(now);
        session.setClientIp(clientIp(servletRequest));
        session.setUserAgentSummary(userAgent(servletRequest));
        session.setCreatedAt(now);
        session.setUpdatedAt(now);
        identityRepository.insertSession(session);

        user.setLastLoginAt(now);
        user.setUpdatedAt(now);
        identityRepository.updateUser(user);

        AuthMeResult context = buildMeResult(user, request.preferredHotelId());
        return new AuthLoginResult(
                accessToken,
                TOKEN_TYPE,
                UtcTimeFormatter.toUtcOffsetDateTime(expiresAt),
                context.user(),
                context.defaultHotelId(),
                context.hotels(),
                context.permissions(),
                context.menus());
    }

    /**
     * 查询当前登录用户上下文。该接口是登录态恢复入口，因此缺少 token 必须返回 401。
     */
    @Override
    @Transactional
    public AuthMeResult currentUser(String authorizationHeader) {
        AuthSessionSnapshot snapshot = resolveRequiredSession(authorizationHeader);
        touchSession(snapshot.sessionId());
        return buildMeResult(snapshot.user(), null);
    }

    /**
     * 登出当前 token。第一版只撤销当前 session，不影响同一用户其他登录态。
     */
    @Override
    @Transactional
    public AuthLogoutResult logout(String authorizationHeader) {
        String token = extractRequiredBearerToken(authorizationHeader);
        PlatformUserSessionEntity session = identityRepository.findSessionByTokenHash(tokenService.hashToken(token))
                .orElseThrow(this::invalidSession);
        if (PlatformUserSessionStatus.REVOKED.name().equals(session.getSessionStatus())
                || PlatformUserSessionStatus.EXPIRED.name().equals(session.getSessionStatus())) {
            return new AuthLogoutResult(true);
        }
        LocalDateTime now = nowUtc();
        session.setSessionStatus(PlatformUserSessionStatus.REVOKED.name());
        session.setRevokedAt(now);
        session.setUpdatedAt(now);
        identityRepository.updateSession(session);
        return new AuthLogoutResult(true);
    }

    /**
     * 可选解析 token。该方法专供请求过滤器使用，不让无效 token 破坏现有业务接口兼容性。
     */
    @Override
    @Transactional
    public Optional<AuthenticatedUserContext> resolveOptionalContext(String authorizationHeader) {
        String token = extractBearerToken(authorizationHeader).orElse(null);
        if (token == null) {
            return Optional.empty();
        }
        return resolveSessionByToken(token, false)
                .map(snapshot -> {
                    touchSession(snapshot.sessionId());
                    return buildSecurityContext(snapshot.user());
                });
    }

    /**
     * 组装当前用户上下文响应，登录成功和 /me 接口共用同一套权限、酒店和菜单逻辑。
     */
    private AuthMeResult buildMeResult(PlatformUserEntity user, String preferredHotelId) {
        List<AuthHotelResult> hotels = hotelAccessService.listAccessibleHotels(user, preferredHotelId);
        String defaultHotelId = hotelAccessService.resolveDefaultHotelId(hotels);
        List<String> permissions = accessControlService.listPermissionCodes(
                user.getId(),
                Boolean.TRUE.equals(user.getSuperAdmin()));
        List<AuthMenuResult> menus = navigationService.listVisibleMenus(permissions);
        return new AuthMeResult(
                new AuthUserResult(
                        String.valueOf(user.getId()),
                        user.getUsername(),
                        user.getDisplayName(),
                        Boolean.TRUE.equals(user.getSuperAdmin())),
                defaultHotelId,
                hotels,
                permissions,
                menus);
    }

    /**
     * 构建请求线程内的用户上下文，供后续业务接口审计和权限收口复用。
     */
    private AuthenticatedUserContext buildSecurityContext(PlatformUserEntity user) {
        List<AuthHotelResult> hotels = hotelAccessService.listAccessibleHotels(user, null);
        List<String> permissions = accessControlService.listPermissionCodes(
                user.getId(),
                Boolean.TRUE.equals(user.getSuperAdmin()));
        return new AuthenticatedUserContext(
                user.getId(),
                user.getUsername(),
                user.getDisplayName(),
                Boolean.TRUE.equals(user.getSuperAdmin()),
                hotelAccessService.resolveDefaultHotelId(hotels),
                hotels.stream().map(AuthHotelResult::hotelId).toList(),
                permissions);
    }

    /**
     * 强制解析当前 token 对应 session；登录态恢复接口必须拿到有效 session。
     */
    private AuthSessionSnapshot resolveRequiredSession(String authorizationHeader) {
        String token = extractRequiredBearerToken(authorizationHeader);
        return resolveSessionByToken(token, true).orElseThrow(this::invalidSession);
    }

    /**
     * 根据明文 token 查找有效 session，并按调用场景决定是否抛出无效登录态异常。
     */
    private Optional<AuthSessionSnapshot> resolveSessionByToken(String token, boolean strict) {
        String tokenHash = tokenService.hashToken(token);
        Optional<PlatformUserSessionEntity> session = identityRepository.findSessionByTokenHash(tokenHash);
        if (session.isEmpty()) {
            return Optional.empty();
        }
        PlatformUserSessionEntity existingSession = session.get();
        if (!PlatformUserSessionStatus.ACTIVE.name().equals(existingSession.getSessionStatus())) {
            return Optional.empty();
        }
        if (existingSession.getExpiresAt() != null && existingSession.getExpiresAt().isBefore(nowUtc())) {
            markExpired(existingSession);
            return Optional.empty();
        }
        Optional<PlatformUserEntity> user = identityRepository.findUserById(existingSession.getUserId())
                .filter(this::isActiveUser);
        if (user.isEmpty() && strict) {
            throw invalidSession();
        }
        return user.map(value -> new AuthSessionSnapshot(existingSession.getId(), existingSession.getExpiresAt(), value));
    }

    /**
     * 刷新 session 最近访问时间；只更新访问时间字段，避免覆盖 token 安全字段。
     */
    private void touchSession(Long sessionId) {
        if (sessionId == null) {
            return;
        }
        PlatformUserSessionEntity session = new PlatformUserSessionEntity();
        session.setId(sessionId);
        session.setLastSeenAt(nowUtc());
        session.setUpdatedAt(nowUtc());
        identityRepository.updateSession(session);
    }

    /**
     * 将过期 session 标记为 EXPIRED，避免后续重复按 ACTIVE 处理。
     */
    private void markExpired(PlatformUserSessionEntity session) {
        session.setSessionStatus(PlatformUserSessionStatus.EXPIRED.name());
        session.setUpdatedAt(nowUtc());
        identityRepository.updateSession(session);
    }

    /**
     * 判断用户是否为可登录状态，禁用用户不能登录或继续使用旧 session。
     */
    private boolean isActiveUser(PlatformUserEntity user) {
        return PlatformUserStatus.ACTIVE.name().equals(user.getUserStatus());
    }

    /**
     * 解析必须存在的 Bearer token；缺失时返回受控 401。
     */
    private String extractRequiredBearerToken(String authorizationHeader) {
        return extractBearerToken(authorizationHeader)
                .orElseThrow(() -> new AuthException(
                        HttpStatus.UNAUTHORIZED,
                        "AUTH_TOKEN_REQUIRED",
                        "请先登录。"));
    }

    /**
     * 从 Authorization 请求头中解析 Bearer token；无效格式按空 token 处理。
     */
    private Optional<String> extractBearerToken(String authorizationHeader) {
        if (authorizationHeader == null || authorizationHeader.isBlank()) {
            return Optional.empty();
        }
        String prefix = TOKEN_TYPE + " ";
        if (!authorizationHeader.regionMatches(true, 0, prefix, 0, prefix.length())) {
            return Optional.empty();
        }
        String token = authorizationHeader.substring(prefix.length()).trim();
        return token.isBlank() ? Optional.empty() : Optional.of(token);
    }

    /**
     * 构建统一的 session 失效异常，避免向前端泄露 token 或账号内部状态。
     */
    private AuthException invalidSession() {
        return new AuthException(
                HttpStatus.UNAUTHORIZED,
                "AUTH_SESSION_INVALID",
                "登录已失效，请重新登录。");
    }

    /**
     * 获取 UTC 当前时间，数据库 LocalDateTime 统一按 UTC 语义保存。
     */
    private LocalDateTime nowUtc() {
        return LocalDateTime.now(ZoneOffset.UTC);
    }

    /**
     * 提取客户端 IP 摘要，第一版只保留请求直接来源。
     */
    private String clientIp(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        String forwardedFor = request.getHeader("X-Forwarded-For");
        if (forwardedFor != null && !forwardedFor.isBlank()) {
            return truncate(forwardedFor.split(",")[0].trim(), 64);
        }
        return truncate(request.getRemoteAddr(), 64);
    }

    /**
     * 提取 User-Agent 摘要，避免过长请求头直接进入 session 表。
     */
    private String userAgent(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        return truncate(request.getHeader("User-Agent"), 256);
    }

    /**
     * 截断外部输入摘要字段，保护数据库字段长度和日志可读性。
     */
    private String truncate(String value, int maxLength) {
        if (value == null || value.length() <= maxLength) {
            return value;
        }
        return value.substring(0, maxLength);
    }
}
