package ru.rock.getolt;

import jakarta.jws.WebService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ru.bitel.bgbilling.kernel.container.managed.ServerContext;
import ru.bitel.bgbilling.modules.inet.common.bean.InetServ;
import ru.bitel.bgbilling.modules.inet.common.bean.InetSessionLog;
import ru.bitel.bgbilling.modules.inet.common.service.InetServService;
import ru.bitel.bgbilling.modules.inet.common.service.InetSessionService;
import ru.bitel.bgbilling.modules.inet.server.InetUtils;
import ru.bitel.bgbilling.modules.inet.server.bean.InetServUtils;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.sql.ConnectionSet;

import java.sql.*;
import java.util.*;
import java.util.regex.Pattern;

// dynservice:ru.rock.getolt.GetOltServiceInterface=ru.rock.getolt.GetOltService

@WebService
interface GetOltServiceInterface {

    // === OltList ===
    List<Map<String, String>> GetOltList();

    // === InetMacList ===
    List<Map<String, String>> GetInetMacList();

    // === InetMac ===
    Map<String, Object> updateMacAddress(int serviceId, String newMac);
    Map<String, Object> getMacByServiceId(int serviceId);
    Map<String, Object> getServiceType(int serviceId);
    List<Map<String, Object>> getServicesByContractId(int contractId);
    Map<String, Object> updateMacByContractAndLogin(int contractId, String login, String newMac);
    Map<String, Object> updateMacByContractId(int contractId, String newMac);
    Map<String, Object> getLastSession(int serviceId);
    Map<String, Object> dropSession(int serviceId);
}

/**
 * Единый сервис GetOLT для BGBilling.
 * Объединяет функционал: список OLT, маппинг MAC→договор, управление MAC и сессиями.
 *
 * Настройки BGBilling (Setup):
 *   server.inet.module.id             — ID модуля inet (по умолчанию 1)
 *   server.address.param.id           — PID параметра адреса договора (по умолчанию 42)
 *   server.rock.getolt.mac.config.key — Ключ MAC ONU в inet_serv.config (для биллингов,
 *                                       где MAC хранится не в macAddress, а в доп. параметрах)
 *   server.rock.getolt.device.type.ids — CSV deviceTypeId, считающихся OLT (опц., фильтр GetOltList)
 */
@WebService(endpointInterface = "ru.rock.getolt.GetOltServiceInterface")
public class GetOltService implements GetOltServiceInterface {

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

    private static final Pattern MAC_PATTERN = Pattern.compile(
        "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$|^[0-9A-Fa-f]{12}$"
    );

    private static final String MAC_FROM_BINARY =
            "LOWER(CONCAT_WS(':', " +
            "LPAD(HEX(SUBSTRING(is2.macAddress, 1, 1)), 2, '0'), " +
            "LPAD(HEX(SUBSTRING(is2.macAddress, 2, 1)), 2, '0'), " +
            "LPAD(HEX(SUBSTRING(is2.macAddress, 3, 1)), 2, '0'), " +
            "LPAD(HEX(SUBSTRING(is2.macAddress, 4, 1)), 2, '0'), " +
            "LPAD(HEX(SUBSTRING(is2.macAddress, 5, 1)), 2, '0'), " +
            "LPAD(HEX(SUBSTRING(is2.macAddress, 6, 1)), 2, '0')))";

    private int getModuleId(Setup setup) {
        try {
            return Integer.parseInt(setup.get("server.inet.module.id"));
        } catch (Exception ex) {
            return 1;
        }
    }

    private boolean isValidMac(String mac) {
        return mac != null && MAC_PATTERN.matcher(mac).matches();
    }

    private String normalizeMac(String mac) {
        if (mac == null) return null;
        return mac.replaceAll("[:-]", "").toUpperCase();
    }

