package com.mosquito.project.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mosquito.project.config.AppConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class UserIntrospectionService {

    private static final Logger log = LoggerFactory.getLogger(UserIntrospectionService.class);

    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;
    private final AppConfig.IntrospectionConfig config;
    private final StringRedisTemplate redisTemplate;
    private final Map<String, CacheEntry> localCache = new ConcurrentHashMap<>();

    public UserIntrospectionService(RestTemplateBuilder builder, AppConfig appConfig, Optional<StringRedisTemplate> redisTemplateOpt) {
        this.config = appConfig.getSecurity().getIntrospection();
        this.restTemplate = builder
            .setConnectTimeout(Duration.ofMillis(config.getTimeoutMillis()))
            .setReadTimeout(Duration.ofMillis(config.getTimeoutMillis()))
            .build();
        this.objectMapper = new ObjectMapper();
        this.redisTemplate = redisTemplateOpt.orElse(null);
    }

    public IntrospectionResponse introspect(String authorizationHeader) {
        String token = extractToken(authorizationHeader);
        if (token == null || token.isBlank()) {
            return IntrospectionResponse.inactive();
        }

        String cacheKey = cacheKey(token);
        IntrospectionResponse cached = readCache(cacheKey);
        if (cached != null) {
            return cached;
        }

        if (config.getUrl() == null || config.getUrl().isBlank()) {
            log.error("Introspection URL is not configured");
            writeCache(cacheKey, IntrospectionResponse.inactive(), config.getNegativeCacheSeconds());
            return IntrospectionResponse.inactive();
        }

        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            if (config.getClientId() != null && !config.getClientId().isBlank()) {
                headers.setBasicAuth(config.getClientId(), config.getClientSecret());
            }

            MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
            body.add("token", token);

            HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
            ResponseEntity<IntrospectionResponse> response = restTemplate.postForEntity(
                config.getUrl(),
                request,
                IntrospectionResponse.class
            );

            IntrospectionResponse result = response.getBody();
            if (result == null) {
                writeCache(cacheKey, IntrospectionResponse.inactive(), config.getNegativeCacheSeconds());
                return IntrospectionResponse.inactive();
            }

            if (!result.isActive()) {
                writeCache(cacheKey, result, config.getNegativeCacheSeconds());
                return result;
            }

            long ttlSeconds = computeTtlSeconds(result.getExp());
            if (ttlSeconds <= 0) {
                IntrospectionResponse inactive = IntrospectionResponse.inactive();
                writeCache(cacheKey, inactive, config.getNegativeCacheSeconds());
                return inactive;
            }

            writeCache(cacheKey, result, ttlSeconds);
            return result;
        } catch (Exception ex) {
            log.warn("Introspection request failed: {}", ex.getMessage());
            IntrospectionResponse inactive = IntrospectionResponse.inactive();
            writeCache(cacheKey, inactive, config.getNegativeCacheSeconds());
            return inactive;
        }
    }

    private String extractToken(String authorizationHeader) {
        if (authorizationHeader == null || authorizationHeader.isBlank()) {
            return null;
        }
        if (authorizationHeader.startsWith("Bearer ")) {
            return authorizationHeader.substring("Bearer ".length()).trim();
        }
        return authorizationHeader.trim();
    }

    private long computeTtlSeconds(long exp) {
        long now = Instant.now().getEpochSecond();
        long delta = exp - now;
        long ttl = Math.min(config.getCacheTtlSeconds(), delta);
        return Math.max(ttl, 0);
    }

    private String cacheKey(String token) {
        return "introspect:" + sha256(token);
    }

    private IntrospectionResponse readCache(String cacheKey) {
        CacheEntry entry = localCache.get(cacheKey);
        if (entry != null && entry.expiresAtMillis > System.currentTimeMillis()) {
            return entry.response;
        }
        if (entry != null) {
            localCache.remove(cacheKey);
        }

        if (redisTemplate == null) {
            return null;
        }
        try {
            String payload = redisTemplate.opsForValue().get(cacheKey);
            if (payload == null) {
                return null;
            }
            return objectMapper.readValue(payload, IntrospectionResponse.class);
        } catch (Exception ex) {
            log.warn("Failed to read introspection cache: {}", ex.getMessage());
            return null;
        }
    }

    private void writeCache(String cacheKey, IntrospectionResponse response, long ttlSeconds) {
        if (ttlSeconds <= 0) {
            return;
        }
        long expiresAtMillis = System.currentTimeMillis() + Duration.ofSeconds(ttlSeconds).toMillis();
        localCache.put(cacheKey, new CacheEntry(response, expiresAtMillis));

        if (redisTemplate == null) {
            return;
        }
        try {
            String payload = objectMapper.writeValueAsString(response);
            redisTemplate.opsForValue().set(cacheKey, payload, Duration.ofSeconds(ttlSeconds));
        } catch (Exception ex) {
            log.warn("Failed to write introspection cache: {}", ex.getMessage());
        }
    }

    private String sha256(String value) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hashed = digest.digest(value.getBytes(StandardCharsets.UTF_8));
            return Base64.getUrlEncoder().withoutPadding().encodeToString(hashed);
        } catch (Exception ex) {
            throw new IllegalStateException("Hashing failed", ex);
        }
    }

    private static class CacheEntry {
        private final IntrospectionResponse response;
        private final long expiresAtMillis;

        private CacheEntry(IntrospectionResponse response, long expiresAtMillis) {
            this.response = response;
            this.expiresAtMillis = expiresAtMillis;
        }
    }
}
