package com.writeoff.module.observability.service;

import com.writeoff.common.exception.BusinessException;
import com.writeoff.module.observability.dto.CreateAlertRuleRequest;
import com.writeoff.module.observability.dto.UpdateAlertRuleRequest;
import com.writeoff.module.observability.model.AlertEventInfo;
import com.writeoff.module.observability.model.AlertRuleInfo;
import com.writeoff.module.observability.model.ObservabilityMetricPoint;
import com.writeoff.security.AuthContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Service
public class ObservabilityService {
    private final JdbcTemplate jdbcTemplate;

    private static final RowMapper<AlertRuleInfo> RULE_ROW_MAPPER = (rs, n) -> new AlertRuleInfo(
            rs.getLong("id"),
            rs.getString("rule_code"),
            rs.getString("rule_name"),
            rs.getString("compare_op"),
            rs.getDouble("threshold_value"),
            rs.getInt("window_minute"),
            rs.getInt("suppress_window_minute"),
            rs.getString("status")
    );

    private static final RowMapper<AlertEventInfo> EVENT_ROW_MAPPER = (rs, n) -> new AlertEventInfo(
            rs.getLong("id"),
            rs.getString("rule_code"),
            rs.getString("metric_code"),
            rs.getDouble("current_value"),
            rs.getDouble("threshold_value"),
            rs.getString("alert_level"),
            rs.getString("status"),
            rs.getString("created_at"),
            rs.getString("recovered_at")
    );

    public ObservabilityService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void recordApiMetric(String apiPath, int statusCode, long durationMs) {
        String statusGroup = statusCode >= 500 ? "5XX" : (statusCode >= 400 ? "4XX" : "2XX");
        insertMetric("API_TOTAL", "status", statusGroup, 1D);
        insertMetric("API_DURATION_MS", "path", normalize(apiPath), (double) durationMs);
    }

    public void recordAsyncMetric(String jobType, String resultStatus) {
        insertMetric("ASYNC_JOB_TOTAL", "jobType", normalize(jobType), 1D);
        if ("FAILED".equalsIgnoreCase(resultStatus)) {
            insertMetric("ASYNC_JOB_FAILED", "jobType", normalize(jobType), 1D);
        }
    }

    public void recordExportMetric(String exportCode, String resultStatus) {
        insertMetric("EXPORT_TOTAL", "exportCode", normalize(exportCode), 1D);
        if (!"SUCCESS".equalsIgnoreCase(resultStatus)) {
            insertMetric("EXPORT_FAILED", "exportCode", normalize(exportCode), 1D);
        }
    }

    public List<ObservabilityMetricPoint> queryMetric(String metricCode, int minutes) {
        int safeMinutes = minutes <= 0 ? 60 : minutes;
        return jdbcTemplate.query(
                "SELECT DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:00') AS minute_slot, metric_code, SUM(metric_value) AS metric_value " +
                        "FROM observability_metric WHERE tenant_id=? AND metric_code=? AND created_at>=DATE_SUB(NOW(), INTERVAL ? MINUTE) " +
                        "GROUP BY DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:00'), metric_code ORDER BY minute_slot ASC",
                (rs, n) -> new ObservabilityMetricPoint(
                        rs.getString("minute_slot"),
                        rs.getString("metric_code"),
                        rs.getDouble("metric_value")
                ),
                tenantId(),
                metricCode,
                safeMinutes
        );
    }

    public List<AlertRuleInfo> listRules() {
        return jdbcTemplate.query(
                "SELECT * FROM alert_rule WHERE tenant_id=? AND is_deleted=0 ORDER BY id DESC",
                RULE_ROW_MAPPER,
                tenantId()
        );
    }

    @Transactional(rollbackFor = Exception.class)
    public AlertRuleInfo createRule(CreateAlertRuleRequest request) {
        String status = normalizeStatus(request.getStatus());
        jdbcTemplate.update(
                "INSERT INTO alert_rule (tenant_id, rule_code, rule_name, compare_op, threshold_value, window_minute, suppress_window_minute, status, created_by, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                tenantId(),
                request.getRuleCode(),
                request.getRuleName(),
                request.getCompareOp(),
                request.getThresholdValue(),
                request.getWindowMinute(),
                normalizeSuppressMinute(request.getSuppressWindowMinute()),
                status,
                safeUserId(),
                safeUserId()
        );
        Long id = jdbcTemplate.queryForObject("SELECT IFNULL(MAX(id),0) FROM alert_rule WHERE tenant_id=?", Long.class, tenantId());
        return findRuleById(id == null ? 0L : id);
    }