    private String formatMac(byte[] macBytes) {
        if (macBytes == null || macBytes.length == 0) return null;
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < macBytes.length; i++) {
            if (i > 0) sb.append(":");
            sb.append(String.format("%02X", macBytes[i] & 0xFF));
        }
        return sb.toString();
    }

    private byte[] getFirstMac(InetServ inetServ) {
        List<byte[]> macList = inetServ.getMacAddressList();
        if (macList != null && !macList.isEmpty()) {
            return macList.get(0);
        }
        return null;
    }

    private static List<Map<String, String>> getResp(String sql, ConnectionSet connectionSet) {
        if (connectionSet == null) {
            logger.error("ConnectionSet is null, cannot execute query");
            return new ArrayList<>();
        }
        Connection con = connectionSet.getConnection();
        List<Map<String, String>> results = new ArrayList<>();
        try (Statement stmt = con.createStatement()) {
            boolean isResultSet = stmt.execute(sql);
            if (isResultSet) {
                try (ResultSet rs = stmt.getResultSet()) {
                    ResultSetMetaData metaData = rs.getMetaData();
                    int columnCount = metaData.getColumnCount();
                    while (rs.next()) {
                        Map<String, String> row = new HashMap<>();
                        for (int i = 1; i <= columnCount; i++) {
                            row.put(metaData.getColumnLabel(i), rs.getString(i));
                        }
                        results.add(row);
                    }
                }
            }
        } catch (SQLException e) {
            logger.info("Ошибка выполнения SQL-запроса: {} e -> {}", sql, e.getMessage());
        }
        return results;
    }

    private static String macFromConfig(String configKey) {
        return String.format(
            "CASE WHEN is2.config LIKE '%%%1$s=%%' " +
            "THEN LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT('\\n', is2.config), '\\n%1$s=', -1), '\\n', 1)) " +
            "ELSE NULL END", configKey);
    }

    // ========================= OltList =========================

    /**
     * Оставляет только цифры и запятые. Нужно, чтобы безопасно подставить
     * CSV-список deviceTypeId из настроек в SQL IN (...).
     */
    private static String sanitizeIntCsv(String csv) {
        if (csv == null) return "";
        return csv.replaceAll("[^0-9,]", "");
    }

    @Override
    public List<Map<String, String>> GetOltList() {
        ServerContext serverContext = ServerContext.get();
        Setup setup = serverContext.getSetup();
        int moduleId = getModuleId(setup);

        // Опциональный фильтр по типам устройств (CSV, напр. "14" или "14,25").
        // Если пусто — фильтр по типу не применяется.
        String deviceTypesCsv = sanitizeIntCsv(setup.get("server.rock.getolt.device.type.ids", ""));
        String typeFilter = deviceTypesCsv.isEmpty()
                ? ""
                : "AND id.deviceTypeId IN (" + deviceTypesCsv + ")";

        ConnectionSet cs = serverContext.getConnectionSet();
        return getResp(String.format(
                """
                SELECT idt.id,
                       idt.invDeviceId,
                       idt.parentId,
                       idt.title,
                       COALESCE(NULLIF(idt.host, ''), id.host) AS host
                FROM inet_device_tree_%1$d idt
                LEFT JOIN inv_device_%1$d id ON id.id = idt.invDeviceId
                WHERE ((idt.host IS NOT NULL AND idt.host <> '')
                    OR (id.host IS NOT NULL AND id.host <> ''))
                  AND (id.dateTo IS NULL OR id.id IS NULL)
                  %2$s
                """, moduleId, typeFilter), cs);
    }

    // ======================= InetMacList =======================

    @Override
    public List<Map<String, String>> GetInetMacList() {
        ServerContext serverContext = ServerContext.get();
        Setup setup = serverContext.getSetup();

        int moduleId = getModuleId(setup);

        int addressPid;
        try {
            addressPid = Integer.parseInt(setup.get("server.address.param.id", "42"));
        } catch (Exception ex) {
            addressPid = 42;
        }

        String macConfigKey = setup.get("server.rock.getolt.mac.config.key", null);

        String macExpr;
        String whereClause;
        if (macConfigKey != null && !macConfigKey.isEmpty()) {
            macExpr = "COALESCE(" + macFromConfig(macConfigKey) + ", " +
                    "NULLIF(" + MAC_FROM_BINARY + ", '00:00:00:00:00:00'))";
            whereClause = "WHERE (" +
                    "(is2.macAddress IS NOT NULL AND LENGTH(is2.macAddress) = 6) " +
                    "OR is2.config LIKE '%" + macConfigKey + "=%')";
        } else {
            macExpr = MAC_FROM_BINARY;
            whereClause = "WHERE is2.macAddress IS NOT NULL AND LENGTH(is2.macAddress) = 6";
        }

        ConnectionSet cs = serverContext.getConnectionSet();
        return getResp(String.format(
                """
                SELECT
                    is2.id AS serviceId,
                    is2.contractId,
                    c.title AS numDogovor,
                    %s AS mac,
                    conn.callingStationId AS macTo,
                    is2.login,
                    c.comment AS abonFIO,
                    cpt.address AS abonAddr
                FROM inet_serv_%d is2
                LEFT JOIN contract c ON c.id = is2.contractId
                LEFT JOIN contract_parameter_type_2 cpt ON cpt.cid = is2.contractId AND cpt.pid = %d
                LEFT JOIN (
                    SELECT ic.servId, ic.callingStationId
                    FROM inet_connection_%d ic
                    GROUP BY ic.callingStationId
                ) AS conn ON conn.servId = is2.id
                %s
                """, macExpr, moduleId, addressPid, moduleId, whereClause), cs);
    }

    // ========================= InetMac =========================

    @Override
    public Map<String, Object> updateMacAddress(int serviceId, String newMac) {
        Map<String, Object> result = new HashMap<>();
        logger.info("updateMacAddress: serviceId={}, newMac={}", serviceId, newMac);

        try {
            if (!isValidMac(newMac)) {
                result.put("status", "error");
                result.put("message", "Невалидный формат MAC. Используйте XX:XX:XX:XX:XX:XX");
                return result;
            }

            ServerContext serverContext = ServerContext.get();
            Setup setup = serverContext.getSetup();
            int moduleId = getModuleId(setup);
            ConnectionSet conSet = serverContext.getConnectionSet();

            InetServService inetServService = serverContext.getService(InetServService.class, moduleId);
            InetServ inetServ = inetServService.inetServGet(serviceId);

            if (inetServ == null) {
                result.put("status", "error");
                result.put("message", "Услуга не найдена");
                return result;
            }

            String normalizedMac = normalizeMac(newMac);
            byte[] macBytes = InetUtils.parseMacAddress(normalizedMac);

            if (macBytes == null || macBytes.length != 6) {
                result.put("status", "error");
                result.put("message", "Ошибка парсинга MAC");
                return result;
            }

            String oldMac = formatMac(getFirstMac(inetServ));

            inetServ.setComment("MAC changed: " + oldMac + " -> " + formatMac(macBytes));
            InetServUtils.updateMacAddress(conSet, moduleId, inetServ, macBytes, false);
            inetServService.inetServUpdate(inetServ, null, false, false, 0);

            logger.info("updateMacAddress OK: serviceId={}, {} -> {}", serviceId, oldMac, formatMac(macBytes));

            result.put("status", "ok");
            result.put("serviceId", serviceId);
            result.put("oldMac", oldMac);
            result.put("newMac", formatMac(macBytes));

        } catch (Exception e) {
            logger.error("updateMacAddress error: serviceId=" + serviceId, e);
            result.put("status", "error");
            result.put("message", e.getMessage());
        }
        return result;
    }

    @Override
    public Map<String, Object> getMacByServiceId(int serviceId) {
        Map<String, Object> result = new HashMap<>();
        logger.info("getMacByServiceId: serviceId={}", serviceId);

        try {
            ServerContext serverContext = ServerContext.get();
            int moduleId = getModuleId(serverContext.getSetup());

            InetServService inetServService = serverContext.getService(InetServService.class, moduleId);
            InetServ inetServ = inetServService.inetServGet(serviceId);

            if (inetServ == null) {
                result.put("status", "error");
                result.put("message", "Услуга не найдена");
                return result;
            }

            result.put("status", "ok");
            result.put("serviceId", serviceId);
            result.put("mac", formatMac(getFirstMac(inetServ)));
            result.put("login", inetServ.getLogin());
            result.put("contractId", inetServ.getContractId());

        } catch (Exception e) {
            logger.error("getMacByServiceId error", e);
            result.put("status", "error");
            result.put("message", e.getMessage());
        }
        return result;
    }

    @Override
    public Map<String, Object> getServiceType(int serviceId) {
        Map<String, Object> result = new HashMap<>();
        logger.info("getServiceType: serviceId={}", serviceId);

        try {
            ServerContext serverContext = ServerContext.get();
            int moduleId = getModuleId(serverContext.getSetup());

            InetServService inetServService = serverContext.getService(InetServService.class, moduleId);
            InetServ inetServ = inetServService.inetServGet(serviceId);

            if (inetServ == null) {
                result.put("status", "error");
                result.put("message", "Услуга не найдена");
                return result;
            }

            String typeTitle = inetServ.getTypeTitle();
            String connectionType = "Unknown";
            if (typeTitle != null) {
                if (typeTitle.contains("IPoE")) connectionType = "IPoE";
                else if (typeTitle.contains("PPPoE")) connectionType = "PPPoE";
            }

            result.put("status", "ok");
            result.put("serviceId", serviceId);
            result.put("typeTitle", typeTitle);
            result.put("connectionType", connectionType);
            result.put("typeId", inetServ.getTypeId());

        } catch (Exception e) {
            logger.error("getServiceType error", e);
            result.put("status", "error");
            result.put("message", e.getMessage());
        }
        return result;
    }

    @Override
    public List<Map<String, Object>> getServicesByContractId(int contractId) {
        List<Map<String, Object>> results = new ArrayList<>();
        logger.info("getServicesByContractId: contractId={}", contractId);

        try {
            ServerContext serverContext = ServerContext.get();
            int moduleId = getModuleId(serverContext.getSetup());

            InetServService inetServService = serverContext.getService(InetServService.class, moduleId);
            List<InetServ> servList = inetServService.inetServList(contractId, "");

            if (servList == null) return results;

            for (InetServ serv : servList) {
                Map<String, Object> item = new HashMap<>();
                item.put("serviceId", serv.getId());
                item.put("login", serv.getLogin());
                item.put("mac", formatMac(getFirstMac(serv)));
                item.put("typeTitle", serv.getTypeTitle());
                item.put("typeId", serv.getTypeId());
                item.put("status", serv.getStatus() != null ? serv.getStatus().getCode() : null);
                item.put("statusTitle", serv.getStatus() != null ? serv.getStatus().getTitle() : null);
                item.put("dateFrom", serv.getDateFrom());
                item.put("dateTo", serv.getDateTo());
                item.put("deviceState", serv.getDeviceState());

                String typeTitle = serv.getTypeTitle();
                String connectionType = "Unknown";
                if (typeTitle != null) {
                    if (typeTitle.contains("IPoE")) connectionType = "IPoE";
                    else if (typeTitle.contains("PPPoE")) connectionType = "PPPoE";
                }
                item.put("connectionType", connectionType);

                results.add(item);
            }
        } catch (Exception e) {
            logger.error("getServicesByContractId error", e);
        }
        return results;
    }

    @Override
    public Map<String, Object> updateMacByContractAndLogin(int contractId, String login, String newMac) {
        Map<String, Object> result = new HashMap<>();
        logger.info("updateMacByContractAndLogin: cid={}, login={}, mac={}", contractId, login, newMac);

        try {
            if (!isValidMac(newMac)) {
                result.put("status", "error");
                result.put("message", "Невалидный формат MAC");
                return result;
            }

            ServerContext serverContext = ServerContext.get();
            int moduleId = getModuleId(serverContext.getSetup());

            InetServService inetServService = serverContext.getService(InetServService.class, moduleId);
            List<InetServ> servList = inetServService.inetServList(contractId, "");

            if (servList == null || servList.isEmpty()) {
                result.put("status", "error");
                result.put("message", "Услуги не найдены");
                return result;
            }

            InetServ targetServ = null;
            for (InetServ serv : servList) {
                if (login.equals(serv.getLogin())) {
                    targetServ = serv;
                    break;
                }
            }

            if (targetServ == null) {
                result.put("status", "error");
                result.put("message", "Услуга с логином '" + login + "' не найдена");
                return result;
            }

            return updateMacAddress(targetServ.getId(), newMac);

        } catch (Exception e) {
            logger.error("updateMacByContractAndLogin error", e);
            result.put("status", "error");
            result.put("message", e.getMessage());
        }
        return result;
    }

    @Override
    public Map<String, Object> updateMacByContractId(int contractId, String newMac) {
        Map<String, Object> result = new HashMap<>();
        logger.info("updateMacByContractId: cid={}, mac={}", contractId, newMac);

        try {
            if (!isValidMac(newMac)) {
                result.put("status", "error");
                result.put("message", "Невалидный формат MAC");
                return result;
            }

            ServerContext serverContext = ServerContext.get();
            int moduleId = getModuleId(serverContext.getSetup());

            InetServService inetServService = serverContext.getService(InetServService.class, moduleId);
            List<InetServ> servList = inetServService.inetServList(contractId, "");

            if (servList == null || servList.isEmpty()) {
                result.put("status", "error");
                result.put("message", "Услуги не найдены");
                return result;
            }

            List<InetServ> activeServices = new ArrayList<>();
            for (InetServ serv : servList) {
                if (serv.getStatus() != null && serv.getStatus().getCode() == 0) {
                    activeServices.add(serv);
                }
            }

            if (activeServices.isEmpty()) {
                result.put("status", "error");
                result.put("message", "Нет активных услуг");
                return result;
            }

            if (activeServices.size() == 1) {
                return updateMacAddress(activeServices.get(0).getId(), newMac);
            }

            result.put("status", "multiple_services");
            result.put("message", "Найдено " + activeServices.size() + " активных услуг. Укажите serviceId.");
            List<Map<String, Object>> servicesList = new ArrayList<>();
            for (InetServ serv : activeServices) {
                Map<String, Object> item = new HashMap<>();
                item.put("serviceId", serv.getId());
                item.put("login", serv.getLogin());
                item.put("mac", formatMac(getFirstMac(serv)));
                item.put("typeTitle", serv.getTypeTitle());
                servicesList.add(item);
            }
            result.put("services", servicesList);
            return result;

        } catch (Exception e) {
            logger.error("updateMacByContractId error", e);
            result.put("status", "error");
            result.put("message", e.getMessage());
        }
        return result;
    }

    @Override
    public Map<String, Object> getLastSession(int serviceId) {
        Map<String, Object> result = new HashMap<>();
        logger.info("getLastSession: serviceId={}", serviceId);

        try {
            ServerContext serverContext = ServerContext.get();
            int moduleId = getModuleId(serverContext.getSetup());

            InetServService inetServService = serverContext.getService(InetServService.class, moduleId);
            InetServ inetServ = inetServService.inetServGet(serviceId);

            if (inetServ == null) {
                result.put("status", "error");
                result.put("message", "Услуга не найдена");
                return result;
            }

            InetSessionService inetSessionService = serverContext.getService(InetSessionService.class, moduleId);
            Set<Integer> servIds = new HashSet<>();
            servIds.add(serviceId);

            List<InetSessionLog> sessionList = inetSessionService.inetSessionAliveList(
                new HashSet<>(), servIds,
                "", "", "", "",
                null, null, null
            ).getList();

            result.put("status", "ok");
            result.put("serviceId", serviceId);
            result.put("login", inetServ.getLogin());

            if (sessionList != null && !sessionList.isEmpty()) {
                InetSessionLog session = sessionList.get(0);
                result.put("sessionActive", true);
                result.put("sessionId", session.getId());
                result.put("sessionStart", session.getSessionStart());
                result.put("ipAddress", session.getCallingStationId());
                result.put("deviceId", session.getDeviceId());
                result.put("deviceState", inetServ.getDeviceState());
                logger.info("getLastSession: active session found for serviceId={}", serviceId);
            } else {
                result.put("sessionActive", false);
                result.put("deviceState", inetServ.getDeviceState());
                logger.info("getLastSession: no active session for serviceId={}", serviceId);
            }

        } catch (Exception e) {
            logger.error("getLastSession error", e);
            result.put("status", "error");
            result.put("message", e.getMessage());
        }
        return result;
    }

    @Override
    public Map<String, Object> dropSession(int serviceId) {
        Map<String, Object> result = new HashMap<>();
        logger.info("dropSession: serviceId={}", serviceId);

        try {
            ServerContext serverContext = ServerContext.get();
            int moduleId = getModuleId(serverContext.getSetup());

            InetSessionService inetSessionService = serverContext.getService(InetSessionService.class, moduleId);
            Set<Integer> servIds = new HashSet<>();
            servIds.add(serviceId);

            List<InetSessionLog> sessionList = inetSessionService.inetSessionAliveList(
                new HashSet<>(), servIds,
                "", "", "", "",
                null, null, null
            ).getList();

            if (sessionList == null || sessionList.isEmpty()) {
                result.put("status", "no_session");
                result.put("message", "Нет активной сессии для сброса");
                return result;
            }

            int droppedCount = 0;
            for (InetSessionLog session : sessionList) {
                try {
                    inetSessionService.connectionClose(session.getConnectionId());
                    droppedCount++;
                    logger.info("dropSession: closed connectionId={}", session.getConnectionId());
                } catch (Exception e) {
                    logger.error("dropSession: failed to close connectionId=" + session.getConnectionId(), e);
                }
            }

            result.put("status", "ok");
            result.put("serviceId", serviceId);
            result.put("droppedSessions", droppedCount);
            result.put("message", "Сброшено сессий: " + droppedCount);

        } catch (Exception e) {
            logger.error("dropSession error", e);
            result.put("status", "error");
            result.put("message", e.getMessage());
        }
        return result;
    }
}
