#include <cctype>
#include <cstdlib>
#include <iomanip>
#include <sstream>

#include "klammerset_registry.h"
#include "error.h"
#include "file.h"
#include "log.h"
#include "util.h"
#include "show.h"
#include "katom.h"

Klammerset_registry::Klammerset_registry()
        : m_parameters(Parameter_set("symbol | desc :name :author :date :requires :files"))
{
}

void Klammerset_registry::add(Klammerset klammerset)
{
    (void)K::log(3, klammerset.m_symbol);
    m_klammersets[klammerset.m_symbol] = klammerset;
    m_symbols.push_back(klammerset.m_symbol);
}

std::optional<Klammerset> Klammerset_registry::add(
    std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, std::vector<Katom>& katoms)
{
    (void)K::log(3, *begin, *(end - 1));
    auto [positional, optional, rest] =
        argument_split(begin + 1, end - 1, m_parameters.m_positional.size());
    auto values = m_parameters.value_map(positional, optional, rest, begin->m_loc);

    modify_type(katom_t::replaced, begin, end);
    auto next_iter = end;
    ignore_whitespace(next_iter, katoms);

    std::string symbol = values["symbol"];
    check_symbol(symbol, begin->m_loc);
    check_declaring_file(symbol, begin->m_loc);
    if (has(symbol)) {
        (void)K::log(2, "Klammerset \"" + symbol + "\" is already loaded; declaration skipped");
        return std::nullopt;
    }

    // The filename lists follow the standard conventions (spaces allowed,
    // standalone "/" separator).  The existence rescue tests names against
    // the declaring file's directory, where relative names are later
    // resolved by Machine::load_klammerset_files.
    fs::path declaring(begin->m_loc.m_filename);
    std::string base_dir =
        fs::exists(declaring) ? declaring.parent_path().string() : "";

    Klammerset klammerset(symbol, values["desc"], begin->m_loc);
    klammerset.m_name = values["name"];
    klammerset.m_author = values["author"];
    klammerset.m_date = values["date"];
    klammerset.m_requires = resolve_filename_list(values["requires"], base_dir);
    klammerset.m_files = resolve_filename_list(values["files"], base_dir);
    add(klammerset);
    return klammerset;
}

void Klammerset_registry::check_symbol(const std::string& symbol, const Locator& loc) const
{
    bool valid = !symbol.empty() && std::isalpha(static_cast<unsigned char>(symbol[0]));
    for (char c : symbol) {
        if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
            valid = false;
        }
    }
    if (!valid) {
        throw Klammerset_error(
            "The klammerset symbol \"" + symbol + "\" is not valid. A symbol begins "
            "with a letter and contains only letters, digits, and underscores.",
            loc);
    }
}

// A klammerset that can be NAMED must live where its name says: symbol X is
// declared in X/X.k (Andy, 2026-08-15).  The convention already governed
// symbol RESOLUTION; making it a requirement of the declaration too is what
// turns the symbol into a function of the path -- and that is what lets the
// already-loaded guard run BEFORE a file is read rather than after, which is
// the whole of the diamond-":requires" fix.  Without it the symbol is known
// only once the file has been parsed, and by then a second read has already
// re-executed the declaring file's own definitions.
//
// It also continues the reasoning behind symbols-only on the command line
// (2026-08-14): if every klammerset is reachable by symbol the set of all of
// them is ENUMERABLE; if every declaration is in X/X.k each one is also
// IDENTIFIABLE from where it sits.
//
// EXEMPT: a declaration that is not in a ".k" file at all.  A document may
// declare a klammerset -- that is how a designer writes one, and kdesc's
// provenance filter depends on it -- and such a set is local to the document:
// nothing can ":requires" it, so it has no identity to protect.
void Klammerset_registry::check_declaring_file(
    const std::string& symbol, const Locator& loc) const
{
    fs::path declaring(loc.m_filename);
    if (declaring.extension() != ".k") {
        return;
    }
    if (declaring.stem() == symbol && declaring.parent_path().filename() == symbol) {
        return;
    }
    throw Klammerset_error(
        "The klammerset \"" + symbol + "\" must be declared in a file named \""
        + symbol + "/" + symbol + ".k\", but this declaration is in \""
        + declaring.filename().string() + "\" (in a directory named \""
        + declaring.parent_path().filename().string() + "\"). A klammerset that "
        "can be named is identified by where it is, so that a symbol names one "
        "file and one file declares one symbol. A klammerset local to a document "
        "may be declared in the document itself.",
        loc);
}

bool Klammerset_registry::has(const std::string& symbol) const
{
    return m_klammersets.count(symbol) > 0;
}

Klammerset Klammerset_registry::get(const std::string& symbol, const Locator& loc) const
{
    if (has(symbol)) {
        return m_klammersets.at(symbol);
    } else {
        throw Klammerset_error("Klammerset " + symbol + " does not exist", loc);
    }
}

// --- The klammerset search path ---

bool is_klammerset_symbol(const std::string& name)
{
    if (name.empty() || !std::isalpha(static_cast<unsigned char>(name[0]))) {
        return false;
    }
    for (char c : name) {
        if (!std::isalnum(static_cast<unsigned char>(c)) && c != '_') {
            return false;
        }
    }
    return true;
}

