Загрузка данных


package org.example.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
@ConfigurationProperties(prefix = "app.folders")

public class AppConfig {
    private String profileAnalyst;
    private String script;
    private String lastProfile;
    private String output;
}

package org.example.controller;

import org.example.service.GeneratorService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

@Slf4j
@RestController
@RequestMapping("/api/generator")
@CrossOrigin(origins = "*")
public class GeneratorController {

    @Autowired
    private GeneratorService generatorService;

    private final AtomicBoolean isGenerating = new AtomicBoolean(false);

    @PostMapping("/generate")
    public Map<String, String> generateProfile(@RequestBody Map<String, String> request) {
        String projectNumber = request.get("projectNumber");

        if (projectNumber == null || projectNumber.isEmpty()) {
            return Map.of("error", "projectNumber не указан");
        }

        if (isGenerating.get()) {
            return Map.of("error", "Генерация уже выполняется");
        }

        String projectType = convertProjectNumberToType(projectNumber);
        if (projectType == null) {
            return Map.of("error", "Неверный номер проекта");
        }

        log.info("Запуск генерации для проекта: {}", projectType);

        CompletableFuture.runAsync(() -> {
            try {
                isGenerating.set(true);
                generatorService.generate(projectType);  // вызываем твой сервис
                log.info("Генерация завершена");
            } catch (Exception e) {
                log.error("Ошибка: {}", e.getMessage());
            } finally {
                isGenerating.set(false);
            }
        });

        return Map.of("status", "success", "message", "Генерация запущена");
    }

    @GetMapping("/status")
    public Map<String, Object> getStatus() {
        return Map.of("isGenerating", isGenerating.get());
    }

    private String convertProjectNumberToType(String projectNumber) {
        switch (projectNumber) {
            case "1": return "WEB";
            case "2": return "MOBILE";
            case "3": return "PPRB";
            default: return null;
        }
    }
}

package org.example.model;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class CsvRow {
    private String name;
    private String uri;
    private String method;
    private int intensity;
    private int sla;
    private String transactionName;
    private String requirement;
    private String comment;
}

package org.example.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OldProfileData {
    private String fullService;  // полный "Вызываемый сервис" (метод + URI)
    private int intensity;
    private int sla;
}

package org.example.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProfileData {
    private int intensity;
    private int sla;
}

package org.example.model;

import lombok.Data;

@Data
public class ProfileEntry {
    private String uri;
    private String method;
    private int intensity;
}


package org.example.model;

import lombok.Data;
import java.util.List;

@Data
public class ScriptOperation {
    private String operation_id;
    private String url;
    private String method;
    private Integer intensity;
    private List<ScriptOperation> child_operations;
}

package org.example.service;

import org.example.model.CsvRow;
import org.example.model.OldProfileData;
import org.example.model.ProfileEntry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors; // ИЗМЕНЕНО: добавлен импорт Collectors

@Slf4j
@Service
public class CsvGeneratorService {

    private static final int DEFAULT_SLA = 3;
    private static final String STATIC_URL = "/ic/ufs/corp-retail/{{version}}/js/app/corp-retail-mf.js";
    private static final String STATIC_OPERATION_ID = "UC00_StaticJS_1_WEB";
    private static final String GET_CONTACTS_OPERATION_ID = "UC29_GET_contacts_WEB";
    private static final int STATIC_INTENSITY_DEFAULT = 300000;
    private static final int STATIC_SLA_DEFAULT = 1;

    @Autowired
    private UriNormalizer uriNormalizer;

    private String urlPrefix;
    private String projectType;

    private Set<String> lastExtraInScript = new HashSet<>();

    public Set<String> getLastExtraInScript() {
        return lastExtraInScript;
    }

    public List<CsvRow> generateProfile(Map<String, List<String>> urlToOpIds,
                                        List<ProfileEntry> profileEntries,
                                        Map<String, OldProfileData> lastProfileData,
                                        String outputFolder,
                                        String outputFilename,
                                        String requirement,
                                        String urlPrefix,
                                        String projectType) {

        this.urlPrefix = urlPrefix;
        this.projectType = projectType;

        List<CsvRow> rowsForCsv = new ArrayList<>();      // для CSV (intensity > 0)
        List<CsvRow> allRowsForExcel = new ArrayList<>(); // для Excel (все операции)

        // ========== 1. Статический запрос (ТОЛЬКО ДЛЯ WEB) ==========
        if ("WEB".equals(projectType)) {
            int staticIntensity = STATIC_INTENSITY_DEFAULT;
            int staticSla = STATIC_SLA_DEFAULT;

            if (lastProfileData.containsKey(STATIC_OPERATION_ID)) {
                OldProfileData data = lastProfileData.get(STATIC_OPERATION_ID);
                staticIntensity = data.getIntensity();
                staticSla = data.getSla();
                log.info("   Найдена в прошлом профиле {}: интенсивность = {}, SLA = {} сек",
                        STATIC_OPERATION_ID, staticIntensity, staticSla);
            } else {
                log.info("  ⚠️ Не найдена в прошлом профиле {}, используем значения по умолчанию: инт = {}, SLA = {} сек",
                        STATIC_OPERATION_ID, staticIntensity, staticSla);
            }

            CsvRow staticRow = new CsvRow(
                    STATIC_OPERATION_ID,
                    STATIC_URL,
                    "GET",
                    staticIntensity,
                    staticSla,
                    STATIC_OPERATION_ID,
                    requirement,
                    ""
            );

            rowsForCsv.add(staticRow);
            allRowsForExcel.add(staticRow);

            log.info("   Добавлен статический запрос: {} (инт: {}, SLA: {})",
                    STATIC_OPERATION_ID, staticIntensity, staticSla);
        } else if ("MOBILE".equals(projectType)) {
            log.debug("   Статический запрос не добавляется для проекта: {}", projectType);
        } else if ("PPRB".equals(projectType)) {
            log.debug("   Статический запрос не добавляется для проекта: {}", projectType);
        }

        // ========== 2. Собираем URI из профиля аналитика ==========
        Map<String, ProfileEntry> profileEntryMap = new HashMap<>();
        for (ProfileEntry entry : profileEntries) {
            if (entry.getIntensity() > 0) {
                String key;
                if ("PPRB".equals(projectType)) {
                    String normalizedUri = uriNormalizer.normalizeUri(entry.getUri(), projectType);
                    key = entry.getMethod() + "|" + normalizedUri;
                } else {
                    key = entry.getMethod() + "|" + entry.getUri();
                }
                profileEntryMap.put(key, entry);
            }
        }

        // ========== 3. Исключаем статический URL из проверок (только для Web) ==========
        int totalScriptOperations = 0;
        for (List<String> opIds : urlToOpIds.values()) {
            totalScriptOperations += opIds.size();
        }

        Map<String, List<String>> filteredUrlToOpIds = new HashMap<>();
        boolean removedStatic = false;

        for (Map.Entry<String, List<String>> entry : urlToOpIds.entrySet()) {
            if ("WEB".equals(projectType) && entry.getKey().equals(STATIC_URL)) {
                removedStatic = true;
                continue;
            }
            filteredUrlToOpIds.put(entry.getKey(), entry.getValue());
        }

        int scriptOperationsAfterRemoval = 0;
        for (List<String> opIds : filteredUrlToOpIds.values()) {
            scriptOperationsAfterRemoval += opIds.size();
        }

        log.info(" Всего операций в скрипте: {}", totalScriptOperations);
        if (removedStatic) {
            log.info("   Удален статический запрос: {}", STATIC_URL);
            log.info(" Скрипт после исключения статики: {} операций", scriptOperationsAfterRemoval);
        } else {
            log.info(" Скрипт: {} операций", scriptOperationsAfterRemoval);
        }

        // ========== 4. Находим операции, которые есть в скрипте, но НЕТ в профиле ==========
        Set<String> scriptKeys = new HashSet<>();
        for (Map.Entry<String, List<String>> scriptEntry : filteredUrlToOpIds.entrySet()) {
            String scriptUri = scriptEntry.getKey();
            for (String opId : scriptEntry.getValue()) {
                String methodFromOpId = extractMethodFromOperationId(opId);
                String key;
                if ("PPRB".equals(projectType)) {
                    String normalizedUri = uriNormalizer.normalizeUri(scriptUri, projectType);
                    key = methodFromOpId + "|" + normalizedUri;
                } else {
                    key = methodFromOpId + "|" + scriptUri;
                }
                scriptKeys.add(key);
            }
        }

        Set<String> profileKeys = new HashSet<>();
        for (ProfileEntry entry : profileEntries) {
            if (entry.getIntensity() > 0) {
                String key;
                if ("PPRB".equals(projectType)) {
                    String normalizedUri = uriNormalizer.normalizeUri(entry.getUri(), projectType);
                    key = entry.getMethod() + "|" + normalizedUri;
                } else {
                    key = entry.getMethod() + "|" + entry.getUri();
                }
                profileKeys.add(key);
            }
        }

        Set<String> extraInScript = new HashSet<>(scriptKeys);
        extraInScript.removeAll(profileKeys);

        this.lastExtraInScript = extraInScript;

        if (!extraInScript.isEmpty()) {
            log.info("\n⚠️ ВНИМАНИЕ! Операции, которые есть в скрипте, но ОТСУТСТВУЮТ в профиле аналитика:");
            log.info("   (нужно удалить из скрипта)");
            log.info("========================================");
            for (String key : extraInScript) {
                String[] parts = key.split("\\|", 2);
                String method = parts[0];
                String uri = parts[1];
                List<String> opIds = filteredUrlToOpIds.get(uri);
                log.info("   {} {} → {}", method, uri, opIds);
            }
            log.info("========================================");
            log.info("  Всего лишних операций в скрипте: {}\n", extraInScript.size());
        } else {
            log.info("\n Все операции из скрипта присутствуют в профиле аналитика\n");
        }

        // ========== 5. Находим операции, которые есть в профиле, но НЕТ в скрипте ==========
        Set<String> extraInProfile = new HashSet<>(profileKeys);
        extraInProfile.removeAll(scriptKeys);

        if (!extraInProfile.isEmpty()) {
            log.info("\n СПИСОК: Операции, которые есть в профиле аналитика, но ОТСУТСТВУЮТ в скрипте:");
            log.info("   (будут созданы как новые запросы)");
            log.info("========================================");
            for (String key : extraInProfile) {
                String[] parts = key.split("\\|", 2);
                String method = parts[0];
                String uri = parts[1];
                log.info("   {} {} (интенсивность: {})", method, uri,
                        profileEntryMap.get(key).getIntensity());
            }
            log.info("========================================");
            log.info("  Всего новых операций (нужно создать): {}\n", extraInProfile.size());
        } else {
            log.info("\n Все операции из профиля аналитика присутствуют в скрипте\n");
        }

        log.debug("\n=== Сравнение с нормализацией ===");
        for (ProfileEntry entry : profileEntries) {
            if (entry.getIntensity() == 0) continue;

            String normalizedProfileUri = uriNormalizer.normalizeUri(entry.getUri(), projectType);
            boolean found = false;
            String matchedScriptUri = null;

            for (String scriptUri : filteredUrlToOpIds.keySet()) {
                String normalizedScriptUri = uriNormalizer.normalizeUri(scriptUri, projectType);
                if (normalizedScriptUri.equals(normalizedProfileUri)) {
                    found = true;
                    matchedScriptUri = scriptUri;
                    break;
                }
            }

            if (!found) {
                log.warn("   НЕ СОВПАДАЕТ: Профиль='{}' -> нормализовано='{}'",
                        entry.getUri(), normalizedProfileUri);
            }
            else {
                log.debug("   СОВПАДАЕТ: Профиль='{}' -> Скрипт='{}'", entry.getUri(), matchedScriptUri);
            }
        }
        log.info("========================================\n");

        // ========== 6. Обрабатываем операции из профиля аналитика ==========
        Set<String> usedOpIds = new HashSet<>();
        for (List<String> opIds : filteredUrlToOpIds.values()) {
            usedOpIds.addAll(opIds);
        }
        if ("WEB".equals(projectType)) {
            usedOpIds.add(STATIC_OPERATION_ID);
        }

        Set<String> matchedOpIds = new HashSet<>();

        int nextUcNum = getNextUcNumber(usedOpIds);

        int matchedCount = 0;
        int newCount = 0;
        int validProfileEntriesCount = 0;

        log.debug("--- Обработка операций из профиля аналитика ---");

        for (ProfileEntry entry : profileEntries) {
            log.debug("  Обработка entry: метод={}, uri={}, интенсивность={}", entry.getMethod(), entry.getUri(), entry.getIntensity());

            if (entry.getIntensity() > 0) {
                validProfileEntriesCount++;
            } else {
                log.info("   Операция с нулевой интенсивностью: {} {} - будет покрашена красным в Excel",
                        entry.getMethod(), entry.getUri());
            }

            String operationId = null;
            String profileMethod = entry.getMethod();

            if (filteredUrlToOpIds.containsKey(entry.getUri())) {
                List<String> opIds = filteredUrlToOpIds.get(entry.getUri());

                for (String opId : opIds) {
                    if (!matchedOpIds.contains(opId)) {
                        String opMethod = extractMethodFromOperationId(opId);
                        if (opMethod.equals(profileMethod)) {
                            operationId = opId;
                            matchedOpIds.add(opId);
                            break;
                        }
                    }
                }

                if (operationId == null) {
                    for (String opId : opIds) {
                        if (!matchedOpIds.contains(opId)) {
                            operationId = opId;
                            matchedOpIds.add(opId);
                            if (entry.getIntensity() > 0) {
                                log.warn("  ⚠️ Метод не совпадает! Профиль: {}, operationId: {} (URI: {})",
                                        profileMethod, opId, entry.getUri());
                            }
                            break;
                        }
                    }
                }

                if (operationId != null) {
                    if (entry.getIntensity() > 0) {
                        if (opIds.size() > 1) {
                            log.debug("   Найден в скрипте (один из {}): {} {} → {} (инт: {})",
                                    opIds.size(), profileMethod, entry.getUri(), operationId, entry.getIntensity());
                        } else {
                            log.debug("   Найден в скрипте: {} {} → {} (инт: {})",
                                    profileMethod, entry.getUri(), operationId, entry.getIntensity());
                        }
                        matchedCount++;
                    }
                } else {
                    if (entry.getIntensity() > 0) {
                        log.warn("  ⚠️ Все operation_id для URI {} уже использованы. Создаём новый.", entry.getUri());
                    }
                    operationId = generateNewOperationId(entry, usedOpIds, nextUcNum);
                    nextUcNum = extractUcNumber(operationId) + 1;
                    usedOpIds.add(operationId);
                    if (entry.getIntensity() > 0) {
                        newCount++;
                    }
                }
            } else {
                operationId = generateNewOperationId(entry, usedOpIds, nextUcNum);
                nextUcNum = extractUcNumber(operationId) + 1;
                usedOpIds.add(operationId);
                if (entry.getIntensity() > 0) {
                    log.info("   Создан новый: {} {} → {} (инт: {})",
                            profileMethod, entry.getUri(), operationId, entry.getIntensity());
                    newCount++;
                } else {
                    log.info("   Создан ID для нулевой операции: {} {} → {} (инт: 0)",
                            profileMethod, entry.getUri(), operationId);
                }
            }

            int sla = DEFAULT_SLA;
            if (lastProfileData.containsKey(operationId)) {
                sla = lastProfileData.get(operationId).getSla();
            }

            String fullUri;
            String uriFromProfile = entry.getUri();

            if ("PPRB".equals(projectType)) {
                // Для PPRB не добавляем префикс и не меняем URI
                fullUri = uriFromProfile;
            } else if (uriFromProfile.startsWith("/ic/ufs/corp-retail/") ||
                    uriFromProfile.startsWith("/ufs/mobile/")) {
                fullUri = uriFromProfile;
            } else if (uriFromProfile.startsWith("/")) {
                fullUri = urlPrefix + uriFromProfile;
            } else {
                fullUri = urlPrefix + "/" + uriFromProfile;
            }

            CsvRow row = new CsvRow(
                    operationId,
                    fullUri,
                    entry.getMethod(),
                    entry.getIntensity(),  // может быть 0
                    sla,
                    operationId,
                    requirement,
                    ""
            );

            // ИЗМЕНЕНО: добавляем ВСЕ операции в список для Excel
            allRowsForExcel.add(row);

            // ИЗМЕНЕНО: в CSV добавляем ТОЛЬКО операции с интенсивностью > 0
            if (entry.getIntensity() > 0) {
                rowsForCsv.add(row);
                log.info("   Добавлена в CSV и Excel: {} -> интенсивность={}", operationId, entry.getIntensity());
            } else {
                log.info("   Добавлена ТОЛЬКО в Excel (не в CSV): {} -> интенсивность=0", operationId);
            }
        }

        // ========== СИНХРОНИЗАЦИЯ СТАТИКИ С UC29 ==========
        if ("WEB".equals(projectType)) {
            Optional<CsvRow> contactsRow = allRowsForExcel.stream()
                    .filter(row -> GET_CONTACTS_OPERATION_ID.equals(row.getName()))
                    .findFirst();

            Optional<CsvRow> staticRow = allRowsForExcel.stream()
                    .filter(row -> STATIC_OPERATION_ID.equals(row.getName()))
                    .findFirst();

            if (contactsRow.isPresent() && staticRow.isPresent()) {
                int contactsIntensity = contactsRow.get().getIntensity();
                CsvRow oldStaticRow = staticRow.get();

                if (contactsIntensity != oldStaticRow.getIntensity()) {
                    CsvRow updatedStaticRow = new CsvRow(
                            oldStaticRow.getName(),
                            oldStaticRow.getUri(),
                            oldStaticRow.getMethod(),
                            contactsIntensity,
                            oldStaticRow.getSla(),
                            oldStaticRow.getTransactionName(),
                            oldStaticRow.getRequirement(),
                            oldStaticRow.getComment()
                    );

                    int index = allRowsForExcel.indexOf(oldStaticRow);
                    allRowsForExcel.set(index, updatedStaticRow);

                    // ИЗМЕНЕНО: также обновляем в rowsForCsv, если там есть
                    int csvIndex = rowsForCsv.indexOf(oldStaticRow);
                    if (csvIndex != -1) {
                        rowsForCsv.set(csvIndex, updatedStaticRow);
                    }

                    log.info(" Синхронизирована интенсивность: {} -> {} (как у {})",
                            STATIC_OPERATION_ID, contactsIntensity, GET_CONTACTS_OPERATION_ID);
                }
            }
        }

        // ========== 7. Сортировка ==========

// Сортировка для Excel
        List<CsvRow> rowsToSort;
        boolean hasStatic = "WEB".equals(projectType) && allRowsForExcel.size() > 0 &&
                allRowsForExcel.get(0).getName().equals(STATIC_OPERATION_ID);

        if (hasStatic) {
            rowsToSort = allRowsForExcel.subList(1, allRowsForExcel.size());
            log.debug("Сортируем {} строк для Excel (без статики)", rowsToSort.size());
        } else {
            rowsToSort = allRowsForExcel;
            log.debug("Сортируем все {} строк для Excel", rowsToSort.size());
        }

        rowsToSort.sort((row1, row2) -> {
            int num1 = extractUcNumber(row1.getName());
            int num2 = extractUcNumber(row2.getName());
            return Integer.compare(num1, num2);
        });

        List<CsvRow> finalRowsForExcel;
        if (hasStatic) {
            finalRowsForExcel = new ArrayList<>();
            finalRowsForExcel.add(allRowsForExcel.get(0));
            finalRowsForExcel.addAll(rowsToSort);
        } else {
            finalRowsForExcel = rowsToSort;
        }

// ========== 8. Сортировка для CSV ==========
        List<CsvRow> csvRowsToSort;
        boolean hasStaticCsv = "WEB".equals(projectType) && rowsForCsv.size() > 0 &&
                rowsForCsv.get(0).getName().equals(STATIC_OPERATION_ID);

        if (hasStaticCsv) {
            csvRowsToSort = rowsForCsv.subList(1, rowsForCsv.size());
            log.debug("Сортируем {} строк для CSV (без статики)", csvRowsToSort.size());
        } else {
            csvRowsToSort = rowsForCsv;
            log.debug("Сортируем все {} строк для CSV", csvRowsToSort.size());
        }

        csvRowsToSort.sort((row1, row2) -> {
            int num1 = extractUcNumber(row1.getName());
            int num2 = extractUcNumber(row2.getName());
            return Integer.compare(num1, num2);
        });

        List<CsvRow> finalRowsForCsv;
        if (hasStaticCsv) {
            finalRowsForCsv = new ArrayList<>();
            finalRowsForCsv.add(rowsForCsv.get(0));
            finalRowsForCsv.addAll(csvRowsToSort);
        } else {
            finalRowsForCsv = csvRowsToSort;
        }

// ========== 9. Сохранение CSV и возврат результата ==========
        log.info("\n--- Статистика ---");
        log.info("   Скрипт (операций): {} операций", scriptOperationsAfterRemoval);
        log.info("   Профиль аналитика (инт > 0): {} операций", validProfileEntriesCount);
        log.info("   Нужно удалить из скрипта: {} операций", extraInScript.size());
        log.info("   Нужно добавить в скрипт: {} операций", extraInProfile.size());
        log.info("   Найдено совпадений: {}", matchedCount);
        log.info("   Создано новых ID: {}", newCount);
        log.info("   Итого в CSV (intensity > 0): {} строк", finalRowsForCsv.size());
        log.info("   Итого в Excel (все операции): {} строк", finalRowsForExcel.size());

        saveCsv(finalRowsForCsv, outputFolder, outputFilename);
        return finalRowsForExcel; }

