#include <stdint.h>
#include <map>
#include <optional>
#include <string>
#include <variant>

#include "nix/flake/flake-primops.hh"
#include "nix/expr/eval.hh"
#include "nix/flake/flake.hh"
#include "nix/flake/flakeref.hh"
#include "nix/flake/settings.hh"
#include "nix/expr/attr-set.hh"
#include "nix/expr/eval-error.hh"
#include "nix/expr/eval-inline.hh"
#include "nix/expr/eval-settings.hh"
#include "nix/expr/symbol-table.hh"
#include "nix/expr/value.hh"
#include "nix/fetchers/attrs.hh"
#include "nix/fetchers/fetchers.hh"
#include "nix/util/error.hh"
#include "nix/util/experimental-features.hh"
#include "nix/util/mounted-source-accessor.hh"
#include "nix/util/pos-idx.hh"
#include "nix/util/pos-table.hh"
#include "nix/util/types.hh"
#include "nix/util/util.hh"
#include "nix/store/store-api.hh"

namespace nix::flake::primops {

PrimOp getFlake(const Settings & settings)
{
    auto prim_getFlake = [&settings](EvalState & state, const PosIdx pos, Value ** args, Value & v) {
        state.forceValue(*args[0], pos);

        LockFlags lockFlags{
            .updateLockFile = false,
            .writeLockFile = false,
            .useRegistries = !state.settings.pureEval && settings.useRegistries,
            .allowUnlocked = !state.settings.pureEval,
        };

        if (args[0]->type() == nPath) {
            auto path = state.realisePath(pos, *args[0]);
            callFlake(state, lockFlake(settings, state, path, lockFlags), v);
        } else {
            std::string flakeRefS(
                state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.getFlake"));

            auto flakeRef = nix::parseFlakeRef(state.fetchSettings, flakeRefS, {}, true);
            if (state.settings.pureEval && !flakeRef.input.isLocked(state.fetchSettings))
                throw Error(
                    "cannot call 'getFlake' on unlocked flake reference '%s', at %s (use --impure to override)",
                    flakeRefS,
                    state.positions[pos]);

            /* Backwards compatibility: since flakes used to be copied to the store eagerly, some users
               relied on being able to do builtins.getFlake on a flakeref with discarded string context.
               So if a flake input has a physical source path that is inside the store, first try to look it up in the
               storeFS. */
            if (auto sourcePath = flakeRef.input.getSourcePath();
                flakeRef.input.getType() == "path" && sourcePath && state.store->isInStore(sourcePath->string())) {
                auto [storePath, subPath] = state.store->toStorePath(sourcePath->string());
                if (auto mount = state.storeFS->getMount(CanonPath(state.store->printStorePath(storePath)))) {
                    auto path = state.storePath(storePath) / CanonPath(subPath);
                    if (!flakeRef.subdir.empty())
                        path = path / flakeRef.subdir;
                    return callFlake(state, lockFlake(settings, state, path, lockFlags), v);
                }
            }

            callFlake(state, lockFlake(settings, state, flakeRef, lockFlags), v);
        }
    };

    return PrimOp{
        .name = "__getFlake",
        .args = {"args"},
        .doc = R"(
          Fetch a flake from a flake reference or a path, and return its output attributes and some metadata. For example:

          ```nix
          (builtins.getFlake "nix/55bc52401966fbffa525c574c14f67b00bc4fb3a").packages.x86_64-linux.nix
          ```

          Unless impure evaluation is allowed (`--impure`), the flake reference
          must be "locked", e.g. contain a Git revision or content hash. An
          example of an unlocked usage is:

          ```nix
          (builtins.getFlake "github:edolstra/dwarffs").rev
          ```
        )",
        .impl = prim_getFlake,
        .experimentalFeature = Xp::Flakes,
    };
}

static void prim_parseFlakeRef(EvalState & state, const PosIdx pos, Value ** args, Value & v)
{
    std::string flakeRefS(
        state.forceStringNoCtx(*args[0], pos, "while evaluating the argument passed to builtins.parseFlakeRef"));
    auto attrs = nix::parseFlakeRef(state.fetchSettings, flakeRefS, {}, true).toAttrs();
    auto binds = state.buildBindings(attrs.size());
    for (const auto & [key, value] : attrs) {
        auto s = state.symbols.create(key);
        auto & vv = binds.alloc(s);
        auto resolved = forceAttr(value);
        std::visit(
            overloaded{
                [&vv, &state](const std::string & value) { vv.mkString(value, state.mem); },
                [&vv](const uint64_t & value) { vv.mkInt(value); },
                [&vv](const Explicit<bool> & value) { vv.mkBool(value.t); }},
            resolved);
    }
    v.mkAttrs(binds);
}

nix::PrimOp parseFlakeRef({
    .name = "__parseFlakeRef",
    .args = {"flake-ref"},
    .doc = R"(
      Parse a flake reference, and return its exploded form.

      For example:

      ```nix
      builtins.parseFlakeRef "github:NixOS/nixpkgs/23.05?dir=lib"
      ```

      evaluates to:

      ```nix
      { dir = "lib"; owner = "NixOS"; ref = "23.05"; repo = "nixpkgs"; type = "github"; }
      ```
    )",
    .impl = prim_parseFlakeRef,
    .experimentalFeature = Xp::Flakes,
});

static void prim_flakeRefToString(EvalState & state, const PosIdx pos, Value ** args, Value & v)
{
    state.forceAttrs(*args[0], noPos, "while evaluating the argument passed to builtins.flakeRefToString");
    fetchers::Attrs attrs;
    for (const auto & attr : *args[0]->attrs()) {
        state.forceValue(*attr.value, attr.pos);
        auto t = attr.value->type();
        if (t == nInt) {
            auto intValue = attr.value->integer().value;

            if (intValue < 0) {
                state
                    .error<EvalError>(
                        "negative value given for flake ref attr %1%: %2%", state.symbols[attr.name], intValue)
                    .atPos(pos)
                    .debugThrow();
            }

            attrs.emplace(state.symbols[attr.name], uint64_t(intValue));
        } else if (t == nBool) {
            attrs.emplace(state.symbols[attr.name], Explicit<bool>{attr.value->boolean()});
        } else if (t == nString) {
            attrs.emplace(state.symbols[attr.name], std::string(attr.value->string_view()));
        } else {
            state
                .error<EvalError>(
                    "flake reference attribute sets may only contain integers, Booleans, "
                    "and strings, but attribute '%s' is %s",
                    state.symbols[attr.name],
                    showType(*attr.value))
                .debugThrow();
        }
    }
    auto flakeRef = FlakeRef::fromAttrs(state.fetchSettings, attrs);
    v.mkString(flakeRef.to_string(), state.mem);
}

nix::PrimOp flakeRefToString({
    .name = "__flakeRefToString",
    .args = {"attrs"},
    .doc = R"(
      Convert a flake reference from attribute set format to URL format.

      For example:

      ```nix
      builtins.flakeRefToString {
        dir = "lib"; owner = "NixOS"; ref = "23.05"; repo = "nixpkgs"; type = "github";
      }
      ```

      evaluates to

      ```nix
      "github:NixOS/nixpkgs/23.05?dir=lib"
      ```
    )",
    .impl = prim_flakeRefToString,
    .experimentalFeature = Xp::Flakes,
});

} // namespace nix::flake::primops