    @Transactional(rollbackFor = Exception.class)
    public AlertRuleInfo updateRule(Long id, UpdateAlertRuleRequest request) {
        assertRuleExists(id);
        String status = normalizeStatus(request.getStatus());
        jdbcTemplate.update(
                "UPDATE alert_rule SET rule_code=?, rule_name=?, compare_op=?, threshold_value=?, window_minute=?, suppress_window_minute=?, status=?, updated_at=CURRENT_TIMESTAMP, updated_by=? " +
                        "WHERE tenant_id=? AND id=?",
                request.getRuleCode(),
                request.getRuleName(),
                request.getCompareOp(),
                request.getThresholdValue(),
                request.getWindowMinute(),
                normalizeSuppressMinute(request.getSuppressWindowMinute()),
                status,
                safeUserId(),
                tenantId(),
                id
        );
        return findRuleById(id);
    }

    public Map<String, Object> evaluateRules() {
        return evaluateRulesInternal(false, 10);
    }

    public Map<String, Object> evaluateRulesAuto(int recoveryWindowMinute) {
        int safeRecovery = recoveryWindowMinute <= 0 ? 10 : recoveryWindowMinute;
        return evaluateRulesInternal(true, safeRecovery);
    }

    private Map<String, Object> evaluateRulesInternal(boolean autoMode, int recoveryWindowMinute) {
        List<AlertRuleInfo> rules = listRules();
        int triggerCount = 0;
        int recoverCount = 0;
        for (AlertRuleInfo rule : rules) {
            if (!"ENABLED".equalsIgnoreCase(rule.getStatus())) {
                continue;
            }
            double current = calculateRuleValue(rule);
            Long activeId = latestActiveEventId(rule.getRuleCode());
            if (hit(rule.getCompareOp(), current, rule.getThresholdValue())) {
                clearRecoverCandidate(activeId);
                if (inSuppressWindow(rule.getRuleCode(), rule.getSuppressWindowMinute())) {
                    continue;
                }
                triggerCount++;
                jdbcTemplate.update(
                        "INSERT INTO alert_event (tenant_id, rule_code, metric_code, current_value, threshold_value, alert_level, status, message, created_by) VALUES (?, ?, ?, ?, ?, ?, 'ACTIVE', ?, ?)",
                        tenantId(),
                        rule.getRuleCode(),
                        metricCodeByRule(rule.getRuleCode()),
                        current,
                        rule.getThresholdValue(),
                        "WARN",
                        "触发告警规则: " + rule.getRuleName(),
                        safeUserId()
                );
            } else if (activeId != null) {
                if (markOrCheckRecoveryReady(activeId, recoveryWindowMinute)) {
                    recoverCount++;
                    jdbcTemplate.update(
                            "UPDATE alert_event SET status='RECOVERED', recovered_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=?",
                            tenantId(),
                            activeId
                    );
                }
            }
        }
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("ruleTotal", rules.size());
        data.put("triggerCount", triggerCount);
        data.put("recoverCount", recoverCount);
        data.put("autoMode", autoMode);
        data.put("recoveryWindowMinute", recoveryWindowMinute);
        return data;
    }

    public List<AlertEventInfo> listEvents() {
        return jdbcTemplate.query(
                "SELECT id, rule_code, metric_code, current_value, threshold_value, alert_level, status, DATE_FORMAT(created_at, '%Y-%m-%d %H:%i:%s') AS created_at, " +
                        "DATE_FORMAT(recovered_at, '%Y-%m-%d %H:%i:%s') AS recovered_at " +
                        "FROM alert_event WHERE tenant_id=? ORDER BY id DESC LIMIT 200",
                EVENT_ROW_MAPPER,
                tenantId()
        );
    }

