#include "extensions/extension_manager.h"

#include <algorithm>
#include <cctype>
#include <fstream>
#include <optional>
#include <sstream>
#include <string_view>
#include <system_error>

#include "browser/url_utils.h"
#include "platform/paths_platform.h"
#include "ui/paths.h"

namespace nebula::extensions {
namespace {

std::string ReadFile(const std::filesystem::path& path) {
    std::ifstream input(path, std::ios::binary);
    if (!input) {
        return {};
    }

    std::ostringstream buffer;
    buffer << input.rdbuf();
    return buffer.str();
}

std::string ToLowerAscii(std::string value) {
    std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
        return static_cast<char>(std::tolower(ch));
    });
    return value;
}

std::string SanitizeJsId(std::string value) {
    for (char& ch : value) {
        if (!(std::isalnum(static_cast<unsigned char>(ch)) || ch == '_' || ch == '-')) {
            ch = '_';
        }
    }
    if (value.empty()) {
        value = "extension";
    }
    return value;
}

std::optional<std::string> ReadStringValue(const std::string& object, std::string_view key) {
    const size_t key_pos = object.find(key);
    if (key_pos == std::string::npos) {
        return std::nullopt;
    }

    size_t colon = object.find(':', key_pos + key.size());
    if (colon == std::string::npos) {
        return std::nullopt;
    }

    size_t quote = object.find('"', colon + 1);
    if (quote == std::string::npos) {
        return std::nullopt;
    }

    std::string value;
    for (size_t i = quote + 1; i < object.size(); ++i) {
        const char ch = object[i];
        if (ch == '"') {
            return value;
        }
        if (ch == '\\' && i + 1 < object.size()) {
            value.push_back(object[++i]);
            continue;
        }
        value.push_back(ch);
    }
    return std::nullopt;
}

std::optional<bool> ReadBoolValue(const std::string& object, std::string_view key) {
    const size_t key_pos = object.find(key);
    if (key_pos == std::string::npos) {
        return std::nullopt;
    }

    size_t colon = object.find(':', key_pos + key.size());
    if (colon == std::string::npos) {
        return std::nullopt;
    }

    ++colon;
    while (colon < object.size() && std::isspace(static_cast<unsigned char>(object[colon]))) {
        ++colon;
    }

    if (object.compare(colon, 4, "true") == 0) {
        return true;
    }
    if (object.compare(colon, 5, "false") == 0) {
        return false;
    }
    return std::nullopt;
}

std::vector<std::string> ReadStringArray(const std::string& object, std::string_view key) {
    std::vector<std::string> values;
    const size_t key_pos = object.find(key);
    if (key_pos == std::string::npos) {
        return values;
    }

    size_t bracket = object.find('[', key_pos + key.size());
    if (bracket == std::string::npos) {
        return values;
    }

    for (size_t i = bracket + 1; i < object.size(); ++i) {
        const char ch = object[i];
        if (ch == ']') {
            break;
        }
        if (ch != '"') {
            continue;
        }

        std::string value;
        for (++i; i < object.size(); ++i) {
            const char inner = object[i];
            if (inner == '"') {
                values.push_back(value);
                break;
            }
            if (inner == '\\' && i + 1 < object.size()) {
                value.push_back(object[++i]);
                continue;
            }
            value.push_back(inner);
        }
    }
    return values;
}

std::vector<std::string> SplitContentScriptObjects(const std::string& json) {
    std::vector<std::string> objects;
    constexpr std::string_view key = "\"content_scripts\"";
    const size_t key_pos = json.find(key);
    if (key_pos == std::string::npos) {
        return objects;
    }

    size_t bracket = json.find('[', key_pos + key.size());
    if (bracket == std::string::npos) {
        return objects;
    }

    int depth = 0;
    size_t object_start = std::string::npos;
    for (size_t i = bracket + 1; i < json.size(); ++i) {
        const char ch = json[i];
        if (ch == '{') {
            if (depth == 0) {
                object_start = i;
            }
            ++depth;
        } else if (ch == '}') {
            if (depth > 0) {
                --depth;
                if (depth == 0 && object_start != std::string::npos) {
                    objects.push_back(json.substr(object_start, i - object_start + 1));
                    object_start = std::string::npos;
                }
            }
        } else if (ch == ']' && depth == 0) {
            break;
        }
    }
    return objects;
}

