package com.njcn.gather.tool.adddata.component;

import com.njcn.gather.tool.adddata.config.AddDataInfluxDbProperties;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataBatchWriteResult;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataInfluxFieldGroup;
import com.njcn.gather.tool.adddata.pojo.bo.AddDataTableDefinition;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * add-data InfluxDB 批量写入组件。
 */
@Component
@RequiredArgsConstructor
public class AddDataInfluxWriter {

    /** InfluxDB 配置。 */
    private final AddDataInfluxDbProperties properties;

    /** 字段映射器。 */
    private final AddDataInfluxFieldMapper fieldMapper;

    /**
     * 写入一个批次。
     *
     * @param definition 表定义
     * @param rows 行数据
     * @return 写入结果
     */
    public AddDataBatchWriteResult writeBatch(AddDataTableDefinition definition, List<List<Object>> rows) {
        if (rows == null || rows.isEmpty()) {
            return new AddDataBatchWriteResult(0L, 0L, 0L, null);
        }
        List<String> lines = new ArrayList<String>();
        try {
            validateConfig();
            for (List<Object> row : rows) {
                lines.addAll(buildRowLineProtocols(definition, row));
            }
            if (lines.isEmpty()) {
                return new AddDataBatchWriteResult(0L, 0L, 0L, null);
            }
            executeWrite(String.join("\n", lines));
            return new AddDataBatchWriteResult(lines.size(), 0L, 0L, null);
        } catch (RuntimeException ex) {
            long failedCount = lines.isEmpty() ? rows.size() : lines.size();
            return new AddDataBatchWriteResult(0L, 0L, failedCount, ex.getMessage());
        }
    }

    /**
     * 构建单行 MySQL 结构对应的 InfluxDB line protocol。
     *
     * @param definition 表定义
     * @param row 行数据
     * @return line protocol 列表
     */
    private List<String> buildRowLineProtocols(AddDataTableDefinition definition, List<Object> row) {
        LocalDateTime timeId = fieldMapper.resolveTimeId(definition.getColumns(), row);
        String lineId = fieldMapper.resolveString(definition.getColumns(), row, "LINEID");
        String phasicType = fieldMapper.resolveString(definition.getColumns(), row, "PHASIC_TYPE");
        String qualityFlag = fieldMapper.resolveString(definition.getColumns(), row, "QUALITYFLAG");
        List<AddDataInfluxFieldGroup> groups = fieldMapper.mapFieldGroups(definition.getTableName(), definition.getColumns(), row);
        List<String> result = new ArrayList<String>();
        for (AddDataInfluxFieldGroup group : groups) {
            Map<String, String> tags = new LinkedHashMap<String, String>();
            tags.put("line_id", lineId);
            tags.put("phasic_type", phasicType);
            tags.put("quality_flag", qualityFlag);
            if (group.getValueType() != null) {
                tags.put("value_type", group.getValueType());
            }
            result.add(buildLineProtocol(definition.getTableName(), tags, group.getFields(), timeId));
        }
        return result;
    }

    /**
     * 构建 InfluxDB line protocol。
     *
     * @param measurement measurement 名称
     * @param tags tag 集合
     * @param fields field 集合
     * @param timeId 时间
     * @return line protocol
     */
    String buildLineProtocol(String measurement, Map<String, String> tags, Map<String, Object> fields, LocalDateTime timeId) {
        StringBuilder line = new StringBuilder(escapeMeasurement(measurement));
        for (Map.Entry<String, String> tag : tags.entrySet()) {
            line.append(",").append(escapeKey(tag.getKey())).append("=").append(escapeKey(tag.getValue()));
        }
        line.append(" ");
        boolean first = true;
        for (Map.Entry<String, Object> field : fields.entrySet()) {
            if (!first) {
                line.append(",");
            }
            line.append(escapeKey(field.getKey())).append("=").append(formatFieldValue(field.getValue()));
            first = false;
        }
        line.append(" ").append(toEpochNanos(timeId));
        return line.toString();
    }

    private void executeWrite(String body) {
        HttpURLConnection connection = null;
        try {
            URL url = new URL(buildWriteUrl());
            connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setConnectTimeout(properties.getConnectTimeoutMs());
            connection.setReadTimeout(properties.getReadTimeoutMs());
            connection.setDoOutput(true);
            byte[] payload = body.getBytes(StandardCharsets.UTF_8);
            connection.setFixedLengthStreamingMode(payload.length);
            try (OutputStream outputStream = connection.getOutputStream()) {
                outputStream.write(payload);
            }
            int status = connection.getResponseCode();
            if (status < 200 || status >= 300) {
                String errorBody = readBody(connection.getErrorStream());
                throw new IllegalStateException("InfluxDB 写入失败：" + errorBody);
            }
        } catch (IOException ex) {
            throw new IllegalStateException("InfluxDB 写入异常：" + ex.getMessage(), ex);
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }

    private String buildWriteUrl() throws IOException {
        StringBuilder url = new StringBuilder(trimRightSlash(properties.getUrl())).append("/write?");
        url.append("db=").append(encode(properties.getDatabase()));
        if (properties.getUsername() != null && !properties.getUsername().trim().isEmpty()) {
            url.append("&u=").append(encode(properties.getUsername().trim()));
        }
        if (properties.getPassword() != null && !properties.getPassword().trim().isEmpty()) {
            url.append("&p=").append(encode(properties.getPassword()));
        }
        return url.toString();
    }

    private void validateConfig() {
        if (properties.getUrl() == null || properties.getUrl().trim().isEmpty()) {
            throw new IllegalStateException("add-data InfluxDB 地址未配置");
        }
        if (properties.getDatabase() == null || properties.getDatabase().trim().isEmpty()) {
            throw new IllegalStateException("add-data InfluxDB database 未配置");
        }
    }

    private String readBody(InputStream stream) throws IOException {
        if (stream == null) {
            return "";
        }
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
        StringBuilder body = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            body.append(line);
        }
        return body.toString();
    }

    private String trimRightSlash(String value) {
        String text = value.trim();
        while (text.endsWith("/")) {
            text = text.substring(0, text.length() - 1);
        }
        return text;
    }

    private String encode(String value) throws IOException {
        return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
    }

    private String escapeMeasurement(String value) {
        return value.replace(",", "\\,").replace(" ", "\\ ");
    }

    private String escapeKey(String value) {
        return value.replace(",", "\\,").replace(" ", "\\ ").replace("=", "\\=");
    }

    private String formatFieldValue(Object value) {
        if (value instanceof Number) {
            return String.valueOf(value);
        }
        return "\"" + String.valueOf(value).replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
    }

    private long toEpochNanos(LocalDateTime timeId) {
        return timeId.toInstant(ZoneOffset.UTC).toEpochMilli() * 1000000L;
    }
}