    private String generateNewOperationId(ProfileEntry entry, Set<String> usedOpIds, int nextUcNum) {
        String method = entry.getMethod();
        String uri = entry.getUri();
        String suffix;

        if ("MOBILE".equals(projectType)) {
            String path = uri;
            path = path.replaceFirst("^/ic/ufs/mobile/corp-retail/rest/", "");
            path = path.replaceFirst("^/ufs/mobile/corp-retail/rest/", "");
            path = path.replaceFirst("^/ufs/", "");
            path = path.replaceFirst("^/business-analytics/", "");
            path = path.replaceFirst("^/dictionaries/", "");
            path = path.replaceFirst("^/", "");

            suffix = path.replaceAll("/", "_");
            suffix = suffix.replaceAll("-", "_");
            suffix = suffix.replaceAll("[^a-zA-Z0-9_]", "_");
            suffix = suffix.replaceAll("_+", "_");
            suffix = suffix.replaceAll("^_|_$", "");

            String methodPrefix = method.toUpperCase();
            suffix = methodPrefix + "_" + suffix;

            if (suffix.length() > 50) {
                suffix = suffix.substring(0, 50);
            }

            while (true) {
                String candidate = String.format("UC%02d_%s_WEB", nextUcNum, suffix);
                if (!usedOpIds.contains(candidate)) {
                    return candidate;
                }
                nextUcNum++;
            }
        } else {
            String pathAfterPrefix = uri;
            if (uri.startsWith(urlPrefix)) {
                pathAfterPrefix = uri.substring(urlPrefix.length());
            }
            if (pathAfterPrefix.startsWith("/")) {
                pathAfterPrefix = pathAfterPrefix.substring(1);
            }
            if (pathAfterPrefix.endsWith("/")) {
                pathAfterPrefix = pathAfterPrefix.substring(0, pathAfterPrefix.length() - 1);
            }
            suffix = pathAfterPrefix.replaceAll("/", "_");
            suffix = suffix.replaceAll("-", "_");
            suffix = suffix.replaceAll("[^a-zA-Z0-9_]", "_");
            suffix = suffix.replaceAll("_+", "_");
            suffix = suffix.replaceAll("^_|_$", "");

            while (true) {
                String candidate = String.format("UC%02d_%s_%s_WEB", nextUcNum, method, suffix);
                if (!usedOpIds.contains(candidate)) {
                    return candidate;
                }
                nextUcNum++;
            }
        }
    }

    private int getNextUcNumber(Set<String> usedOpIds) {
        if (usedOpIds == null || usedOpIds.isEmpty()) {
            return 1;
        }

        int maxNum = 0;
        for (String opId : usedOpIds) {
            int num = extractUcNumber(opId);
            if (num > maxNum && num < 1000) {
                maxNum = num;
            }
        }
        return maxNum + 1;
    }

    private int extractUcNumber(String operationId) {
        if (operationId == null || !operationId.startsWith("UC")) {
            return 0;
        }
        try {
            int underscoreIndex = operationId.indexOf('_', 2);
            if (underscoreIndex == -1) {
                return 0;
            }
            return Integer.parseInt(operationId.substring(2, underscoreIndex));
        } catch (Exception e) {
            return 0;
        }
    }

    private void saveCsv(List<CsvRow> rows, String outputFolder, String outputFilename) {
        File folder = new File(outputFolder);
        if (!folder.exists()) {
            folder.mkdirs();
        }

        File outputFile = new File(folder, outputFilename);

        try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile, StandardCharsets.UTF_8))) {
            writer.println("Название;Интенсивность выполнения;Требование к времени отклика;Название транзакции;Требование;Комментарий");

            for (CsvRow row : rows) {
                writer.printf("%s;%d;%d;%s;%s;%s%n",
                        row.getName(),
                        row.getIntensity(),
                        row.getSla(),
                        row.getTransactionName(),
                        row.getRequirement(),
                        row.getComment()
                );
            }

            log.info("\n Профиль сохранён: {}", outputFile.getAbsolutePath());
            log.info(" Всего строк в CSV: {}", rows.size());

        } catch (IOException e) {
            log.error("Ошибка при сохранении CSV: {}", e.getMessage());
        }
    }

    private String extractMethodFromOperationId(String operationId) {
        if (operationId == null) return "GET";

        if (operationId.contains("_POST_") || operationId.contains("POST_")) {
            return "POST";
        }
        if (operationId.contains("_GET_") || operationId.contains("GET_")) {
            return "GET";
        }

        String[] parts = operationId.split("_");
        for (String part : parts) {
            if (part.equalsIgnoreCase("POST")) {
                return "POST";
            }
            if (part.equalsIgnoreCase("GET")) {
                return "GET";
            }
        }

        return "GET";
    }
}

package org.example.service;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.example.model.CsvRow;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Slf4j
@Service
public class ExcelReportService {

    @Autowired
    private UriNormalizer uriNormalizer;

    private static final String DOC_FOLDER = "/home/retb_test/PPRB_OTP/Tools/GeneratorProfile" + "/DocumentForTheScript";
    private static final String TEMPLATE_FILE = "Анкета ЕФС web и mobile.xlsx";
    private static final String STATIC_OPERATION_ID = "UC00_StaticJS_1_WEB";
    private static final String GET_CONTACTS_OPERATION_ID = "UC29_GET_contacts_WEB";

    // Индексы колонок для WEB (8 колонок)
    private static final int COL_SCRIPT = 0;
    private static final int COL_SERVICE_WEB = 1;
    private static final int COL_TPH_WEB = 2;
    private static final int COL_SLA_WEB = 3;
    private static final int COL_LOGIN_WEB = 4;
    private static final int COL_THREADS_WEB = 5;
    private static final int COL_PACING_WEB = 6;
    private static final int COL_ITERATIONS_WEB = 7;

    // Индексы колонок для MOBILE (8 колонок)
    private static final int COL_SCRIPT_MOB = 0;
    private static final int COL_SERVICE_MOB = 1;
    private static final int COL_TPH_MOB = 2;
    private static final int COL_SLA_MOB = 3;
    private static final int COL_LOGIN_MOB = 4;
    private static final int COL_THREADS_MOB = 5;
    private static final int COL_PACING_MOB = 6;
    private static final int COL_ITERATIONS_MOB = 7;

