package com.writeoff.security;

import com.writeoff.common.exception.BusinessException;
import com.writeoff.common.exception.ErrorCodes;
import com.writeoff.common.web.RequestIdContext;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import com.writeoff.module.observability.service.ObservabilityService;
import com.writeoff.module.system.service.OperationAuditLogService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
public class AuthInterceptor implements HandlerInterceptor {
    private static final long SESSION_ACTIVITY_TOUCH_INTERVAL_SECONDS = 60;
    private final JwtTokenService jwtTokenService;
    private final PermissionService permissionService;
    private final OperationAuditLogService operationAuditLogService;
    private final ObservabilityService observabilityService;
    private final JdbcTemplate jdbcTemplate;
    private final long idleTimeoutMinutes;
    private static final Pattern BIZ_ID_PATTERN = Pattern.compile("/(\\d+)(/|$)");

    public AuthInterceptor(JwtTokenService jwtTokenService,
                           PermissionService permissionService,
                           OperationAuditLogService operationAuditLogService,
                           ObservabilityService observabilityService,
                           JdbcTemplate jdbcTemplate,
                           @Value("${app.security.idle-timeout-minutes:60}") long idleTimeoutMinutes) {
        this.jwtTokenService = jwtTokenService;
        this.permissionService = permissionService;
        this.operationAuditLogService = operationAuditLogService;
        this.observabilityService = observabilityService;
        this.jdbcTemplate = jdbcTemplate;
        this.idleTimeoutMinutes = idleTimeoutMinutes <= 0 ? 60 : idleTimeoutMinutes;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        request.setAttribute("_startAtMs", System.currentTimeMillis());
        String requestId = resolveRequestId(request.getHeader("X-Request-Id"));
        RequestIdContext.set(requestId);
        response.setHeader("X-Request-Id", requestId);
        String path = request.getRequestURI();
        if (path.startsWith("/api/auth/login")
                || path.startsWith("/api/auth/platform-login")
                || path.startsWith("/api/auth/password-public-key")
                || path.startsWith("/api/auth/password-setup")
                || path.startsWith("/api/auth/refresh")
                || path.startsWith("/api/auth/logout")
                || path.startsWith("/api/system/health")
                || path.startsWith("/api/health")
                || path.startsWith("/api/captcha")) {
            return true;
        }
        String auth = request.getHeader("Authorization");
        if (auth == null || !auth.startsWith("Bearer ")) {
            throw new BusinessException(ErrorCodes.UNAUTHORIZED, "未登录或Token无效");
        }
        String token = auth.substring("Bearer ".length());
        try {
            Claims claims = jwtTokenService.parse(token);
            Long userId = claims.get("uid", Number.class).longValue();
            AuthScope scope = AuthScope.fromClaim(claims.get("scope", String.class));
            Number sidNum = claims.get("sid", Number.class);
            Long sessionId = sidNum == null ? null : sidNum.longValue();
            Long tenantId = null;
            if (scope == AuthScope.TENANT) {
                Number tidNum = claims.get("tid", Number.class);
                if (tidNum == null) {
                    throw new BusinessException(ErrorCodes.UNAUTHORIZED, "租户会话缺少租户信息");
                }
                tenantId = tidNum.longValue();
                ensureTenantSessionValid(userId, tenantId);
                ensureAccessSessionValid(sessionId, userId, tenantId, scope);
            } else {
                ensurePlatformSessionValid(userId);
                ensureAccessSessionValid(sessionId, userId, null, scope);
            }
            AuthContext.set(userId, tenantId, scope);

            if (handler instanceof HandlerMethod) {
                HandlerMethod hm = (HandlerMethod) handler;
                RequirePermission rp = hm.getMethodAnnotation(RequirePermission.class);
                if (rp != null) {
                    if (rp.domain() == PermissionDomain.PLATFORM) {
                        if (scope != AuthScope.PLATFORM || !permissionService.hasPlatformPermission(userId, rp.value())) {
                            throw new BusinessException(ErrorCodes.NO_PERMISSION, "无操作权限");
                        }
                    } else {
                        if (scope != AuthScope.TENANT || !permissionService.hasPermission(userId, rp.value())) {
                            throw new BusinessException(ErrorCodes.NO_PERMISSION, "无操作权限");
                        }
                    }
                }
            }
            return true;
        } catch (ExpiredJwtException e) {
            throw new BusinessException(ErrorCodes.TOKEN_EXPIRED, "Token已过期");
        } catch (JwtException e) {
            throw new BusinessException(ErrorCodes.UNAUTHORIZED, "未登录或Token无效");
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            String path = request.getRequestURI();
            if (path != null && path.startsWith("/api/")) {
                Object startAtObj = request.getAttribute("_startAtMs");
                long startAt = startAtObj instanceof Long ? (Long) startAtObj : System.currentTimeMillis();
                long durationMs = Math.max(0L, System.currentTimeMillis() - startAt);
                observabilityService.recordApiMetric(path, response.getStatus(), durationMs);
                String method = request.getMethod();
                boolean shouldLog = !"GET".equalsIgnoreCase(method)
                        || path.startsWith("/api/platform/")
                        || path.contains("/export")
                        || path.contains("/download")
                        || path.contains("/login");
                if (shouldLog) {
                    if (path.contains("/export") || path.contains("/download")) {
                        observabilityService.recordExportMetric(path, ex == null && response.getStatus() < 400 ? "SUCCESS" : "FAILED");
                    }
                    String actionCode = "API_CALL";
                    if (handler instanceof HandlerMethod) {
                        HandlerMethod hm = (HandlerMethod) handler;
                        RequirePermission rp = hm.getMethodAnnotation(RequirePermission.class);
                        if (rp != null) {
                            actionCode = rp.auditAction() == null || rp.auditAction().trim().isEmpty()
                                    ? rp.value()
                                    : rp.auditAction().trim();
                        } else {
                            actionCode = hm.getMethod().getName();
                        }
                    }
                    AuthScope scope = AuthContext.scope();
                    if (scope == null) {
                        if (path.contains("/platform-login")) {
                            scope = AuthScope.PLATFORM;
                        } else if (path.contains("/auth/login")) {
                            scope = AuthScope.TENANT;
                        }
                    }
                    operationAuditLogService.log(
                            AuthContext.tenantId(),
                            AuthContext.userId(),
                            scope,
                            actionCode,
                            resolveBizType(path),
                            resolveBizId(path),
                            method,
                            path,
                            request.getQueryString(),
                            RequestIdContext.get(),
                            response.getStatus(),
                            ex == null && response.getStatus() < 400,
                            ex == null ? null : ex.getMessage(),
                            request.getRemoteAddr(),
                            request.getHeader("User-Agent")
                    );
                }
            }
        } catch (Exception ignored) {
        }
        AuthContext.clear();
        RequestIdContext.clear();
    }

