#include "lix/libutil/args/root.hh"
#include "lix/libcmd/command.hh"
#include "lix/libmain/common-args.hh"
#include "lix/libutil/config.hh"
#include "lix/libutil/current-process.hh"
#include "lix/libexpr/eval.hh"
#include "lix/libexpr/eval-settings.hh"
#include "lix/libstore/globals.hh"
#include "lix/libcmd/legacy.hh"
#include "lix/libutil/namespaces.hh"
#include "lix/libmain/shared.hh"
#include "lix/libstore/store-api.hh"
#include "lix/libstore/filetransfer.hh"
#include "lix/libutil/finally.hh"
#include "lix/libutil/json.hh"
#include "lix/libmain/loggers.hh"
#include "lix/libcmd/markdown.hh"
#include "add-to-store.hh"
#include "build-remote.hh"
#include "builtin-builder.hh"
#include "build.hh"
#include "bundle.hh"
#include "cat.hh"
#include "config.hh"
#include "copy.hh"
#include "daemon.hh"
#include "derivation-add.hh"
#include "derivation-show.hh"
#include "derivation.hh"
#include "develop.hh"
#include "diff-closures.hh"
#include "doctor.hh"
#include "dump-path.hh"
#include "edit.hh"
#include "eval.hh"
#include "flake.hh"
#include "fmt.hh"
#include "hash.hh"
#include "log.hh"
#include "ls.hh"
#include "make-content-addressed.hh"
#include "nar.hh"
#include "nix-build.hh"
#include "nix-channel.hh"
#include "nix-collect-garbage.hh"
#include "nix-copy-closure.hh"
#include "nix-env.hh"
#include "nix-instantiate.hh"
#include "nix-store.hh"
#include "optimise-store.hh"
#include "path-from-hash-part.hh"
#include "path-info.hh"
#include "ping-store.hh"
#include "prefetch.hh"
#include "profile.hh"
#include "realisation.hh"
#include "registry.hh"
#include "repl.hh"
#include "run.hh"
#include "search.hh"
#include "sigs.hh"
#include "store-copy-log.hh"
#include "store-delete.hh"
#include "store-gc.hh"
#include "store-repair.hh"
#include "store.hh"
#include "upgrade-nix.hh"
#include "verify.hh"
#include "why-depends.hh"

#include <sys/types.h>
#include <sys/socket.h>
#include <ifaddrs.h>
#include <netdb.h>
#include <netinet/in.h>

