package backend.academy.service.impl;

import backend.academy.model.LogRecord;
import backend.academy.model.LogRecordField;
import backend.academy.model.LogReport;
import backend.academy.model.Statistics;
import backend.academy.model.cli.params.AnalyzerParam;
import backend.academy.service.Parser;
import backend.academy.service.Processor;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Stream;

public class LogProcessor implements Processor<LogReport> {

    private final Parser<LogRecord> parser;

    public LogProcessor(Parser<LogRecord> parser) {
        this.parser = parser;
    }

    @Override
    public LogReport process(Stream<String> logRecordStream, AnalyzerParam param) {

        Statistics statistics = new Statistics();
        logRecordStream
            .map(logRecordString -> {
                try {
                    return parser.parse(logRecordString);
                } catch (Exception e) {
                    System.err.println("Failed to parse log record: " + logRecordString + " - " + e.getMessage());
                    return null;
                }
            })
            .filter(Objects::nonNull)
            .filter(logRecord -> doAdditionalFilter(param, logRecord))
            .filter(logRecord -> logRecord.time().isBefore(param.endTime()))
            .filter(logRecord -> logRecord.time().isAfter(param.startTime()))
            .forEach(logRecord -> collectStatistics(logRecord, statistics));

        return generateReport(statistics);
    }

    private boolean doAdditionalFilter(AnalyzerParam param, LogRecord logRecord) {

        if (Objects.isNull(param.filterField())) {
            return true;
        }

        LogRecordField field = param.filterField();
        String value = param.filterValue();

        Pattern pattern = Pattern.compile(value);
        return switch (field) {
            case REMOTE_ADDRESS -> matchField(logRecord.remoteAddr(), pattern);
            case REMOTE_USER -> matchField(logRecord.remoteUser(), pattern);
            case TIME -> matchField(logRecord.time().toString(), pattern);
            case REQUEST -> matchField(logRecord.request(), pattern);
            case STATUS -> matchField(logRecord.status().toString(), pattern);
            case BODY_BYTES -> matchField(logRecord.bodyBytes().toString(), pattern);
            case HTTP_REFERER -> matchField(logRecord.httpReferer(), pattern);
            case HTTP_USER_AGENT -> matchField(logRecord.httpUserAgent(), pattern);
        };
    }

    private boolean matchField(String value, Pattern pattern) {
        return pattern.matcher(value).matches();
    }

    private LogReport generateReport(Statistics statistics) {
        LogReport report = new LogReport();
        report.mostRequestedResource(statistics.getTopResources());
        report.mostOccurringStatusCode(statistics.getTopStatusCodes());
        report.averageResponseSize(statistics.getAverage());
        report.percentileOfSize(statistics.getPercentileOfSize());
        report.uniqueRemoteAddrCount(statistics.uniqueRemoteAddresses().size());
        report.errorRate(statistics.getErrorRate());
        report.requestsCount(statistics.total());
        return report;
    }

    private void collectStatistics(LogRecord logRecord, Statistics statistics) {
        statistics.bodyBytesSent().add(logRecord.bodyBytes());
        statistics.statusCodes().merge(logRecord.status(), 1, Integer::sum);
        statistics.uniqueRemoteAddresses().add(logRecord.remoteAddr());
        statistics.total(statistics.total() + 1);

        String[] request = logRecord.request().split(" ");
        if (request.length >= 2) {
            statistics.sourceRequests().merge(request[1], 1, Integer::sum);
        }
    }
}


package backend.academy.model;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import com.google.common.math.Quantiles;
import com.google.common.primitives.Ints;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Statistics {

    private final Map<String, Integer> sourceRequests = new TreeMap<>();

    private final SortedMap<Integer, Integer> statusCodes = new TreeMap<>();
    private final List<Integer> bodyBytesSent = new ArrayList<>();
    private final Set<String> uniqueRemoteAddresses = new HashSet<>();

    private int total = 0;

    public PriorityQueue<Map.Entry<String, Integer>> getTopResources() {
        PriorityQueue<Map.Entry<String, Integer>> topResources = new PriorityQueue<>(
            (e1, e2) -> Integer.compare(e2.getValue(), e1.getValue())
        );
        topResources.addAll(sourceRequests.entrySet());
        return topResources;
    }

    public PriorityQueue<Map.Entry<Integer, Integer>> getTopStatusCodes() {
        PriorityQueue<Map.Entry<Integer, Integer>> topStatusCodes = new PriorityQueue<>(
            (e1, e2) -> Integer.compare(e2.getValue(), e1.getValue())
        );
        topStatusCodes.addAll(statusCodes.entrySet());
        return topStatusCodes;
    }

    public Double getErrorRate() {
        int n = 0;
        for (Map.Entry<Integer, Integer> entry : statusCodes.entrySet()) {
            if (entry.getValue() / 100 == 4 || entry.getValue() / 100 == 5) {
                n++;
            }
        }
        return (double) n / (double) total;
    }

    public Double getAverage() {
        return bodyBytesSent.stream()
            .mapToInt(Integer::intValue)
            .average()
            .orElse(0);
    }

    public Double getPercentileOfSize() {
        if (bodyBytesSent.isEmpty()) {
            return 0.0;
        }
        return Quantiles.percentiles().index(95).compute(Ints.toArray(bodyBytesSent));
    }
}

package backend.academy.model;

import java.time.ZonedDateTime;

public record LogRecord(
    String remoteAddr,
    String remoteUser,
    ZonedDateTime time,
    String request,
    Integer status,
    Integer bodyBytes,
    String httpReferer,
    String httpUserAgent
) {
}

package backend.academy.model;

import java.util.Map;
import java.util.PriorityQueue;
import backend.academy.model.cli.params.AnalyzerParam;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class LogReport {

    private PriorityQueue<Map.Entry<String, Integer>> mostRequestedResource;
    private PriorityQueue<Map.Entry<Integer, Integer>> mostOccurringStatusCode;
    private Double averageResponseSize;
    private Double percentileOfSize;        // 95%
    private Integer uniqueRemoteAddrCount;  // количество уникальных ip адресов
    private Double errorRate;               // процент запросов с ошибкой 4**, 5**
    private Integer requestsCount;          // количество запросов
    private AnalyzerParam fileInfo;
}

напиши 5 тестов на LogProcessor в одном файле