    public Map<String, Object> exportMetricSummary(int minutes) {
        int safeMinutes = minutes <= 0 ? 60 : minutes;
        Double total = jdbcTemplate.queryForObject(
                "SELECT IFNULL(SUM(metric_value),0) FROM observability_metric WHERE tenant_id=? AND metric_code='EXPORT_TOTAL' AND created_at>=DATE_SUB(NOW(), INTERVAL ? MINUTE)",
                Double.class,
                tenantId(),
                safeMinutes
        );
        Double failed = jdbcTemplate.queryForObject(
                "SELECT IFNULL(SUM(metric_value),0) FROM observability_metric WHERE tenant_id=? AND metric_code='EXPORT_FAILED' AND created_at>=DATE_SUB(NOW(), INTERVAL ? MINUTE)",
                Double.class,
                tenantId(),
                safeMinutes
        );
        List<ObservabilityMetricPoint> points = queryMetric("EXPORT_TOTAL", safeMinutes);
        Map<String, Object> data = new LinkedHashMap<String, Object>();
        data.put("minutes", safeMinutes);
        data.put("total", total == null ? 0D : total);
        data.put("failed", failed == null ? 0D : failed);
        data.put("points", points);
        return data;
    }

    private void insertMetric(String metricCode, String labelKey, String labelValue, Double value) {
        jdbcTemplate.update(
                "INSERT INTO observability_metric (tenant_id, metric_code, label_key, label_value, metric_value, created_by) VALUES (?, ?, ?, ?, ?, ?)",
                tenantId(),
                metricCode,
                labelKey,
                labelValue,
                value,
                safeUserId()
        );
    }

    private double calculateRuleValue(AlertRuleInfo rule) {
        String code = rule.getRuleCode();
        int minutes = rule.getWindowMinute() == null || rule.getWindowMinute() <= 0 ? 5 : rule.getWindowMinute();
        if ("API_5XX_RATE".equalsIgnoreCase(code)) {
            Double total = jdbcTemplate.queryForObject(
                    "SELECT IFNULL(SUM(metric_value),0) FROM observability_metric WHERE tenant_id=? AND metric_code='API_TOTAL' AND created_at>=DATE_SUB(NOW(), INTERVAL ? MINUTE)",
                    Double.class,
                    tenantId(),
                    minutes
            );
            Double f5xx = jdbcTemplate.queryForObject(
                    "SELECT IFNULL(SUM(metric_value),0) FROM observability_metric WHERE tenant_id=? AND metric_code='API_TOTAL' AND label_key='status' AND label_value='5XX' AND created_at>=DATE_SUB(NOW(), INTERVAL ? MINUTE)",
                    Double.class,
                    tenantId(),
                    minutes
            );
            double all = total == null ? 0D : total;
            double bad = f5xx == null ? 0D : f5xx;
            return all == 0 ? 0D : (bad * 100D / all);
        }
        if ("ASYNC_BACKLOG".equalsIgnoreCase(code)) {
            Integer cnt = jdbcTemplate.queryForObject(
                    "SELECT COUNT(1) FROM async_job WHERE tenant_id=? AND status IN ('READY','RUNNING')",
                    Integer.class,
                    tenantId()
            );
            return cnt == null ? 0D : cnt.doubleValue();
        }
        if ("ASYNC_FAILED_RATE".equalsIgnoreCase(code)) {
            Double total = jdbcTemplate.queryForObject(
                    "SELECT IFNULL(SUM(metric_value),0) FROM observability_metric WHERE tenant_id=? AND metric_code='ASYNC_JOB_TOTAL' AND created_at>=DATE_SUB(NOW(), INTERVAL ? MINUTE)",
                    Double.class,
                    tenantId(),
                    minutes
            );
            Double failed = jdbcTemplate.queryForObject(
                    "SELECT IFNULL(SUM(metric_value),0) FROM observability_metric WHERE tenant_id=? AND metric_code='ASYNC_JOB_FAILED' AND created_at>=DATE_SUB(NOW(), INTERVAL ? MINUTE)",
                    Double.class,
                    tenantId(),
                    minutes
            );
            double all = total == null ? 0D : total;
            double bad = failed == null ? 0D : failed;
            return all == 0 ? 0D : (bad * 100D / all);
        }
        return 0D;
    }

    private boolean hit(String op, double current, Double threshold) {
        double th = threshold == null ? 0D : threshold;
        String compare = op == null ? ">=" : op.trim();
        if (">".equals(compare)) {
            return current > th;
        }
        if ("<".equals(compare)) {
            return current < th;
        }
        if ("<=".equals(compare)) {
            return current <= th;
        }
        return current >= th;
    }