namespace nix {

void registerLegacyCommands()
{
    registerLegacyNixEnv();
    registerLegacyNixBuildAndNixShell();
    registerLegacyNixInstantiate();
    registerLegacyNixCopyClosure();
    registerLegacyNixCollectGarbage();
    registerLegacyNixChannel();
    registerLegacyNixStore();
    registerLegacyBuildRemote();
    registerLegacyNixDaemon();
    registerLegacyNixPrefetchUrl();
    registerLegacyNixHash();
    registerLegacyBuiltinBuilder();
}

void registerNixHelp();

void registerCommands()
{
    // keep-sorted start
    registerNixBuild();
    registerNixBundle();
    registerNixCat();
    registerNixConfig();
    registerNixCopy();
    registerNixDaemon();
    registerNixDerivation();
    registerNixDerivationAdd();
    registerNixDerivationShow();
    registerNixDevelop();
    registerNixDoctor();
    registerNixEdit();
    registerNixEval();
    registerNixFlake();
    registerNixFmt();
    registerNixHash();
    registerNixHelp();
    registerNixLog();
    registerNixLs();
    registerNixMakeContentAddressed();
    registerNixNar();
    registerNixPathInfo();
    registerNixProfile();
    registerNixRealisation();
    registerNixRegistry();
    registerNixRepl();
    registerNixRun();
    registerNixSearch();
    registerNixSigs();
    registerNixStore();
    registerNixStoreAdd();
    registerNixStoreCopyLog();
    registerNixStoreDelete();
    registerNixStoreDiffClosures();
    registerNixStoreDumpPath();
    registerNixStoreGc();
    registerNixStoreOptimise();
    registerNixStorePathFromHashPart();
    registerNixStorePing();
    registerNixStorePrefetchFile();
    registerNixStoreRepair();
    registerNixStoreVerify();
    registerNixUpgradeNix();
    registerNixWhyDepends();
    // keep-sorted end
}

static bool haveProxyEnvironmentVariables()
{
    static const std::vector<std::string> proxyVariables = {
        // keep-sorted start
        "FTP_PROXY",
        "HTTPS_PROXY",
        "HTTP_PROXY",
        "ftp_proxy",
        "http_proxy",
        "https_proxy"
        // keep-sorted end
    };
    for (auto & proxyVariable: proxyVariables) {
        if (getEnv(proxyVariable).has_value()) {
            return true;
        }
    }
    return false;
}

/* Check if we have a non-loopback/link-local network interface. */
static bool haveInternet()
{
    struct ifaddrs * addrs;

    if (getifaddrs(&addrs))
        return true;

    Finally free([&]() { freeifaddrs(addrs); });

    for (auto i = addrs; i; i = i->ifa_next) {
        if (!i->ifa_addr) continue;
        if (i->ifa_addr->sa_family == AF_INET) {
            if (ntohl(reinterpret_cast<sockaddr_in *>(i->ifa_addr)->sin_addr.s_addr) != INADDR_LOOPBACK) {
                return true;
            }
        } else if (i->ifa_addr->sa_family == AF_INET6) {
            if (!IN6_IS_ADDR_LOOPBACK(&reinterpret_cast<sockaddr_in6 *>(i->ifa_addr)->sin6_addr) &&
                !IN6_IS_ADDR_LINKLOCAL(&reinterpret_cast<sockaddr_in6 *>(i->ifa_addr)->sin6_addr))
                return true;
        }
    }

    if (haveProxyEnvironmentVariables()) return true;

    return false;
}

std::string programPath;

struct NixArgs : virtual MultiCommand, virtual MixCommonArgs, virtual RootArgs
{
    bool useNet = true;
    bool refresh = false;
    bool helpRequested = false;
    bool showVersion = false;

    AsyncIoRoot & aio_;
    AsyncIoRoot & aio() override { return aio_; }

    NixArgs(const std::string & programName, AsyncIoRoot & aio)
        /* NOTE: when using lix, the command map is empty as `lix-command` is not stabilized neither designed.
         * `lix` is only used for external commands. */
        : MultiCommand(programName == "lix" ? CommandMap() : CommandRegistry::getCommandsFor({}), programName == "lix")
        , MixCommonArgs(programName)
        , aio_(aio)
    {
        categories.clear();
        categories[catHelp] = "Help commands";
        categories[Command::catDefault] = "Main commands";
        categories[catSecondary] = "Infrequently used commands";
        categories[catUtility] = "Utility/scripting commands";
        categories[catNixInstallation] = "Commands for upgrading or troubleshooting your Nix installation";

        if (programName != "lix") {
            addFlag({
                .longName = "help",
                .description = "Show usage information.",
                .category = miscCategory,
                .handler = {[this]() { this->helpRequested = true; }},
            });

            addFlag({
                .longName = "print-build-logs",
                .shortName = 'L',
                .description = "Print full build logs on standard error.",
                .category = loggingCategory,
                .handler = {[&]() {
                    loggerSettings.logFormat.setDefault(loggerSettings.logFormat.get().withLogs());
                }},
                .experimentalFeature = Xp::NixCommand,
            });

            addFlag({
                .longName = "version",
                .description = "Show version information.",
                .category = miscCategory,
                .handler = {[&]() { showVersion = true; }},
            });

            addFlag({
                .longName = "offline",
                .aliases = {"no-net"}, // FIXME: remove
                .description = "Disable substituters and consider all previously downloaded files up-to-date.",
                .category = miscCategory,
                .handler = {[&]() { useNet = false; }},
                .experimentalFeature = Xp::NixCommand,
            });

            addFlag({
                .longName = "refresh",
                .description = "Consider all previously downloaded files out-of-date.",
                .category = miscCategory,
                .handler = {[&]() { refresh = true; }},
                .experimentalFeature = Xp::NixCommand,
            });
        }
    }