    public void generateExcelReport(List<CsvRow> newRows, String projectType,
                                    Map<String, Integer> oldIntensityByService,
                                    Set<String> oldServices,
                                    Set<String> extraInScript) {

        try {
            File templateFile = new File(DOC_FOLDER, TEMPLATE_FILE);
            String pagePrefix = "WEB".equals(projectType) ? "WEB" : "MOB";

            Workbook workbook;
            if (templateFile.exists()) {
                workbook = new XSSFWorkbook(new FileInputStream(templateFile));
            } else {
                workbook = new XSSFWorkbook();
            }

            // Удаляем пустые страницы
            for (int i = workbook.getNumberOfSheets() - 1; i >= 0; i--) {
                String sheetName = workbook.getSheetName(i);
                if (sheetName.startsWith(pagePrefix) && workbook.getSheetAt(i).getLastRowNum() == 0) {
                    workbook.removeSheetAt(i);
                }
            }

            Sheet lastSheet = findLastSheetByPrefix(workbook, pagePrefix);
            int nextReleaseNumber;
            String newSheetName;

            if (lastSheet == null) {
                nextReleaseNumber = 5038;
                newSheetName = pagePrefix + " 5.038";
            } else {
                int lastReleaseNumber = extractReleaseNumber(lastSheet.getSheetName());
                nextReleaseNumber = lastReleaseNumber + 1;
                newSheetName = pagePrefix + " " + formatReleaseNumber(nextReleaseNumber);
            }

            Sheet newSheet = workbook.createSheet(newSheetName);

            if (lastSheet != null) {
                copySheetStructure(lastSheet, newSheet);
                removeDelayAndFixHeaders(newSheet, projectType);
                removeHeaderRowIfExists(newSheet);
            } else {
                createHeaderRow(newSheet, projectType);
            }

            fillSheetWithData(newSheet, newRows, oldIntensityByService, oldServices, projectType, extraInScript);

            removeHeaderRowIfExists(newSheet);

            try (FileOutputStream fos = new FileOutputStream(templateFile)) {
                workbook.write(fos);
            }

            workbook.close();
            log.info(" Excel отчёт обновлён: добавлена страница {}", newSheetName);

        } catch (IOException e) {
            log.error("Ошибка при создании Excel отчёта: {}", e.getMessage());
        }
    }

    private void removeDelayAndFixHeaders(Sheet sheet, String projectType) {
        Row oldHeaderRow = sheet.getRow(0);
        if (oldHeaderRow != null) {
            sheet.removeRow(oldHeaderRow);
        }

        Row newHeaderRow = sheet.createRow(0);
        String[] headers;

        if ("MOBILE".equals(projectType)) {
            headers = new String[]{"Скрипт", "Вызываемый сервис", "TPH", "SLA", "Логинов в час",
                    "Количество потоков", "Pacing", "Количество итераций"};
        } else {
            headers = new String[]{"Скрипт", "Вызываемый сервис", "TPH", "SLA", "Логинов в час",
                    "Количество потоков", "Pacing", "Количество итераций"};
        }

        for (int i = 0; i < headers.length; i++) {
            Cell cell = newHeaderRow.createCell(i);
            cell.setCellValue(headers[i]);
            sheet.setColumnWidth(i, (i == 7 || i == 8) ? 8000 : 5000);
        }

        Font headerFont = sheet.getWorkbook().createFont();
        headerFont.setBold(true);
        CellStyle headerStyle = sheet.getWorkbook().createCellStyle();
        headerStyle.setFont(headerFont);
        setBorders(headerStyle);

        for (int i = 0; i < headers.length; i++) {
            newHeaderRow.getCell(i).setCellStyle(headerStyle);
        }

        int maxCol = headers.length;
        for (int r = 1; r <= sheet.getLastRowNum(); r++) {
            Row row = sheet.getRow(r);
            if (row != null) {
                for (int c = row.getLastCellNum() - 1; c >= maxCol; c--) {
                    Cell cell = row.getCell(c);
                    if (cell != null) {
                        row.removeCell(cell);
                    }
                }
            }
        }

        //log.info(" Заголовок пересоздан для проекта: {}", projectType);
    }

    private void removeHeaderRowIfExists(Sheet sheet) {
        for (int r = sheet.getLastRowNum(); r >= 0; r--) {
            Row row = sheet.getRow(r);
            if (row == null) continue;

            for (int c = 0; c < row.getLastCellNum(); c++) {
                Cell cell = row.getCell(c);
                if (cell != null) {
                    String cellValue = null;
                    try {
                        cellValue = cell.getStringCellValue();
                    } catch (IllegalStateException e) {
                        continue;
                    }
                    if (cellValue != null && cellValue.contains("Наименование операции")) {
                        sheet.shiftRows(r + 1, sheet.getLastRowNum(), -1);
                        log.info(" Удалена строка с содержимым: {}", cellValue);
                        break;
                    }
                }
            }
        }
    }