bool MatchSegment(const std::string& pattern, const std::string& value) {
    size_t p = 0;
    size_t v = 0;
    size_t star_p = std::string::npos;
    size_t star_v = std::string::npos;

    while (v < value.size()) {
        if (p < pattern.size() && (pattern[p] == '?' || pattern[p] == value[v])) {
            ++p;
            ++v;
        } else if (p < pattern.size() && pattern[p] == '*') {
            star_p = p++;
            star_v = v;
        } else if (star_p != std::string::npos) {
            p = star_p + 1;
            v = ++star_v;
        } else {
            return false;
        }
    }

    while (p < pattern.size() && pattern[p] == '*') {
        ++p;
    }
    return p == pattern.size();
}

bool MatchUrlPattern(const std::string& pattern, const std::string& url) {
    if (pattern == "<all_urls>") {
        return url.starts_with("http://") || url.starts_with("https://") || url.starts_with("file:");
    }

    const std::string lower_pattern = ToLowerAscii(pattern);
    const std::string lower_url = ToLowerAscii(url);
    return MatchSegment(lower_pattern, lower_url);
}

bool UrlMatchesAny(const std::vector<std::string>& patterns, const std::string& url) {
    for (const auto& pattern : patterns) {
        if (MatchUrlPattern(pattern, url)) {
            return true;
        }
    }
    return false;
}

bool IsSafeRelativePath(const std::string& relative) {
    if (relative.empty() || relative.find('\\') != std::string::npos) {
        return false;
    }
    if (relative.front() == '/' || relative.find("..") != std::string::npos) {
        return false;
    }
    return true;
}

std::string JsStringLiteral(const std::string& value) {
    return "\"" + nebula::browser::JsonEscape(value) + "\"";
}

std::string BuildCssInjectScript(const std::string& guard_id, const std::string& css) {
    return "(function(){"
           "var id=" +
           JsStringLiteral(guard_id) +
           ";"
           "if(document.getElementById(id))return;"
           "var style=document.createElement('style');"
           "style.id=id;"
           "style.textContent=" +
           JsStringLiteral(css) +
           ";"
           "(document.head||document.documentElement).appendChild(style);"
           "})();";
}

std::string BuildJsInjectScript(const std::string& guard_id, const std::string& source) {
    return "(function(){"
           "var k=" +
           JsStringLiteral(guard_id) +
           ";"
           "if(window[k])return;window[k]=true;"
           "try{" +
           source +
           "\n}catch(e){console.error('[Nebula Extension]',e);}"
           "})();";
}

void WriteInstallReadme(const std::filesystem::path& extensions_dir) {
    const auto readme = extensions_dir / "INSTALL.txt";
    if (std::filesystem::exists(readme)) {
        return;
    }

    std::ofstream output(readme, std::ios::binary | std::ios::trunc);
    if (!output) {
        return;
    }

    const std::string dir_utf8 = nebula::platform::PathToUtf8(extensions_dir);
    output << "Nebula Extensions\n"
           << "=================\n\n"
           << "This machine's install directory:\n"
           << "  " << dir_utf8 << "\n\n"
           << "Drop an unpacked extension folder here (or copy/paste it into this directory).\n"
           << "Each folder must contain a manifest.json file.\n\n"
           << "Typical locations by OS:\n"
           << "  Windows:  %LOCALAPPDATA%\\Nebula\\User Data\\extensions\\\n"
           << "  macOS:    ~/Library/Application Support/Nebula/User Data/extensions/\n"
           << "  Linux:    ~/.local/share/Nebula/User Data/extensions/\n\n"
           << "Example layout:\n"
           << "  extensions/\n"
           << "    my-extension/\n"
           << "      manifest.json\n"
           << "      content.js\n\n"
           << "Then open Settings > Plugins and click Reload Plugins\n"
           << "(or restart Nebula).\n\n"
           << "See examples/extensions/hello-nebula in the Nebula source tree\n"
           << "for a starter package.\n";
}

}  // namespace

void ExtensionManager::EnsureInstallReadme() const {
    const auto dir = nebula::ui::GetExtensionsDirectory();
    if (!dir.empty()) {
        WriteInstallReadme(dir);
    }
}