    std::map<std::string, std::vector<std::string>> aliases = {
        // keep-sorted start
        {"add-to-store", {"store", "add-path"}},
        {"cat-nar", {"nar", "cat"}},
        {"cat-store", {"store", "cat"}},
        {"copy-sigs", {"store", "copy-sigs"}},
        {"dev-shell", {"develop"}},
        {"diff-closures", {"store", "diff-closures"}},
        {"dump-path", {"store", "dump-path"}},
        {"hash-file", {"hash", "file"}},
        {"hash-path", {"hash", "path"}},
        {"ls-nar", {"nar", "ls"}},
        {"ls-store", {"store", "ls"}},
        {"make-content-addressable", {"store", "make-content-addressed"}},
        {"optimise-store", {"store", "optimise"}},
        {"ping-store", {"store", "ping"}},
        {"show-config", {"config", "show"}},
        {"show-derivation", {"derivation", "show"}},
        {"sign-paths", {"store", "sign"}},
        {"to-base16", {"hash", "to-base16"}},
        {"to-base32", {"hash", "to-base32"}},
        {"to-base64", {"hash", "to-base64"}},
        {"verify", {"store", "verify"}},
        // keep-sorted end
    };

    bool aliasUsed = false;

    Strings::iterator rewriteArgs(Strings & args, Strings::iterator pos) override
    {
        if (aliasUsed || command || pos == args.end()) return pos;
        auto arg = *pos;
        auto i = aliases.find(arg);
        if (i == aliases.end()) return pos;
        printTaggedWarning(
            "'%s' is a deprecated alias for '%s'", arg, concatStringsSep(" ", i->second)
        );
        pos = args.erase(pos);
        for (auto j = i->second.rbegin(); j != i->second.rend(); ++j)
            pos = args.insert(pos, *j);
        aliasUsed = true;
        return pos;
    }

    std::string description() override
    {
        return "a tool for reproducible and declarative configuration management";
    }

    std::string doc() override
    {
        return
          #include "nix.md"
          ;
    }

    void run() override
    {
        command->second->run();
    }

    // Plugins may add new subcommands.
    void pluginsInited() override
    {
        commands = CommandRegistry::getCommandsFor({});
    }

    std::string dumpCli()
    {
        auto res = JSON::object();

        res["args"] = toJSON();

        auto stores = JSON::object();
        for (auto & implem : *StoreImplementations::registered) {
            auto storeConfig = implem.getConfig();
            auto storeName = storeConfig->name();
            auto & j = stores[storeName];
            j["doc"] = storeConfig->doc();
            j["settings"] = storeConfig->toJSON();
            j["experimentalFeature"] = storeConfig->experimentalFeature();
        }
        res["stores"] = std::move(stores);

        return res.dump();
    }
};

/* Render the help for the specified subcommand to stdout using
   lowdown. */
static void showHelp(AsyncIoRoot & aio, std::vector<std::string> subcommand, NixArgs & toplevel)
{
    auto mdName = subcommand.empty() ? "nix" : fmt("nix3-%s", concatStringsSep("-", subcommand));

    Evaluator evaluator(aio, {}, aio.blockOn(openStore("dummy://")));
    auto state = evaluator.begin(aio);

    Value vGenerateManpage = state->eval(evaluator.parseExprFromString(
#include "generate-manpage.nix.gen.hh"
        , CanonPath::root
    ));

    Value vDump = {NewValueAs::string, toplevel.dumpCli()};

    Value vRes = state->callFunction(vGenerateManpage, evaluator.builtins.get("false"), noPos);
    vRes = state->callFunction(vRes, vDump, noPos);

    auto attr = vRes.attrs()->get(evaluator.symbols.create(mdName + ".md"));
    if (!attr)
        throw UsageError("`nix` has no subcommand '%s'", concatStringsSep("", subcommand));

    auto markdown =
        state->forceString(attr->value, noPos, "while evaluating the lowdown help text");

    withPager([&](Pager & pager) { pager << renderMarkdownToTerminal(markdown) << "\n"; });
}

static NixArgs & getNixArgs(Command & cmd)
{
    return dynamic_cast<NixArgs &>(cmd.getRoot());
}

struct CmdHelp : Command
{
    std::vector<std::string> subcommand;