    private String metricCodeByRule(String ruleCode) {
        if ("API_5XX_RATE".equalsIgnoreCase(ruleCode)) {
            return "API_TOTAL";
        }
        if ("ASYNC_BACKLOG".equalsIgnoreCase(ruleCode)) {
            return "ASYNC_BACKLOG";
        }
        if ("ASYNC_FAILED_RATE".equalsIgnoreCase(ruleCode)) {
            return "ASYNC_JOB_FAILED";
        }
        return "UNKNOWN";
    }

    private AlertRuleInfo findRuleById(Long id) {
        List<AlertRuleInfo> list = jdbcTemplate.query(
                "SELECT * FROM alert_rule WHERE tenant_id=? AND id=? AND is_deleted=0",
                RULE_ROW_MAPPER,
                tenantId(),
                id
        );
        if (list.isEmpty()) {
            throw new BusinessException(10003, "告警规则不存在");
        }
        return list.get(0);
    }

    private void assertRuleExists(Long id) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM alert_rule WHERE tenant_id=? AND id=? AND is_deleted=0",
                Integer.class,
                tenantId(),
                id
        );
        if (count == null || count == 0) {
            throw new BusinessException(10003, "告警规则不存在");
        }
    }

    private String normalizeStatus(String status) {
        String val = status == null ? "ENABLED" : status.trim().toUpperCase();
        if (!"ENABLED".equals(val) && !"DISABLED".equals(val)) {
            throw new BusinessException(10001, "状态仅支持 ENABLED/DISABLED");
        }
        return val;
    }

    private String normalize(String v) {
        return v == null ? "" : v.trim();
    }

    private int normalizeSuppressMinute(Integer m) {
        if (m == null || m < 0) {
            return 0;
        }
        return m;
    }

    private boolean inSuppressWindow(String ruleCode, Integer suppressWindowMinute) {
        int m = suppressWindowMinute == null ? 0 : suppressWindowMinute;
        if (m <= 0) {
            return false;
        }
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM alert_event WHERE tenant_id=? AND rule_code=? AND status='ACTIVE' AND created_at>=DATE_SUB(NOW(), INTERVAL ? MINUTE)",
                Integer.class,
                tenantId(),
                ruleCode,
                m
        );
        return count != null && count > 0;
    }

    private Long latestActiveEventId(String ruleCode) {
        List<Long> ids = jdbcTemplate.queryForList(
                "SELECT id FROM alert_event WHERE tenant_id=? AND rule_code=? AND status='ACTIVE' ORDER BY id DESC LIMIT 1",
                Long.class,
                tenantId(),
                ruleCode
        );
        return ids.isEmpty() ? null : ids.get(0);
    }

    private void clearRecoverCandidate(Long activeId) {
        if (activeId == null) {
            return;
        }
        jdbcTemplate.update(
                "UPDATE alert_event SET recover_candidate_at=NULL WHERE tenant_id=? AND id=? AND status='ACTIVE'",
                tenantId(),
                activeId
        );
    }

    private boolean markOrCheckRecoveryReady(Long activeId, int recoveryWindowMinute) {
        List<Map<String, Object>> list = jdbcTemplate.queryForList(
                "SELECT recover_candidate_at FROM alert_event WHERE tenant_id=? AND id=? AND status='ACTIVE' LIMIT 1",
                tenantId(),
                activeId
        );
        if (list.isEmpty()) {
            return false;
        }
        Object val = list.get(0).get("recover_candidate_at");
        if (val == null) {
            jdbcTemplate.update(
                    "UPDATE alert_event SET recover_candidate_at=CURRENT_TIMESTAMP WHERE tenant_id=? AND id=? AND status='ACTIVE'",
                    tenantId(),
                    activeId
            );
            return false;
        }
        Integer ready = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) FROM alert_event WHERE tenant_id=? AND id=? AND status='ACTIVE' AND recover_candidate_at IS NOT NULL AND recover_candidate_at<=DATE_SUB(NOW(), INTERVAL ? MINUTE)",
                Integer.class,
                tenantId(),
                activeId,
                recoveryWindowMinute
        );
        return ready != null && ready > 0;
    }

    private Long safeUserId() {
        Long userId = AuthContext.userId();
        return userId == null ? 0L : userId;
    }

    private Long tenantId() {
        return AuthContext.requireTenantId();
    }
}
