#include "process_yt_tables.h"

#include "mapreduce/yt/interface/operation-inl.h"
#include "maps/libs/geolib/include/distance.h"
#include "maps/libs/geolib/include/point.h"
#include "maps/wikimap/mapspro/services/mrc/long_tasks/drive_activity_stat/lib/strings.h"
#include "yt_tables_types.h"

#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/io.h>
#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/operation.h>
#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/serialization.h>
#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/schema.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/yt/include/common.h>

#include <maps/libs/http/include/url.h>

#include <library/cpp/yson/node/node.h>
#include <mapreduce/yt/interface/common.h>
#include <mapreduce/yt/interface/operation.h>

#include <algorithm>
#include <chrono>
#include <cstdint>


// Required for Y_SAVELOAD_JOB
template <>
class TSerializer<maps::mrc::drive_activity_stat::DevicesRegistryRecord> {
public:
    static void Save(IOutputStream* s, const maps::mrc::drive_activity_stat::DevicesRegistryRecord& v)
    {
        ::Save(s, TString(v.vin));
        ::Save(s, TString(v.signalqSN));
        ::Save(s, v.telematicsImei);
    }

    static void Load(IInputStream* s, maps::mrc::drive_activity_stat::DevicesRegistryRecord& v)
    {
        TString vin;
        TString signalqSN;
        ::Load(s, vin);
        ::Load(s, signalqSN);
        ::Load(s, v.telematicsImei);
        v.vin = vin;
        v.signalqSN = signalqSN;
    }
};

namespace maps::mrc::drive_activity_stat {

namespace {

const std::string REQUEST_PREFIX = "http://localhost";

class ParseFirmwareUpdaterRequestsMapper : public yt::Mapper
{
public:
    ParseFirmwareUpdaterRequestsMapper() = default;
    ParseFirmwareUpdaterRequestsMapper(const std::string& firmwareUpdaterHost)
        : firmwareUpdaterHost_(firmwareUpdaterHost)
    {}

    void Do(yt::Reader* reader, yt::Writer* writer) override
    {
        for (; reader->IsValid(); reader->Next()) {
            auto serviceLog =
                yt::deserialize<ServiceLogRecord>(reader->GetRow());
            if (serviceLog.vhost != firmwareUpdaterHost_ ||
                    !serviceLog.request.has_value())
            {
                continue;
            }

            auto requestURL = http::URL(REQUEST_PREFIX + serviceLog.request.value());

            if (!isFirmwareUpdateRequest(requestURL))
            {
                continue;
            }

            auto optDeviceId = requestURL.optParam("deviceid");

            if (!optDeviceId.has_value()) {
                continue;
            }

            ParsedFirmwareUpdateRequest parsedRequest{
                .time = serviceLog.eventTimePoint(),
                .deviceId = optDeviceId.value(),
                .firmwareVersion = serviceLog.userAgent
            };

            writer->AddRow(yt::serialize(parsedRequest));
        }
    }

    Y_SAVELOAD_JOB(firmwareUpdaterHost_);
private:
    static bool isFirmwareUpdateRequest(const http::URL& url) {
        static const std::string FIRMWARE_UPDATE_REQUEST = "/firmware/1.x/target_state";
        return url.path() == FIRMWARE_UPDATE_REQUEST;
    }

    TString firmwareUpdaterHost_;

};

REGISTER_MAPPER(ParseFirmwareUpdaterRequestsMapper);


class AggregateByDeviceReducer: public maps::mrc::yt::Reducer
{
public:

    /// If duration between two consecutive requests is less than threshold
    /// we consider that the device was continuously active
    static constexpr std::chrono::minutes INACTIVITY_INTERVAL_THRESHOLD{30};