    private String resolveBizType(String path) {
        if (path == null) {
            return "unknown";
        }
        String p = path.replace("/api/", "");
        int idx = p.indexOf("/");
        if (idx <= 0) {
            return p;
        }
        return p.substring(0, idx);
    }

    private String resolveBizId(String path) {
        if (path == null) {
            return null;
        }
        Matcher matcher = BIZ_ID_PATTERN.matcher(path);
        if (matcher.find()) {
            return matcher.group(1);
        }
        return null;
    }

    private void ensureTenantSessionValid(Long userId, Long tenantId) {
        java.util.List<java.util.Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT u.status, u.valid_from, u.valid_to, t.status AS tenant_status " +
                        "FROM sys_user u JOIN tenant t ON t.id=u.tenant_id " +
                        "WHERE u.id=? AND u.tenant_id=? AND u.is_deleted=0 AND t.is_deleted=0 LIMIT 1",
                userId,
                tenantId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        java.util.Map<String, Object> row = rows.get(0);
        String userStatus = String.valueOf(row.get("status"));
        String tenantStatus = String.valueOf(row.get("tenant_status"));
        if (!"ENABLED".equals(userStatus) || !"ENABLED".equals(tenantStatus)) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime validFrom = toLocalDateTime(row.get("valid_from"));
        LocalDateTime validTo = toLocalDateTime(row.get("valid_to"));
        if (validFrom != null && now.isBefore(validFrom)) {
            throw new BusinessException(ErrorCodes.ACCOUNT_EXPIRED, "账号尚未生效");
        }
        if (validTo != null && now.isAfter(validTo)) {
            throw new BusinessException(ErrorCodes.ACCOUNT_EXPIRED, "账号已过有效期");
        }
    }

    private void ensurePlatformSessionValid(Long userId) {
        java.util.List<java.util.Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT status, valid_from, valid_to FROM platform_user WHERE id=? AND is_deleted=0 LIMIT 1",
                userId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        java.util.Map<String, Object> row = rows.get(0);
        String userStatus = String.valueOf(row.get("status"));
        if (!"ENABLED".equals(userStatus)) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        LocalDateTime now = LocalDateTime.now();
        LocalDateTime validFrom = toLocalDateTime(row.get("valid_from"));
        LocalDateTime validTo = toLocalDateTime(row.get("valid_to"));
        if (validFrom != null && now.isBefore(validFrom)) {
            throw new BusinessException(ErrorCodes.ACCOUNT_EXPIRED, "账号尚未生效");
        }
        if (validTo != null && now.isAfter(validTo)) {
            throw new BusinessException(ErrorCodes.ACCOUNT_EXPIRED, "账号已过有效期");
        }
    }

