/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.hertzbeat.manager.config;

import com.usthe.sureness.subject.PrincipalMap;
import com.usthe.sureness.subject.SubjectSum;
import com.usthe.sureness.util.SurenessContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import lombok.extern.slf4j.Slf4j;
import org.apache.hertzbeat.collector.dispatch.DispatchConstants;
import org.apache.hertzbeat.common.constants.NetworkConstants;
import org.apache.hertzbeat.common.util.JsonUtil;
import org.apache.hertzbeat.manager.service.AccountService;
import org.apache.hertzbeat.manager.service.impl.AccountServiceImpl;
import org.jspecify.annotations.NonNull;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * Interceptor to validate that managed API tokens have not been revoked (deleted).
 * <p>
 * This interceptor runs AFTER the Sureness authentication filter to ensure:
 * 1. Only authenticated requests are checked (avoids unnecessary DB hits)
 * 2. lastUsedTime is only updated for successfully authenticated requests
 * <p>
 * Responsibility split:
 * - <b>Expiration</b>: handled by Sureness/JWT natively via the {@code exp} claim
 * - <b>Revocation</b>: handled here by checking the token's status in the database
 * - <b>Last used time</b>: updated here on each valid request (throttled)
 * <p>
 * Only tokens containing the "managed" claim (generated by the new token management system)
 * are validated against the database. Legacy tokens without this claim are allowed to pass
 * through for backward compatibility.
 * </p>
 */
@Component
@Slf4j
public class ApiTokenValidationFilter implements HandlerInterceptor {

    private static final String ROLES_CLAIM = "roles";
    private static final String TOKEN_VALIDATION_UNAVAILABLE = "Token validation unavailable";

    private final AccountService accountService;

    public ApiTokenValidationFilter(AccountService accountService) {
        this.accountService = accountService;
    }

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler)
        throws IOException {
        SubjectSum subject = SurenessContextHolder.getBindSubject();
        if (subject == null || !isManagedToken(subject)) {
            return true;
        }
        String authorization = request.getHeader(NetworkConstants.AUTHORIZATION);

        if (authorization != null && authorization.startsWith(DispatchConstants.BEARER)) {
            String token = authorization.substring(DispatchConstants.BEARER.length()).trim();
            if (!token.isEmpty()) {
                try {
                    String rejectReason = checkManagedToken(subject, token);
                    if (rejectReason != null) {
                        return writeError(response, HttpStatus.UNAUTHORIZED, rejectReason);
                    }
                } catch (RuntimeException e) {
                    log.warn("Managed token validation failed", e);
                    return writeError(response, HttpStatus.SERVICE_UNAVAILABLE, TOKEN_VALIDATION_UNAVAILABLE);
                }
            }
        }

        return true;
    }

    /**
     * Check if a managed token should be rejected.
     * <p>
     * Returns a rejection reason string if the token should be rejected, null if allowed.
     * Checks:
     * 1. Only managed tokens (with "managed" claim) are validated — legacy tokens pass through
     * 2. Token must exist and be active in the database (not deleted/revoked)
     * 3. Token owner must still exist, remain active, and still own the claimed roles
     * If the token is valid, updates the last used time.
     * </p>
     */
    private String checkManagedToken(SubjectSum subject, String token) {
        String rejectReason = accountService.checkTokenStatus(token);
        if (rejectReason != null) {
            return rejectReason;
        }
        rejectReason = accountService.checkManagedTokenAccess(getCurrentUserId(subject), extractClaimedRoles(subject));
        if (rejectReason != null) {
            return rejectReason;
        }
        try {
            accountService.touchTokenLastUsedTime(token);
        } catch (RuntimeException e) {
            log.debug("Failed to update token last used time", e);
        }
        return null;
    }

    private boolean isManagedToken(SubjectSum subject) {
        PrincipalMap principalMap = subject.getPrincipalMap();
        if (principalMap == null) {
            return false;
        }
        Object managed = principalMap.getPrincipal(AccountServiceImpl.CLAIM_MANAGED);
        return managed instanceof Boolean ? (Boolean) managed : Boolean.parseBoolean(String.valueOf(managed));
    }

    @SuppressWarnings("unchecked")
    private List<String> extractClaimedRoles(SubjectSum subject) {
        Object roles = subject.getRoles();
        if (roles instanceof List<?>) {
            return (List<String>) roles;
        }
        PrincipalMap principalMap = subject.getPrincipalMap();
        if (principalMap == null) {
            return Collections.emptyList();
        }
        Object claimedRoles = principalMap.getPrincipal(ROLES_CLAIM);
        if (claimedRoles instanceof List<?>) {
            return (List<String>) claimedRoles;
        }
        return Collections.emptyList();
    }

    private String getCurrentUserId(SubjectSum subject) {
        Object principal = subject.getPrincipal();
        return principal == null ? null : String.valueOf(principal);
    }

    private boolean writeError(HttpServletResponse response, HttpStatus status, String message) throws IOException {
        response.setStatus(status.value());
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        try (PrintWriter writer = response.getWriter()) {
            writer.write(JsonUtil.toJson(Map.of("code", status.value(), "msg", message)));
        }
        return false;
    }
}