    void Do(maps::mrc::yt::Reader* reader, maps::mrc::yt::Writer* writer) override
    {
        std::string deviceId;
        std::optional<std::string> firmwareVersion;
        chrono::TimePoint lastUpdateRequest;
        chrono::TimePoint::duration activePeriodDuration{};
        Date lastDate{};
        bool isInitialized = false;

        auto writeActivityPerDay =
            [&](Date date, auto duration)
            {
                writer->AddRow(
                    yt::serialize(
                        DeviceDailyActivityRecord{
                            .date = date,
                            .deviceId = deviceId,
                            .firmwareVersion = firmwareVersion,
                            .activePeriodDuration =
                                std::chrono::duration_cast<std::chrono::minutes>(duration)
                        }
                    )
                );
            };

        for (; reader->IsValid(); reader->Next()) {
            auto updateRequest =
                yt::deserialize<ParsedFirmwareUpdateRequest>(reader->GetRow());
            Date requestDate = std::chrono::time_point_cast<Date::duration>(
                    updateRequest.time);

            if (!isInitialized) {
                deviceId = updateRequest.deviceId;
                firmwareVersion = updateRequest.firmwareVersion;
                lastDate = requestDate;
                lastUpdateRequest = updateRequest.time;
                isInitialized = true;
                continue;
            }

            ASSERT(
                deviceId == updateRequest.deviceId && firmwareVersion ==
                    updateRequest.firmwareVersion);

            auto intervalBetweenRequests = updateRequest.time - lastUpdateRequest;

            if (requestDate != lastDate) {
                if (intervalBetweenRequests < INACTIVITY_INTERVAL_THRESHOLD) {
                    chrono::TimePoint midnightTime =
                        std::chrono::time_point_cast<chrono::TimePoint::duration>(requestDate);
                    activePeriodDuration += midnightTime - lastUpdateRequest;

                    writeActivityPerDay(lastDate, activePeriodDuration);

                    lastDate = requestDate;
                    activePeriodDuration = updateRequest.time - midnightTime;
                } else {
                    writeActivityPerDay(lastDate, activePeriodDuration);
                    lastDate = requestDate;
                    activePeriodDuration = {};
                }

            } else if (intervalBetweenRequests < INACTIVITY_INTERVAL_THRESHOLD) {
                activePeriodDuration += intervalBetweenRequests;
            }

            lastUpdateRequest = updateRequest.time;
        }

        if (isInitialized && activePeriodDuration.count() > 0) {
            writeActivityPerDay(lastDate, activePeriodDuration);
        }
    }
};

REGISTER_REDUCER(AggregateByDeviceReducer);

/// Copies the rows from the first table where rows occurs
class MergeTablesReducer: public maps::mrc::yt::Reducer
{
public:
    void Do(maps::mrc::yt::Reader* reader, maps::mrc::yt::Writer* writer) override
    {
        std::optional<size_t> tableIdx;
        for (auto& cursor : *reader) {
            /// YT guaranties that the order of input rows is coherent
            /// with order of input tables.
            if (!tableIdx.has_value()) {
                tableIdx = cursor.GetTableIndex();
            } else if (cursor.GetTableIndex() != tableIdx.value()) {
                /// Ignore the rest of the tables
                return;
            }

            writer->AddRow(cursor.GetRow());
        }

    }
};

REGISTER_REDUCER(MergeTablesReducer);


class ParseCarsharingTelematicsMapper : public yt::Mapper
{
public:
    ParseCarsharingTelematicsMapper() = default;
    ParseCarsharingTelematicsMapper(std::vector<DevicesRegistryRecord> devicesRegistry)
        : devicesRegistry_(std::move(devicesRegistry))
    {}

    void Do(yt::Reader* reader, yt::Writer* writer) override
    {
        auto imeiSet = telematicsImeiSet();
        for (; reader->IsValid(); reader->Next()) {
            NYT::TNode row = reader->GetRow();
            if (row["type"].AsString() != "BLACKBOX_RECORDS" ||
                    row["event"].AsString() != "incoming")
            {
                continue;
            }

            int64_t imei = row["imei"].AsInt64();
            if (!imeiSet.contains(imei)) {
                continue;
            }

            for (const auto& timedPosition : extractTimedPosition(row["data"]["records"])) {
                writer->AddRow(
                    yt::serialize(
                        ParsedTelematicsRecord{
                            .telematicsImei = imei,
                            .time = timedPosition.time,
                            .geoPos = timedPosition.pos
                        }
                    )
                );
            }
        }
    }

    Y_SAVELOAD_JOB(devicesRegistry_);

private:
    std::set<int64_t> telematicsImeiSet() const {
        std::set<int64_t> result;
        for (const auto& device : devicesRegistry_) {
            result.insert(device.telematicsImei);
        }
        return result;
    }

    struct TimedPosition {
        chrono::TimePoint time;
        geolib3::Point2 pos;
    };

    std::vector<TimedPosition> extractTimedPosition(const NYT::TNode& records)
    {
        std::vector<TimedPosition> result;
        for (auto recordsNode: records.AsList())
            try {
                auto timestampNode = recordsNode["timestamp"];
                auto time = chrono::sinceEpochToTimePoint<std::chrono::seconds>(
                    timestampNode.AsInt64());
                auto subrecordsNode = recordsNode["subrecords"];
                for (const auto& subrecord: subrecordsNode.AsList()) {
                    if (subrecord["type"].AsString() == "position_data") {
                        double lon = subrecord["lon"].AsDouble();
                        double lat = subrecord["lat"].AsDouble();

                        if (lon == 0 || lat == 0) {
                            // Ignore uninitialized coordinates
                            continue;
                        }

                        result.push_back(
                            {.time = time, .pos = geolib3::Point2(lon, lat)});
                    }
                }
            } catch (const std::exception& ex) {
                WARN() << "Failed to parse record: " << ex.what();
            }
        return result;
    }

