package com.eactive.eai.common.inflow.dual;

import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.commons.lang3.StringUtils;

import com.eactive.eai.common.inflow.AbstractInflowControlManager;
import com.eactive.eai.common.inflow.Bucket;
import com.eactive.eai.common.inflow.CustomGroupBucket;
import com.eactive.eai.common.inflow.InflowTargetVO;
import com.eactive.eai.common.inflow.InflowType;
import com.eactive.eai.common.lifecycle.LifecycleException;
import com.eactive.eai.common.util.ApplicationContextProvider;
import com.eactive.eai.common.util.Logger;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import io.github.bucket4j.local.LocalBucket;

/**
 * 어댑터/인터페이스 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자.
 *
 * <p>클라이언트 유량제어는 {@link ClientDualInflowControlManager}로 분리됨.
 *
 * <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
 * <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager</pre>
 */
public class DualInflowControlManager extends AbstractInflowControlManager implements DualBucket {

    private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);

    // InflowControlUtil이 리플렉션으로 호출하는 진입점
    public static synchronized Bucket getBucket() {
        return ApplicationContextProvider.getContext().getBean(DualInflowControlManager.class);
    }

    private volatile Map<String, DualCustomBucket> adapterBucketMap   = new ConcurrentHashMap<>();
    private volatile Map<String, DualCustomBucket> interfaceBucketMap = new ConcurrentHashMap<>();

    private final ConcurrentHashMap<String, TargetMetrics> adapterMetricsMap   = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, TargetMetrics> interfaceMetricsMap = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, TargetMetrics> groupMetricsMap     = new ConcurrentHashMap<>();

    public static class TargetMetrics {
        public final AtomicLong allowed           = new AtomicLong();
        public final AtomicLong rejectedPerSecond = new AtomicLong();
        public final AtomicLong rejectedThreshold = new AtomicLong();
        public volatile long    lastResetTime     = System.currentTimeMillis();

        public void reset() {
            allowed.set(0);
            rejectedPerSecond.set(0);
            rejectedThreshold.set(0);
            lastResetTime = System.currentTimeMillis();
        }
    }

    // -------------------------------------------------------------------------
    // Lifecycle
    // -------------------------------------------------------------------------

    @Override
    public void start() throws LifecycleException {
        super.start();
        try {
            initAdapters();
            initInterfaces();
        } catch (Exception e) {
            throw new LifecycleException("DualInflowControlManager init failed: " + e.getMessage());
        }
    }

    // -------------------------------------------------------------------------
    // 초기화 / 리로드
    // -------------------------------------------------------------------------

    private void initAdapters() throws Exception {
        this.adapterBucketMap = loadBucketMap(InflowType.ADAPTER);
        if (logger.isInfo()) logger.info("DualInflowControlManager] initAdapters: " + adapterBucketMap.size() + " buckets");
    }

    private void initInterfaces() throws Exception {
        this.interfaceBucketMap = loadBucketMap(InflowType.INTERFACE);
        if (logger.isInfo()) logger.info("DualInflowControlManager] initInterfaces: " + interfaceBucketMap.size() + " buckets");
    }

    protected Map<String, DualCustomBucket> loadBucketMap(InflowType type) throws Exception {
        Map<String, DualCustomBucket> newMap = new HashMap<>();
        for (InflowTargetVO vo : inflowControlDAO.getInflowList(type)) {
            DualCustomBucket bucket = makeBucket(vo);
            if (bucket != null) newMap.put(vo.getName(), bucket);
        }
        return newMap;
    }

    @Override
    public synchronized void reloadAdapter() throws Exception {
        initAdapters();
        adapterMetricsMap.clear();
    }

    @Override
    public synchronized void reloadAdapter(String adapter) throws Exception {
        reloadBucketMap(adapterBucketMap, InflowType.ADAPTER, adapter);
        TargetMetrics m = adapterMetricsMap.get(adapter);
        if (m != null) m.reset();
    }

    @Override
    public synchronized void reloadInterface() throws Exception {
        initInterfaces();
        interfaceMetricsMap.clear();
    }

    @Override
    public synchronized void reloadInterface(String inter) throws Exception {
        reloadBucketMap(interfaceBucketMap, InflowType.INTERFACE, inter);
        TargetMetrics m = interfaceMetricsMap.get(inter);
        if (m != null) m.reset();
    }

    @Override
    public synchronized void reloadGroup() throws Exception {
        super.reloadGroup();
        groupMetricsMap.clear();
    }

    @Override
    public synchronized void reloadGroup(String groupId) throws Exception {
        super.reloadGroup(groupId);
        TargetMetrics m = groupMetricsMap.get(groupId);
        if (m != null) m.reset();
    }

    protected void reloadBucketMap(Map<String, DualCustomBucket> targetMap, InflowType type, String name) throws Exception {
        Map<String, InflowTargetVO> updated = inflowControlDAO.getInflowMap(type, name);
        if (updated == null) return;
        for (InflowTargetVO vo : updated.values()) {
            DualCustomBucket bucket = makeBucket(vo);
            if (bucket != null) targetMap.put(vo.getName(), bucket);
            else                targetMap.remove(vo.getName());
        }
    }

    // -------------------------------------------------------------------------
    // 어댑터/인터페이스 메트릭 접근자
    // -------------------------------------------------------------------------

    public TargetMetrics getAdapterMetrics(String adapter)    { return adapterMetricsMap.get(adapter); }
    public TargetMetrics getInterfaceMetrics(String inter)    { return interfaceMetricsMap.get(inter); }

    public Map<String, TargetMetrics> getAllAdapterMetrics()   { return Collections.unmodifiableMap(adapterMetricsMap); }
    public Map<String, TargetMetrics> getAllInterfaceMetrics() { return Collections.unmodifiableMap(interfaceMetricsMap); }

    public void resetAdapterMetrics(String adapter)  { TargetMetrics m = adapterMetricsMap.get(adapter);   if (m != null) m.reset(); }
    public void resetInterfaceMetrics(String inter)  { TargetMetrics m = interfaceMetricsMap.get(inter);   if (m != null) m.reset(); }

    public void resetAllAdapterMetrics()   { adapterMetricsMap.values().forEach(TargetMetrics::reset); }
    public void resetAllInterfaceMetrics() { interfaceMetricsMap.values().forEach(TargetMetrics::reset); }

    // -------------------------------------------------------------------------
    // 그룹 메트릭 — isGroupPass() 카운팅 및 접근자
    // -------------------------------------------------------------------------

    @Override
    public String isGroupPass(String groupId) {
        String result = super.isGroupPass(groupId);
        TargetMetrics m = groupMetricsMap.computeIfAbsent(groupId, k -> new TargetMetrics());
        if (result == null) {
            m.allowed.incrementAndGet();
        } else if (CustomGroupBucket.RESULT_BLOCKED_PER_SECOND.equals(result)) {
            m.rejectedPerSecond.incrementAndGet();
        } else {
            m.rejectedThreshold.incrementAndGet();
        }
        return result;
    }

    public TargetMetrics getGroupMetrics(String groupId) {
        return groupMetricsMap.get(groupId);
    }

    public Map<String, TargetMetrics> getAllGroupMetrics() {
        return Collections.unmodifiableMap(groupMetricsMap);
    }

    public void resetGroupMetrics(String groupId) {
        TargetMetrics m = groupMetricsMap.get(groupId);
        if (m != null) m.reset();
    }

    public void resetAllGroupMetrics() {
        groupMetricsMap.values().forEach(TargetMetrics::reset);
    }

    // -------------------------------------------------------------------------
    // DualBucket — 차단 원인 포함 메서드
    // -------------------------------------------------------------------------

    @Override
    public String isAdapterPassDetail(String adapter) {
        DualCustomBucket b = adapterBucketMap.get(adapter);
        if (b == null) return DualCustomBucket.RESULT_PASS;
        String result = b.tryConsume();
        recordMetrics(adapterMetricsMap, adapter, result);
        return result;
    }

    @Override
    public String isInterfacePassDetail(String inter) {
        DualCustomBucket b = interfaceBucketMap.get(inter);
        if (b == null) return DualCustomBucket.RESULT_PASS;
        String result = b.tryConsume();
        recordMetrics(interfaceMetricsMap, inter, result);
        return result;
    }

    protected void recordMetrics(ConcurrentHashMap<String, TargetMetrics> metricsMap, String key, String result) {
        TargetMetrics m = metricsMap.computeIfAbsent(key, k -> new TargetMetrics());
        if (result == null)                                                  m.allowed.incrementAndGet();
        else if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(result)) m.rejectedPerSecond.incrementAndGet();
        else                                                                  m.rejectedThreshold.incrementAndGet();
    }

    // -------------------------------------------------------------------------
    // Bucket 인터페이스 오버라이드 — 이중 버킷으로 교체 (boolean 반환 유지)
    // -------------------------------------------------------------------------

    @Override
    public boolean isAdapterPass(String adapter) {
        return isAdapterPassDetail(adapter) == DualCustomBucket.RESULT_PASS;
    }

    @Override
    public boolean isInterfacePass(String inter) {
        return isInterfacePassDetail(inter) == DualCustomBucket.RESULT_PASS;
    }

    // -------------------------------------------------------------------------
    // 어댑터/인터페이스 키·VO·삭제 — Dual 맵 기준으로 오버라이드
    // -------------------------------------------------------------------------

    @Override
    public String[] getAdapterAllKeys() {
        String[] keys = adapterBucketMap.keySet().toArray(new String[0]);
        Arrays.sort(keys);
        return keys;
    }

    @Override
    public String[] getInterfaceAllKeys() {
        String[] keys = interfaceBucketMap.keySet().toArray(new String[0]);
        Arrays.sort(keys);
        return keys;
    }

    @Override
    public InflowTargetVO getAdapterInflowThreashold(String adapter) {
        DualCustomBucket b = adapterBucketMap.get(adapter);
        return (b == null) ? null : b.getInflowTargetVo();
    }

    @Override
    public InflowTargetVO getInterfaceInflowThreashold(String inter) {
        DualCustomBucket b = interfaceBucketMap.get(inter);
        return (b == null) ? null : b.getInflowTargetVo();
    }

    @Override
    public synchronized void removeAdapter(String adapter) {
        adapterBucketMap.remove(adapter);
        adapterMetricsMap.remove(adapter);
    }

    @Override
    public synchronized void removeInterface(String inter) {
        interfaceBucketMap.remove(inter);
        interfaceMetricsMap.remove(inter);
    }

    // -------------------------------------------------------------------------
    // 모니터링용 접근자
    // -------------------------------------------------------------------------

    public DualCustomBucket getAdapterBucket(String adapter) {
        return adapterBucketMap.get(adapter);
    }

    public DualCustomBucket getInterfaceBucket(String inter) {
        return interfaceBucketMap.get(inter);
    }

    public Map<String, DualCustomBucket> getAdapterBucketMap() {
        return adapterBucketMap;
    }

    public Map<String, DualCustomBucket> getInterfaceBucketMap() {
        return interfaceBucketMap;
    }

    // -------------------------------------------------------------------------
    // 버킷 팩토리
    // -------------------------------------------------------------------------

    /**
     * perSecond 버킷: greedy refill — 초당 N개 요청을 균등하게 허용.
     * threshold 버킷: intervally refill — 기간(시/일/월 등) 단위 쿼터를 기간 시작 시 일괄 충전.
     * Bandwidth.simple은 두 버킷 모두 greedy로 생성되어 시간 단위 구분이 없어지므로 classic으로 교체.
     */
    protected DualCustomBucket makeBucket(InflowTargetVO vo) {
        if (!AbstractInflowControlManager.isInflowTarget(vo)) {
            return null;
        }

        LocalBucket perSecondBucket = null;
        LocalBucket thresholdBucket = null;

        if (vo.getThresholdPerSecond() > 0) {
            perSecondBucket = Bucket4j.builder()
                    .addLimit(Bandwidth.classic(vo.getThresholdPerSecond(),
                            Refill.greedy(vo.getThresholdPerSecond(), Duration.ofSeconds(1))))
                    .build();
        }

        if (vo.getThreshold() > 0 && StringUtils.isNotBlank(vo.getThresholdTimeUnit())) {
            Duration period = AbstractInflowControlManager.getPeriod(vo.getThresholdTimeUnit());
            thresholdBucket = Bucket4j.builder()
                    .addLimit(Bandwidth.classic(vo.getThreshold(),
                            Refill.intervally(vo.getThreshold(), period)))
                    .build();
        }

        return new DualCustomBucket(perSecondBucket, thresholdBucket, vo);
    }
}