    private Sheet findLastSheetByPrefix(Workbook workbook, String prefix) {
        Sheet lastSheet = null;
        int maxRelease = -1;

        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            String sheetName = workbook.getSheetName(i);
            if (sheetName.startsWith(prefix)) {
                int releaseNum = extractReleaseNumber(sheetName);
                if (releaseNum > maxRelease) {
                    maxRelease = releaseNum;
                    lastSheet = workbook.getSheetAt(i);
                }
            }
        }
        return lastSheet;
    }

    private int extractReleaseNumber(String sheetName) {
        Pattern pattern = Pattern.compile("(\\d+)\\.(\\d+)");
        Matcher matcher = pattern.matcher(sheetName);
        if (matcher.find()) {
            int major = Integer.parseInt(matcher.group(1));
            int minor = Integer.parseInt(matcher.group(2));
            return major * 1000 + minor;
        }
        return 0;
    }

    private String formatReleaseNumber(int releaseNumber) {
        int major = releaseNumber / 1000;
        int minor = releaseNumber % 1000;
        return major + "." + String.format("%03d", minor);
    }

    private void createHeaderRow(Sheet sheet, String projectType) {
        Row headerRow = sheet.createRow(0);
        String[] headers;

        if ("MOBILE".equals(projectType)) {
            headers = new String[]{"Скрипт", "Вызываемый сервис", "TPH", "SLA", "Логинов в час",
                    "Количество потоков", "Pacing", "Количество итераций"};
        } else {
            headers = new String[]{"Скрипт", "Вызываемый сервис", "TPH", "SLA", "Логинов в час",
                    "Количество потоков", "Pacing", "Количество итераций"};
        }

        for (int i = 0; i < headers.length; i++) {
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(headers[i]);
            sheet.setColumnWidth(i, (i == 7 || i == 8) ? 8000 : 5000);
        }

        Font headerFont = sheet.getWorkbook().createFont();
        headerFont.setBold(true);
        CellStyle headerStyle = sheet.getWorkbook().createCellStyle();
        headerStyle.setFont(headerFont);
        setBorders(headerStyle);

        for (int i = 0; i < headers.length; i++) {
            headerRow.getCell(i).setCellStyle(headerStyle);
        }
    }

    private void copySheetStructure(Sheet source, Sheet target) {
        if (source.getRow(0) != null) {
            for (int i = 0; i < source.getRow(0).getLastCellNum(); i++) {
                target.setColumnWidth(i, source.getColumnWidth(i));
            }
        }

        for (int r = 0; r <= source.getLastRowNum(); r++) {
            Row sourceRow = source.getRow(r);
            if (sourceRow != null) {
                Row targetRow = target.createRow(r);
                for (int c = 0; c < sourceRow.getLastCellNum(); c++) {
                    Cell sourceCell = sourceRow.getCell(c);
                    if (sourceCell != null) {
                        Cell targetCell = targetRow.createCell(c);

                        switch (sourceCell.getCellType()) {
                            case STRING:
                                targetCell.setCellValue(sourceCell.getStringCellValue());
                                break;
                            case NUMERIC:
                                targetCell.setCellValue(sourceCell.getNumericCellValue());
                                break;
                            case BOOLEAN:
                                targetCell.setCellValue(sourceCell.getBooleanCellValue());
                                break;
                            case FORMULA:
                                targetCell.setCellFormula(sourceCell.getCellFormula());
                                break;
                            default:
                                targetCell.setCellValue("");
                        }

                        CellStyle newStyle = target.getWorkbook().createCellStyle();
                        newStyle.cloneStyleFrom(sourceCell.getCellStyle());
                        targetCell.setCellStyle(newStyle);
                    }
                }
            }
        }
    }

    private void fillSheetWithData(Sheet sheet, List<CsvRow> newRows,
                                   Map<String, Integer> oldIntensityByService,
                                   Set<String> oldServices,
                                   String projectType,
                                   Set<String> extraInScript) {

        // ========== СИНХРОНИЗАЦИЯ СТАТИКИ С UC29 (ДЛЯ WEB) ==========
        if ("WEB".equals(projectType)) {
            CsvRow contactsRow = null;
            CsvRow staticRow = null;
            int contactsIndex = -1;
            int staticIndex = -1;

            // Ищем оба запроса
            for (int i = 0; i < newRows.size(); i++) {
                CsvRow row = newRows.get(i);
                if (GET_CONTACTS_OPERATION_ID.equals(row.getName())) {
                    contactsRow = row;
                    contactsIndex = i;
                } else if (STATIC_OPERATION_ID.equals(row.getName())) {
                    staticRow = row;
                    staticIndex = i;
                }
            }

            // Если нашли оба - синхронизируем
            if (contactsRow != null && staticRow != null) {
                int contactsIntensity = contactsRow.getIntensity();
                if (contactsIntensity != staticRow.getIntensity()) {
                    CsvRow updatedStaticRow = new CsvRow(
                            staticRow.getName(),
                            staticRow.getUri(),
                            staticRow.getMethod(),
                            contactsIntensity,
                            staticRow.getSla(),
                            staticRow.getTransactionName(),
                            staticRow.getRequirement(),
                            staticRow.getComment()
                    );
                    newRows.set(staticIndex, updatedStaticRow);
                    log.info(" Excel: синхронизирована интенсивность {} -> {} (как у {})",
                            STATIC_OPERATION_ID, contactsIntensity, GET_CONTACTS_OPERATION_ID);
                }
            }
        }

        // ========== ДЛЯ MOBILE: полная перезапись страницы ==========
        if ("MOBILE".equals(projectType)) {

            // Удаляем все строки кроме заголовка (строка 0)
            for (int r = sheet.getLastRowNum(); r >= 1; r--) {
                Row row = sheet.getRow(r);
                if (row != null) {
                    sheet.removeRow(row);
                }
            }

            // ПРИНУДИТЕЛЬНО удаляем строку 2, если она существует и пустая
            Row row2 = sheet.getRow(2);
            if (row2 != null) {
                // Проверяем, пустая ли строка
                boolean isEmpty = true;
                for (int c = 0; c < row2.getLastCellNum(); c++) {
                    Cell cell = row2.getCell(c);
                    if (cell != null && cell.getCellType() != CellType.BLANK) {
                        isEmpty = false;
                        break;
                    }
                }
                if (isEmpty) {
                    // Сдвигаем строки начиная с 3 вверх, чтобы закрыть строку 2
                    if (sheet.getLastRowNum() >= 2) {
                        sheet.shiftRows(3, sheet.getLastRowNum(), -1);
                    }
                }
            }

            log.info(" MOBILE страница очищена, будет создано {} новых строк", newRows.size());

            Workbook workbook = sheet.getWorkbook();

            CellStyle defaultStyle = workbook.createCellStyle();
            setBorders(defaultStyle);

            CellStyle goodStyle = workbook.createCellStyle();
            goodStyle.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex());
            goodStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            setBorders(goodStyle);

            CellStyle badStyle = workbook.createCellStyle();
            badStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
            badStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
            setBorders(badStyle);

            int currentRow = 1;
            for (CsvRow csvRow : newRows) {
                Row dataRow = sheet.createRow(currentRow);
                String redKey = csvRow.getMethod() + "|" + csvRow.getUri();
                // ДОБАВИТЬ ОТЛАДКУ ДЛЯ КАЖДОЙ ОПЕРАЦИИ С 0
                if (csvRow.getIntensity() == 0) {
                    log.info("MOBILE: найдена операция с 0 - метод={}, uri={}, redKey={}",
                            csvRow.getMethod(), csvRow.getUri(), redKey);
                    log.info("MOBILE: extraInScript.contains(redKey) = {}",
                            extraInScript != null && extraInScript.contains(redKey));
                    log.info("MOBILE: oldServices.contains(serviceKey) = {}",
                            oldServices != null && oldServices.contains(
                                    uriNormalizer.normalizeService(csvRow.getMethod(), csvRow.getUri(), projectType)));
                }

                boolean shouldBeRed = csvRow.getIntensity() == 0
                        && extraInScript != null
                        && extraInScript.contains(redKey);


                if (csvRow.getIntensity() == 0) {
                    log.debug("MOBILE: shouldBeRed = {}", shouldBeRed);
                }

                if (shouldBeRed) {
                    log.info(" MOBILE: красим красным {} {} (интенсивность=0)",
                            csvRow.getMethod(), csvRow.getUri());
                }

                String serviceKey = uriNormalizer.normalizeService(csvRow.getMethod(), csvRow.getUri(), projectType);
                boolean isNew = oldServices != null && !oldServices.contains(serviceKey);
                Integer oldTph = oldIntensityByService != null ? oldIntensityByService.get(serviceKey) : null;
                boolean isIntensityChanged = oldTph != null && !oldTph.equals(csvRow.getIntensity()) && csvRow.getIntensity() > 0;

                int currentTph = csvRow.getIntensity();
                int iterations = shouldBeRed ? 1 : calculateIterations(currentTph);
                int threads = shouldBeRed ? 1 : calculateThreads(currentTph);

                // Определяем стиль для всей строки
                CellStyle rowStyle;
                if (shouldBeRed) {
                    rowStyle = badStyle;
                } else if (isNew) {
                    rowStyle = goodStyle;
                } else {
                    rowStyle = defaultStyle;
                }

                // Заполняем все ячейки
                Cell cellScript = dataRow.createCell(COL_SCRIPT_MOB);
                cellScript.setCellValue(csvRow.getName());
                cellScript.setCellStyle(rowStyle);

                Cell cellService = dataRow.createCell(COL_SERVICE_MOB);
                cellService.setCellValue(csvRow.getMethod() + " " + csvRow.getUri());
                cellService.setCellStyle(rowStyle);

                Cell cellTph = dataRow.createCell(COL_TPH_MOB);
                cellTph.setCellValue(currentTph);
                cellTph.setCellStyle(rowStyle);

                Cell cellSla = dataRow.createCell(COL_SLA_MOB);
                cellSla.setCellValue(csvRow.getSla());
                cellSla.setCellStyle(rowStyle);

                Cell cellIterations = dataRow.createCell(COL_ITERATIONS_MOB);
                cellIterations.setCellValue(iterations);
                cellIterations.setCellStyle(rowStyle);

                Cell cellThreads = dataRow.createCell(COL_THREADS_MOB);
                cellThreads.setCellValue(threads);
                cellThreads.setCellStyle(rowStyle);

                // Логины в час - ФОРМУЛА
                Cell cellLogin = dataRow.createCell(COL_LOGIN_MOB);
                if (shouldBeRed) {
                    cellLogin.setCellValue(0);
                } else {
                    String tphColLetter = getColumnLetter(COL_TPH_MOB);
                    String iterColLetter = getColumnLetter(COL_ITERATIONS_MOB);
                    String formula = String.format("ROUNDUP(%s%d/%s%d,0)",
                            tphColLetter, currentRow + 1, iterColLetter, currentRow + 1);
                    cellLogin.setCellFormula(formula);
                }
                cellLogin.setCellStyle(rowStyle);

                Cell cellPacing = dataRow.createCell(COL_PACING_MOB);
                if (shouldBeRed) {
                    cellPacing.setCellValue(0);
                } else {
                    // Проверяем, нужно ли скорректировать итерации
                    int pacing = 0;
                    if (iterations > 0 && threads > 0) {
                        pacing = (int) Math.round(3600.0 / (((double) currentTph / iterations) / threads));
                    }

                    if (pacing > 300 && iterations > 1) {
                        // Список допустимых значений итераций (от меньшего к большему)
                        int[] validIterations = {1, 5, 10, 20, 40, 60};

                        int newIterations = iterations;
                        int newPacing = pacing;

                        // Ищем подходящее значение итераций
                        for (int i = validIterations.length - 1; i >= 0; i--) {
                            int candidateIterations = validIterations[i];
                            if (candidateIterations < iterations) {
                                int candidatePacing = (int) Math.round(3600.0 / (((double) currentTph / candidateIterations) / threads));
                                if (candidatePacing <= 300) {
                                    newIterations = candidateIterations;
                                    newPacing = candidatePacing;
                                    break;
                                }
                            }
                        }

                        // Обновляем итерации в ячейке
                        Cell iterCell = dataRow.getCell(COL_ITERATIONS_MOB);
                        if (iterCell != null && newIterations != iterations) {
                            iterCell.setCellValue(newIterations);
                            log.debug(" MOBILE: скорректированы итерации: {} -> {} (пейсинг стал {})",
                                    iterations, newIterations, newPacing);
                        }
                    }

                    // Ставим формулу с обновленными итерациями
                    String loginColLetter = getColumnLetter(COL_LOGIN_MOB);
                    String threadsColLetter = getColumnLetter(COL_THREADS_MOB);
                    String formula = String.format("ROUND(3600/(%s%d/%s%d),0)",
                            loginColLetter, currentRow + 1, threadsColLetter, currentRow + 1);
                    cellPacing.setCellFormula(formula);
                }
                cellPacing.setCellStyle(rowStyle);

                // Если изменилась интенсивность (и не красный, и не новый) - красим только TPH зелёным
                if (!shouldBeRed && !isNew && isIntensityChanged) {
                    cellTph.setCellStyle(goodStyle);
                    log.info(" MOBILE: изменилась интенсивность {} {}: {} -> {}",
                            csvRow.getMethod(), csvRow.getUri(), oldTph, currentTph);
                }

                currentRow++;
            }

            // Добавляем строку ИТОГО
            int totalRowNum = currentRow;
            Row totalRow = sheet.createRow(totalRowNum);

            Cell totalLabel = totalRow.createCell(COL_SCRIPT_MOB);
            totalLabel.setCellValue("ИТОГО");
            totalLabel.setCellStyle(defaultStyle);

            int lastDataRow = currentRow;
            if (lastDataRow < 2) lastDataRow = 2;

            String tphColLetter = getColumnLetter(COL_TPH_MOB);
            String loginColLetter = getColumnLetter(COL_LOGIN_MOB);
            String threadsColLetter = getColumnLetter(COL_THREADS_MOB);

            Cell tphFormulaCell = totalRow.createCell(COL_TPH_MOB);
            tphFormulaCell.setCellFormula(String.format("SUM(%s2:%s%d)", tphColLetter, tphColLetter, lastDataRow));
            tphFormulaCell.setCellStyle(defaultStyle);

            Cell loginFormulaCell = totalRow.createCell(COL_LOGIN_MOB);
            loginFormulaCell.setCellFormula(String.format("SUM(%s2:%s%d)", loginColLetter, loginColLetter, lastDataRow));
            loginFormulaCell.setCellStyle(defaultStyle);

            Cell threadsFormulaCell = totalRow.createCell(COL_THREADS_MOB);
            threadsFormulaCell.setCellFormula(String.format("SUM(%s2:%s%d)", threadsColLetter, threadsColLetter, lastDataRow));
            threadsFormulaCell.setCellStyle(defaultStyle);

            log.info(" MOBILE страница создана, {} операций", newRows.size());
            return;
        }

        // ========== ДЛЯ WEB ==========
        // Очищаем страницу перед заполнением (удаляем все строки кроме заголовка)
        for (int r = sheet.getLastRowNum(); r >= 1; r--) {
            Row row = sheet.getRow(r);
            if (row != null) {
                sheet.removeRow(row);
            }
        }

        // ПРИНУДИТЕЛЬНО УДАЛЯЕМ СТРОКУ 2, ЕСЛИ ОНА ПУСТАЯ
        Row row2 = sheet.getRow(2);
        if (row2 != null) {
            boolean isEmpty = true;
            for (int c = 0; c < row2.getLastCellNum(); c++) {
                Cell cell = row2.getCell(c);
                if (cell != null && cell.getCellType() != CellType.BLANK) {
                    if (cell.getCellType() == CellType.STRING && !cell.getStringCellValue().isEmpty()) {
                        isEmpty = false;
                        break;
                    } else if (cell.getCellType() == CellType.NUMERIC) {
                        isEmpty = false;
                        break;
                    }
                }
            }
            if (isEmpty && sheet.getLastRowNum() >= 3) {
                sheet.shiftRows(3, sheet.getLastRowNum(), -1);
            }
        }

        //log.info(" WEB страница очищена, будет создано {} новых строк", newRows.size());

        Workbook workbook = sheet.getWorkbook();

        CellStyle goodStyle = workbook.createCellStyle();
        goodStyle.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex());
        goodStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        setBorders(goodStyle);

        CellStyle badStyle = workbook.createCellStyle();
        badStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
        badStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        setBorders(badStyle);

        CellStyle defaultStyle = workbook.createCellStyle();
        setBorders(defaultStyle);

        int colScript = COL_SCRIPT;
        int colService = COL_SERVICE_WEB;
        int colTph = COL_TPH_WEB;
        int colSla = COL_SLA_WEB;
        int colLogin = COL_LOGIN_WEB;
        int colThreads = COL_THREADS_WEB;
        int colPacing = COL_PACING_WEB;
        int colIterations = COL_ITERATIONS_WEB;

        // 1. Находим удалённые операции
        Set<String> newServiceKeys = newRows.stream()
                .map(row -> uriNormalizer.normalizeService(row.getMethod(), row.getUri(), projectType))
                .collect(Collectors.toSet());

        Set<String> deletedServiceKeys = new HashSet<>(oldServices);
        deletedServiceKeys.removeAll(newServiceKeys);

        // 2. Красим удалённые строки в красный
        int maxCol = colIterations;

        for (int r = 1; r <= sheet.getLastRowNum(); r++) {
            Row row = sheet.getRow(r);
            if (row != null && colService >= 0) {
                Cell serviceCell = row.getCell(colService);
                if (serviceCell != null) {
                    try {
                        String fullService = serviceCell.getStringCellValue();
                        String method = fullService.substring(0, fullService.indexOf(' '));
                        String uri = fullService.substring(fullService.indexOf(' ') + 1);
                        String serviceKey = uriNormalizer.normalizeService(method, uri, projectType);
                        if (deletedServiceKeys.contains(serviceKey)) {
                            for (int c = 0; c <= maxCol; c++) {
                                Cell cell = row.getCell(c);
                                if (cell == null) cell = row.createCell(c);
                                cell.setCellStyle(badStyle);
                            }
                            log.info("   Покрашена (удалена из профиля): {}", serviceKey);
                        }
                    } catch (Exception e) {
                        log.warn("  Ошибка при обработке строки {}: {}", r, e.getMessage());
                        continue;
                    }
                }
            }
        }

        // 3. Обработка статики для WEB
        CsvRow staticRow = null;
        Iterator<CsvRow> iterator = newRows.iterator();
        while (iterator.hasNext()) {
            CsvRow row = iterator.next();
            if ("UC00_StaticJS_1_WEB".equals(row.getName())) {
                staticRow = row;
                iterator.remove();
                break;
            }
        }

        if (staticRow != null) {
            Integer staticRowNum = null;
            for (int r = 1; r <= sheet.getLastRowNum(); r++) {
                Row row = sheet.getRow(r);
                if (row != null && colScript >= 0) {
                    Cell scriptCell = row.getCell(colScript);
                    if (scriptCell != null) {
                        String scriptValue = null;
                        try {
                            scriptValue = scriptCell.getStringCellValue();
                        } catch (IllegalStateException e) {
                            continue;
                        }
                        if ("UC00_StaticJS_1_WEB".equals(scriptValue)) {
                            staticRowNum = r;
                            break;
                        }
                    }
                }
            }

            Row staticRowExcel;
            int staticRowIndex;
            if (staticRowNum == null) {
                int lastRowNum = sheet.getLastRowNum();
                if (lastRowNum >= 1) {
                    sheet.shiftRows(1, lastRowNum, 1);
                }
                staticRowExcel = sheet.createRow(1);
                staticRowIndex = 1;
                staticRowExcel.createCell(colScript).setCellValue(staticRow.getName());
                staticRowExcel.createCell(colService).setCellValue(staticRow.getMethod() + " " + staticRow.getUri());
                log.info(" Добавлена строка со статическим запросом UC00_StaticJS_1_WEB");
            } else {
                staticRowExcel = sheet.getRow(staticRowNum);
                staticRowIndex = staticRowNum;
                log.info(" Обновлена статическая операция UC00_StaticJS_1_WEB");
            }

            String serviceKey = uriNormalizer.normalizeService(staticRow.getMethod(), staticRow.getUri(), projectType);
            boolean isNew = oldServices != null && !oldServices.contains(serviceKey);
            Integer oldTph = oldIntensityByService != null ? oldIntensityByService.get(serviceKey) : null;
            boolean isChanged = oldTph != null && !oldTph.equals(staticRow.getIntensity());

            CellStyle staticStyle = (isNew || isChanged) ? goodStyle : defaultStyle;

            if (isChanged) {
                log.info(" Статика {} изменила интенсивность: {} -> {}",
                        STATIC_OPERATION_ID, oldTph, staticRow.getIntensity());
            }

            Cell tphCell = staticRowExcel.getCell(colTph);
            if (tphCell == null) tphCell = staticRowExcel.createCell(colTph);
            tphCell.setCellValue(staticRow.getIntensity());
            tphCell.setCellStyle(staticStyle);

            Cell slaCell = staticRowExcel.getCell(colSla);
            if (slaCell == null) slaCell = staticRowExcel.createCell(colSla);
            slaCell.setCellValue(staticRow.getSla());
            slaCell.setCellStyle(defaultStyle);

            int iterations = calculateIterations(staticRow.getIntensity());
            Cell iterCell = staticRowExcel.getCell(colIterations);
            if (iterCell == null) iterCell = staticRowExcel.createCell(colIterations);
            iterCell.setCellValue(iterations);
            iterCell.setCellStyle(defaultStyle);

            int threads = calculateThreads(staticRow.getIntensity());
            Cell threadsCell = staticRowExcel.getCell(colThreads);
            if (threadsCell == null) threadsCell = staticRowExcel.createCell(colThreads);
            threadsCell.setCellValue(threads);
            threadsCell.setCellStyle(defaultStyle);

            // Логины в час для статики = 0
            Cell loginCell = staticRowExcel.getCell(colLogin);
            if (loginCell == null) loginCell = staticRowExcel.createCell(colLogin);
            loginCell.setCellValue(0);
            loginCell.setCellStyle(defaultStyle);

            // Pacing для статики - ФОРМУЛА =ОКРУГЛ(3600/(C2/F2);0)
            Cell pacingCell = staticRowExcel.getCell(colPacing);
            if (pacingCell == null) pacingCell = staticRowExcel.createCell(colPacing);
            String tphColLetter = getColumnLetter(colTph);
            String threadsColLetter = getColumnLetter(colThreads);
            String pacingFormula = String.format("ROUND(3600/(%s%d/%s%d),0)",
                    tphColLetter, staticRowIndex + 1, threadsColLetter, staticRowIndex + 1);
            pacingCell.setCellFormula(pacingFormula);
            pacingCell.setCellStyle(defaultStyle);
        }

        // 4. Строим карту существующих строк по service
        Map<String, Integer> existingRowsMap = new HashMap<>();
        int maxRowNum = 1;
        for (int r = 1; r <= sheet.getLastRowNum(); r++) {
            Row row = sheet.getRow(r);
            if (row != null) {
                Cell serviceCell = row.getCell(colService);
                if (serviceCell != null) {
                    try {
                        String fullService = serviceCell.getStringCellValue();
                        String method = fullService.substring(0, fullService.indexOf(' '));
                        String uri = fullService.substring(fullService.indexOf(' ') + 1);
                        String serviceKey = uriNormalizer.normalizeService(method, uri, projectType);
                        existingRowsMap.put(serviceKey, r);

                        // Запоминаем максимальный индекс существующей строки
                        if (r > maxRowNum) {
                            maxRowNum = r;
                        }
                    } catch (Exception e) {
                        continue;
                    }
                }
            }
        }

        // 5. Удаляем старую строку ИТОГО
        for (int r = sheet.getLastRowNum(); r >= 0; r--) {
            Row row = sheet.getRow(r);
            if (row != null) {
                Cell firstCell = row.getCell(0);
                if (firstCell != null) {
                    String value = null;
                    try {
                        value = firstCell.getStringCellValue();
                    } catch (IllegalStateException e) {
                        continue;
                    }
                    if ("ИТОГО".equals(value)) {
                        sheet.removeRow(row);
                    }
                }
            }
        }

        // 6. Обновляем существующие строки
        Set<String> processedNames = new HashSet<>();

        for (CsvRow row : newRows) {
            String serviceKey = uriNormalizer.normalizeService(row.getMethod(), row.getUri(), projectType);

            // ИЗМЕНЕНО: упрощённая логика - красный если интенсивность = 0
            String redKey = row.getMethod() + "|" + row.getUri();
            boolean shouldBeRed = row.getIntensity() == 0
                    && extraInScript != null
                    && extraInScript.contains(redKey);

            if (shouldBeRed) {
                log.info(" WEB: красим красным {} {} (интенсивность=0)",
                        row.getMethod(), row.getUri());
            }

            if (processedNames.contains(row.getName())) {
                log.warn("⚠️ Пропущен дубликат операции в newRows: {}", row.getName());
                continue;
            }

            boolean isNew = oldServices != null && !oldServices.contains(serviceKey);
            Integer oldTph = oldIntensityByService != null ? oldIntensityByService.get(serviceKey) : null;
            boolean isIntensityChanged = oldTph != null && !oldTph.equals(row.getIntensity()) && row.getIntensity() > 0;

            Integer existingRowNum = existingRowsMap.get(serviceKey);

            Row dataRow;
            int currentRowNum;
            if (existingRowNum != null) {
                dataRow = sheet.getRow(existingRowNum);
                currentRowNum = existingRowNum;
                if (existingRowNum > maxRowNum) maxRowNum = existingRowNum;
            } else {
                maxRowNum++;
                currentRowNum = maxRowNum;
                dataRow = sheet.createRow(maxRowNum);
                if (colScript >= 0) {
                    dataRow.createCell(colScript).setCellValue(row.getName());
                }
                dataRow.createCell(colService).setCellValue(row.getMethod() + " " + row.getUri());
            }

            processedNames.add(row.getName());

            // ========== ЛОГИКА ПРИМЕНЕНИЯ СТИЛЕЙ ==========

            // 1. Красные строки (вся строка красная)
            if (shouldBeRed) {
                for (int c = 0; c <= colIterations; c++) {
                    Cell cell = dataRow.getCell(c);
                    if (cell == null) cell = dataRow.createCell(c);
                    cell.setCellStyle(badStyle);
                }
                fillRowDataWeb(dataRow, row, currentRowNum, colScript, colService, colTph, colSla,
                        colIterations, colThreads, colLogin, colPacing, badStyle, false);
                continue;
            }

            // 2. Новые операции (вся строка зелёная)
            if (isNew) {
                for (int c = 0; c <= colIterations; c++) {
                    Cell cell = dataRow.getCell(c);
                    if (cell == null) cell = dataRow.createCell(c);
                    cell.setCellStyle(goodStyle);
                }
                fillRowDataWeb(dataRow, row, currentRowNum, colScript, colService, colTph, colSla,
                        colIterations, colThreads, colLogin, colPacing, goodStyle, false);
                continue;
            }

            // 3. Существующие операции (обычный стиль)
            fillRowDataWeb(dataRow, row, currentRowNum, colScript, colService, colTph, colSla,
                    colIterations, colThreads, colLogin, colPacing, defaultStyle, false);

            // 4. Если изменилась интенсивность - красим только TPH зелёным
            if (isIntensityChanged) {
                Cell tphCell = dataRow.getCell(colTph);
                if (tphCell != null) {
                    tphCell.setCellStyle(goodStyle);
                    log.info(" Изменилась интенсивность: {} {}: {} -> {}",
                            row.getMethod(), row.getUri(), oldTph, row.getIntensity());
                }
            }
        }

        // Удаляем пустые строки
        for (int r = sheet.getLastRowNum(); r > maxRowNum; r--) {
            Row row = sheet.getRow(r);
            if (row != null) {
                sheet.removeRow(row);
            }
        }

        // ИТОГО
        int totalRowNum = maxRowNum + 1;
        Row totalRow = sheet.createRow(totalRowNum);

        int lastDataRow = maxRowNum;
        if (lastDataRow < 2) lastDataRow = 2;

        String tphColLetter = getColumnLetter(colTph);
        String loginColLetter = getColumnLetter(colLogin);
        String threadsColLetter = getColumnLetter(colThreads);

        Cell tphFormulaCell = totalRow.createCell(colTph);
        tphFormulaCell.setCellFormula("SUM(" + tphColLetter + "2:" + tphColLetter + (lastDataRow + 1) + ")");
        tphFormulaCell.setCellStyle(defaultStyle);

        Cell loginFormulaCell = totalRow.createCell(colLogin);
        loginFormulaCell.setCellFormula("SUM(" + loginColLetter + "2:" + loginColLetter + (lastDataRow + 1) + ")");
        loginFormulaCell.setCellStyle(defaultStyle);

        Cell threadsFormulaCell = totalRow.createCell(colThreads);
        threadsFormulaCell.setCellFormula("SUM(" + threadsColLetter + "2:" + threadsColLetter + (lastDataRow + 1) + ")");
        threadsFormulaCell.setCellStyle(defaultStyle);

        Cell totalLabel = totalRow.createCell(colScript >= 0 ? colScript : colService);
        totalLabel.setCellValue("ИТОГО");
        totalLabel.setCellStyle(defaultStyle);
    }

    private void fillRowDataWeb(Row dataRow, CsvRow row, int rowNum,
                                int colScript, int colService, int colTph, int colSla,
                                int colIterations, int colThreads, int colLogin, int colPacing,
                                CellStyle style, boolean isStatic) {

        int currentTph = row.getIntensity();
        int iterations = calculateIterations(currentTph);
        int threads = calculateThreads(currentTph);

        // Скрипт
        if (colScript >= 0) {
            Cell cell = dataRow.getCell(colScript);
            if (cell == null) cell = dataRow.createCell(colScript);
            cell.setCellValue(row.getName());
            cell.setCellStyle(style);
        }

        // Вызываемый сервис
        Cell serviceCell = dataRow.getCell(colService);
        if (serviceCell == null) serviceCell = dataRow.createCell(colService);
        serviceCell.setCellValue(row.getMethod() + " " + row.getUri());
        serviceCell.setCellStyle(style);

        // TPH
        Cell tphCell = dataRow.getCell(colTph);
        if (tphCell == null) tphCell = dataRow.createCell(colTph);
        tphCell.setCellValue(currentTph);
        tphCell.setCellStyle(style);

        // SLA
        Cell slaCell = dataRow.getCell(colSla);
        if (slaCell == null) slaCell = dataRow.createCell(colSla);
        slaCell.setCellValue(row.getSla());
        slaCell.setCellStyle(style);

        // Итерации
        Cell iterCell = dataRow.getCell(colIterations);
        if (iterCell == null) iterCell = dataRow.createCell(colIterations);
        iterCell.setCellValue(iterations);
        iterCell.setCellStyle(style);

        // Потоки
        Cell threadsCell = dataRow.getCell(colThreads);
        if (threadsCell == null) threadsCell = dataRow.createCell(colThreads);
        threadsCell.setCellValue(threads);
        threadsCell.setCellStyle(style);

        // Логины в час - ФОРМУЛА
        Cell loginCell = dataRow.getCell(colLogin);
        if (loginCell == null) loginCell = dataRow.createCell(colLogin);
        if (isStatic) {
            loginCell.setCellValue(0);
        } else {
            String tphColLetter = getColumnLetter(colTph);
            String iterColLetter = getColumnLetter(colIterations);
            String formula = String.format("ROUNDUP(%s%d/%s%d,0)",
                    tphColLetter, rowNum + 1, iterColLetter, rowNum + 1);
            loginCell.setCellFormula(formula);
        }
        loginCell.setCellStyle(style);

        Cell pacingCell = dataRow.getCell(colPacing);
        if (pacingCell == null) pacingCell = dataRow.createCell(colPacing);

        if (isStatic) {
            String tphColLetter = getColumnLetter(colTph);
            String threadsColLetter = getColumnLetter(colThreads);
            String formula = String.format("ROUND(3600/(%s%d/%s%d),0)",
                    tphColLetter, rowNum + 1, threadsColLetter, rowNum + 1);
            pacingCell.setCellFormula(formula);
        } else {
            // Проверяем, нужно ли скорректировать итерации
            int pacing = (int) Math.round(3600.0 / (((double) currentTph / iterations) / threads));

            if (pacing > 300 && iterations > 1) {
                // Список допустимых значений итераций (от меньшего к большему)
                int[] validIterations = {1, 5, 10, 20, 40, 60};

                int newIterations = iterations;
                int newPacing = pacing;

                // Ищем подходящее значение итераций
                for (int i = validIterations.length - 1; i >= 0; i--) {
                    int candidateIterations = validIterations[i];
                    if (candidateIterations < iterations) {
                        int candidatePacing = (int) Math.round(3600.0 / (((double) currentTph / candidateIterations) / threads));
                        if (candidatePacing <= 300) {
                            newIterations = candidateIterations;
                            newPacing = candidatePacing;
                            break;
                        }
                    }
                }

                if (iterCell != null && newIterations != iterations) {
                    iterCell.setCellValue(newIterations);
                    log.debug(" Скорректированы итерации: {} -> {} (пейсинг стал {})",
                            iterations, newIterations, newPacing);
                }

                // Используем скорректированные итерации для формулы
                String loginColLetter = getColumnLetter(colLogin);
                String threadsColLetter = getColumnLetter(colThreads);
                String formula = String.format("ROUND(3600/(%s%d/%s%d),0)",
                        loginColLetter, rowNum + 1, threadsColLetter, rowNum + 1);
                pacingCell.setCellFormula(formula);
            } else {
                // Пейсинг и так OK, просто ставим формулу
                String loginColLetter = getColumnLetter(colLogin);
                String threadsColLetter = getColumnLetter(colThreads);
                String formula = String.format("ROUND(3600/(%s%d/%s%d),0)",
                        loginColLetter, rowNum + 1, threadsColLetter, rowNum + 1);
                pacingCell.setCellFormula(formula);
            }
        }
        pacingCell.setCellStyle(style);
    }

    private void setBorders(CellStyle style) {
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
    }

    private int calculateThreads(int tph) {
        if (tph <= 100) return 3;
        if (tph <= 200) return 6;
        if (tph <= 500) return 8;
        if (tph <= 1000) return 10;
        if (tph <= 10000) return 20;
        if (tph <= 15000) return 24;
        if (tph <= 20000) return 28;
        if (tph <= 25000) return 32;
        if (tph <= 30000) return 36;
        if (tph <= 40000) return 40;
        if (tph <= 50000) return 44;
        return 60;
    }

    private int calculateIterations(int tph) {
        if (tph <= 900) return 1;
        if (tph <= 1000) return 10;
        if (tph <= 20000) return 20;
        if (tph <= 40000) return 40;
        return 60;
    }

    private String getColumnLetter(int colIndex) {
        StringBuilder letter = new StringBuilder();
        while (colIndex >= 0) {
            letter.insert(0, (char) ('A' + (colIndex % 26)));
            colIndex = colIndex / 26 - 1;
        }
        return letter.toString();
    }

    private String cleanUriFromParams(String uri) {
        if (uri == null) return null;
        String cleaned = uri.replaceAll("[\\p{Cf}]", "");
        int queryIndex = cleaned.indexOf('?');
        if (queryIndex != -1) {
            return cleaned.substring(0, queryIndex);
        }
        return cleaned;
    }

    public void generatePprbExcelReport(List<CsvRow> newRows, String projectType,
                                        Map<String, Integer> oldIntensityByService,
                                        Set<String> oldServices,
                                        Set<String> extraInScript) {

        List<CsvRow> filteredRows = new ArrayList<>();
        for (CsvRow row : newRows) {
            String normalizedUri = uriNormalizer.normalizeUri(row.getUri(), "PPRB");
            String redKey = row.getMethod() + "|" + normalizedUri;

            // Операция с интенсивностью 0 И отсутствует в скрипте - пропускаем
            boolean shouldSkip = row.getIntensity() == 0
                    && extraInScript != null
                    && !extraInScript.contains(redKey);

            if (shouldSkip) {
                log.info(" ППРБ: пропускаем операцию {} {} (интенсивность=0, нет в скрипте)",
                        row.getMethod(), row.getUri());
                continue;
            }

            filteredRows.add(row);
        }

        try {
            File templateFile = new File(DOC_FOLDER, TEMPLATE_FILE);
            String pagePrefix = "PPRB";

            Workbook workbook;
            if (templateFile.exists()) {
                workbook = new XSSFWorkbook(new FileInputStream(templateFile));
            } else {
                workbook = new XSSFWorkbook();
            }

            // Удаляем пустые страницы ППРБ
            for (int i = workbook.getNumberOfSheets() - 1; i >= 0; i--) {
                String sheetName = workbook.getSheetName(i);
                if (sheetName.startsWith(pagePrefix) && workbook.getSheetAt(i).getLastRowNum() == 0) {
                    workbook.removeSheetAt(i);
                }
            }

            Sheet lastSheet = findLastSheetByPrefix(workbook, pagePrefix);
            int nextReleaseNumber;
            String newSheetName;

            if (lastSheet == null) {
                nextReleaseNumber = 6026;
                newSheetName = pagePrefix + " 6.026";
            } else {
                int lastReleaseNumber = extractReleaseNumber(lastSheet.getSheetName());
                nextReleaseNumber = lastReleaseNumber + 1;
                newSheetName = pagePrefix + " " + formatReleaseNumber(nextReleaseNumber);
            }

            Sheet newSheet = workbook.createSheet(newSheetName);

            if (lastSheet != null) {
                copySheetStructure(lastSheet, newSheet);
                removeDelayAndFixHeadersPprb(newSheet);
                removeHeaderRowIfExists(newSheet);
            } else {
                createHeaderRowPprb(newSheet);
            }

            fillSheetWithDataPprb(newSheet, filteredRows, oldIntensityByService, oldServices, extraInScript);

            removeHeaderRowIfExists(newSheet);

            try (FileOutputStream fos = new FileOutputStream(templateFile)) {
                workbook.write(fos);
            }

            workbook.close();
            log.info(" ППРБ Excel отчёт обновлён: добавлена страница {}", newSheetName);

        } catch (IOException e) {
            log.error("Ошибка при создании ППРБ Excel отчёта: {}", e.getMessage());
        }
    }

    private void createHeaderRowPprb(Sheet sheet) {
        Row headerRow = sheet.createRow(0);
        String[] headers = {"Скрипт", "Вызываемый сервис", "TPH", "SLA", "Количество потоков", "Pacing"};

        for (int i = 0; i < headers.length; i++) {
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(headers[i]);
            sheet.setColumnWidth(i, 5000);
        }

        Font headerFont = sheet.getWorkbook().createFont();
        headerFont.setBold(true);
        CellStyle headerStyle = sheet.getWorkbook().createCellStyle();
        headerStyle.setFont(headerFont);
        setBorders(headerStyle);

        for (int i = 0; i < headers.length; i++) {
            headerRow.getCell(i).setCellStyle(headerStyle);
        }
    }

    private void removeDelayAndFixHeadersPprb(Sheet sheet) {
        Row oldHeaderRow = sheet.getRow(0);
        if (oldHeaderRow != null) {
            sheet.removeRow(oldHeaderRow);
        }
        createHeaderRowPprb(sheet);
    }

    private void fillSheetWithDataPprb(Sheet sheet, List<CsvRow> newRows,
                                       Map<String, Integer> oldIntensityByService,
                                       Set<String> oldServices,
                                       Set<String> extraInScript) {

        // Очищаем страницу перед заполнением
        for (int r = sheet.getLastRowNum(); r >= 1; r--) {
            Row row = sheet.getRow(r);
            if (row != null) {
                sheet.removeRow(row);
            }
        }

        // ПРИНУДИТЕЛЬНО УДАЛЯЕМ СТРОКУ 2, ЕСЛИ ОНА ПУСТАЯ
        Row row2 = sheet.getRow(2);
        if (row2 != null) {
            boolean isEmpty = true;
            for (int c = 0; c < row2.getLastCellNum(); c++) {
                Cell cell = row2.getCell(c);
                if (cell != null && cell.getCellType() != CellType.BLANK) {
                    if (cell.getCellType() == CellType.STRING && !cell.getStringCellValue().isEmpty()) {
                        isEmpty = false;
                        break;
                    } else if (cell.getCellType() == CellType.NUMERIC) {
                        isEmpty = false;
                        break;
                    }
                }
            }
            if (isEmpty && sheet.getLastRowNum() >= 3) {
                sheet.shiftRows(3, sheet.getLastRowNum(), -1);
            }
        }

        log.info(" ППРБ страница очищена, будет создано {} новых строк", newRows.size());

        Workbook workbook = sheet.getWorkbook();

        CellStyle goodStyle = workbook.createCellStyle();
        goodStyle.setFillForegroundColor(IndexedColors.LIGHT_GREEN.getIndex());
        goodStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        setBorders(goodStyle);

        CellStyle badStyle = workbook.createCellStyle();
        badStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
        badStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        setBorders(badStyle);

        CellStyle defaultStyle = workbook.createCellStyle();
        setBorders(defaultStyle);

        // Индексы колонок для ППРБ
        int colScript = 0;
        int colService = 1;
        int colTph = 2;
        int colSla = 3;
        int colThreads = 4;
        int colPacing = 5;

        int currentRow = 1;

        for (CsvRow row : newRows) {
            String normalizedUri = uriNormalizer.normalizeUri(row.getUri(), "PPRB");
            String serviceKey = uriNormalizer.normalizeService(row.getMethod(), row.getUri(), "PPRB");
            String redKey = row.getMethod() + "|" + normalizedUri;

            boolean shouldBeRed = row.getIntensity() == 0
                    && extraInScript != null
                    && extraInScript.contains(redKey);

            boolean isNew = oldServices == null || !oldServices.contains(serviceKey);
            Integer oldTph = oldIntensityByService != null ? oldIntensityByService.get(serviceKey) : null;
            boolean isIntensityChanged = oldTph != null && !oldTph.equals(row.getIntensity()) && row.getIntensity() > 0;

            Row dataRow = sheet.createRow(currentRow);

            // Определяем стиль для всей строки
            CellStyle rowStyle;
            if (shouldBeRed) {
                rowStyle = badStyle;
                log.info(" ППРБ: красим красным {} {} (интенсивность=0)",
                        row.getMethod(), row.getUri());
            } else if (isNew) {
                rowStyle = goodStyle;
                log.info(" ППРБ: новая операция {} {}", row.getMethod(), row.getUri());
            } else {
                rowStyle = defaultStyle;
            }

            int currentTph = row.getIntensity();
            int threads = calculateThreads(currentTph);

            // Скрипт
            Cell cellScript = dataRow.createCell(colScript);
            cellScript.setCellValue(row.getName());
            cellScript.setCellStyle(rowStyle);

            // Вызываемый сервис
            Cell cellService = dataRow.createCell(colService);
            cellService.setCellValue(row.getMethod() + " " + row.getUri());
            cellService.setCellStyle(rowStyle);

            // TPH
            Cell cellTph = dataRow.createCell(colTph);
            cellTph.setCellValue(currentTph);
            cellTph.setCellStyle(rowStyle);

            // SLA
            Cell cellSla = dataRow.createCell(colSla);
            cellSla.setCellValue(row.getSla());
            cellSla.setCellStyle(rowStyle);

            // Количество потоков
            Cell cellThreads = dataRow.createCell(colThreads);
            cellThreads.setCellValue(threads);
            cellThreads.setCellStyle(rowStyle);

            Cell cellPacing = dataRow.createCell(colPacing);
            if (shouldBeRed) {
                cellPacing.setCellValue(0);
            } else {
                String tphColLetter = getColumnLetter(colTph);
                String threadsColLetter = getColumnLetter(colThreads);
                // Формула с MIN - не даст пейсингу быть больше 300
                String formula = String.format("MIN(ROUND(3600/(%s%d/%s%d),0), 300)",
                        tphColLetter, currentRow + 1, threadsColLetter, currentRow + 1);
                cellPacing.setCellFormula(formula);
            }
            cellPacing.setCellStyle(rowStyle);


            // Если изменилась интенсивность - красим только TPH зелёным
            if (!shouldBeRed && !isNew && isIntensityChanged) {
                cellTph.setCellStyle(goodStyle);
                log.info(" ППРБ: изменилась интенсивность {} {}: {} -> {}",
                        row.getMethod(), row.getUri(), oldTph, currentTph);
            }

            currentRow++;
        }

        // Добавляем строку ИТОГО
        int totalRowNum = currentRow;
        Row totalRow = sheet.createRow(totalRowNum);

        Cell totalLabel = totalRow.createCell(colScript);
        totalLabel.setCellValue("ИТОГО");
        totalLabel.setCellStyle(defaultStyle);

        int lastDataRow = currentRow;
        if (lastDataRow < 2) lastDataRow = 2;

        String tphColLetter = getColumnLetter(colTph);
        String threadsColLetter = getColumnLetter(colThreads);

        Cell tphFormulaCell = totalRow.createCell(colTph);
        tphFormulaCell.setCellFormula("SUM(" + tphColLetter + "2:" + tphColLetter + lastDataRow + ")");
        tphFormulaCell.setCellStyle(defaultStyle);

        Cell threadsFormulaCell = totalRow.createCell(colThreads);
        threadsFormulaCell.setCellFormula("SUM(" + threadsColLetter + "2:" + threadsColLetter + lastDataRow + ")");
        threadsFormulaCell.setCellStyle(defaultStyle);

        log.info(" ППРБ страница создана, {} операций", newRows.size());
    }
}
package org.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;

