#include "agentty/tool/spec.hpp"
#include "agentty/tool/tools.hpp"
#include "agentty/tool/util/arg_reader.hpp"
#include "agentty/tool/util/fs_helpers.hpp"
#include "agentty/tool/util/tool_args.hpp"

#include <algorithm>
#include <cstdio>
#include <cstdint>
#include <filesystem>
#include <sstream>
#include <string>
#include <system_error>
#include <vector>

#include <nlohmann/json.hpp>

namespace agentty::tools {

using json = nlohmann::json;
namespace fs = std::filesystem;

namespace {

struct ListDirArgs {
    std::string root;
    bool recursive;
    int max_depth;
    std::string display_description;
};

std::expected<ListDirArgs, ToolError> parse_list_dir_args(const json& j) {
    util::ArgReader ar(j);
    // Clamp max_depth so a runaway value (model passes 999) can't drag the
    // walker through the entire filesystem before the watchdog fires.
    int max_depth = std::clamp(ar.integer("max_depth", 3), 1, 16);
    return ListDirArgs{
        ar.str("path", "."),
        ar.boolean("recursive", false),
        max_depth,
        ar.str("display_description", ""),
    };
}

ExecResult run_list_dir(const ListDirArgs& a) {
    // Workspace boundary check at execute-time (not parse-time) so the
    // default "." root canonicalises against the active cwd at the
    // moment of the call, not at args-construction. Same behaviour
    // applies to grep/glob/find_definition.
    auto wp = util::make_workspace_path_checked(a.root, "list_dir");
    if (!wp) return std::unexpected(std::move(wp.error()));
    std::error_code ec;
    if (!fs::exists(wp->path(), ec))
        return std::unexpected(ToolError::not_found("directory not found: " + a.root));
    if (!fs::is_directory(wp->path(), ec))
        return std::unexpected(ToolError::not_a_directory("not a directory: " + a.root));

    std::ostringstream out;
    int count = 0;

    auto format_size = [](uintmax_t bytes) -> std::string {
        char buf[32];
        if (bytes < 1024) { std::snprintf(buf, sizeof(buf), "%juB", bytes); return buf; }
        if (bytes < 1024*1024) { std::snprintf(buf, sizeof(buf), "%.1fK", bytes/1024.0); return buf; }
        if (bytes < 1024*1024*1024) { std::snprintf(buf, sizeof(buf), "%.1fM", bytes/(1024.0*1024.0)); return buf; }
        std::snprintf(buf, sizeof(buf), "%.1fG", bytes/(1024.0*1024.0*1024.0)); return buf;
    };

    auto list_entry = [&](const fs::directory_entry& entry, int depth) {
        if (count > 1000) return;
        std::string indent(depth * 2, ' ');
        auto fn = entry.path().filename().string();
        if (entry.is_directory(ec)) {
            out << indent << fn << "/\n";
        } else if (entry.is_regular_file(ec)) {
            auto sz = entry.file_size(ec);
            out << indent << fn << "  " << format_size(ec ? 0 : sz) << "\n";
        } else if (entry.is_symlink(ec)) {
            // read_symlink can fail on Windows when the user lacks
            // Developer Mode / admin privileges, on a broken symlink, or
            // on filesystems that report symlink-ish entries without
            // exposing the target (some FUSE mounts). Don't emit an
            // empty `name -> ` row when that happens — show "<unreadable>"
            // so the model can tell the entry exists but the target
            // can't be resolved here.
            std::error_code link_ec;
            auto target = fs::read_symlink(entry.path(), link_ec);
            if (link_ec) {
                out << indent << fn << " -> <unreadable: "
                    << link_ec.message() << ">\n";
            } else {
                out << indent << fn << " -> " << target.string() << "\n";
            }
        }
        count++;
    };

    if (a.recursive) {
        for (auto it = fs::recursive_directory_iterator(wp->path(),
                    fs::directory_options::skip_permission_denied, ec);
             it != fs::recursive_directory_iterator(); it.increment(ec)) {
            if (ec) { ec.clear(); continue; }
            if (it.depth() > a.max_depth) { it.disable_recursion_pending(); continue; }

            const auto& entry = *it;
            auto fn = entry.path().filename().string();
            const bool is_dir = entry.is_directory(ec);

            // Stop descending into known-noisy directories at EVERY depth,
            // including depth 0 (the user's root may itself contain
            // `node_modules`, `build`, `target`, `.git`, etc.). The earlier
            // `it.depth() > 0` gate let those slip through when they sat at
            // the top level — exactly the case `list_dir(recursive=true)`
            // on a project root hits. We still render the directory once
            // so the model sees it exists; we just don't iterate the
            // hundreds of thousands of files inside.
            if (is_dir && util::should_skip_dir(fn)) {
                list_entry(entry, it.depth());
                it.disable_recursion_pending();
                if (count > 1000) { out << "[>1000 entries, truncated]\n"; break; }
                continue;
            }
            // Hidden dirs (`.config`, `.cache`, etc.) below the root are
            // suppressed — keeps recursive listings of a project clean.
            // At depth 0 we surface them so `list_dir(~)` still works.
            if (is_dir && it.depth() > 0 && fn.starts_with(".")) {
                it.disable_recursion_pending();
                continue;
            }

            list_entry(*it, it.depth());
            if (count > 1000) { out << "[>1000 entries, truncated]\n"; break; }
        }
    } else {
        std::vector<fs::directory_entry> entries;
        for (auto& e : fs::directory_iterator(wp->path(), ec))
            entries.push_back(e);
        std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
            bool da = a.is_directory(), db = b.is_directory();
            if (da != db) return da > db;
            return a.path().filename() < b.path().filename();
        });
        for (auto& e : entries) list_entry(e, 0);
    }
    if (count == 0) return ToolOutput{"empty directory", std::nullopt};
    std::string body = out.str();
    if (!a.display_description.empty())
        body = a.display_description + "\n" + body;
    return ToolOutput{std::move(body), std::nullopt};
}

} // namespace

ToolDef tool_list_dir() {
    ToolDef t;
    constexpr const auto& kSpec = spec::require<"list_dir">();
    t.name = ToolName{std::string{kSpec.name}};
    t.description = "List the contents of a directory. Shows file type, size, and name. "
                    "Use this to explore project structure before reading files.";
    t.input_schema = json{
        {"type","object"},
        {"properties", {
            {"display_description", {{"type","string"},
                {"description","One-line summary shown in the UI. Optional."}}},
            {"path",      {{"type","string"}, {"description","Directory to list (default: cwd)"}}},
            {"recursive", {{"type","boolean"}, {"description","List recursively (default: false)"}}},
            {"max_depth", {{"type","integer"}, {"description","Max depth for recursive listing (default: 3)"}}},
        }},
    };
    t.effects = kSpec.effects;
    t.eager_input_streaming = kSpec.eager_input_streaming;
    t.execute = util::adapt<ListDirArgs>(parse_list_dir_args, run_list_dir);
    return t;
}

} // namespace agentty::tools