    private LocalDateTime toLocalDateTime(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof LocalDateTime) {
            return (LocalDateTime) value;
        }
        if (value instanceof java.sql.Timestamp) {
            return ((java.sql.Timestamp) value).toLocalDateTime();
        }
        if (value instanceof java.util.Date) {
            return new java.sql.Timestamp(((java.util.Date) value).getTime()).toLocalDateTime();
        }
        if (value instanceof String) {
            String text = String.valueOf(value).trim();
            if (text.isEmpty()) {
                return null;
            }
            try {
                return LocalDateTime.parse(text.replace(' ', 'T'));
            } catch (Exception ignored) {
            }
        }
        return null;
    }

    private void ensureAccessSessionValid(Long sessionId, Long userId, Long tenantId, AuthScope scope) {
        // Require session binding so admin revoke can take effect immediately.
        if (sessionId == null || sessionId <= 0) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        java.util.List<java.util.Map<String, Object>> rows = jdbcTemplate.queryForList(
                "SELECT user_id, tenant_id, scope, status, issued_at, expires_at, last_used_at, is_deleted FROM auth_refresh_token WHERE id=? LIMIT 1",
                sessionId
        );
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        java.util.Map<String, Object> row = rows.get(0);
        int isDeleted = toFlagInt(row.get("is_deleted"));
        if (isDeleted == 1) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        Long rowUserId = ((Number) row.get("user_id")).longValue();
        Long rowTenantId = row.get("tenant_id") == null ? null : ((Number) row.get("tenant_id")).longValue();
        String rowScope = String.valueOf(row.get("scope"));
        String rowStatus = String.valueOf(row.get("status"));
        LocalDateTime issuedAt = toLocalDateTime(row.get("issued_at"));
        LocalDateTime expiresAt = toLocalDateTime(row.get("expires_at"));
        LocalDateTime lastUsedAt = toLocalDateTime(row.get("last_used_at"));
        LocalDateTime now = LocalDateTime.now();
        if (!rowUserId.equals(userId) || !scope.name().equals(rowScope)) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        if (scope == AuthScope.TENANT && (rowTenantId == null || !rowTenantId.equals(tenantId))) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        if (scope == AuthScope.PLATFORM && rowTenantId != null) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        if (!"ACTIVE".equals(rowStatus)) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        LocalDateTime lastActivityAt = lastUsedAt == null ? issuedAt : lastUsedAt;
        if (lastActivityAt == null || now.isAfter(lastActivityAt.plusMinutes(idleTimeoutMinutes))) {
            revokeIdleSession(sessionId);
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "浼氳瘽澶辨晥");
        }
        if (expiresAt == null || now.isAfter(expiresAt)) {
            throw new BusinessException(ErrorCodes.SESSION_INVALID, "会话失效");
        }
        touchSessionActivity(sessionId, lastUsedAt, now);
    }

    private String resolveRequestId(String requestIdHeader) {
        String requestId = requestIdHeader == null ? "" : requestIdHeader.trim();
        if (!requestId.isEmpty()) {
            return requestId;
        }
        return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
    }

    private int toFlagInt(Object value) {
        if (value == null) {
            return 0;
        }
        if (value instanceof Number) {
            return ((Number) value).intValue();
        }
        if (value instanceof Boolean) {
            return ((Boolean) value) ? 1 : 0;
        }
        String text = String.valueOf(value).trim();
        if ("true".equalsIgnoreCase(text)) {
            return 1;
        }
        if ("false".equalsIgnoreCase(text)) {
            return 0;
        }
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException ex) {
            return 0;
        }
    }

    private void revokeIdleSession(Long sessionId) {
        jdbcTemplate.update(
                "UPDATE auth_refresh_token SET status='REVOKED', revoked_at=CURRENT_TIMESTAMP, revoked_reason='IDLE_TIMEOUT', updated_at=CURRENT_TIMESTAMP " +
                        "WHERE id=? AND status='ACTIVE' AND is_deleted=0",
                sessionId
        );
    }

    private void touchSessionActivity(Long sessionId, LocalDateTime lastUsedAt, LocalDateTime now) {
        if (lastUsedAt != null && java.time.Duration.between(lastUsedAt, now).getSeconds() < SESSION_ACTIVITY_TOUCH_INTERVAL_SECONDS) {
            return;
        }
        jdbcTemplate.update(
                "UPDATE auth_refresh_token SET last_used_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP " +
                        "WHERE id=? AND status='ACTIVE' AND is_deleted=0",
                sessionId
        );
    }
}