@Slf4j
@Service
public class FileScanService {

    // Базовые папки
    private static final String BASE_PROFILE_ANALYST_FOLDER = "/home/retb_test/PPRB_OTP/Tools/GeneratorProfile" + "/profile_prom";
    private static final String BASE_SCRIPT_FOLDER = "/home/retb_test/PPRB_OTP/Tools/GeneratorProfile" + "/script";
    private static final String BASE_LAST_PROFILE_FOLDER = "/home/retb_test/PPRB_OTP/Tools/GeneratorProfile" + "/profile_last";
    private static final String BASE_OUTPUT_FOLDER = "/home/retb_test/PPRB_OTP/Tools/GeneratorProfile" + "/profile_current";

    public String getProfileAnalystFolder(String projectType) {
        return BASE_PROFILE_ANALYST_FOLDER + "/" + getProjectFolderName(projectType);
    }

    public String getScriptFolder(String projectType) {
        return BASE_SCRIPT_FOLDER + "/" + getProjectFolderName(projectType);
    }

    public String getLastProfileFolder(String projectType) {
        return BASE_LAST_PROFILE_FOLDER + "/" + getProjectFolderName(projectType);
    }

    public String getOutputFolder(String projectType) {
        return BASE_OUTPUT_FOLDER + "/" + getProjectFolderName(projectType);
    }

