#include "chat_protocol.h"

#include <nlohmann/json.hpp>

#include <algorithm>
#include <cctype>
#include <optional>
#include <utility>

namespace dragonx::chat {
namespace {

bool isHexString(const std::string& value)
{
    for (unsigned char ch : value) {
        if (!std::isxdigit(ch)) return false;
    }
    return true;
}

bool isCiphertextPayloadCandidate(const std::string& value)
{
    return !value.empty() && value.size() <= kHushChatMemoByteLimit &&
           value.size() % 2 == 0 && isHexString(value);
}

bool isContactPayloadCandidate(const std::string& value)
{
    return !value.empty() && value.size() <= kHushChatMemoByteLimit && value.front() != '{';
}

bool readRequiredString(const nlohmann::json& object,
                        const char* key,
                        std::string& value,
                        std::string& error)
{
    auto it = object.find(key);
    if (it == object.end()) {
        error = std::string("missing field: ") + key;
        return false;
    }
    if (!it->is_string()) {
        error = std::string("field is not a string: ") + key;
        return false;
    }
    value = it->get<std::string>();
    return true;
}

bool readRequiredInt(const nlohmann::json& object,
                     const char* key,
                     int& value,
                     std::string& error)
{
    auto it = object.find(key);
    if (it == object.end()) {
        error = std::string("missing field: ") + key;
        return false;
    }
    if (!it->is_number_integer()) {
        error = std::string("field is not an integer: ") + key;
        return false;
    }
    value = it->get<int>();
    return true;
}

HushChatHeaderParseResult fail(std::string error)
{
    HushChatHeaderParseResult result;
    result.error = std::move(error);
    return result;
}

} // namespace

const char* hushChatHeaderTypeName(HushChatHeaderType type)
{
    switch (type) {
    case HushChatHeaderType::Message:
        return "Memo";
    case HushChatHeaderType::ContactRequest:
        return "Cont";
    }
    return "Unknown";
}

const char* hushChatMemoGroupingIssueName(HushChatMemoGroupingIssue issue)
{
    switch (issue) {
    case HushChatMemoGroupingIssue::InvalidHeader:
        return "InvalidHeader";
    case HushChatMemoGroupingIssue::MissingPayload:
        return "MissingPayload";
    case HushChatMemoGroupingIssue::DuplicateHeader:
        return "DuplicateHeader";
    case HushChatMemoGroupingIssue::OversizedMemo:
        return "OversizedMemo";
    }
    return "Unknown";
}

HushChatHeaderParseResult parseHushChatHeaderMemo(const std::string& memo)
{
    if (memo.empty()) return fail("empty memo");
    if (memo.size() > kHushChatMemoByteLimit) return fail("memo exceeds HushChat memo byte limit");
    if (memo.front() != '{') return fail("memo is not a HushChat header JSON object");

    nlohmann::json object;
    try {
        object = nlohmann::json::parse(memo);
    } catch (const nlohmann::json::parse_error& e) {
        return fail(std::string("invalid JSON: ") + e.what());
    }
    if (!object.is_object()) return fail("header memo JSON is not an object");

    HushChatHeader header;
    std::string type;
    std::string error;
    if (!readRequiredInt(object, "h", header.header_number, error)) return fail(error);
    if (!readRequiredInt(object, "v", header.version, error)) return fail(error);
    if (!readRequiredString(object, "z", header.reply_zaddr, error)) return fail(error);
    if (!readRequiredString(object, "cid", header.conversation_id, error)) return fail(error);
    if (!readRequiredString(object, "t", type, error)) return fail(error);
    if (!readRequiredString(object, "e", header.secretstream_header_hex, error)) return fail(error);
    if (!readRequiredString(object, "p", header.public_key_hex, error)) return fail(error);
    // Optional sender compose time (Unix seconds). Absent on older senders — leave sent_at = 0 so the
    // receiver falls back to the tx/receive time. Read leniently; never fail the header on a bad value.
    if (auto it = object.find("ts"); it != object.end() && it->is_number_integer())
        header.sent_at = it->get<std::int64_t>();

    if (header.header_number < 1) return fail("header number must be positive");
    if (header.version != kHushChatSupportedVersion) return fail("unsupported HushChat version");
    if (header.reply_zaddr.empty()) return fail("reply z-address is empty");
    if (header.conversation_id.empty()) return fail("conversation id is empty");

    if (type == "Memo") {
        header.type = HushChatHeaderType::Message;
    } else if (type == "Cont") {
        header.type = HushChatHeaderType::ContactRequest;
    } else {
        return fail("unknown HushChat header type");
    }

    if (header.public_key_hex.size() != kHushChatPublicKeyHexLength || !isHexString(header.public_key_hex)) {
        return fail("public key must be 32 bytes encoded as hex");
    }

    if (header.type == HushChatHeaderType::Message) {
        if (header.secretstream_header_hex.size() != kHushChatSecretstreamHeaderHexLength ||
            !isHexString(header.secretstream_header_hex)) {
            return fail("message header must include a 24 byte secretstream header encoded as hex");
        }
    } else if (!header.secretstream_header_hex.empty()) {
        return fail("contact request header must not include a secretstream header");
    }

    HushChatHeaderParseResult result;
    result.ok = true;
    result.header = std::move(header);
    return result;
}

HushChatMemoGroupingResult groupHushChatMemoOutputs(const std::vector<HushChatMemoOutput>& outputs)
{
    struct OrderedOutput {
        std::size_t input_index = 0;
        HushChatMemoOutput output;
    };

    std::vector<OrderedOutput> ordered;
    ordered.reserve(outputs.size());
    for (std::size_t index = 0; index < outputs.size(); ++index) {
        ordered.push_back(OrderedOutput{index, outputs[index]});
    }
    std::stable_sort(ordered.begin(), ordered.end(), [](const OrderedOutput& left, const OrderedOutput& right) {
        if (left.output.position == right.output.position) return left.input_index < right.input_index;
        return left.output.position < right.output.position;
    });

    HushChatMemoGroupingResult result;
    std::optional<HushChatMemoOutput> pending_header_output;
    std::optional<HushChatHeader> pending_header;
    // A payload memo seen BEFORE its header. The full-node daemon shuffles the two 0-value memo
    // outputs of a chat tx (transaction_builder ShuffleOutputs), so the header is NOT guaranteed to
    // occupy the lower position. We hold an orphan payload until its header arrives and pair them
    // regardless of on-chain order (a chat tx carries exactly one header + one payload).
    std::optional<HushChatMemoOutput> pending_payload_output;

    auto addIssue = [&](HushChatMemoGroupingIssue issue, std::size_t position, std::string detail) {
        result.issues.push_back(HushChatMemoGroupingIssueInfo{issue, position, std::move(detail)});
    };

    auto payloadMatchesHeader = [&](const std::string& memo) {
        return pending_header->type == HushChatHeaderType::Message
            ? isCiphertextPayloadCandidate(memo)
            : isContactPayloadCandidate(memo);
    };

    auto emitPair = [&](const HushChatMemoOutput& payloadOutput) {
        HushChatMemoPair pair;
        pair.header = std::move(*pending_header);
        pair.header_position = pending_header_output->position;
        pair.payload_position = payloadOutput.position;
        pair.payload_memo = payloadOutput.memo;
        result.pairs.push_back(std::move(pair));
        pending_header_output.reset();
        pending_header.reset();
    };

    // Pair a held (early) payload with the now-pending header, if it matches the header's type.
    auto tryPairHeldPayload = [&]() {
        if (!pending_header || !pending_payload_output) return;
        if (payloadMatchesHeader(pending_payload_output->memo)) {
            emitPair(*pending_payload_output);
            pending_payload_output.reset();
        }
    };

    auto clearPendingAsMissing = [&]() {
        if (!pending_header_output) return;
        addIssue(HushChatMemoGroupingIssue::MissingPayload,
                 pending_header_output->position,
                 "header did not have a matching payload memo");
        pending_header_output.reset();
        pending_header.reset();
    };

    for (const auto& entry : ordered) {
        const auto& output = entry.output;
        if (output.memo.size() > kHushChatMemoByteLimit) {
            addIssue(HushChatMemoGroupingIssue::OversizedMemo,
                     output.position,
                     "memo exceeds HushChat memo byte limit");
            continue;
        }

        if (!output.memo.empty() && output.memo.front() == '{') {
            auto parsed = parseHushChatHeaderMemo(output.memo);
            if (!parsed.ok) {
                addIssue(HushChatMemoGroupingIssue::InvalidHeader, output.position, parsed.error);
                continue;
            }

            if (pending_header_output) {
                addIssue(HushChatMemoGroupingIssue::DuplicateHeader,
                         output.position,
                         "encountered another HushChat header before a payload");
                clearPendingAsMissing();
            }

            pending_header_output = output;
            pending_header = std::move(parsed.header);
            tryPairHeldPayload();  // the payload may have arrived first (shuffled output order)
            continue;
        }

        // Non-header memo.
        if (pending_header_output && pending_header) {
            if (payloadMatchesHeader(output.memo)) {
                emitPair(output);
            } else {
                ++result.ignored_memo_count;
            }
        } else if (!pending_payload_output && !output.memo.empty()) {
            // No header yet — hold this as a candidate payload for a header still to come.
            pending_payload_output = output;
        } else {
            ++result.ignored_memo_count;
        }
    }

    clearPendingAsMissing();
    if (pending_payload_output) ++result.ignored_memo_count;  // orphan payload, no header arrived
    return result;
}

HushChatTransactionExtractionResult extractHushChatTransactionMetadata(
    const HushChatTransactionInput& transaction,
    bool featureEnabled)
{
    HushChatTransactionExtractionResult result;
    result.feature_enabled = featureEnabled;
    if (!featureEnabled || transaction.txid.empty()) return result;

    auto grouped = groupHushChatMemoOutputs(transaction.outputs);
    result.ignored_memo_count = grouped.ignored_memo_count;
    result.issues.reserve(grouped.issues.size());
    for (const auto& issue : grouped.issues) {
        result.issues.push_back(HushChatMemoGroupingIssueInfo{
            issue.issue,
            issue.position,
            hushChatMemoGroupingIssueName(issue.issue)
        });
    }

    result.metadata.reserve(grouped.pairs.size());
    for (const auto& pair : grouped.pairs) {
        HushChatTransactionMetadata metadata;
        metadata.txid = transaction.txid;
        metadata.type = pair.header.type;
        metadata.conversation_id = pair.header.conversation_id;
        metadata.reply_zaddr = pair.header.reply_zaddr;
        metadata.header_position = pair.header_position;
        metadata.payload_position = pair.payload_position;
        metadata.payload_size = pair.payload_memo.size();
        metadata.sender_public_key_hex = pair.header.public_key_hex;
        metadata.secretstream_header_hex = pair.header.secretstream_header_hex;
        metadata.payload_memo = pair.payload_memo;
        metadata.sent_at = pair.header.sent_at;   // carry the sender's compose time (0 if absent)
        result.metadata.push_back(std::move(metadata));
    }

    return result;
}

} // namespace dragonx::chat