void ExtensionManager::LoadDisabledState() {
    enabled_overrides_.clear();
    const auto path = nebula::ui::GetExtensionsStatePath();
    if (path.empty()) {
        return;
    }

    const std::string json = ReadFile(path);
    if (json.empty()) {
        return;
    }

    constexpr std::string_view key = "\"disabled\"";
    const size_t key_pos = json.find(key);
    if (key_pos == std::string::npos) {
        return;
    }

    for (const auto& id : ReadStringArray(json, key)) {
        enabled_overrides_[id] = false;
    }
}

void ExtensionManager::SaveDisabledState() const {
    const auto path = nebula::ui::GetExtensionsStatePath();
    if (path.empty()) {
        return;
    }

    std::string json = "{\n  \"disabled\": [";
    bool first = true;
    for (const auto& extension : extensions_) {
        if (extension.enabled) {
            continue;
        }
        if (!first) {
            json += ", ";
        }
        first = false;
        json += "\"" + nebula::browser::JsonEscape(extension.id) + "\"";
    }
    json += "]\n}\n";

    std::filesystem::path temp_path = path;
    temp_path += ".tmp";
    {
        std::ofstream output(temp_path, std::ios::binary | std::ios::trunc);
        if (!output) {
            return;
        }
        output << json;
    }

    std::error_code ec;
    std::filesystem::remove(path, ec);
    ec.clear();
    std::filesystem::rename(temp_path, path, ec);
}

bool ExtensionManager::LoadExtensionDirectory(const std::filesystem::path& directory) {
    const auto manifest_path = directory / "manifest.json";
    if (!std::filesystem::is_regular_file(manifest_path)) {
        return false;
    }

    Extension extension;
    extension.directory = directory;
    extension.id = nebula::platform::PathToUtf8(directory.filename());

    const std::string manifest = ReadFile(manifest_path);
    if (manifest.empty()) {
        extension.load_error = true;
        extension.load_error_message = "Unable to read manifest.json";
        extension.name = extension.id;
        extensions_.push_back(std::move(extension));
        return true;
    }

    if (auto id = ReadStringValue(manifest, "\"id\""); id && !id->empty()) {
        extension.id = *id;
    }
    extension.name = ReadStringValue(manifest, "\"name\"").value_or(extension.id);
    extension.version = ReadStringValue(manifest, "\"version\"").value_or("0.0.0");
    extension.description = ReadStringValue(manifest, "\"description\"").value_or("");
    extension.enabled = ReadBoolValue(manifest, "\"enabled\"").value_or(true);

    if (const auto override = enabled_overrides_.find(extension.id);
        override != enabled_overrides_.end()) {
        extension.enabled = override->second;
    }

    for (const auto& object : SplitContentScriptObjects(manifest)) {
        ContentScript script;
        script.matches = ReadStringArray(object, "\"matches\"");
        script.js_files = ReadStringArray(object, "\"js\"");
        script.css_files = ReadStringArray(object, "\"css\"");
        script.run_at = ReadStringValue(object, "\"run_at\"").value_or("document_end");

        if (script.matches.empty()) {
            script.matches.push_back("<all_urls>");
        }

        bool ok = true;
        for (const auto& relative : script.js_files) {
            if (!IsSafeRelativePath(relative)) {
                ok = false;
                extension.load_error_message = "Unsafe js path: " + relative;
                break;
            }
            const auto path = directory / relative;
            const std::string source = ReadFile(path);
            if (source.empty() && !std::filesystem::is_regular_file(path)) {
                ok = false;
                extension.load_error_message = "Missing js file: " + relative;
                break;
            }
            script.js_sources.push_back(source);
        }

        if (ok) {
            for (const auto& relative : script.css_files) {
                if (!IsSafeRelativePath(relative)) {
                    ok = false;
                    extension.load_error_message = "Unsafe css path: " + relative;
                    break;
                }
                const auto path = directory / relative;
                const std::string source = ReadFile(path);
                if (source.empty() && !std::filesystem::is_regular_file(path)) {
                    ok = false;
                    extension.load_error_message = "Missing css file: " + relative;
                    break;
                }
                script.css_sources.push_back(source);
            }
        }

        if (!ok) {
            extension.load_error = true;
            break;
        }

        extension.content_scripts.push_back(std::move(script));
    }

    extensions_.push_back(std::move(extension));
    return true;
}