    private String getProjectFolderName(String projectType) {
        switch (projectType) {
            case "WEB":
                return "EFS_Web";
            case "MOBILE":
                return "EFS_Mobile";
            case "PPRB":
                return "PPRB";
            default:
                return "EFS_Web";
        }
    }

    public String getOutputFilename(String projectType) {
        switch (projectType) {
            case "WEB":
                return "BusinessApp_EFS_Web_Profile.csv";
            case "MOBILE":
                return "BusinessApp_EFS_Mobile_Profile.csv";
            case "PPRB":
                return "BusinessApp_PPRB_Profile.csv";
            default:
                return "BusinessApp_EFS_Web_Profile.csv";
        }
    }

    public String getUrlPrefix(String projectType) {
        switch (projectType) {
            case "WEB":
                return "/ic/ufs/corp-retail/";
            case "MOBILE":
                return "/ufs/mobile/corp-retail/rest/";
            case "PPRB":
                return "/api/";
            default:
                return "/ic/ufs/corp-retail/";
        }
    }

    public String getRequirement(String projectType) {
        switch (projectType) {
            case "WEB":
                return "BusinessApp_EFS_Web_main";
            case "MOBILE":
                return "BussinesApp_EFS_Mobile_main";
            case "PPRB":
                return "BusinessApp_PPRB_main";
            default:
                return "BusinessApp_EFS_Web_main";
        }
    }

    public File findFirstFileByExtension(String folderPath, String extension) {
        File folder = new File(folderPath);
        if (!folder.exists() || !folder.isDirectory()) {
            log.error("Папка не найдена: {}", folderPath);
            return null;
        }

        File[] files = folder.listFiles((dir, name) -> name.toLowerCase().endsWith(extension.toLowerCase()));

        if (files == null || files.length == 0) {
            log.warn("Файлов с расширением {} не найдено в папке: {}", extension, folderPath);
            return null;
        }

        File found = files[0];
        log.info("Найден файл: {}", found.getName());
        return found;
    }

    public boolean ensureFolderExists(String folderPath) {
        File folder = new File(folderPath);
        if (!folder.exists()) {
            log.info("Создаю папку: {}", folderPath);
            return folder.mkdirs();
        }
        return true;
    }
}

package org.example.service;