    CmdHelp()
    {
        expectArgs({
            .label = "subcommand",
            .handler = {&subcommand},
        });
    }

    std::string description() override
    {
        return "show help about `nix` or a particular subcommand";
    }

    std::string doc() override
    {
        return
          #include "help.md"
          ;
    }

    Category category() override { return catHelp; }

    void run() override
    {
        assert(parent);
        MultiCommand * toplevel = parent;
        while (toplevel->parent) toplevel = toplevel->parent;
        showHelp(aio(), subcommand, getNixArgs(*this));
    }
};


struct CmdHelpStores : Command
{
    std::string description() override
    {
        return "show help about store types and their settings";
    }

    std::string doc() override
    {
        return
          #include "help-stores.md"
          ;
    }

    Category category() override { return catHelp; }

    void run() override
    {
        showHelp(aio(), {"help-stores"}, getNixArgs(*this));
    }
};

void registerNixHelp()
{
    registerCommand<CmdHelp>("help");
    registerCommand<CmdHelpStores>("help-stores");
}

int mainWrapped(AsyncIoRoot & aio, int argc, char ** argv)
{
    /* The chroot helper needs to be run before any threads have been
       started. */
    if (argc > 0 && argv[0] == chrootHelperName) {
        chrootHelper(argc, argv);
        return 0;
    }

    initNix();
    initLibExpr();

    GlobalConfig::registerGlobalConfig(daemonAuthorizationSettings);
    GlobalConfig::registerGlobalConfig(nixDevelopSettings);

#if __linux__
    if (getuid() == 0) {
        try {
            saveMountNamespace();
            if (unshare(CLONE_NEWNS) == -1)
                throw SysError("setting up a private mount namespace");
        } catch (Error & e) { }
    }
    #endif

    programPath = argv[0];
    auto programName = std::string(baseNameOf(programPath));

    if (argc > 1 && std::string_view(argv[1]) == "__build-remote") {
        programName = "build-remote";
        argv++; argc--;
    }

    // Clean up the progress bar if shown using --log-format in a legacy command too.
    // Otherwise, this is a harmless no-op.
    Finally f([] { logger->pause(); });

    {
        registerLegacyCommands();
        auto legacy = (*LegacyCommandRegistry::commands)[programName];
        if (legacy) {
            return legacy(
                aio,
                std::string(baseNameOf(argv[0])),
                Strings(argv + 1, argv + argc),
                {argv + 1, argv + argc}
            );
        }
    }

    evalSettings.pureEval.setDefault(true);

    loggerSettings.logFormat.autoValue = LogFormat::BarWithLogs;

    // FIXME: stop messing about with log verbosity depending on if it is interactive use
    if (isatty(STDERR_FILENO)) {
        verbosity = lvlNotice;
    } else {
        verbosity = lvlInfo;
    }

    registerCommands();
    // NOTE: out of over-cautiousness for backward compatibility,
    // the program name had always been `nix` for a long time.
    // Only when we invoke it as `lix`, we should propagate `lix`.
    NixArgs args(programName == "lix" ? programName : "nix", aio);

    if (argc == 2 && std::string(argv[1]) == "__dump-cli") {
        logger->cout(args.dumpCli());
        return 0;
    }

    if (argc == 2 && std::string(argv[1]) == "__dump-language") {
        experimentalFeatureSettings.experimentalFeatures.override(ExperimentalFeatures{}
            | Xp::Flakes);
        evalSettings.pureEval.override(false);
        Evaluator state(aio, {}, aio.blockOn(openStore("dummy://")));
        auto res = JSON::object();
        res["builtins"] = ({
            auto builtinsJson = JSON::object();
            auto builtins = state.builtins.env.values[0].attrs();
            for (auto & builtin : *builtins) {
                auto b = JSON::object();
                if (!builtin.value.isPrimOp()) {
                    continue;
                }
                auto primOp = builtin.value.primOp();
                if (!primOp->doc) {
                    continue;
                }
                b["arity"] = primOp->arity;
                b["args"] = primOp->args;
                b["doc"] = trim(stripIndentation(primOp->doc));
                b["experimental-feature"] = primOp->experimentalFeature;
                builtinsJson[std::string(state.symbols[builtin.name])] = std::move(b);
            }
            std::move(builtinsJson);
        });
        res["constants"] = ({
            auto constantsJson = JSON::object();
            for (auto & [name, info] : state.builtins.constantInfos) {
                auto c = JSON::object();
                if (!info.doc) continue;
                c["doc"] = trim(stripIndentation(info.doc));
                c["type"] = showType(info.type, false);
                c["impure-only"] = info.impureOnly;
                constantsJson[name] = std::move(c);
            }
            std::move(constantsJson);
        });
        logger->cout("%s", res);
        return 0;
    }

    if (argc == 2 && std::string(argv[1]) == "__dump-xp-features") {
        logger->cout(documentExperimentalFeatures().dump());
        return 0;
    }

    if (argc == 2 && std::string(argv[1]) == "__dump-dp-features") {
        logger->cout(documentDeprecatedFeatures().dump());
        return 0;
    }

    try {
        args.parseCmdline({argv + 1, argv + argc});
    } catch (UsageError &) {
        if (!args.helpRequested && !args.completions) throw;
    }

    // HACK: after args.parseCmdline() we re-create the default logger, to apply --option flags.
    createDefaultLogger();

    if (args.completions) {
        switch (args.completions->type) {
        case Completions::Type::Normal:
            logger->cout("normal"); break;
        case Completions::Type::Filenames:
            logger->cout("filenames"); break;
        case Completions::Type::Attrs:
            logger->cout("attrs"); break;
        }
        for (auto & s : args.completions->completions)
            logger->cout(s.completion + "\t" + trim(s.description));
        return 0;
    }

    if (args.helpRequested) {
        std::vector<std::string> subcommand;
        MultiCommand * command = &args;
        while (command) {
            if (command && command->command) {
                subcommand.push_back(command->command->first);
                command = dynamic_cast<MultiCommand *>(&*command->command->second);
            } else
                break;
        }
        showHelp(aio, subcommand, args);
        return 0;
    }

    if (args.showVersion) {
        printVersion(programName);
        return 0;
    }

    if (!args.command)
        throw UsageError("no subcommand specified");

    experimentalFeatureSettings.require(
        args.command->second->experimentalFeature());

    if (args.useNet && !haveInternet()) {
        printTaggedWarning(
            "you don't have Internet access; disabling some network-dependent features"
        );
        args.useNet = false;
    }

    if (!args.useNet) {
        // FIXME: should check for command line overrides only.
        settings.useSubstitutes.setDefault(false);
        settings.tarballTtl.setDefault(std::numeric_limits<unsigned int>::max());
        fileTransferSettings.tries.setDefault(1);
        fileTransferSettings.maxConnectTimeout.setDefault(1);
        fileTransferSettings.initialConnectTimeout.setDefault(1);
    }

    if (args.refresh) {
        settings.tarballTtl.override(0);
        settings.ttlNegativeNarInfoCache.override(0);
        settings.ttlPositiveNarInfoCache.override(0);
    }

    if (args.command->second->forceImpureByDefault()) {
        evalSettings.pureEval.setDefault(false);
    }
    args.run();

    return 0;
}
}

int main(int argc, char * * argv)
{
    if (argc < 1) {
        std::cerr << "no, we don't have pkexec at home. provide argv[0]." << std::endl;
        std::abort();
    }

    return nix::handleExceptions(argv[0], [&]() {
        nix::AsyncIoRoot aio;
        return nix::mainWrapped(aio, argc, argv);
    });
}