std::vector<std::string> klammerset_search_dirs(const std::string& local_dir)
{
    std::vector<std::string> result {};
    // A repeated directory (e.g. the local stage already IS
    // $KLAMMERTEXT_HOME) adds nothing and clutters error messages.
    auto push_unique = [&result](const std::string& dir) {
        std::string canonical = fs::weakly_canonical(dir).string();
        if (!is_in(canonical, result)) {
            result.push_back(canonical);
        }
    };
    if (!local_dir.empty()) {
        push_unique(local_dir);
    }
    // Read ONCE per process, not once per resolution.  A search path decides
    // WHICH FILE a symbol means -- an identity question, not a value one -- so
    // it must not move while a document is being processed: if it did, "which
    // klammerset is X" would depend on evaluation order and "kdesc
    // --klammersets" could not be a complete answer, which is the reason the
    // command line takes symbols only.
    //
    // It could move.  The embedded Python shares the process, so an @eval
    // doing os.environ[...] = ... changed what a later getenv here returned;
    // a document could extend its own search path between one declaration and
    // the next.  Nothing intended that -- it fell out of a fresh getenv and an
    // in-process interpreter.
    static const std::string paths = [] {
        const char* env = std::getenv("KLAMMERTEXT_KLAMMERSETS");
        if (env && *env) {
            return std::string(env);
        }
        if (const char* home = std::getenv("HOME"); home && *home) {
            return std::string(home) + "/.klammertext/klammersets";
        }
        return std::string();
    }();
    std::stringstream ss(paths);
    std::string dir;
    while (std::getline(ss, dir, ':')) {
        if (!dir.empty()) {
            push_unique(dir);
        }
    }
    if (const char* kthome = std::getenv(klammertext_home_var.c_str()); kthome && *kthome) {
        push_unique(kthome);
    }
    return result;
}

fs::path resolve_klammerset_symbol(
    const std::string& symbol, const std::string& local_dir, const Locator& loc)
{
    std::vector<std::string> dirs = klammerset_search_dirs(local_dir);
    for (const std::string& dir : dirs) {
        fs::path candidate = fs::path(dir) / symbol / (symbol + ".k");
        if (fs::exists(candidate) && fs::is_regular_file(candidate)) {
            return fs::weakly_canonical(candidate);
        }
    }
    throw Klammerset_error(
        "The klammerset \"" + symbol + "\" was not found. A symbol x names the "
        "declaration file x/x.k in one of the search directories:\n  "
        + join(dirs, "\n  ")
        + "\nEnter \"kdesc --klammersets\" to list the available klammersets.",
        loc);
}

std::string describe_klammerset_search(const std::string& local_dir, int margin)
{
    std::string tab(margin, ' ');
    std::stringstream ss {};
    std::map<std::string, std::string> first_hit {};
    for (const std::string& dir : klammerset_search_dirs(local_dir)) {
        if (!fs::is_directory(dir)) {
            continue;
        }
        for (const auto& entry : fs::directory_iterator(dir)) {
            if (!entry.is_directory()) {
                continue;
            }
            std::string symbol = entry.path().filename().string();
            fs::path declaration = entry.path() / (symbol + ".k");
            if (!is_klammerset_symbol(symbol) || !fs::is_regular_file(declaration)) {
                continue;
            }
            ss << tab << symbol << sp_arrow << declaration.string();
            if (first_hit.count(symbol)) {
                ss << "  (shadowed by " << first_hit[symbol] << ")";
            } else {
                first_hit[symbol] = declaration.string();
            }
            ss << "\n";
        }
    }
    if (first_hit.empty()) {
        ss << tab << "(no klammersets found)\n";
    }
    return ss.str();
}

std::string Klammerset_registry::describe(int margin, bool long_format) const
{
    std::string tab(margin, ' ');
    std::stringstream ss {};
    std::vector<std::string> descs {};
    for (const std::string& symbol : m_symbols) {
        descs.push_back(m_klammersets.at(symbol).m_desc);
    }
    auto symbol_width = max_length(m_symbols);
    auto desc_width = max_length(descs);
    for (const std::string& symbol : m_symbols) {
        const Klammerset& ks = m_klammersets.at(symbol);
        if (long_format) {
            ss << tab << std::setfill(' ') << std::setw(symbol_width) << std::left << symbol << "  "
               << std::setw(desc_width) << std::left << ks.m_desc << "  "
               << ks.m_loc.str() << "\n";
            if (!ks.m_name.empty())
                ss << tab << std::string(symbol_width, ' ') << "  name: " << ks.m_name << "\n";
            if (!ks.m_author.empty())
                ss << tab << std::string(symbol_width, ' ') << "  author: " << ks.m_author << "\n";
            if (!ks.m_date.empty())
                ss << tab << std::string(symbol_width, ' ') << "  date: " << ks.m_date << "\n";
            if (!ks.m_requires.empty())
                ss << tab << std::string(symbol_width, ' ') << "  requires: " << join(ks.m_requires, " / ") << "\n";
            if (!ks.m_files.empty())
                ss << tab << std::string(symbol_width, ' ') << "  files: " << join(ks.m_files, " / ") << "\n";
        } else {
            ss << tab << std::setfill(' ') << std::setw(symbol_width) << std::left << symbol
               << sp_arrow << ks << "\n";
        }
    }
    return ss.str();
}

void Klammerset_registry::set_loaded_files(const std::string& symbol, const strings_t& files)
{
    auto it = m_klammersets.find(symbol);
    if (it != m_klammersets.end()) {
        it->second.m_loaded_files = files;
    }
}

strings_t Klammerset_registry::loaded_files() const
{
    strings_t result {};
    for (const auto& [symbol, klammerset] : m_klammersets) {
        result.insert(result.end(),
                      klammerset.m_loaded_files.begin(), klammerset.m_loaded_files.end());
    }
    return result;
}