import lombok.extern.slf4j.Slf4j;
import org.example.model.CsvRow;
import org.example.model.OldProfileData;
import org.example.model.ProfileEntry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class GeneratorService {

    @Autowired
    private FileScanService fileScanService;

    @Autowired
    private ScriptParserService scriptParserService;

    @Autowired
    private ProfileAnalystParserService profileAnalystParserService;

    @Autowired
    private LastProfileReaderService lastProfileReaderService;

    @Autowired
    private CsvGeneratorService csvGeneratorService;

    @Autowired
    private ExcelReportService excelReportService;

    @Autowired
    private UriNormalizer uriNormalizer;

    public void generate(String projectType) {
        log.info("\n========================================");
        log.info("  ГЕНЕРАТОР ПРОФИЛЯ НАГРУЗОЧНОГО ТЕСТИРОВАНИЯ");
        log.info("  Проект: {}", projectType);
        log.info("========================================\n");

        String profileAnalystFolder = fileScanService.getProfileAnalystFolder(projectType);
        String scriptFolder = fileScanService.getScriptFolder(projectType);
        String lastProfileFolder = fileScanService.getLastProfileFolder(projectType);
        String outputFolder = fileScanService.getOutputFolder(projectType);

        String outputFilename = fileScanService.getOutputFilename(projectType);
        String urlPrefix = fileScanService.getUrlPrefix(projectType);
        String requirement = fileScanService.getRequirement(projectType);

        fileScanService.ensureFolderExists(profileAnalystFolder);
        fileScanService.ensureFolderExists(scriptFolder);
        fileScanService.ensureFolderExists(lastProfileFolder);
        fileScanService.ensureFolderExists(outputFolder);

        log.info(" Поиск JSON скрипта в папке: {}", scriptFolder);
        File scriptFile = fileScanService.findFirstFileByExtension(scriptFolder, ".json");
        if (scriptFile == null) {
            log.error(" JSON скрипт не найден! Завершение работы.");
            return;
        }

        Map<String, List<String>> urlToOpIds = scriptParserService.parseScriptAndBuildUrlToOpIdMap(scriptFile, urlPrefix);

        log.info("\n Поиск CSV профиля аналитика в папке: {}", profileAnalystFolder);
        File profileAnalystFile = fileScanService.findFirstFileByExtension(profileAnalystFolder, ".csv");
        if (profileAnalystFile == null) {
            log.error(" CSV профиль аналитика не найден! Завершение работы.");
            return;
        }

        List<ProfileEntry> profileEntries = profileAnalystParserService.parseProfileAnalyst(profileAnalystFile, projectType);
        if (profileEntries.isEmpty()) {
            log.error(" Профиль аналитика пуст! Завершение работы.");
            return;
        }

        log.info("\n Поиск прошлого профиля в папке: {}", lastProfileFolder);
        File lastProfileFile = fileScanService.findFirstFileByExtension(lastProfileFolder, ".xlsx");

        // Загружаем данные из прошлого профиля
        Map<String, OldProfileData> lastProfileData = lastProfileReaderService.loadLastProfile(lastProfileFile);

        log.info("");

        // Генерируем профиль (CSV уже сохраняется внутри этого метода)
        List<CsvRow> rows = csvGeneratorService.generateProfile(urlToOpIds, profileEntries, lastProfileData,
                outputFolder, outputFilename, requirement, urlPrefix, projectType);

        Set<String> extraInScript = csvGeneratorService.getLastExtraInScript();

        // Логируем для контроля
        if (!extraInScript.isEmpty()) {
            log.info("\n Передаём в ExcelReportService {} операций для покраски красным:", extraInScript.size());
            for (String key : extraInScript) {
                log.info("   {}", key);
            }
        }

        if ("WEB".equals(projectType) || "MOBILE".equals(projectType)) {
            // Существующая логика для WEB и MOBILE
            generateWebMobileReport(projectType, rows, lastProfileData, extraInScript, outputFolder, outputFilename);
        } else if ("PPRB".equals(projectType)) {
            // Новая логика для ППРБ
            generatePprbReport(projectType, rows, lastProfileData, extraInScript, outputFolder, outputFilename);
        }

        log.info("\n========================================");
        log.info("   РАБОТА ЗАВЕРШЕНА");
        log.info("========================================");
    }

    private void generateWebMobileReport(String projectType, List<CsvRow> rows,
                                         Map<String, OldProfileData> lastProfileData,
                                         Set<String> extraInScript,
                                         String outputFolder, String outputFilename) {

        // Загружаем старые данные
        Map<String, Integer> oldIntensityByService = new HashMap<>();
        Set<String> oldServices = new HashSet<>();

        log.info("=== Загрузка данных из прошлого профиля ===");

        for (Map.Entry<String, OldProfileData> entry : lastProfileData.entrySet()) {
            String opName = entry.getKey();
            OldProfileData data = entry.getValue();

            if (data.getFullService() != null && !data.getFullService().isEmpty()) {
                String fullService = data.getFullService();
                String method;
                String uri;

                if (fullService.contains(" ")) {
                    method = fullService.substring(0, fullService.indexOf(' '));
                    uri = fullService.substring(fullService.indexOf(' ') + 1);
                } else {
                    log.warn("  ⚠️ Не удалось разделить fullService: {}", fullService);
                    continue;
                }

                String serviceKey = uriNormalizer.normalizeService(method, uri, projectType);
                oldIntensityByService.put(serviceKey, data.getIntensity());
                oldServices.add(serviceKey);
                log.info("   Загружен: {} -> интенсивность: {}", serviceKey, data.getIntensity());
            } else {
                log.warn("  ⚠️ Нет fullService для операции: {}, пробуем восстановить из operationId", opName);
                String method = extractMethodFromOperationId(opName);
                String uri = extractUriFromOperationId(opName);

                if (uri != null && !uri.isEmpty()) {
                    String normalizedUri = uriNormalizer.normalizeUri(uri, projectType);
                    String serviceKey = method + " " + normalizedUri;
                    oldIntensityByService.put(serviceKey, data.getIntensity());
                    oldServices.add(serviceKey);
                    log.info("   Восстановлен: {} -> интенсивность: {}", serviceKey, data.getIntensity());
                }
            }
        }

        // Разделяем строки для Excel
        List<CsvRow> rowsForExcel = new ArrayList<>();

        for (CsvRow row : rows) {
            if (row.getIntensity() > 0) {
                rowsForExcel.add(row);
            } else {
                String serviceKey = uriNormalizer.normalizeService(row.getMethod(), row.getUri(), projectType);
                if (oldServices.contains(serviceKey)) {
                    rowsForExcel.add(row);
                    log.info(" Операция с интенсивностью 0, но была в прошлом профиле: {} {} -> будет красной в Excel",
                            row.getMethod(), row.getUri());
                }
                else {
                    log.debug(" Пропущена (интенсивность 0 и нет в прошлом профиле): {} {}",
                            row.getMethod(), row.getUri());
                }
            }
        }

        // Отправляем в Excel
        excelReportService.generateExcelReport(rowsForExcel, projectType, oldIntensityByService, oldServices, extraInScript);
    }

    private void generatePprbReport(String projectType, List<CsvRow> rows,
                                    Map<String, OldProfileData> lastProfileData,
                                    Set<String> extraInScript,
                                    String outputFolder, String outputFilename) {

        // Загружаем старые данные (если есть)
        Map<String, Integer> oldIntensityByService = new HashMap<>();
        Set<String> oldServices = new HashSet<>();

        if (lastProfileData != null && !lastProfileData.isEmpty()) {
            log.info("=== Загрузка данных из прошлого профиля ППРБ ===");
            for (Map.Entry<String, OldProfileData> entry : lastProfileData.entrySet()) {
                OldProfileData data = entry.getValue();
                if (data.getFullService() != null && !data.getFullService().isEmpty()) {
                    String fullService = data.getFullService();
                    if (fullService.contains(" ")) {
                        String method = fullService.substring(0, fullService.indexOf(' '));
                        String uri = fullService.substring(fullService.indexOf(' ') + 1);

                        // ОТЛАДКА: что пришло из прошлого профиля
                        log.info("  raw: method='{}', uri='{}'", method, uri);

                        String serviceKey = uriNormalizer.normalizeService(method, uri, projectType);

                        // ОТЛАДКА: что получилось после нормализации
                        log.info("  normalized: serviceKey='{}'", serviceKey);

                        oldIntensityByService.put(serviceKey, data.getIntensity());
                        oldServices.add(serviceKey);
                    }
                }
            }
        }

        List<CsvRow> rowsForExcel = new ArrayList<>(rows);
        excelReportService.generatePprbExcelReport(rowsForExcel, projectType, oldIntensityByService, oldServices, extraInScript);
    }

    private String extractMethodFromOperationId(String operationId) {
        if (operationId == null) return "GET";
        if (operationId.contains("POST")) return "POST";
        if (operationId.contains("GET")) return "GET";

        String[] parts = operationId.split("_");
        for (String part : parts) {
            if (part.equalsIgnoreCase("POST") || part.equalsIgnoreCase("GET")) {
                return part.toUpperCase();
            }
        }
        return "GET";
    }

    private String extractUriFromOperationId(String operationId) {
        if (operationId == null || !operationId.contains("_")) {
            return operationId;
        }

        String withoutUc = operationId.substring(operationId.indexOf('_') + 1);
        withoutUc = withoutUc.replaceFirst("^POST_", "");
        withoutUc = withoutUc.replaceFirst("^GET_", "");
        withoutUc = withoutUc.replace("_WEB", "");

        String uri = "/ic/ufs/corp-retail/rest/" + withoutUc.replace("_", "/");
        return uri;
    }
}

package org.example.service;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.example.model.OldProfileData;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileInputStream;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Service
public class LastProfileReaderService {

    private static final int DEFAULT_SLA = 3;
    private static final int DEFAULT_INTENSITY = 300000;

    public Map<String, OldProfileData> loadLastProfile(File lastProfileFile) {
        Map<String, OldProfileData> operationToData = new HashMap<>();

        log.info("=== ЗАГРУЗКА ПРОШЛОГО ПРОФИЛЯ ===");
        log.info("Файл: {}", lastProfileFile != null ? lastProfileFile.getAbsolutePath() : "null");

        if (lastProfileFile == null || !lastProfileFile.exists()) {
            log.warn("Файл прошлого профиля не найден");
            return operationToData;
        }

        log.info("Файл существует, размер: {} байт", lastProfileFile.length());

        try (FileInputStream fis = new FileInputStream(lastProfileFile);
             Workbook workbook = new XSSFWorkbook(fis)) {

            //log.info("Количество листов в файле: {}", workbook.getNumberOfSheets());

            Sheet sheet = workbook.getSheetAt(0);
            //log.info("Первый лист: {}", sheet.getSheetName());
            //log.info("Последняя строка: {}", sheet.getLastRowNum());

            Row headerRow = null;
            int headerRowNum = -1;
            for (int r = 0; r <= 5; r++) {
                Row row = sheet.getRow(r);
                if (row == null) continue;

                // Ищем в любой колонке строки
                for (int i = 0; i < row.getLastCellNum(); i++) {
                    Cell cell = row.getCell(i);
                    if (cell != null) {
                        String cellValue = getCellValueAsString(cell);
                        if (cellValue != null && (cellValue.contains("Наименование операции") ||
                                cellValue.contains("Скрипт") ||
                                cellValue.contains("Вызываемый сервис"))) {
                            headerRow = row;
                            headerRowNum = r;
                            break;
                        }
                    }
                }
                if (headerRow != null) break;
            }

            if (headerRow == null) {
                log.error("Заголовок не найден");
                return operationToData;
            }

            //log.info("Заголовки найдены в строке {}", headerRowNum);

            // Выводим заголовки
          /*  log.info("Заголовки колонок:");
            for (int i = 0; i < headerRow.getLastCellNum(); i++) {
                Cell cell = headerRow.getCell(i);
                String value = cell != null ? getCellValueAsString(cell) : "null";
                log.info("  Колонка {}: '{}'", i, value);
            } */

            // Динамически определяем индексы колонок
            int colService = findColumn(headerRow, "Вызываемый сервис");
            int colIntensity = findColumn(headerRow, "TPH");
            int colSla = findColumn(headerRow, "SLA");
            int colOperationName = findColumn(headerRow, "Скрипт");

            log.info("Найдены колонки: service={}, intensity={}, sla={}, operationName={}",
                    colService, colIntensity, colSla, colOperationName);

            if (colService == -1) {
                log.warn("Колонка 'Вызываемый сервис' не найдена");
            }

            int rowCount = 0;
            int skippedCount = 0;
            int rowNumber = 0;

            for (Row row : sheet) {
                rowNumber++;

                if (rowNumber == 1) {
                    continue; // пропускаем заголовок
                }

                // Получаем полный "Вызываемый сервис" (может быть null)
                String fullService = null;
                if (colService != -1) {
                    Cell serviceCell = row.getCell(colService);
                    if (serviceCell != null) {
                        fullService = getCellValueAsString(serviceCell);
                    }
                }

                // Получаем интенсивность
                int intensity = DEFAULT_INTENSITY;
                if (colIntensity != -1) {
                    Cell intensityCell = row.getCell(colIntensity);
                    if (intensityCell != null) {
                        Integer parsedIntensity = getCellValueAsInteger(intensityCell);
                        if (parsedIntensity != null) {
                            intensity = parsedIntensity;
                        }
                    }
                }

                // Получаем SLA
                int sla = DEFAULT_SLA;
                if (colSla != -1) {
                    Cell slaCell = row.getCell(colSla);
                    if (slaCell != null) {
                        Integer parsedSla = getCellValueAsInteger(slaCell);
                        if (parsedSla != null) {
                            sla = parsedSla;
                        }
                    }
                }

                // Получаем operationId
                String operationName = null;
                if (colOperationName != -1) {
                    Cell nameCell = row.getCell(colOperationName);
                    if (nameCell != null) {
                        operationName = getCellValueAsString(nameCell);
                    }
                }

                if (operationName == null || operationName.isEmpty()) {
                    log.debug("Строка {} пропущена: нет operationName", rowNumber);
                    skippedCount++;
                    continue;
                }

                operationToData.put(operationName, new OldProfileData(fullService, intensity, sla));
                rowCount++;

                if (rowCount <= 10) {
                    log.info("  Загружена операция {}: '{}' -> интенсивность={}", rowCount, operationName, intensity);
                }
            }

            log.info("Загружено {} операций из прошлого профиля (пропущено строк: {})", rowCount, skippedCount);

        } catch (Exception e) {
            log.error("Ошибка при чтении прошлого профиля: {}", e.getMessage(), e);
        }

        return operationToData;
    }

    /**
     * Найти индекс колонки по названию
     */
    private int findColumn(Row headerRow, String... possibleNames) {
        for (int i = 0; i < headerRow.getLastCellNum(); i++) {
            Cell cell = headerRow.getCell(i);
            if (cell != null) {
                String cellValue = getCellValueAsString(cell);
                if (cellValue != null) {
                    for (String name : possibleNames) {
                        if (cellValue.equalsIgnoreCase(name) || cellValue.contains(name)) {
                            return i;
                        }
                    }
                }
            }
        }
        return -1;
    }

