package com.yaoai.admin.audit.aspect;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yaoai.admin.audit.AdminAuditService;
import com.yaoai.admin.audit.annotation.AdminAudited;
import com.yaoai.admin.auth.StpAdminUtil;
import com.yaoai.domain.entity.AdminAuditLog;
import com.yaoai.domain.entity.AdminUser;
import com.yaoai.domain.mapper.AdminUserMapper;
import com.yaoai.domain.mapper.PlanMapper;
import com.yaoai.domain.mapper.TenantBalanceMapper;
import com.yaoai.domain.mapper.TenantMapper;
import com.yaoai.domain.mapper.TenantQuotaMapper;
import com.yaoai.domain.mapper.UserMapper;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AdminAuditAspect {

    private final AdminAuditService adminAuditService;
    private final AdminUserMapper adminUserMapper;
    private final TenantMapper tenantMapper;
    private final TenantBalanceMapper tenantBalanceMapper;
    private final TenantQuotaMapper tenantQuotaMapper;
    private final UserMapper userMapper;
    private final PlanMapper planMapper;
    private final ObjectMapper objectMapper;

    @Around("@annotation(audited)")
    public Object around(ProceedingJoinPoint joinPoint, AdminAudited audited) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Map<String, Object> args = collectArgs(signature.getParameterNames(), joinPoint.getArgs());
        String resourceId = resolveResourceId(audited.resourceIdParam(), args);
        Object before = loadResourceSnapshot(audited.resource(), resourceId, args);
        Object result = null;
        Throwable failure = null;

        try {
            result = joinPoint.proceed();
            return result;
        } catch (Throwable e) {
            failure = e;
            throw e;
        } finally {
            Object after = failure == null ? loadResourceSnapshot(audited.resource(), resourceId, args) : null;
            writeAudit(audited, args, resourceId, before, after, result, failure);
        }
    }

    private void writeAudit(AdminAudited audited, Map<String, Object> args, String resourceId, Object before, Object after, Object result, Throwable failure) {
        try {
            AdminUser admin = currentAdmin();
            HttpServletRequest request = currentRequest();

            AdminAuditLog log = new AdminAuditLog();
            if (admin != null) {
                log.setAdminUserId(admin.getId());
                log.setAdminUsername(admin.getUsername());
            }
            log.setAction(audited.action());
            log.setResourceType(audited.resource());
            log.setResourceId(resourceId);
            log.setBeforeValue(toJson(payload("args", args, "snapshot", before)));
            if (failure == null) {
                log.setAfterValue(toJson(payload("result", result, "snapshot", after)));
                log.setStatus("SUCCESS");
            } else {
                log.setStatus("FAILED");
                log.setErrorMessage(failure.getMessage());
            }
            if (request != null) {
                log.setIp(getClientIp(request));
                log.setUserAgent(request.getHeader("User-Agent"));
            }
            log.setRequestId(MDC.get("requestId"));
            log.setCreatedAt(LocalDateTime.now());
            adminAuditService.record(log);
        } catch (Exception e) {
            log.warn("Failed to write admin audit log: action={}, resource={}", audited.action(), audited.resource(), e);
        }
    }

    private Object loadResourceSnapshot(String resource, String resourceId, Map<String, Object> args) {
        try {
            return switch (resource) {
                case "TENANT" -> tenantSnapshot(parseLong(resourceId));
                case "USER" -> userMapper.selectById(parseLong(resourceId));
                case "PLAN" -> planMapper.selectById(parseLong(resourceId));
                case "ADMIN_USER" -> adminUserSnapshot(resourceId, args);
                default -> null;
            };
        } catch (Exception e) {
            log.debug("Skip admin audit snapshot: resource={}, id={}", resource, resourceId, e);
            return null;
        }
    }

    private Map<String, Object> tenantSnapshot(Long tenantId) {
        if (tenantId == null) {
            return null;
        }
        Map<String, Object> snapshot = new LinkedHashMap<>();
        snapshot.put("tenant", tenantMapper.selectById(tenantId));
        snapshot.put("balance", tenantBalanceMapper.findByTenantId(tenantId));
        snapshot.put("quota", tenantQuotaMapper.selectOne(Wrappers.<com.yaoai.domain.entity.TenantQuota>lambdaQuery()
                .eq(com.yaoai.domain.entity.TenantQuota::getTenantId, tenantId)));
        return snapshot;
    }

    private Map<String, Object> payload(String firstKey, Object firstValue, String secondKey, Object secondValue) {
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put(firstKey, firstValue);
        payload.put(secondKey, secondValue);
        return payload;
    }

    private AdminUser adminUserSnapshot(String resourceId, Map<String, Object> args) {
        Long id = parseLong(resourceId);
        if (id != null) {
            return adminUserMapper.selectById(id);
        }
        Object username = args.get("username");
        if (username == null) {
            username = resolveNestedArg("username", args);
        }
        if (username == null) {
            return null;
        }
        return adminUserMapper.selectOne(Wrappers.<AdminUser>lambdaQuery()
                .eq(AdminUser::getUsername, String.valueOf(username)));
    }

    private Long parseLong(String value) {
        if (value == null || value.isBlank()) {
            return null;
        }
        try {
            return Long.parseLong(value);
        } catch (NumberFormatException e) {
            return null;
        }
    }

    private AdminUser currentAdmin() {
        if (!StpAdminUtil.isLogin()) {
            return null;
        }
        return adminUserMapper.selectById(StpAdminUtil.getLoginIdAsLong());
    }

    private HttpServletRequest currentRequest() {
        if (RequestContextHolder.getRequestAttributes() instanceof ServletRequestAttributes attrs) {
            return attrs.getRequest();
        }
        return null;
    }

    private Map<String, Object> collectArgs(String[] names, Object[] values) {
        Map<String, Object> args = new LinkedHashMap<>();
        for (int i = 0; i < values.length; i++) {
            Object value = values[i];
            if (value instanceof HttpServletRequest) {
                continue;
            }
            String name = names != null && i < names.length ? names[i] : "arg" + i;
            args.put(name, redact(name, value));
        }
        return args;
    }

    private Object redact(String name, Object value) {
        String lower = name.toLowerCase();
        if (lower.contains("password") || lower.contains("token")) {
            return "***";
        }
        if (value == null || value instanceof String || value instanceof Number || value instanceof Boolean) {
            return value;
        }
        if (value instanceof Map<?, ?> map) {
            Map<String, Object> sanitized = new LinkedHashMap<>();
            map.forEach((key, item) -> sanitized.put(String.valueOf(key), redact(String.valueOf(key), item)));
            return sanitized;
        }
        return sanitizeBean(value);
    }

    @SuppressWarnings("unchecked")
    private Object sanitizeBean(Object value) {
        try {
            Map<String, Object> sanitized = objectMapper.convertValue(value, Map.class);
            Map<String, Object> redacted = new LinkedHashMap<>();
            sanitized.forEach((key, item) -> redacted.put(key, redact(key, item)));
            return redacted;
        } catch (IllegalArgumentException e) {
            return value;
        }
    }

    private Object resolveNestedArg(String propertyName, Map<String, Object> args) {
        for (Object value : args.values()) {
            Object nested = readProperty(value, propertyName);
            if (nested != null) {
                return nested;
            }
        }
        return null;
    }

    private Object readProperty(Object value, String propertyName) {
        if (value == null) {
            return null;
        }
        if (value instanceof Map<?, ?> map) {
            return map.get(propertyName);
        }
        try {
            for (PropertyDescriptor descriptor : Introspector.getBeanInfo(value.getClass()).getPropertyDescriptors()) {
                if (propertyName.equals(descriptor.getName()) && descriptor.getReadMethod() != null) {
                    return descriptor.getReadMethod().invoke(value);
                }
            }
        } catch (Exception ignored) {
            return null;
        }
        return null;
    }

    private String resolveResourceId(String paramName, Map<String, Object> args) {
        if (paramName == null || paramName.isBlank()) {
            return null;
        }
        Object value = args.get(paramName);
        if (value == null) {
            value = resolveNestedArg(paramName, args);
        }
        return value == null ? null : String.valueOf(value);
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip != null && !ip.isBlank() && !"unknown".equalsIgnoreCase(ip)) {
            return ip.split(",")[0].trim();
        }
        ip = request.getHeader("X-Real-IP");
        if (ip != null && !ip.isBlank() && !"unknown".equalsIgnoreCase(ip)) {
            return ip;
        }
        return request.getRemoteAddr();
    }

    private String toJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (JsonProcessingException e) {
            return "{\"serializationError\":\"" + e.getOriginalMessage() + "\"}";
        }
    }
}