    std::vector<DevicesRegistryRecord> devicesRegistry_;
};

REGISTER_MAPPER(ParseCarsharingTelematicsMapper);


/// Calculates total device movements duration per day
class EvalTelematicsActiveTimeReducer : public yt::Reducer
{
public:

    void Do(maps::mrc::yt::Reader* reader, maps::mrc::yt::Writer* writer) override
    {
        /// Ignore change of position that is less than MOVEMENT_THRESHOLD_METERS
        constexpr double MOVEMENT_THRESHOLD_METERS = 0.;
        /// Exclude from total active time periods when device has not been moved
        /// more than INACTIVITY_DURATION_THRESHOLD
        constexpr std::chrono::seconds INACTIVITY_DURATION_THRESHOLD{120};

        int64_t imei = 0;
        chrono::TimePoint lastPositionTime;
        geolib3::Point2 lastPos;
        chrono::TimePoint::duration activePeriodDuration{};

        Date lastDate{};
        bool isInitialized = false;

        auto writeActivityPerDay =
            [&](Date date, auto duration)
            {
                if (duration.count() > 0) {
                    writer->AddRow(
                        yt::serialize(
                            TelematicsDailyActivityRecord{
                                .date = date,
                                .telematicsImei = imei,
                                .activePeriodDuration =
                                    std::chrono::duration_cast<std::chrono::minutes>(duration)
                            }
                        )
                    );
                }

            };

        for (; reader->IsValid(); reader->Next()) {
            auto record =
                yt::deserialize<ParsedTelematicsRecord>(reader->GetRow());
            Date recordDate = std::chrono::time_point_cast<Date::duration>(
                    record.time);

            if (!isInitialized) {
                imei = record.telematicsImei;
                lastPos = record.geoPos;
                lastDate = recordDate;
                lastPositionTime = record.time;
                isInitialized = true;
                continue;
            }

            ASSERT(imei == record.telematicsImei);

            auto intervalBetweenEvents = record.time - lastPositionTime;

            if (recordDate != lastDate) {
                writeActivityPerDay(lastDate, activePeriodDuration);

                lastDate = recordDate;
                lastPos = record.geoPos;
                lastPositionTime = record.time;
                activePeriodDuration = {};

            } else {
                if (intervalBetweenEvents > INACTIVITY_DURATION_THRESHOLD) {
                    lastPos = record.geoPos;
                    lastPositionTime = record.time;
                } else if (geolib3::fastGeoDistance(lastPos, record.geoPos) >
                        MOVEMENT_THRESHOLD_METERS)
                {
                    activePeriodDuration += intervalBetweenEvents;
                    lastPos = record.geoPos;
                    lastPositionTime = record.time;
                }
            }
        }

        if (isInitialized) {
            writeActivityPerDay(lastDate, activePeriodDuration);
        }
    }

};

REGISTER_REDUCER(EvalTelematicsActiveTimeReducer)

std::vector<std::string>
listTables(NYT::IClientBase& client, const std::string& ytDirPath)
{
    auto list = client.List(NYT::TYPath(ytDirPath));

    std::vector<std::string> result;
    result.reserve(list.size());
    for (const auto& node : list) {
        result.push_back(node.AsString());
    }
    std::sort(result.begin(), result.end());
    return result;
}

} // namespace

void sortTable(NYT::IClientBase& client,
    const std::string& tablePath,
    const std::vector<std::string>& columns)
{
    NYT::TSortColumns ytColumns;
    for (const auto& column : columns) {
        ytColumns.Add(TString(column));
    }

    client.Sort(NYT::TSortOperationSpec()
        .AddInput(TString(tablePath))
        .Output(TString(tablePath))
        .SortBy(ytColumns));
}

std::vector<std::string>
evalTablesToProcess(NYT::IClientBase& client,
           const std::string& ytDirPath,
           const std::optional<std::string>& lastProcessedTable)
{
    auto tables = listTables(client, ytDirPath);
    if (!lastProcessedTable.has_value()) {
        return tables;
    }
    auto lastProcessedTableIt =
        std::find(tables.begin(), tables.end(), lastProcessedTable.value());

    if (lastProcessedTableIt == tables.end()) {
        /// Assume that lastProcessedTable is far behind current
        /// logs range
        return tables;
    }
    return std::vector<std::string>(std::next(lastProcessedTableIt), tables.end());
}


void calculateCameraActivity(
    NYT::IClientBase& client,
    const std::string& firmwareUpdaterHost,
    const std::string& ytDirPath,
    const std::vector<std::string>& tables,
    const std::string& outputTablePath,
    std::optional<yt::PoolType> poolType)
{
    NYT::TMapReduceOperationSpec operationSpec;

    for (const auto& table : tables) {
        operationSpec.AddInput<NYT::TNode>(
            NYT::TRichYPath(TString(ytDirPath + "/" + table))
                .AddRange(
                    NYT::TReadRange::FromKeys(
                        TString(firmwareUpdaterHost),
                        // upper limit is exclusive
                        TString(firmwareUpdaterHost + "-1")))
            );
    }
    operationSpec.AddOutput<NYT::TNode>(
        NYT::TRichYPath(TString(outputTablePath))
            .Schema(yt::getSchemaOf<DeviceDailyActivityRecord>())
            .OptimizeFor(NYT::OF_SCAN_ATTR));
    operationSpec.ReduceBy({DEVICE_ID, FIRMWARE_VERSION});
    operationSpec.SortBy({DEVICE_ID, FIRMWARE_VERSION, TIME});

    client.MapReduce(
        operationSpec,
        new ParseFirmwareUpdaterRequestsMapper(firmwareUpdaterHost),
        new AggregateByDeviceReducer(),
        NYT::TOperationOptions().Spec(
            yt::baseCpuOperationSpec("calculate_firmware_updater_clients_activity", poolType)
        )
    );
    sortTable(client, outputTablePath, {DATE, DEVICE_ID});
}


template<typename Record>
void mergeTables(
    NYT::IClientBase& client,
    const std::string& inputTablePath,
    const std::string& outputTablePath,
    std::optional<yt::PoolType> poolType,
    const std::vector<std::string>& sortColumns
)
{
    client.Reduce(
        NYT::TReduceOperationSpec()
            .AddInput<NYT::TNode>(TString(inputTablePath))
            .AddInput<NYT::TNode>(TString(outputTablePath))
            .AddOutput<NYT::TNode>(
                NYT::TRichYPath(TString(outputTablePath))
                    .Schema(yt::getSchemaOf<Record>())
                    .OptimizeFor(NYT::OF_SCAN_ATTR))
            .ReduceBy({DATE}),
        new MergeTablesReducer(),
        NYT::TOperationOptions().Spec(
            yt::baseCpuOperationSpec("merge_acitvity_tables", poolType)));
    sortTable(client, outputTablePath, sortColumns);
}

void mergeCameraActivityTables(
    NYT::IClientBase& client,
    const std::string& inputTablePath,
    const std::string& outputTablePath,
    std::optional<yt::PoolType> poolType
)
{
    mergeTables<DeviceDailyActivityRecord>(client, inputTablePath, outputTablePath, poolType, {DATE, DEVICE_ID});
}


void calculateCarsharingTelematicsActivity(
    NYT::IClientBase& client,
    const std::vector<DevicesRegistryRecord>& devicesRegistry,
    const std::string& ytDirPath,
    const std::vector<std::string>& tables,
    const std::string& outputTablePath,
    std::optional<yt::PoolType> poolType)
{
    if (devicesRegistry.empty()) {
        return;
    }

    NYT::TMapReduceOperationSpec operationSpec;

    for (const auto& table : tables) {
        operationSpec.AddInput<NYT::TNode>(
            NYT::TRichYPath(TString(ytDirPath + "/" + table)));
    }
    operationSpec.AddOutput<NYT::TNode>(
        NYT::TRichYPath(TString(outputTablePath))
            .Schema(yt::getSchemaOf<TelematicsDailyActivityRecord>())
            .OptimizeFor(NYT::OF_SCAN_ATTR));
    operationSpec.ReduceBy({TELEMATICS_IMEI});
    operationSpec.SortBy({TELEMATICS_IMEI, TIME});

    client.MapReduce(
        operationSpec,
        new ParseCarsharingTelematicsMapper(devicesRegistry),
        new EvalTelematicsActiveTimeReducer(),
        NYT::TOperationOptions().Spec(
            yt::baseCpuOperationSpec("calculate_carsharing_telematics_activity", poolType)
        )
    );
    sortTable(client, outputTablePath, {DATE});
}

void mergeCarsharingTelematicsActivityTables(
    NYT::IClientBase& client,
    const std::string& inputTablePath,
    const std::string& outputTablePath,
    std::optional<yt::PoolType> poolType)
{
    mergeTables<TelematicsDailyActivityRecord>(client, inputTablePath, outputTablePath, poolType, {DATE});
}

} // namespace maps::mrc::drive_activity_stat