    /**
     * Получить значение ячейки как строку
     */
    private String getCellValueAsString(Cell cell) {
        if (cell == null) return null;

        switch (cell.getCellType()) {
            case STRING:
                return cell.getStringCellValue().trim();
            case NUMERIC:
                double numValue = cell.getNumericCellValue();
                // Проверка на NaN и Infinity
                if (Double.isNaN(numValue) || Double.isInfinite(numValue)) {
                    return null;
                }
                if (numValue == (long) numValue) {
                    return String.valueOf((long) numValue);
                } else {
                    return String.valueOf(numValue);
                }
            case BOOLEAN:
                return String.valueOf(cell.getBooleanCellValue());
            case FORMULA:
                try {
                    return cell.getStringCellValue();
                } catch (IllegalStateException e) {
                    try {
                        double numericValue = cell.getNumericCellValue();
                        if (numericValue == (long) numericValue) {
                            return String.valueOf((long) numericValue);
                        } else {
                            return String.valueOf(numericValue);
                        }
                    } catch (Exception ex) {
                        return null;
                    }
                }
            default:
                return null;
        }
    }

    /**
     * Получить значение ячейки как Integer
     */
    private Integer getCellValueAsInteger(Cell cell) {
        if (cell == null) return null;

        try {
            switch (cell.getCellType()) {
                case NUMERIC:
                    return (int) cell.getNumericCellValue();

                case STRING:
                    String strValue = cell.getStringCellValue().trim();
                    if (strValue.isEmpty()) return null;
                    strValue = strValue.replaceAll("[^0-9]", "");
                    if (strValue.isEmpty()) return null;
                    return Integer.parseInt(strValue);

                case FORMULA:
                    try {
                        return (int) cell.getNumericCellValue();
                    } catch (Exception e) {
                        String formulaValue = cell.getStringCellValue();
                        if (formulaValue != null && !formulaValue.isEmpty()) {
                            formulaValue = formulaValue.replaceAll("[^0-9]", "");
                            if (!formulaValue.isEmpty()) {
                                return Integer.parseInt(formulaValue);
                            }
                        }
                        return null;
                    }

                default:
                    return null;
            }
        } catch (Exception e) {
            log.debug("Не удалось преобразовать ячейку в число: {}", e.getMessage());
            return null;
        }
    }
}

package org.example.service;

import org.example.model.ProfileEntry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Service
public class ProfileAnalystParserService {

    private static final String MOBILE_WRONG_PREFIX = "/ic/ufs/mobile/corp-retail/rest/";
    private static final String MOBILE_CORRECT_PREFIX = "/ufs/mobile/corp-retail/rest/";

    public List<ProfileEntry> parseProfileAnalyst(File profileFile, String projectType) {
        List<ProfileEntry> entries = new ArrayList<>();

        try (BufferedReader reader = new BufferedReader(
                new FileReader(profileFile, StandardCharsets.UTF_8))) {

            String line;
            boolean isFirstLine = true;
            int lineNumber = 0;

            while ((line = reader.readLine()) != null) {
                lineNumber++;
                line = line.trim();
                if (line.isEmpty()) continue;

                if (isFirstLine && (line.startsWith("URI") || line.toLowerCase().contains("uri"))) {
                    isFirstLine = false;
                    continue;
                }
                isFirstLine = false;

                String[] parts = line.split(",", 2);
                if (parts.length < 2) {
                    log.warn("Строка {} пропущена (неверный формат): {}", lineNumber, line);
                    continue;
                }

                String uriWithMethod = parts[0].trim();
                int intensity;
                try {
                    intensity = Integer.parseInt(parts[1].trim());
                } catch (NumberFormatException e) {
                    log.warn("Строка {} пропущена (неверная интенсивность): {}", lineNumber, line);
                    continue;
                }

                // Очищаем URI от невидимых символов
                uriWithMethod = cleanUri(uriWithMethod);

                String uri, method;
                if (uriWithMethod.contains(" ")) {
                    String[] splitParts = uriWithMethod.split(" ", 2);  // ← переименовано
                    String first = splitParts[0].trim();
                    String second = splitParts[1].trim();

                    // Определяем, что где: если первая часть начинается с / - это URI
                    if (first.startsWith("/")) {
                        uri = first;
                        method = second.toUpperCase();
                    } else {
                        method = first.toUpperCase();
                        uri = second;
                    }
                } else {
                    uri = uriWithMethod;
                    method = "GET";
                }

                // ========== НОРМАЛИЗАЦИЯ ПРЕФИКСА ДЛЯ МОБИЛКИ ==========
                if ("MOBILE".equals(projectType)) {
                    // Приводим к нижнему регистру для проверки
                    String lowerUri = uri.toLowerCase();

                    // Убираем /ic/ufs/... → /ufs/...
                    if (lowerUri.startsWith("/ic/ufs/")) {
                        uri = "/ufs/" + uri.substring(5);  // убираем "/ic/ufs/"
                        log.debug("  Исправлен префикс /ic/ufs/ → /ufs/: {}", uri);
                    }

                    // Убираем лишний /fs/ если появился (например, /ufs/fs/mobile/... → /ufs/mobile/...)
                    uri = uri.replaceFirst("/fs/", "/");

                    // Убираем множественные слеши
                    uri = uri.replaceAll("/+", "/");

                }
                // =====================================================

                // Дополнительная очистка
                uri = cleanUri(uri);

                if (uri == null || uri.isEmpty()) {
                    log.warn("Строка {} пропущена (URI пуст после очистки): {}", lineNumber, line);
                    continue;
                }

                ProfileEntry entry = new ProfileEntry();
                entry.setUri(uri);
                entry.setMethod(method);
                entry.setIntensity(intensity);
                entries.add(entry);

                log.debug("Добавлена операция: метод={}, uri={}, интенсивность={}", method, uri, intensity);
            }

            log.info("Загружено {} записей из профиля аналитика", entries.size());

        } catch (Exception e) {
            log.error("Ошибка при парсинге профиля аналитика: {}", e.getMessage());
        }

        return entries;
    }

    /**
     * Агрессивная очистка URI от невидимых символов
     */
    private String cleanUri(String uri) {
        if (uri == null) return null;

        // Убираем все управляющие символы
        String cleaned = uri.replaceAll("[\\p{Cntrl}]", "");

        // Убираем неразрывные пробелы и другие спецсимволы
        cleaned = cleaned.replaceAll("[\\u00A0\\u2000-\\u200F\\u2028-\\u202F\\u205F\\u3000]", " ");

        // Убираем символ � и другие непечатные
        cleaned = cleaned.replaceAll("[^\\x20-\\x7E]", "");

        // Заменяем множественные пробелы на один
        cleaned = cleaned.replaceAll("\\s+", " ");

        // Убираем пробелы в начале и конце
        cleaned = cleaned.trim();

        return cleaned;
    }
}

package org.example.service;

import com.google.gson.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileReader;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@Service
public class ScriptParserService {

    private final Gson gson = new Gson();

    public Map<String, List<String>> parseScriptAndBuildUrlToOpIdMap(File scriptFile, String urlPrefix) {
        Map<String, List<String>> urlToOpIds = new HashMap<>();

        try (FileReader reader = new FileReader(scriptFile)) {
            JsonElement root = JsonParser.parseReader(reader);

            AtomicInteger totalFound = new AtomicInteger(0);
            AtomicInteger businessOpsCount = new AtomicInteger(0);
            AtomicInteger authOps = new AtomicInteger(0);

            traverseJson(root, urlToOpIds, urlPrefix, totalFound, businessOpsCount, authOps);

            // Подсчитываем реальное количество уникальных операций (по operation_id)
            Set<String> uniqueOperationIds = new HashSet<>();
            for (List<String> opIds : urlToOpIds.values()) {
                uniqueOperationIds.addAll(opIds);
            }

            log.info("========================================");
            log.info("Статистика парсинга скрипта:");
            log.info("  Всего операций с url и operation_id: {}", totalFound.get());
            log.info("   Бизнес операции (префикс {}): {}", urlPrefix, businessOpsCount.get());
            log.info("   Операции авторизации (остальные): {}", authOps.get());
            log.info("========================================");
            log.info("  Уникальных URI: {}", urlToOpIds.size());
            log.info("  Уникальных операций (по operation_id): {}", uniqueOperationIds.size());
            log.info("========================================");

        } catch (Exception e) {
            log.error("Ошибка при парсинге скрипта: {}", e.getMessage(), e);
        }

        return urlToOpIds;
    }

    private void traverseJson(JsonElement element,
                              Map<String, List<String>> urlToOpIds,
                              String urlPrefix,
                              AtomicInteger totalFound,
                              AtomicInteger businessOpsCount,
                              AtomicInteger authOps) {
        if (element == null) return;

        if (element.isJsonObject()) {
            JsonObject obj = element.getAsJsonObject();

            if (obj.has("url") && obj.has("operation_id") && obj.has("method")) {
                totalFound.incrementAndGet();
                String url = obj.get("url").getAsString();
                String operationId = obj.get("operation_id").getAsString();

                String cleanUrl = cleanUrl(url);

                if (cleanUrl != null && cleanUrl.startsWith(urlPrefix)) {
                    businessOpsCount.incrementAndGet();

                    // Добавляем operation_id в список для этого URI
                    urlToOpIds.computeIfAbsent(cleanUrl, k -> new ArrayList<>()).add(operationId);
                    log.debug("   Бизнес: {} → {}", cleanUrl, operationId);
                } else {
                    authOps.incrementAndGet();
                    log.debug("   Авторизация: {} → {}", cleanUrl, operationId);
                }
            }

            for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
                traverseJson(entry.getValue(), urlToOpIds, urlPrefix,
                        totalFound, businessOpsCount, authOps);
            }

        } else if (element.isJsonArray()) {
            JsonArray array = element.getAsJsonArray();
            for (JsonElement item : array) {
                traverseJson(item, urlToOpIds, urlPrefix,
                        totalFound, businessOpsCount, authOps);
            }
        }
    }

    private String cleanUrl(String url) {
        if (url == null) return null;

        String cleaned = url.trim();
        int queryIndex = cleaned.indexOf('?');
        if (queryIndex != -1) {
            cleaned = cleaned.substring(0, queryIndex);
        }
        if (cleaned.endsWith("/")) {
            cleaned = cleaned.substring(0, cleaned.length() - 1);
        }
        return cleaned;
    }
}

package org.example.service;

import org.springframework.stereotype.Component;

@Component
public class UriNormalizer {

    public String normalizeUri(String uri, String projectType) {
        if (uri == null) return null;

        // 1. Убираем невидимые символы
        String cleaned = uri.replaceAll("[\\p{Cf}]", "");

        // 2. Заменяем все виды пробелов на обычный
        cleaned = cleaned.replaceAll("[\\s\\u00A0]+", " ");

        // 3. Убираем пробелы в начале и конце
        cleaned = cleaned.trim();

        // 4. Приводим к нижнему регистру для единообразия
        cleaned = cleaned.toLowerCase();

        // 5. Убираем префиксы для разных проектов
        if ("WEB".equals(projectType)) {
            cleaned = cleaned.replaceFirst("^/ic/ufs/corp-retail/rest/?", "");
            cleaned = cleaned.replaceFirst("^/ic/ufs/corp-retail/?", "");
            cleaned = cleaned.replaceFirst("^/rest/?", "");
        } else if ("MOBILE".equals(projectType)) {
            cleaned = cleaned.replaceFirst("^/ic/ufs/mobile/corp-retail/rest/?", "");
            cleaned = cleaned.replaceFirst("^/ufs/mobile/corp-retail/rest/?", "");
        } else if ("PPRB".equals(projectType)) {
            // Сначала убираем ведущий слеш
            if (cleaned.startsWith("/")) {
                cleaned = cleaned.substring(1);
            }
            // Потом убираем /api/ в начале
            cleaned = cleaned.replaceFirst("^api/", "");
            // Убираем все вхождения /api/ (для случаев /api//api/)
            cleaned = cleaned.replaceAll("/api/", "/");
        }

        // 6. Убираем множественные слеши (один раз после всех замен)
        cleaned = cleaned.replaceAll("/+", "/");

        // 7. Убираем ведущий и trailing слеши
        if (cleaned.startsWith("/")) cleaned = cleaned.substring(1);
        if (cleaned.endsWith("/")) cleaned = cleaned.substring(0, cleaned.length() - 1);

        // 8. Заменяем множественные подчёркивания на одно (если есть)
        cleaned = cleaned.replaceAll("_+", "_");

        // 9. Убираем любые оставшиеся пробелы
        cleaned = cleaned.replaceAll(" ", "");

        return cleaned;
    }

    public String normalizeService(String method, String uri, String projectType) {
        String normalizedMethod = method.toUpperCase();
        String normalizedUri = normalizeUri(uri, projectType);
        return normalizedMethod + " " + normalizedUri;
    }

    public String normalizeString(String input) {
        if (input == null) return null;
        String cleaned = input.replaceAll("[\\p{Cf}]", "");
        cleaned = cleaned.replaceAll("[\\s\\u00A0]+", " ");
        cleaned = cleaned.trim();
        cleaned = cleaned.toLowerCase();
        return cleaned;
    }
}

package org.example;

import org.example.service.GeneratorService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@Slf4j
@SpringBootApplication
public class ProfileGeneratorApplication implements CommandLineRunner {
    @Autowired
    private GeneratorService generatorService;

    public static void main(String[] args) {
        // Проверяем, есть ли параметр для автоматического запуска
        if (args.length > 0 && !args[0].equals("--server")) {
            // Режим автоматического запуска без веб-сервера
            SpringApplication app = new SpringApplication(ProfileGeneratorApplication.class);
            app.setWebApplicationType(WebApplicationType.NONE);
            app.run(args);
        } else {
            // Обычный запуск с веб-сервером
            SpringApplication.run(ProfileGeneratorApplication.class, args);
        }
    }

    @Override
    public void run(String... args) throws Exception {
        // Если передан параметр для автоматической генерации
        if (args.length > 0) {
            String projectNumber = args[0];
            String projectType = convertProjectNumberToType(projectNumber);
            if (projectType != null) {
                log.info("Автоматический запуск для проекта: {}", projectType);
                generatorService.generate(projectType);
                //System.exit(0);
            } else {
                log.error("Неверный номер проекта: {}", projectNumber);
                System.exit(1);
            }
        }
    }

    private String convertProjectNumberToType(String projectNumber) {
        switch (projectNumber) {
            case "1": return "WEB";
            case "2": return "MOBILE";
            case "3": return "PPRB";
            default: return null;
        }
    }
}