#include "core/json/json.h"
#include "absl/strings/str_cat.h"
#include "common/Random.h"
#include "common/counters/Counters_impl.h"
#include "core/GlobalState.h"
#include "core/packages/PackageDB.h"

using namespace std;

namespace sorbet::core {

namespace {

// Maps StrictLevel to the PascalCase names used in proto/File.proto's StrictLevel enum.
// These names differ from SigilTraits<StrictLevel>::toString(), which uses lowercase names
// (e.g., "true" vs "True"). We use the proto enum names here so that the JSON output is
// backwards-compatible with the previous protobuf-based serialization.
string_view strictLevelToJSONString(StrictLevel level) {
    switch (level) {
        case StrictLevel::None:
            return "None"sv;
        case StrictLevel::Internal:
            // We should never attempt to serialize any state that had internal errors.
            Exception::raise("Should never attempt to serialize StrictLevel::Internal to JSON");
        case StrictLevel::Ignore:
            return "Ignore"sv;
        case StrictLevel::False:
            return "False"sv;
        case StrictLevel::True:
            return "True"sv;
        case StrictLevel::Strict:
            return "Strict"sv;
        case StrictLevel::Strong:
            return "Strong"sv;
        case StrictLevel::Max:
            return "Max"sv;
        case StrictLevel::Autogenerated:
            return "Autogenerated"sv;
        case StrictLevel::Stdlib:
            return "Stdlib"sv;
    }
}

} // namespace

// Writes a single file entry as a JSON object to the writer.
// Fields set to their proto3 default values are omitted, matching the behavior of
// the previous protobuf JSON serialization (proto3 omits default-valued fields).
void JSON::fileToJSON(rapidjson::PrettyWriter<rapidjson::StringBuffer> &writer, const GlobalState &gs, FileRef file,
                      long untypedUsages) {
    const auto &data = file.data(gs);

    writer.StartObject();

    auto path = data.path();
    if (!path.empty()) {
        writer.Key("path");
        writer.String(path.data(), path.size());
    }

    // Omit enum fields when they equal the proto3 default (enum value 0 = "None")
    if (data.originalSigil != StrictLevel::None) {
        writer.Key("sigil");
        auto s = strictLevelToJSONString(data.originalSigil);
        writer.String(s.data(), s.size());
    }

    if (data.strictLevel != StrictLevel::None) {
        writer.Key("strict");
        auto s = strictLevelToJSONString(data.strictLevel);
        writer.String(s.data(), s.size());
    }

    auto minErr = data.minErrorLevel();
    if (minErr != StrictLevel::None) {
        writer.Key("min_error_level");
        auto s = strictLevelToJSONString(minErr);
        writer.String(s.data(), s.size());
    }

    // "compiled" is always CompiledNone (proto3 default), so always omitted.

    if (untypedUsages != 0) {
        writer.Key("untyped_usages");
        writer.Int(untypedUsages);
    }

    const auto &packageDB = gs.packageDB();
    if (packageDB.enabled()) {
        const auto &pkgName = packageDB.getPackageNameForFile(file);
        if (pkgName.exists()) {
            auto pkg = packageDB.getPackageInfo(pkgName).show(gs);
            if (!pkg.empty()) {
                writer.Key("pkg");
                writer.String(pkg.data(), pkg.size());
            }
        }
    }

    writer.EndObject();
}

// ----- Symbol table JSON serialization ----------------------------------------

namespace {

// Maps NameKind to the proto enum name strings from proto/Name.proto.
string_view nameKindToJSONString(NameKind kind) {
    switch (kind) {
        case NameKind::UTF8:
            return "UTF8"sv;
        case NameKind::UNIQUE:
            return "UNIQUE"sv;
        case NameKind::CONSTANT:
            return "CONSTANT"sv;
    }
}

// Maps UniqueNameKind to the proto enum name strings from proto/Name.proto.
string_view uniqueNameKindToJSONString(UniqueNameKind kind) {
    switch (kind) {
        case UniqueNameKind::Desugar:
            return "DESUGAR"sv;
        case UniqueNameKind::Namer:
            return "NAMER"sv;
        case UniqueNameKind::MangleRename:
            return "MANGLE_RENAME"sv;
        case UniqueNameKind::Singleton:
            return "SINGLETON"sv;
        case UniqueNameKind::Overload:
            return "OVERLOAD"sv;
        case UniqueNameKind::TypeVarName:
            return "TYPE_VAR_NAME"sv;
        case UniqueNameKind::PositionalArg:
            return "POSITIONAL_ARG"sv;
        case UniqueNameKind::MangledKeywordArg:
            return "MANGLED_KEYWORD_ARG"sv;
        case UniqueNameKind::ResolverMissingClass:
            return "RESOLVER_MISSING_CLASS"sv;
        case UniqueNameKind::TEnum:
            return "OPUS_ENUM"sv;
        case UniqueNameKind::Struct:
            return "STRUCT"sv;
        case UniqueNameKind::Packager:
            return "PACKAGER"sv;
        case UniqueNameKind::DesugarCsend:
            Exception::raise("UniqueNameKind::DesugarCsend should only be used in Extract to Variable");
    }
}

void nameToJSON(rapidjson::PrettyWriter<rapidjson::StringBuffer> &writer, const GlobalState &gs, NameRef name) {
    writer.StartObject();

    // NameKind has no UNKNOWN_KIND=0 in C++; all values map to non-zero proto enums,
    // so kind is always written.
    auto kind = name.kind();
    writer.Key("kind");
    auto s = nameKindToJSONString(kind);
    writer.String(s.data(), s.size());

    auto nameStr = name.show(gs);
    if (!nameStr.empty()) {
        writer.Key("name");
        writer.String(nameStr.data(), nameStr.size());
    }

    // Write unique field for UNIQUE names. Proto code sets NOT_UNIQUE for all names,
    // but proto3 omits NOT_UNIQUE (enum value 0), so it only appears for UNIQUE names
    // where the actual UniqueNameKind is set.
    if (kind == NameKind::UNIQUE) {
        auto uniqueKind = name.dataUnique(gs)->uniqueNameKind;
        writer.Key("unique");
        auto s = uniqueNameKindToJSONString(uniqueKind);
        writer.String(s.data(), s.size());
    }

    writer.EndObject();
}

void argumentToJSON(rapidjson::PrettyWriter<rapidjson::StringBuffer> &writer, const GlobalState &gs,
                    const ParamInfo &param) {
    writer.StartObject();

    writer.Key("name");
    nameToJSON(writer, gs, param.name);

    // Proto3 omits false bools (default value)
    if (param.flags.isKeyword) {
        writer.Key("isKeyword");
        writer.Bool(true);
    }
    if (param.flags.isRepeated) {
        writer.Key("isRepeated");
        writer.Bool(true);
    }
    if (param.flags.isDefault) {
        writer.Key("isDefault");
        writer.Bool(true);
    }
    if (param.flags.isShadow) {
        writer.Key("isShadow");
        writer.Bool(true);
    }
    if (param.flags.isBlock) {
        writer.Key("isBlock");
        writer.Bool(true);
    }

    writer.EndObject();
}

} // namespace

void JSON::symbolToJSON(rapidjson::PrettyWriter<rapidjson::StringBuffer> &writer, const GlobalState &gs, SymbolRef sym,
                        bool showFull) {
    writer.StartObject();

    // Fields are written in proto field number order to match proto3 JSON output.
    // id (field 1)
    auto id = sym.rawId();
    if (id != 0) {
        writer.Key("id");
        writer.Int(id);
    }

    // name (field 2) — always a message, proto3 omits if not set, but it's always set here
    writer.Key("name");
    nameToJSON(writer, gs, sym.name(gs));

    // kind (field 3)
    string_view kindStr;
    if (sym.isClassOrModule()) {
        kindStr = "CLASS_OR_MODULE"sv;
    } else if (sym.isStaticField(gs)) {
        kindStr = "STATIC_FIELD"sv;
    } else if (sym.isField(gs)) {
        kindStr = "FIELD"sv;
    } else if (sym.isMethod()) {
        kindStr = "METHOD"sv;
    } else if (sym.isTypeMember()) {
        kindStr = "TYPE_MEMBER"sv;
    } else if (sym.isTypeParameter()) {
        kindStr = "TYPE_ARGUMENT"sv;
    }
    if (!kindStr.empty()) {
        writer.Key("kind");
        writer.String(kindStr.data(), kindStr.size());
    }

    // superClass (field 6) — only for CLASS_OR_MODULE
    if (sym.isClassOrModule()) {
        auto klass = sym.asClassOrModuleRef();
        if (klass.data(gs)->superClass().exists()) {
            auto superClassId = SymbolRef(klass.data(gs)->superClass()).rawId();
            if (superClassId != 0) {
                writer.Key("superClass");
                writer.Int(superClassId);
            }
        }
    }

    // mixins (field 8) — only for CLASS_OR_MODULE
    if (sym.isClassOrModule()) {
        auto klass = sym.asClassOrModuleRef();
        auto mixins = klass.data(gs)->mixins();
        if (!mixins.empty()) {
            writer.Key("mixins");
            writer.StartArray();
            for (auto thing : mixins) {
                writer.Int(SymbolRef(thing).rawId());
            }
            writer.EndArray();
        }
    }

    // children (field 14) — for CLASS_OR_MODULE and METHOD
    if (sym.isClassOrModule()) {
        // Collect children first to check if array is non-empty
        vector<SymbolRef> children;
        for (auto pair : sym.asClassOrModuleRef().data(gs)->membersStableOrderSlow(gs)) {
            if (pair.first == Names::singleton() || pair.first == Names::attached() ||
                pair.first == Names::mixedInClassMethods() || pair.first == Names::Constants::AttachedClass()) {
                continue;
            }
            if (!pair.second.exists()) {
                continue;
            }
            if (!showFull && !pair.second.isPrintable(gs)) {
                continue;
            }
            children.emplace_back(pair.second);
        }
        if (!children.empty()) {
            writer.Key("children");
            writer.StartArray();
            for (auto child : children) {
                symbolToJSON(writer, gs, child, showFull);
            }
            writer.EndArray();
        }
    } else if (sym.isMethod()) {
        vector<SymbolRef> children;
        for (auto typeParam : sym.asMethodRef().data(gs)->typeParameters()) {
            if (!typeParam.exists()) {
                continue;
            }
            if (!showFull && !typeParam.data(gs)->isPrintable(gs)) {
                continue;
            }
            children.emplace_back(typeParam);
        }
        if (!children.empty()) {
            writer.Key("children");
            writer.StartArray();
            for (auto child : children) {
                symbolToJSON(writer, gs, child, showFull);
            }
            writer.EndArray();
        }
    }

    // aliasTo (field 16) — only for STATIC_FIELD with AliasType
    if (sym.isStaticField(gs)) {
        auto field = sym.asFieldRef();
        if (core::isa_type<core::AliasType>(field.data(gs)->resultType)) {
            auto type = core::cast_type_nonnull<AliasType>(field.data(gs)->resultType);
            auto aliasId = type.symbol.rawId();
            if (aliasId != 0) {
                writer.Key("aliasTo");
                writer.Int(aliasId);
            }
        }
    }

    // arguments (field 17) — only for METHOD
    if (sym.isMethod()) {
        auto &params = sym.asMethodRef().data(gs)->parameters;
        if (!params.empty()) {
            writer.Key("arguments");
            writer.StartArray();
            for (auto &param : params) {
                argumentToJSON(writer, gs, param);
            }
            writer.EndArray();
        }
    }

    writer.EndObject();
}

// ----- Metrics JSON serialization ---------------------------------------------

string JSON::metricsToJSON(const CounterState &counters, string_view prefix, string_view repo, string_view branch,
                           string_view sha, string_view status) {
    rapidjson::StringBuffer buffer;
    rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
    writer.SetIndent(' ', 1);

    writer.StartObject();

    // Fields in proto field number order: repo(1), sha(2), status(3), branch(4), timestamp(5), uuid(6), metrics(10)
    if (!repo.empty()) {
        writer.Key("repo");
        writer.String(repo.data(), repo.size());
    }

    if (!sha.empty()) {
        writer.Key("sha");
        writer.String(sha.data(), sha.size());
    }

    if (!status.empty()) {
        writer.Key("status");
        writer.String(status.data(), status.size());
    }

    if (!branch.empty()) {
        writer.Key("branch");
        writer.String(branch.data(), branch.size());
    }

    // Proto3 serializes int64 as a string in JSON.
    auto unix_timestamp = chrono::seconds(time(nullptr));
    if (unix_timestamp.count() != 0) {
        writer.Key("timestamp");
        auto ts = to_string(unix_timestamp.count());
        writer.String(ts.data(), ts.size());
    }

    // UUID version 1 as specified in RFC 4122
    string uuid = fmt::format("{:#08x}-{:#04x}-{:#04x}-{:#04x}-{:#08x}{:#04x}", (unsigned long long)Random::uniformU8(),
                              Random::uniformU4(), Random::uniformU4(0, 0x0fff) | 0x4000,
                              Random::uniformU4(0, 0x3fff) | 0x8000, (unsigned long long)Random::uniformU8(), rand());
    if (!uuid.empty()) {
        writer.Key("uuid");
        writer.String(uuid.data(), uuid.size());
    }

    counters.counters->canonicalize();

    // Collect all metric entries
    bool hasMetrics = false;
    auto startMetrics = [&]() {
        if (!hasMetrics) {
            writer.Key("metrics");
            writer.StartArray();
            hasMetrics = true;
        }
    };

    auto writeMetric = [&](string_view name, double value) {
        startMetrics();
        writer.StartObject();
        writer.Key("name");
        writer.String(name.data(), name.size());
        writer.Key("value");
        // Proto3 JSON serializes double values that are exact integers without a decimal point.
        // rapidjson's Double() always includes a decimal, so we use Int64() for integral values.
        if (value == static_cast<int64_t>(value)) {
            writer.Int64(static_cast<int64_t>(value));
        } else {
            writer.Double(value);
        }
        writer.EndObject();
    };

    for (auto &cat : counters.counters->countersByCategory) {
        CounterImpl::CounterType sum = 0;
        for (auto &e : cat.second) {
            sum += e.second;
            writeMetric(absl::StrCat(prefix, ".", cat.first, ".", e.first), e.second);
        }
        writeMetric(absl::StrCat(prefix, ".", cat.first, ".total"), sum);
    }

    for (auto &hist : counters.counters->histograms) {
        CounterImpl::CounterType sum = 0;
        int histMin = hist.second.begin()->first;
        int histMax = hist.second.begin()->first;
        for (auto &e : hist.second) {
            sum += e.second;
            histMin = min(histMin, e.first);
            histMax = max(histMin, e.first);
        }
        CounterImpl::CounterType running = 0;
        vector<pair<int, bool>> percentiles = {{25, false}, {50, false}, {75, false}, {90, false}};
        for (auto &e : hist.second) {
            running += e.second;
            for (auto &pct : percentiles) {
                if (pct.second) {
                    continue;
                }
                if (running >= sum * pct.first / 100) {
                    pct.second = true;
                    writeMetric(absl::StrCat(prefix, ".", hist.first, ".p", pct.first), e.first);
                }
            }
        }
        writeMetric(absl::StrCat(prefix, ".", hist.first, ".min"), histMin);
        writeMetric(absl::StrCat(prefix, ".", hist.first, ".max"), histMax);
        writeMetric(absl::StrCat(prefix, ".", hist.first, ".total"), sum);
    }

    for (auto &e : counters.counters->counters) {
        writeMetric(absl::StrCat(prefix, ".", e.first), e.second);
    }

    if (hasMetrics) {
        writer.EndArray();
    }

    writer.EndObject();

    string result(buffer.GetString(), buffer.GetLength());
    result.push_back('\n');
    return result;
}

} // namespace sorbet::core