void ExtensionManager::Reload() {
    extensions_.clear();
    EnsureInstallReadme();
    LoadDisabledState();

    const auto root = nebula::ui::GetExtensionsDirectory();
    if (root.empty() || !std::filesystem::is_directory(root)) {
        return;
    }

    std::error_code ec;
    for (const auto& entry : std::filesystem::directory_iterator(root, ec)) {
        if (!entry.is_directory(ec)) {
            continue;
        }
        const auto name = nebula::platform::PathToUtf8(entry.path().filename());
        if (name.empty() || name.front() == '.') {
            continue;
        }
        LoadExtensionDirectory(entry.path());
    }

    std::sort(extensions_.begin(), extensions_.end(),
              [](const Extension& a, const Extension& b) { return a.name < b.name; });
}

bool ExtensionManager::SetEnabled(const std::string& id, bool enabled) {
    bool found = false;
    for (auto& extension : extensions_) {
        if (extension.id != id) {
            continue;
        }
        extension.enabled = enabled;
        found = true;
        break;
    }

    if (!found) {
        return false;
    }

    if (enabled) {
        enabled_overrides_.erase(id);
    } else {
        enabled_overrides_[id] = false;
    }
    SaveDisabledState();
    return true;
}

std::string ExtensionManager::ListJson() const {
    std::string json = "[";
    for (size_t i = 0; i < extensions_.size(); ++i) {
        const auto& extension = extensions_[i];
        if (i > 0) {
            json += ",";
        }
        json += "{";
        json += "\"id\":\"" + nebula::browser::JsonEscape(extension.id) + "\",";
        json += "\"name\":\"" + nebula::browser::JsonEscape(extension.name) + "\",";
        json += "\"version\":\"" + nebula::browser::JsonEscape(extension.version) + "\",";
        json += "\"description\":\"" + nebula::browser::JsonEscape(extension.description) + "\",";
        json += "\"dir\":\"" +
                nebula::browser::JsonEscape(nebula::platform::PathToUtf8(extension.directory)) +
                "\",";
        json += "\"enabled\":";
        json += extension.enabled && !extension.load_error ? "true" : "false";
        json += ",\"loadError\":";
        json += extension.load_error ? "true" : "false";
        json += ",\"loadErrorMessage\":\"" +
                nebula::browser::JsonEscape(extension.load_error_message) + "\"";
        json += ",\"categories\":[\"extension\"]";
        json += ",\"authors\":[]";
        json += "}";
    }
    json += "]";
    return json;
}

void ExtensionManager::InjectMatching(CefRefPtr<CefBrowser> browser, const std::string& url) const {
    if (!browser || url.empty()) {
        return;
    }

    CefRefPtr<CefFrame> frame = browser->GetMainFrame();
    if (!frame) {
        return;
    }

    if (nebula::ui::IsNebulaInternalUrl(url) || nebula::ui::IsChromiumNewTabUrl(url)) {
        return;
    }

    for (const auto& extension : extensions_) {
        if (!extension.enabled || extension.load_error) {
            continue;
        }

        const std::string ext_guard = SanitizeJsId(extension.id);
        size_t script_index = 0;
        for (const auto& script : extension.content_scripts) {
            if (!UrlMatchesAny(script.matches, url)) {
                ++script_index;
                continue;
            }

            for (size_t i = 0; i < script.css_sources.size(); ++i) {
                const std::string guard =
                    "nebula-ext-css-" + ext_guard + "-" + std::to_string(script_index) + "-" +
                    std::to_string(i);
                frame->ExecuteJavaScript(BuildCssInjectScript(guard, script.css_sources[i]), url, 0);
            }

            for (size_t i = 0; i < script.js_sources.size(); ++i) {
                const std::string guard =
                    "__nebulaExt_" + ext_guard + "_" + std::to_string(script_index) + "_" +
                    std::to_string(i);
                frame->ExecuteJavaScript(BuildJsInjectScript(guard, script.js_sources[i]), url, 0);
            }
            ++script_index;
        }
    }
}

}  // namespace nebula::extensions
